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 연결을 잡은 것이다.
아래는 Wireshark로 DTLS 연결을 잡은 것이다.