Erlang으로 SSL 연결 만들어보기

SSL/TLS에 대해서는 그 배경 지식에 대해서 전문적으로 알고 있는 입장이 아니라서, 여기서는 간단하게 지난 번에 만들었던 cowboy 기반 웹소켓 서버 소스에 wss (secured websocket) 관련 코드를 추가하였다. 그리고 DTLS 연결 테스트도 해봤는데, Erlang/OTP 소스의 ssl 모듈에 있는 예제에 한 줄 추가하여 만들었다.

wss 연결 만들기

한 번 기본 웹소켓 서버 코드를 작성했다면 여기에 SSL 관련 코드를 추가하는 건 어렵지 않다. 먼저 myapp.app.src 파일에서 시작 어플리케이션에 ssl을 추가한다.

   {applications,
    [kernel,
     stdlib,
-    cowboy
+    cowboy,
+    ssl
    ]},
   {env,[]},
   {modules, []},

그 후 myapp_app.erl에서 ws가 아닌 wss로 서버가 실행되도록 한다. 추가한 작업은 옵션에 인증서 파일 위치를 추가한 것과 cowboy:start_clear/4 대신 cowboy:start_tls/4를 사용한 것 뿐이다. 인증서는 self-signed 사설 인증서로, 직접 생성한 것은 아니고 cowboy 소스의 examples/ssl_hello_world/ 에 있는 인증서를 그대로 사용했다. 다음 위치에서 바로 받을 수 있으며, 프로젝트 디렉토리에 priv/ 디렉토리를 만들고 다시 ssl/ 하위 디렉토리를 만들어서 여기에 넣어주었다.

priv/ 디렉토리는 리소스 파일 등 어플리케이션 실행에 필요한 데이터 파일들을 담아두는 곳이다. code:priv_dir/1은 이 디렉토리의 절대 경로 정보를 반환하는 함수이다. 빌드 후에는 빌드 결과물이 있는 위치로 리소스 파일들이 따라서 복사되기 때문에 사전에 정확한 경로를 알 수 없기 때문이다.

     ]}],
   Dispatch = cowboy_router:compile(Routes),
   NAcceptors = 100,
-  TransOpts = [{ip, {0, 0, 0, 0}}, {port, 8080}],
+  PrivDir = code:priv_dir(myapp),
+   = [{ip, {0, 0, 0, 0}}, {port, 8080}
+               , {cacertfile, PrivDir ++ "/ssl/cowboy-ca.crt"}
+               , {certfile, PrivDir ++ "/ssl/server.crt"}
+               , {keyfile, PrivDir ++ "/ssl/server.key"}
+              ],
   ProtoOpts = #{env => #{dispatch => Dispatch}},
-  {ok, _} = cowboy:start_clear(http, NAcceptors, TransOpts, ProtoOpts),
+  {ok, _} = cowboy:start_tls(https, NAcceptors, TransOpts, ProtoOpts),
   wssvr_sup:start_link().

클라이언트 측도 수정한다. ws_client.js 내의 웹소켓 url을 'ws'에서 'wss'로 바꾸는 작업 뿐이다.

 var WebSocket = require('ws');
-var ws = new WebSocket('ws://localhost:8080/');
+var ws = new WebSocket('wss://localhost:8080/');
 
 ws.on('open', function open() {

하지만 곧바로 node로 실행하면 상대 서버가 self-signed 인증서를 사용했다는 메시지와 함께 오류 처리해버린다.

$ node ws_client.js 
error: Error: self signed certificate in certificate chain

디폴트로 self-signed 인증서 사용을 막았기 때문이다. 이 경우 NODE_TLS_REJECT_UNAUTHORIZED 환경 변수와 함께 사용하면 된다.

$ NODE_TLS_REJECT_UNAUTHORIZED=0 node ws_client.js

요즘은 letsencrypt를 이용한 인증서를 많이 사용하는 추세이다. 도메인만 갖고 있으면 무료로 만들 수 있어서 편하다. 여기서는 letsencrypt를 사용하는 방법은 건너뛴다. letsencrypt로 만든 인증서는 보통 /etc/letsencrypt/live/(domain-name)/ 에 있는 것으로 사용할 수 있다. 이 디렉토리를 priv/ 디렉토리 밑에 cert/ 란 이름으로 symbolic link를 만들어 연결했다 (이름은 다른 것으로 바꿔도 된다). 그리고 위의 myapp_app.erl 수정 내용 중 TransOpts 변수의 인증서 설정만 다음과 같이 self-signed 사설 인증서에서 letsencrypt 인증서로 바꿨다.

   TransOpts = [{ip, {0, 0, 0, 0}}, {port, 8080}
-               , {cacertfile, PrivDir ++ "/ssl/cowboy-ca.crt"}
-               , {certfile, PrivDir ++ "/ssl/server.crt"}
-               , {keyfile, PrivDir ++ "/ssl/server.key"}
+               , {cacertfile, PrivDir ++ "/cert/chain.pem"}
+               , {certfile, PrivDir ++ "/cert/cert.pem"}
+               , {keyfile, PrivDir ++ "/cert/privkey.pem"}

이 때 웹소켓 클라이언트에서 NODE_TLS_REJECT_UNAUTHORIZED 환경 변수는 필요 없다.

DTLS 연결 만들기

SSL 연결 관련한 코딩에는 경험이 없기도 했고, DTLS에 관해서는 문서화된 내용도 없기 때문에 (OTP 20부터 포함될 것이라고 한다) Erlang/OTP 소스의 ssl 모듈에 있는 client_server.erl 예제를 이용해서 테스트를 했다. 이 예제는 다음과 같은 간단한 구성으로 loopback 인터페이스 상으로 TLS 연결을 하도록 되어 있다.

     start/0
     +-------+
     |       |
     |       |
     +---+---+         init_connect/1
         |    (spawn)  +-------+
         +------------>+       |
         |             |       |
         |             +---+---+
         |                 |
         |                 |
         v    (connect)    v
ssl:listen/2 <========== ssl:connect/3

기존의 TLS 연결 예제를 아래와 같이 수정하여 DTLS로 연결하도록 했다. mk_opts/1 함수는 서버 측에서 호출하는 ssl:listen/2 및 클라이언트 측 ssl:connect/3 양쪽 함수에 파라미터로 전달될 옵션을 생성하는 함수이다. 이 함수를 통해서 {protocol, dtls} 옵션을 더 추가하여 전달하게 된다.

 mk_opts(Role) ->
     Dir = filename:join([code:lib_dir(ssl), "examples", "certs", "etc"]),
     [{active, false},
      {verify, 2},
      {depth, 2},
+     {protocol, dtls},
      {cacertfile, filename:join([Dir, Role, "cacerts.pem"])},
      {certfile, filename:join([Dir, Role, "cert.pem"])},
      {keyfile, filename:join([Dir, Role, "key.pem"])}].

사실, 테스트 해보면 TLS로 연결될 때나 DTLS로 연결될 때나 터미널 상에 찍히는 로그 메시지로는 그 차이를 알기 어렵다. 이에 관해서는 tcpdump나 Wireshark 통해서 찍어보는 것이 제일 좋다. 다음은 Wireshark로 TLS 연결을 잡은 것이다.

TLS 캡쳐

아래는 Wireshark로 DTLS 연결을 잡은 것이다.

DTLS 캡쳐