#HTTPS #SSL #PKI

크로미움 분석으로 알게 된 사실 중 한 가지는 http보다 https로 접속한 웹 페이지에서 더 많은 자바스크립트 메소드를 사용할 수 있다는 것이다. 즉, 크롬은 스킴(scheme)에 따라 차등화된 권한을 웹 페이지에 부여한다. ”http://“, ”https://“, “chrome://“ 순으로 더 강력한? 메소드를 호출할 수 있다. chrome 스킴이야 당연히 크롬의 설정 페이지(chrome://settings)처럼 크롬의 운영과 관련된 부분들을 다루므로 기능적으로나 보안적으로나 다른 스킴들과는 다르긴 하겠지…라며 쉽게 납득했지만, http와 https 스킴의 권한이 다른 것에는 꽤 놀랐었다.

크롬이 http 스킴의 웹 페이지에 대해서 안전하지 않다고 경고하기 시작한 후부터 https 스킴이 아닌 웹 사이트를 찾아보기 힘들다. 그런데 웹 개발자 입장에서는 https로 개발 중인 웹 사이트를 테스트하는 것이 번거롭다. 웹 사이트가 안전한 사이트라고 증명해주는 인증서(certificate)를 구매하여야 하기 때문이다. 본 포스트 작성에 참고한 사이트들을 보면서 좀 더 쉽게 테스트 하고자 하는 개발자들이 이미 수두룩 했음을 알 수 있었다.

만약, localhost를 https로 접속할 수 있다면 얼마나 좋을까하는 생각을 했다. 나는 웹 개발에도 관심은 있지만 그보다 자바스크립트 엔진 쪽에 관심이 있어서 찾아보기 시작했는데 이미 훌륭한 시행착오들이 있었다. 그냥 그대로 따라하면 되는 부분들이다. 하지만 인증서를 비롯한 https의 전반적인 원리가 궁금하여 함께 따로 정리해 보았다. 참고로, 자바스크립트 엔진은 https에 대하여 ‘secure context’에서 구동되어 더 많은 권한의 메소드들의 호출을 허락한다. 아래 웹 페이지를 참고.

https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts

HTTPS(HTTP over SSL)

의문 한 가지. 크롬은 http 스킴의 웹 페이지를 접속하면 안전하지 않은 사이트라고 경고한다. 즉, https 스킴의 웹 페이지는 안전하다는 것인데 안전하다는 것을 크롬은 어떻게 알 수 있는 것인가? 바로 절대적으로 신뢰할 수 있는 제3자를 통해 알 수 있는 것이다. 이 때 사용되는 것이 인증서이다. 전체적인 메커니즘을 이해하기 위해서는 먼저 ‘비대칭 암호화’(Asymmetric Cryptography), ‘전자서명’(Digital Signature)에 대하여 알아야 했다.

비대칭 암호화(= 공개키 암호화)

대칭키 암호화에서는 데이터를 암호화하거나 복호화하는데 사용되는 키가 동일하다. 대칭키 암호화의 단점은 키가 유출되면 데이터도 유출된다는 점이다. 이 때문에 혼자만 키를 간직하는 파일 압축, 윈도우의 볼륨 암호 기능인 비트락커(BitLocker) 등에서 사용된다. 만약, 두 사람이 “안전한” 암호 통신을 하고 싶다면? 이 경우에 비대칭 암호화가 활용된다. 비대칭 암호화에서는 암호화할 때 쓰는 키와 복호화할 때 쓰는 키가 다르다. 쌍을 이루는 두 키, A와 B가 있을 때, A키로 암호화하면 B키로 복호화되고, B키로 암호화하면 A키로 복호화된다. 본 포스트는 비대칭 암호화에 대한 수학적인 내용은 담지 않았다.

공개키 서명검증, 공개키 암복호

과연 비대칭 암호화는 어디에 쓸모 있는 걸까? 앞서 말했듯이 두 사람 간의 암호 통신에 사용될 수 있다. 이를 위해서 키 쌍 중 하나를 비밀키(private key), 다른 하나를 공개키(public key)로 지정한다. 비밀키는 절대 공개되어서는 안되며, 공개키는 공개되어도 무방하다. 아니, 오히려 공개되어야 의미가 있다. 키를 공개한다고? 이 특이한 특징 때문에 비대칭 암호화를 “공개키 암호화”라고도 부른다. 공개키 암호화로 두 가지 일을 할 수 있다. 바로 ‘공개키 서명검증’과 ‘공개키 암복호’가 바로 그것이다. 먼저, 공개키 서명은 다음의 절차로 이루어진다.

공개키 서명검증

  1. 철수 : 데이터 원문과 원문을 비밀키로 암호화한 암호문을 함께 송신.
  2. 영희 : 원문과 암호문를 수신하여 원문과 공개키로 복호화한 복호문을 비교.

영희는 복호문과 원문이 일치할 때 데이터를 보낸 사람이 철수임을 확신할 수 있다. 이 때 철수가 보낸 암호문은 공개키를 가지고 있는 누구라도 복호화 될 수 있다. 이 과정이 인감을 이용한 진본 확인, 신원 확인 절차와 동일하기 때문에 “서명”이라고 불린다.

그리고 공개키 암복호는 다음의 절차로 이루어진다.

공개키 암복호

  1. 영희 : 데이터 원문을 공개키로 암호화한 암호문을 송신.
  2. 철수 : 암호문을 수신하여 비밀키로 복호화.

앞의 공개키 서명 과정과 비교하면 철수는 비공개키를 이용하여 영희가 공개키로 암호화한 암호문을 복호화할 수 있다. 다만, 비밀키는 말그대로 철수만 갖고 있으므로 철수 외 그 누구도 암호문을 복호화할 수 없다. 즉, 두 사람은 서로의 공개키를 교환함으로써 서로의 신원을 확신하고, 서로의 공개키로 데이터를 암호화하여 보냄으로써 상호 간에 안전하게 데이터를 주고 받을 수 있다.

공개키 암호만을 이용한 전자 통신은 성능이 떨어진다고 알려져 있다. 개인적인 생각으로 다자간 암호 통신 시에는 송신할 데이터를 모든 수신자들의 공개키로 각각 암호화해야하고, 각 수신자들에게 개별적으로 송신해야 할 것 이므로 매우 불편할 것이다. 실제로 공개키 암호화는 대칭키를 공유하기 위한 용도로만 사용하며 그 대칭키를 이용하여 실제 통신이 이루어진다.

공개키 기반 구조(PKI : Public Key Infrastructure)

단순히 공개키 암호화만 알아서 암호 통신은 불가능하다. ‘공개키 기반 구조’는 공개키 암호화를 기반으로 한 암호 통신에 필요한 모든 요소들을 포함하는 용어이다. 여기에는 이후에 설명할 인증서(Certificate)와 인증기관(CA : Certificate Authority)에 대한 내용이 포함된다.

SSL(Secure Sockets Layer)

웹이 발전하면서 웹을 통한 안전한 통신이 요구되었다. 왜냐면 사용자가 보는 웹 페이지가 변조되었는지 여부를 판단해야 했기에. 이를 위해 일찍이 넷스케이프(Netscape)사는 PKI 구조에 따라 ‘SSL’을 개발하였다. 이후, 표준으로 제정되면서 TLS(Transport Layer Security)라는 이름으로 변경되었다. https는 이 SSL 위에서 동작하는 웹 프로토콜이다.

HTTPS의 전체적인 동작 과정

https 프로토콜의 동작은 다음과 같다. 앞서 공개키 서명에서는 송신자가 데이터의 원본과 원본을 비밀키로 암호화한 암호문을 보내면 수신자는 공개키로 암호문을 복호화한 복호문과 원문의 일치 여부로 송신자의 신원을 검증할 수 있다. 이 때, 암호문을 “송신자의 서명"이라고 한다. 이와 비슷하게 https 프로토콜에서 데이터의 원본에는 사용자가 접속하고자 하는 ‘서버의 도메인’과 ‘서버 공개키’가 들어있다. 데이터의 원본과 ‘서버 서명’을 묶어 ‘인증서’(certificate)라고 부른다.(실제로는 서버 서명이 아니라 CA 서명이 들어 있지만 설명을 위해서 가정헤보면) 어? 그런데 서버 공개키가 인증서에 들어간다? 앞서 살펴본 공개키 서명에서는 수신자가 송신자의 공개키를 이미 알고 있다고 가정한 부분이 깨진다. 이는 심각한 보안 상 결함으로 이어진다. 예를 들어 “https://www.google.com”에 접속한 사용자가 구글 서버로부터 구글 서버의 공개키를 포함한 인증서를 받았다. 당연히 인증서에 포함된 공개키로 해당 인증서는 검증이 될 것이다. 문제는 구글 서버가 진짜 구글 서버가 맞느냐는 것이다. 만약, 사용자가 주소를 잘못 쳐서 접속한 “googlee.com”이 마침 악의적인 의도를 가지고 “google.com"을 사칭하고 있는 상황이라면?

인증기관(CA : Certificate Authority)

결국, 구글 서버로부터 받은 것으로 추측되는 인증서가 신뢰가능한 지를 검증해야만 한다. 그렇지 않으면 우리는 미국 산호세에 있는 구글 본사에 직접 가서 구글 서버의 공개키를 받아야만 한다. 마찬가지로 네이버를 이용하려면 분당에 있는 네이버 본사에 직접 가서 공개키를 받아야만 한다. 하지만, 그럴 수 없으니 제3자를 통해서 인증서를 검증하는 부분이 PKI에 포함되어 있다.이 제3자를 ‘인증기관’(CA : Certificate Authority)이라고 한다. CA의 역할은 사용자가 접속한 서버의 인증서를 신뢰할 수 있는지를 대신 검증해주는 것이다.

인증서 체인(Certificate Chain)

다시, 사용자가 “https://www.google.com”에 접속하는 시나리오에서 사용자는 구글 서버로부터 인증서를 전달받을 것이다. 이 인증서에는 ‘서버 도메인’, ‘서버 공개키’ 뿐만 아니라 ‘CA 도메인’, 그리고 서버가 아닌 ‘CA 서명’이 들어있다. 이 인증서를 검증하려면 서명이 CA의 것이기 때문에 CA의 공개키가 필요하다. 이 때, 인증서의 서명이 검증되었다는 의미는 다음과 같이 연쇄 해석이 가능하다.

  • 인증서의 서명이 검증되었다
  • == 인증서는 CA에 의해 서명된 진본이다
  • == 인증서를 보낸 서버는 믿을 수 있다
  • == 인증서에 포함된 공개키는 서버의 것이 맞다

그렇다면 CA의 공개키는 도대체 어디에 있는가? 바로 운영체제와 크롬 브라우저가 미리 포함하고 있는 ‘CA 인증서’에 있다. 결국, CA 인증서로 서버 인증서를 검증하는 모양새이다. 이를 ‘인증서 체인’이라고 한다. CA 자체는 어찌할 수 없이 신뢰할 수 밖에 없다. 이 때문에 크랙된 운영체제나 웹 브라우저를 쓰는 등 믿을 수 없는 소스로부터 받은 소프트웨어를 사용하는 것은 보안 상 좋지 않다.

Self-signed SSL

위와 같은 복잡한 PKI 구조 때문에 https로 서비스되는 웹 사이트를 미리 테스트하는 것은 쉽지 않다. 테스트 하고자 하는 웹 사이트의 도메인과 서버 공개키를 CA에 보내어 서버 인증서를 발급받아야 하기 때문이다. 다시 한 번 정리하면 이 인증서에는 다음의 내용이 포함되어 있다.

  • 서버 도메인
  • 서버 공개키
  • CA 도메인
  • CA 서명

결국 웹 사이트 개발자 입장에서 겨우 테스트용 웹 사이트를 위해 CA로부터 인증서를 발급받아야 하는 상황이다.(어차피, 엄격한 CA는 테스트 용도의 웹 사이트를 위해 인증서를 발급해 줄 것 같지 않다)

만약, 내가 직접 CA의 역할을 할 수 있다면 얼마나 좋을까. 이미 Self Signed SSL이라는 키워드로 검색하면 많은 글들이 쏟아져 나온다. 그 중에서도 localhost에 대한 인증서를 스스로 직접 서명하는 과정을 소개하려고 한다.

1. (CA측) CA 비밀키 생성

CA(root CA)의 역할을 하기 위해서는 CA로서의 비밀키와 공개키를 생성해야 한다. 아래 명령어는 OpenSSL이라는 프로그램을 이용하여 대표적인 공개키 암호 알고리즘인 RSA(Rivest–Shamir–Adleman)로 길이가 2048 비트인 비밀키를 생성하여 rootCA.key에 저장한다. des3 옵션은 비밀키를 대칭키 알고리즘인 3DES(Triple Data Encryption Standard)로 한번 더 암호화하라는 의미이다. 비밀키는 절대 유출되어서는 안되기 때문에 한번 더 보호하기 위함이며, 아래 명령어를 입력하는 즉시 암호키를 요구할 것이다.

$ openssl genrsa -des3 -out rootCA.key 2048
// rootCA.key 생성됨

2. (CA측) CA 인증서 생성

아래 명령어를 통하여 CA용 비밀키와 쌍을 이루는 CA용 공개키를 생성하고, 그 공개키와 CA의 기본 정보에 대해 CA 비밀키로 서명한 CA 인증서를 생성한다. 이렇게 생성된 CA 인증서는 웹 브라우저 또는 운영체제에 신뢰할 수 있는 인증서로 등록할 것이다. 추후, 로컬 웹 서버로부터 받는 서버 인증서의 신뢰 여부는 이 CA 인증서를 이용하여 판단된다. x509 옵션은 X.509 포맷의 인증서를 생성하겠다는 의미이고, nodes(node의 복수형이 아니다…) 옵션은 CA용 공개키를 암호화하지 않겠다는 의미이다. sha256 옵션은 서명 생성에 SHA256 해시 알고리즘을 사용하겠다는 의미이다. 사실, 서명 생성 시에 데이터를 그대로 비밀키로 암호화하는 것이 아니라 데이터를 해시값으로 가공한 것을 비밀키로 암호화한다. days 옵션은 인증서의 유효기간을 설정하는 것이다. 이 명령어는 1,024일간 유효한 인증서를 생성한다. 결국 rootCA.crt라는 CA 인증서가 생성된다.

$ openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 1024 -out rootCA.crt
// rootCA.crt 생성됨

위 명령어를 치면 CA 공개키 생성 및 CA 서명 생성에 CA 개인키를 쓰기 위하여 암호를 물어볼 것이다. 이 암호는 앞서 CA 개인키를 DES3로 암호화할 때 쓴 대칭키를 입력하면 된다. 이어 CA로서의 기본적인 정보를 몇 가지 물어볼 것이다. 이를 테면 CA의 국적이라던지 이름, 도메인 등의 정보들이다.

3. (서버측) 서버 비밀키 생성(생략 가능)

이제 서버 비밀키를 생성할 차례이다. CA 비밀키를 생성했던 방법과 동일하게 아래처럼 입력하면 된다. des3 옵션이 없는데 des3 옵션은 기본 옵션이다. 일단, 서버 비밀키를 지금 생성하지 않고 다음 과정에서 생성할 것이다.

$ openssl genrsa -out server.key 2048 
// server.key 생성됨

4. (서버측) 서버 비밀키/공개키 및 CSR 생성

통상적으로 웹 개발자는 서버 도메인과 서버 공개키를 CA에 보내어 CA 개인키로 서명이 된 서버 인증서를 발급하여 달라는 요청을 하여야 한다. 이 요청을 CSR(Certificate Signing Request)이라고 한다. CSR 생성에 필요한 정보들은 별도의 파일에 작성하여 OpenSSL에 제공할 수 있다. 아래 server.csr.cnf를 보면 “[dn]” 항목에 서버의 정보를 적게 되어 있다. 이 때, CN 필드를 정확히 입력해야 한다. 아무리 CA의 서명을 받은 서버 인증서라도 사용자가 접속하려는 사이트의 도메인이 CN 필드에 기재된 도메인에 포함되지 않으면 브라우저는 접속을 차단한다. 차단되는 이유는 CSR 작성 시에 오타가 발생했거나, CA의 비밀키가 유출되어 엉뚱한 사이트에 대해 인증서가 잘못 발급된 경우를 의심해 볼 수 있다. 두 경우 모두 심각한 보안 사고로 이어진다. 디지노타라는 CA는 비밀키가 유출되어 파산에 이르렀다고 한다.

# server.csr.cnf (for creating csr)
[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn

[dn]
C=US
ST=RandomState
L=RandomCity
O=RandomOrganization
OU=RandomOrganizationUnit
emailAddress=hello@example.com
CN = localhost

아래 명령어는 위에서 작성한 server.csr.cnf에 기재된 서버 정보를 바탕으로 server.csr이라는 CSR을 생성한다. 이 때, newkey 옵션으로 생성한 서버 개인키도 활용한다. CSR 생성 시에 서버 공개키가 포함된다고 설명했는데, 사실, OpenSSL이 비밀키를 생성할 적에는 비밀키 뿐만 아니라 공개키도 이미 포함을 하고 있다. 이에 대해서는 아래에 추가로 설명한다. 어쨋든 이렇게 만들어진 CSR은 서버 비밀키로 서명된다. 결국, 서버 비밀키인 server.key와 CSR인 server.csr이 생성된다.

$ openssl req -new -sha256 -nodes -out server.csr \
    -newkey rsa:2048 -keyout server.key \
    -config <( cat server.csr.cnf )
 // server.key, server.csr 생성됨

5. (CA측) 서버 인증서 생성

CA 입장에서는 서버로부터 받은 CSR을 CA 개인키로 서명한 서버 인증서를 서버에 보내주어야 한다. 인증서 생성에는 확장 필드를 추가할 수 있다. 아래의 v3.ext 파일을 보면 인증서의 포맷인 X.509의 표준의 세 번째 확장에 대한 내용이 담겨있다. 이 확장에는 하나의 인증서로 여러 도메인을 신뢰하게 해 줄 수 있는 SAN(Subject Alternative Name) 필드 등이 있다.

# v3.ext(for creating X509 v3 certificate)
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names

[alt_names]
DNS.1 = localhost

아래 명령어는 앞서 생성한 CSR인 server.csr에 앞서 작성한 v3.ext에 담긴 X.509 확장 필드를 붙여 서버 인증서를 생성한다. 이 인증서는 CA 개인키인 rootCA.key로 서명된다. 결국, server.crt라는 서버 인증서가 만들어진다.

$ openssl x509 -req -in server.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial \
-out server.crt -days 500 -sha256 -extfile v3.ext
// server.crt 생성됨

+. 비밀키/공개키의 생성

위 과정을 따라하면서 궁금했던 점은 비밀키와 공개키는 한 쌍임에도 불구하고 명시적으로 공개키를 생성하는 과정이 없었다는 점이다. 비밀키에 공개키가 포함되어 있는 것이 아닌가 의심 되었지만 OpenSSL의 genrsa 명령어는 비밀키를 생성한다고만 되어있었다. 실제로 genrsa로 생성되는 것은 비밀키와 공개키 생성에 필요한 여러가지 팩터들이고, 이들을 이용하여 필요할 때마다 빠르게 비밀키와 공개키를 생성한다고 한다.

https://stackoverflow.com/a/44350448