2015 년, 내 프로그래밍 언어는 …

2014 년, 내 프로그래밍 언어는 … 에 이어서.

작년 한 해 동안 주로 사용한 언어는,

  • C++ (제한적인 C++1x,y,z)
  • Python
  • …과 거의 비등한 양의 shell script (거의 bash 지만)

회사에서 만들고 있는 게임 서버 엔진 (+ 이걸로 만드는 게임 서버 혹은 게임 서버 개발 지원) 으로 거의 C++ 만 쓰고 지낸 듯 하다. 그래도 엔진 자체만 빼고는 C++11 이후를 써도 되서 꽤 편하게 짠 듯.

Python은 대략 이전에도 썼던 log 분석, 웹 서비스 stub 등에서 많이 썼고, bash 는 패키징 (.deb, .rpm …) 할 때 주로 썼으니..

올해도 아마 C++을 제일 많이 쓰는 한 해가 될 것 같구만.

프로그래머의 일상: 어떤 디스크 풀

커널 보안 취약점 때문에 회사에서 쓰는 VM들을 패치하고 있는데, 디스크 풀이 뜬다.

?!?!?!?!

디스크 풀은 다른 방식으로 감지하고 있어서 대체 뭘까 하고 고민하다가, 같이 일하는 수원님이 “inode 풀 아니냐” 라고 하시기에,

$ df -i 
Filesystem Inodes IUsed IFree IUse% Mounted on 
/dev/xvda1 524288 517380 6908 99% / 
none 127041 2 127039 1% /sys/fs/cgroup 
udev 125746 388 125358 1% /dev 
tmpfs 127041 318 126723 1% /run 
none 127041 1 127040 1% /run/lock 
none 127041 1 127040 1% /run/shm 
none 127041 2 127039 1% /run/user

그것이 실제로 일어났습니다?

뭐가 이렇게 inode를 다 쓰고 있나 했더니 신경 안 썼던 리눅스 헤더 패키지 (linux-headers-{version}, linux-headers-{version}-generic) 가 inode를 다 쓰고 있더란;

$ find /usr/src/linux-headers-3.13.0-76-generic/ /usr/src/linux-headers-3.13.0-76 | wc -l
24813

버전마다 25k 정도 쓰고 있는데, 총 inode가 520k; 근데 헤더 패키지가 20개 깔려있더라.
dpkg -P 로 몇 개 헤더 패키지를 지우고 나니 설치가 정상적으로 설치가 된다.

세 줄 요약:

  • 디스크 풀은 디스크 용량이 아니라 inode가 모자라서 날 수도 있다.
  • df -h 말고 df -i 도 보자.
  • 리눅스 헤더 패키지 싸우자 (…주기적으로 obosolete 패키지를 제거합시다 (…))

Python 으로 JSON 빨리 처리하기

지난 며칠 동안 모 모바일 게임의 알파테스트가 있었다. 그리고 갑이 뽑아달라는 ad-hoc 통계를 뽑느라 로그 처리를 왕창 할 일이 있었다.

대략,

  • 로그 데이터는 하나의 JSON 문서로, 각 파일에 한 줄씩 차지하고 있다
  • 파일은 날짜 별로 쪼개져서 서버 로그 디렉터리에 남아있다

내부 테스트나 첫날 정도까지는 로그 처리가 순식간에 끝날 수준이라 (k초 수준; k < 10) 별로 신경 안 썼는데, 막상 데이터가 커지기 시작하니 오래 걸리기 시작.
(오래 걸린다 == 실행하고 결과 나올 때까지 걸리는 시간이 내 집중력이 유지되는 시간보다 길다)
그리고 ad-hoc 통계를 뽑으려다 보니 내가 실행하는 횟수가 계속 늘어서 걸리는 시간 줄이려고 삽질한 기록이 아래와 같다.

진행

대략 로그가 1 GiB 정도 쌓인 시점에서, 로그 처리를 완료하고 통계 데이터가 나올 때까지 55초 정도 걸리더라. (i7-2600 / 메모리 16 GiB / 샘숭 840pro)
일단 아래 두 가지로 급한 불은 껐고, 알파테스트가 끝나갈 때야 왜 빨라졌는지 알게 되었다.

일단 PyPy 를 끼얹어 본다.

메모리야 남아돌고, CPU가 100%로 튀는 상황이니 일단 PyPy 를 가져다가 써봤다.
Ubuntu 14.04 에는 PyPy 2.2.1 이 있다. 이걸 설치하고 동작하니 대략 33초까지 줄더라. (40% 향상)

최신 버전 릴리즈되는 PPA가 없나 검색해보니, 있더라: https://launchpad.net/~pypy.
여기서 PyPy 4.0.1을 가져다 쓰면 28초까지 실행 시간이 줄어들더라. (49% 향상)

mmap 도 끼얹어 봅니다.

이전에 하재승 군이 mmap 언급했던 게 생각나서 적용해보았다.
조금 더 빨라진다. 이젠 23초 수준.

여하튼 이 상태로 알파테스트 종료까지 갔더니 대략 로그 2GiB 처리하는데 53~55초 수준. 아마 처음 상태로 끝까지 갔으면 (끝나는 날 오후에도 갑이 통계 추가해달랬으니..) 내 멘탈은 더욱더 남아나지 않았을 듯 (…).

왜 빨라졌는가

왜 빨라졌는지 파악 하지 못하고 있었으니 측정해보자.
vmprof 프로파일러 를 붙여서 돌려봤다. (2:20 소요)

Python 2.7 실행 결과

loads 밑 그 하위 함수들 (decode, raw_decode) 들에서 잔뜩 시간을 쓰고 있다. 즉, 대부분의 시간을 JSON 디코딩하는데 쓰고 있다는 소리 (json.loads 함수라서)

PyPy 로 구동한건 다음과 같다. 0:55 소요.

PyPy 4.0.1 실행 결과

작은 루프를 계속해서 돌기 때문인지 대부분의 구간이 JIT 번역된 코드를 실행하고 있다. 여기서도 json.loads 호출의 비율이 크지만 Python 2.7 보다는 그 비율이 낮다.
왜 JSON 성능이 PyPy 쪽이 좋은지 확인해보니 PyPy 2.2.0 릴리즈 노트에 그런 내용이 있더란.

JSON decoding is now very fast (JSON encoding was already very fast)

근데 JSON 성능은 (보통은?) 외부 C library 에 의존해서 성능을 올린 경우가 많다. 그래서 전통의(?) simplejson 과 ujson 을 붙여서 테스트해봤다.
simplejson 으론 성능 향상이 없더라. (PyPy 의 경우 오히려 느려졌다)
C기반 JSON 구현체의 python 바인딩인 ujson 을 시도해봤다.

Python 2.7 + ujson 실행 결과

… ujson C 코드 디버깅 심볼이 없어서인지 제대로 프로파일링 안된 것 같긴한데, 같은 Python 2.7이면서도 시간은 대폭 줄었다. 1:19. (거의 절반)
그래도 통계 데이터를 실제로 해석하는 작은 루프들 성능이 JIT 쪽이 나은지 전체 시간은 여전히 PyPy 쪽이 더 좋다.

요약

  • vmprof 좋아요 씁시다.
  • Python 표준 라이브러리의 JSON은 너무 느리다. CPython을 써야한다면 ujson, 그게 아니면 PyPy 쓰자.

구글 두들에 나온 Hedy Lamarr 글을 읽고

며칠 전 구글 두들 (2015-11-09) 로 Hedy Lamarrs 라는 배우, 발명가인 여성에 대한 글을 읽었다. 트위터에 올라오는 것들 보다가 예전에 전공 과목 배울 때 내용이랑 섞여있는 게 있길레 간단히 정리.

흔히 약어로 쓰는 SS (spread spectrum), DS (direct sequence), FH (frequency hopping) 같은 걸 쓰고 있으니 참고해서 읽어주시길.

Hedy Lamarrs 와 FH

위키백과 항목에 따르면

Lamarr and Antheil realized that radio-controlled torpedoes, while important in the naval war, could easily be jammed, which caused the torpedo to go off course. With the knowledge she had gained about torpedoes from her first husband, and using a method similar to the way piano rolls work, they designed a frequency-hopping system that would continually change the radio signals sent to the torpedo.

즉, Lamarr 와 공동 발명자가 고안해 낸 것은 일종의 FHSS 시스템. Spread spectrum 방식 자체가 a. 전체 대역을 다 관찰하지 않고는 노이즈 플로어만 상승시키는 걸로 보이고 b. 특정 (좁은) 대역의 재밍으로는 통신을 재밍하기 어려운 장점 떄문에 풀려던 문제 (어뢰의 유도 신호를 재밍당하지 않게 하는 것) 에 적합하다는 것.

Wireless LAN, …

현재 사용하고 있는 무선랜 표준 (WLAN; 802.11) 과 발명 사이에 관계 있다는 설명글들이 쭉 올라오는데, 아주 거칠게 말하면 큰 관련은 없다 — 작게는 있다(?). 무선랜 대역인 2.4 GHz (다른 대역도 있지만) 에는 전자레인지가 동작 중인 경우 엄청난 양의 노이즈가 생기기 때문에 이를 피하기 위한 방법을 사용한다.

링크한 글에서도 얘기하지만 대부분의 사람이 경험해보지 못한 legacy 802.11 표준을 제외하곤 FHSS는 사용하지 않는다. (대부분의 경우 802.11b, g, n, ac 순서로 경험했으리라)
간단히 정리하자면

  • Legacy 802.11: DSSS or FHSS
  • 802.11a: OFDM
  • 802.11b: DSSS
  • 802.11g, ac: OFDM

그래서 (현재의) 무선랜 기술과 크게 관련있는 건 아니라고 적었던 것.

2G/3G 무선 통신

한국에서 사용하는 2G 무선 기술이 IS-95 인데 (혹은 그 이후의 IS-2000; 3G), 이 기술은 CDMA 를 써서 여러 사용자의 통신을 지원한다. 그리고 이 CMDA가 사용하는 방식이 DSSS.

GPS?

이건 왜지. P코드 때문이라고 하기엔 너무 먼데 (…….)

Bluetooth

이게 왜 안나왔는지는 정말 모르겠지만. Bluetooth 통신은 FHSS를 쓴다. 차라리 직접적으로 연관관계가 있는 이쪽을 위주로 정리해줬어도 좋았을지도.

음 그렇지만 FHSS도 spread-spectrum 자체가 맞기도하고, 넓은 대역에 pseudo-random number sequence를 써서 특정 형태의 재밍을 피할 수 있는 무선 통신 방식…을 생각하면 뭐 죄다 적용될 수는 있을지도?

nginx + ngx_pagespeed

꽤나 예전에 동아리 서버에는 적용해봤는데 — 동아리 서버는 apache 웹서버를 써서 구글에서 바이너리로 배포하는 mod_pagespeed 를 썼지만 — 이 블로그가 쓰는 웹 서버는 nginx를 써서 적용 못하고 있었다.
Nginx 는 다 좋은데 동적으로 로드 가능한 모듈을 지원하질 않아서 소스에 넣고 빌드해줘야하는 것. 개인 웹 서버라 개발툴 없이 돌리는 상태를 유지하고 싶어서 회사 개발머신을 빌려 데비안 패키지만 만들고 옮겨서 설치하는 것으로. 아래는 그 기록.

빌드 기록

빌드 과정은 소스에서 ngx_pagespeed 빌드하기 를 따라하되 nginx 소스코드와 빌드 자체는 ubuntu 14.04의 debian 빌드 스크립트를 이용했다.

소스 코드 얻기

nginx 소스 패키지와 ngx_pagespeed 소스 타르볼, 그리고 PSOL 타르볼을 얻는다:

# nginx (1.4.6) 소스코드 + 빌드스크립트
$ apt-get source nginx

# ngx_pagespeed 릴리즈 아카이브
$ wget https://github.com/pagespeed/ngx_pagespeed/archive/release-1.9.32.10-beta.zip

# PSOL
$ wget https://dl.google.com/dl/page-speed/psol/1.9.32.10.tar.gz

# 압축 해제 및 필요한 구조로 변경
$ tar -zxf 1.9.32.10.tar.gz
$ unzip release-1.9.32.10-beta.zip

# PSOL 을 ngx_pagespeed 밑으로
$ mv psol/ ngx_pagespeed-release-1.9.32.10-beta/

# ngx_pagespeed 를 모듈 디렉터리로 이전
$ mv ngx_pagespeed-release-1.9.32.10-beta/ nginx-1.4.6/debian/modules/

nginx 빌드룰 수정

데비안 패키지의 빌드룰은 “{패키지-버전}/debian/rules” 에 있다. 여기선 nginx-1.4.6/debian/rules 를 열어 nginx-full 에 해당하는 config.status.full 항의 맨 뒤에 “--add-module=$(MODULESDIR)/ngx_pagespeed-release-1.9.32.10-beta \” 를 추가해서 아래와 같이 수정하다:

config.status.full: config.env.full
  cd $(BUILDDIR_full) && ./configure  \
      $(common_configure_flags) \
      --with-http_addition_module \
      --with-http_dav_module \
      --with-http_geoip_module \
      --with-http_gzip_static_module \
      --with-http_image_filter_module \
      --with-http_spdy_module \
      --with-http_sub_module \
      --with-http_xslt_module \
      --with-mail \
      --with-mail_ssl_module \
      --add-module=$(MODULESDIR)/nginx-auth-pam \
      --add-module=$(MODULESDIR)/nginx-dav-ext-module \
      --add-module=$(MODULESDIR)/nginx-echo \
      --add-module=$(MODULESDIR)/nginx-upstream-fair \
      --add-module=$(MODULESDIR)/ngx_http_substitutions_filter_module \
      --add-module=$(MODULESDIR)/ngx_pagespeed-release-1.9.32.10-beta \
            >$@

데비안 패키지 생성

$ cd nginx-1.4.6/
$ debuild -uc -us -rfakeroot -b
$ ls -1 ../*.deb
../nginx_1.4.6-1ubuntu3.3_all.deb
../nginx-common_1.4.6-1ubuntu3.3_all.deb
../nginx-core_1.4.6-1ubuntu3.3_amd64.deb
../nginx-core-dbg_1.4.6-1ubuntu3.3_amd64.deb
../nginx-doc_1.4.6-1ubuntu3.3_all.deb
../nginx-extras_1.4.6-1ubuntu3.3_amd64.deb
../nginx-extras-dbg_1.4.6-1ubuntu3.3_amd64.deb
../nginx-full_1.4.6-1ubuntu3.3_amd64.deb
../nginx-full-dbg_1.4.6-1ubuntu3.3_amd64.deb
../nginx-light_1.4.6-1ubuntu3.3_amd64.deb
../nginx-light-dbg_1.4.6-1ubuntu3.3_amd64.deb
../nginx-naxsi_1.4.6-1ubuntu3.3_amd64.deb
../nginx-naxsi-dbg_1.4.6-1ubuntu3.3_amd64.deb
../nginx-naxsi-ui_1.4.6-1ubuntu3.3_all.deb

…적당히 기다리고 나면 탑 디렉터리에 .deb 파일 수 개가 생긴다. 위에서 빌드 룰 수정한 nginx-full 패키지를 가져가서 서버에 설치.[1] 빌드 시간을 줄이고 싶다면 debian/control 을 수정하는 것도 (다음 번엔) 고려해야겠다.

설정하기

ngx_pagespeed : Nginx의 PageSpeed 모듈 :: Outsider’s Dev Story 를 참조해서 설정했다. 기본 값 + JavaScript 비동기 로딩 정도 설정함.

총평

Google PageSpeed Insight 점수가 15점 쯤 올랐다 (…). 행복하군 (야)
대신 앞으론 ubuntu 쪽 패키지 업데이트 / Google 쪽 릴리즈 마다 새로 빌드해야한다 (이쪽은 전혀 달갑지 않다…)

PS. (아마도) GPL인 debian/rules 파일을 일부 인용했는데, 이 글 라이센스를 CC-BY 대신 GFDL로 해야하나? 아니면 공정 사용이니 그냥 CC-BY 해도되나?(…)

  1. 빌드할 때 좀 괴롭더라. -j8 주니 빌드 실패한다. 빌드 스크립트에 무슨 삽질을 해놓은걸까? []

TLS: SHA1 해시의 종말이 다가오고 있다

ars technica: SHA1 암호학적 해시가 연말 전에 (쉽게) 무력화된다 라는 글이 올라왔다. (2015-10-08)

저 글에서도 나오고, 이 블로그의 이전 글에서 링크한 Bruce Schneier의 2012년 예측 에서는 2018년이면 조직범죄자들 정도의 재정이면 충분히 깰 수 있는 수준이 될거고 예측했었다. 하지만 새로 나온 논문에 따르면 이미 현재 기준으로 $75k ~ $125k 정도면 충돌을 찾을 수 있다 라는 것. (이전 예측치는 2018년에 $175k 정도)

즉, 지금이라도 어느 정도의 재정 규모가 되는 정부건 정부 단체건, 혹은 조직범죄자들이건 이걸 깨려는 시도를 할 수 있는 수준으로 계산 비용이 내려와 있다는 얘기. 다행히도 언급된 논문에서 찾은건 chosen prefix attack 은 아니어서, 당장 인증서를 위조해내는 정도는 아님. 하지만 여태까지 그래왔고, 앞으로도 그렇겠지만 공격은 항상 향상되지 악화될리가 없다. 그러니 근미래에 뚫린 상황 — $100k 수준에서 chose-prefix attack이 가능해지는 상황 — 에 진입할지도 모른다. (Google 은 2017년 1월부터 SHA1 해시를 쓴 인증서를 안전하지 않은 것으로 간주하기로 했다(Chrome 41이후) )

여하튼 해야할 일:

  • 사용 중인 인증서가 SHA-1 해시로 서명되어 있는가? (직접 쓰는 인증서, intermediate 인증서 모두) 그러면 바꿀 것. 아마 이미 발급된 인증서에 대해서 무료(?) 재서명을 해줄지도 모름
  • SHA-1 해시로 서명된 사이트에 접속하고 그 사이트와 안전하게 통신하는게 중요하다면 그 사이트 사용을 멈추거나 (예를 들어 국민은행?) 바꾸라고 요구해야 한다
  • 조만간 주요 웹 브라우저에서 SHA-1 해시를 이용해서 서명한 인증서들을 안전하지 않다 라고 표시하는 업데이트가 나올테니 일찍일찍 설치해둘 것

정도를 해야.

아 그리고 SHA-1 해시를 root 인증서 에 쓴 건 상관없다. 아침에 일어났다가 불현듯 이게 떠올라서 — 이 블로그가 쓰는 인증서의 Root CA는 SHA-1 서명; 구글도 같은 Root CA 씀(…) — 검색해보니 Gradually sunsetting SHA-1 이란 글 발견. 해당 부분을 발췌하면,

Note: SHA-1-based signatures for trusted root certificates are not a problem because TLS clients trust them by their identity, rather than by the signature of their hash.

즉, root 인증서의 경우 공개키(pub-key)를 가지고 있는거고, (자가) 서명한 signature 자체는 검증할 일이 없기 때문에 상관없다는 것. (대조할 대상 이 아예 없으니까)

C++11: scoped_lock + “짜증나는 파싱”

boost::thread 에는 scoped_lock 이라는 블럭 스코프 동안 유효한 락이 있다. 멀티스레드 프로그래밍 하는 사람들은 유용하게 쓰고 있으리라 생각한다.
이 클래스를 엉터리로 쓰고 있는 다음 코드를 보자. main 에서 미리 mutex 에 락을 걸고 들어가기 때문에, scoped_lock 이 락을 걸지 못해서 데드락에 빠질 것처럼 보인다.

#include <iostream>

#include <boost/thread/mutex.hpp>


struct foo {
  boost::mutex mutex_;

  void Run() {
    boost::mutex::scoped_lock(mutex_);
    std::cout << "Should not be reached" << std::endl;
  }
};


int main (int argc, char** argv) {
  foo bar;
  bar.mutex_.lock();  // 미리 mutex 를 획득; 데드락을 의도함
  bar.Run();
  bar.mutex_.unlock();
  return 0;
}

하지만 그렇지 않다.

“Should not be reached” 를 출력하고 프로그램은 정상 종료한다. 왜 그런가 설명하자면, C++의 가장 짜증나는 파싱 규칙 (C++’s (most) vexing parse) 때문.

C++의 가장 짜증나는 파싱 (C++ ‘s most vexing parse) 은 아주 거칠게 말하자면, 선언으로 해석할 수 있는건 선언으로 해석한다 란 의미다. 그래서 boost::mutex::scoped_lock(mutex_)mutex_ 란 이름의 scoped_lock선언 한다. mutex_ 에 대한 scoped_lock 을 초기화하는게 아니라.
문법적으론 둘 다 가능해 보이지만, 실제로는 선언으로 우선 해석하기에 foo::Run() 은 락을 획득하지 않는다.

대충 이런 느낌의 코드가 지난 주에 회사 코드베이스에서 나와서 “나는 리뷰 때 무얼했는가”라면서 괴로워함 ㅠㅠ

C++11 이후에는 이 문제가 아주 간단히 풀린다. “Effective Modern C++”의 Item 7에서 나오는 것처럼, 괄호가 아니라 중괄호를 쓰면 이런 문제가 없다. 변수 선언인지 초기화인지를 의도에 따라 적당한 문법을 쓸 수 있다. 다음과 같이 바꾸면 의도한 대로 데드락에 빠진다.

struct foo {
  boost::mutex mutex_;

  void Run() {
    boost::mutex::scoped_lock {mutex_};  // 초기화 목록으로 변경; 하지만 정상적인 동작은 아니다
    std::cout << "Should not be reached" << std::endl;
  }
};

그런 의미에서 C++11 이후를 씁시다! (…)

Updated : 아래 dkim 님이 코멘트한대로 이건 의도한 동작이 되는 것은 아니다.
C++에서 이름 없는 변수의 생명 주기는 expression 이 끝나는 순간 소멸한다. 그래서 C++11의 초기화 문법을 써봐야 데드락이 걸리긴하지만, (원래 의도일) 스코프 락을 제대로 획득한 상황은 아니다.

C++14: lambda 함수의 캡처 목록

C++11 lambda 함수에 값을 전달하는 방법은 복사/참조 두 가지 뿐이라서 expression 을 전달하진 못한다. lambda를 아주 거칠게 묘사하면 특정 인자를 받는 functor struct를 자동으로 생성하고 이거의 타입 추론을 자동으로 해주는 정도다. 그러니 생성자에 해당하는 lambda 캡처 목록에 “생성자에 해당하는게 있는데 왜 expression은 생성자에 못 넘기고, 변수 혹은 변수의 참조만 넘길까” 라고 생각하는건 매우 타당한 의문인 것.

예를 들어 C++11 에서 매우 거대한 std::unordered_map<k , v> 를 캡처한다고 치면 정말 복사를 해야 한다. 혹은 std::shared_ptr< ...> 를 써서 넘기거나, 아니면 “Effective Modern C++” 의 item 32에서 다루는 std::bind 트릭을 써야 한다.

unordered_map<int, string> m = large_object_factory();
// 타이머 이벤트를 건다. 수 초 후에 m 가지고 계산 수행
SetTimer(10000, [m]() {
   /* m 가지고 뭔가 수행 */
});

위처럼하면 m 을 통채로 복사해야해서 별로 효율적이지 않다. std::shared_ptr 를 쓰면,

auto pm = make_shared<unordered_map<int, string>>( \
    large_object_factory());
// 타이머 이벤트를 건다. 수 초 후에 m 가지고 계산 수행
SetTimer(10000, [pm]() {
   /* pm 가지고 뭔가 수행 */
});

shared_ptr 생성/복사 오버헤드 정도로 처리할 수 있다. 위 코드가 동작은 하지만, 간결하진 않다. C++03 에서 C++1x 로 넘어올 때의 간결해진 코드 베이스를 생각하면 정말 맘에 안든다.

그래서 캡처 문법을 일반화해서 C++14 에는 lambda 구문에 capture init-list 라는 개념이 생겼다. 해당 드래프트에서는 generalized lambda capture 라고 부르는데, up-value를 캡처하는게 변수 이름만으로 하는게 아니라 일반적인 expression 을 다룰 수 있게 되었기 때문. expression 이 되니까 std::move 를 써서 값을 옮겨버리는 것도 가능하다.

즉, 다음과 같은 코드가 가능해진다. 잘 보면 생성자처럼도 보인다. (?)

unordered_map<int, string> m = large_object_factory();
// 타이머 이벤트를 건다. 수 초 후에 m 가지고 계산 수행
SetTimer(10000, [_m = std::move(m)]() {
   /* _m 가지고 뭔가 수행 */
});

즉, C++11 에서는 혼용할 수 없는 lambda 캡처 목록에서 expression 사용이 가능해진 것 (즉, rvalue 참조를 써서 효율적으로 복사하는게 가능해진 것).

TLS cipher suite 읽는 법

작년부터 시작해서 보안 버그가 일종의 PR 결전장이 된 것 같은데 — 매우 핫한 이름을 붙여가면서 — 그런 류의 글에 나오는 TLS/SSL cipher suite 읽는 법을 간략히 정리.

TLS (혹은 옛이름(?)인 SSL) cipher suite은 아래와 같은 형태로 쓴다:

  • 이전 글의 국민은행이 사용하던 RSA_WITH_AES_256_CBC_SHA 라거나,
  • 현재 위키백과 설정인 ECDHE_ECDSA_WITH_AES_128_GCM,
  • 이 블로그의 현재 설정 값인 ECDHE_RSA_WITH_AES_128_GCM 이라거나.

이 각각의 기능 구분은 아래와 같다

  • 키 교환: 밑의 벌크 암호화 알고리즘에서 사용할 키를 교환할 방법
  • 인증: 상대방이 정말로 스스로가 생각하는 상대방이 맞는지 확인할 방법
  • 벌크 암호화: 대칭키를 써서 실제로 메시지를 암호화 하는 방법
  • 메시지 인증: 이 메시지가 정말로 상대방이 보낸게 맞는지 확인할 방법

이렇게 4 부분으로 쪼개진다.

키 교환과 인증

그 중 위에서 WITH 보다 앞에 있는 부분이 다음 두 가지 기능을 담당한다.

키 교환

실제로 주고 받을 메시지를 암호화 할 때 대칭키 암호화 방식을 쓰게 되는데, 여기에서 사용할 공유 비밀 정보 (shared secret) 을 생성해야 한다. 이 정보를 생성하기 위한 방식이 몇 가지 있는데, 대략, RSA, Diffie-Hellman (이하 DH), 그리고 elliptic-curve DH (ECDH) 알고리즘을 이용한다. (그리고 키 교환할 때마다 새 키를 생성하게하는 ephemeral DH (EDH), EECDH 도 있다)

인증

키를 교환한 상대방이 정말로 자기가 생각하는 상대방이 맞는지 확인하기 위해서 흔히 말하는 인증서 시스템을 이용한다. 이건 현재 RSA 혹은 DSA, ECDSA 같은 알고리즘을 이용한다. 다만 서버에선 이 값을 바로 바꿀 수 있는건 아니다. 인증서를 만들 때 인증서 서명 요청(CSR)을 작성하는데, 이 때 선택한 키 알고리즘을 서버 인증서를 이용한 인증할 때 쓴다.

즉, 국민은행 인증서는 RSA 알고리즘의 공개키를 CSR에 넣어서 제출했었기 때문에, 키 교환 알고리즘은 다음과 같은게 된다:

  • RSA: 키 교환 / 인증 모두 처리
  • DH-RSA: 키 교환은 Diffie-Hellman, 인증은 RSA
  • EECDH-RSA: …
  • … (…)

암호화 및 암호화된 메시지 인증처리

실제 암호화와 암호화된 메시지의 무결성을 검사하는게 WITH 뒷쪽 부분이다.

암호화

키를 교환해서 생성한 shared-secret으로 키를 샏성해서 암호화를 진행하는데, 암호화 자체는 (상대적으로 키교환보다 훨씬 CPU 오버헤드가 적은) 대칭키 암호화 방식인 AES (128/256), 3DES (…), RC4 등을 사용한다. 흔히 말하는 cipher 는 이 부분만 가리키는 것 (…)

메시지 인증 (무결성)

그리고 이렇게 암호화한 메시지가 내가 암호화한게 맞다고 주장하는 부분이 message authentication code; MAC 이다.
이 부분은 흔히 암호학적 해시함수를 이용한 HMAC 방식으로 처리하는데, 그래서 AES_256_CBC + SHA1 하는 식으로 대칭키 암호화 알고리즘 과 MAC 을 생성하기 위한 해시 함수 의 조합으로 쓴다.

위에서 RSA로 키 교환 + 인증을 한꺼번에 처리한 것처럼, 암호화 및 MAC 처리를 한꺼번에 처리하는 방식도 있다. 앞에서 HMAC과 같이 쓴 방식은 CBC 모드로 동작하는 경우이고, AEAD 로 동작하는 모드 (mode-of-operation) 이 있는 암호화 알고리즘이 있다. AES-GCM 같은게 이에 해당한다.

요약

TLS cipher suite이 있으면 쪼개서 읽는다:

키교환 방식인증 방식WITH암호화 알고리즘메시지 인증 방식

다만 키교환+인증은 한 몸일 수 있고, 비슷하게 암호화+메시지 인증도 한 몸 일 수 있다.

은행은 왜 이렇게 오래된 인증서 알고리즘 / 암호화 알고리즘을 쓰는 걸까

Disclaimer: 작성자 본인은 암호학 전문가가 아닙니다. 이글에선 무엇도 보장하지 않고 이글을 참고해서 하는 일에 대해선 어떤 책임도 지지 않습니다.

국민은행이 모던 브라우저에서 인터넷 뱅킹이 된다기에 시험 해 봄. 근데 들어가자마자 뜨는 워닝이 너무 우울해서 몇 가지를 확인.

국민은행 접속 시 뜨는 경고

인증서가 SHA1 해시를 이용

인증서가 유효한지 확인하기 위해서는 해당 인증서가 유효한지 확인하기 위해 인증서의 해시 값을 구하고, 이를 서명해둔다. 이 때 암호화 해시 함수; cryptographic hash function 을 이용하는데, 여기서는 SHA1 해시 함수를 쓰고 있더라.

현재 구글이나 모질라에서는 SHA1 대신 그 보다 안전한 다름 함수를 쓸 것을 권한다. Bruce Schneier는 2012년에 2015년이면 약 USD 700k 정도면 충돌하는 다른 해시 값을 찾을 수 있다고 추정 하기도 했다. 즉, 현실적으로 저 해시에 해당하는 인증서를 위조(…)할 가능성이 생긴다. 게다가 인증서가 2015년에 생성한 것이던데.

암호화 알고리즘이 구닥다리 (no PFS!)

지원하는 암호화 알고리즘 조합이 키 교환은 RSA만 이용하고 — 그래서 키가 털리면 이전 모든 통신을 복호화해 볼 수 있고 — 메시지 암호화 알고리즘도 AES-128/256-CBC 나 RC4, 3DES-EDE (…) 를 지원한다. 세션 동안만 유효한 암호화 키 (ephemeral key)를 쓰지 않는다. 으으 지금은 2015년 같은데. 게다가 쓰지말라고한지 2년은 된 RC4?

게다가 아직 SSL v3가 남아있다. 혹시 POODLE이라고 들어는 보셨는지… 다행히 TLS_FALLBACK_SCSV 를 써서 TLS v1 이상을 쓰는 브라우저로 접속하면 당장 문제는 아니긴하겠지만.

그들을 위한 변명 …

SHA1 알고리즘의 대체인 SHA2의 호환성을 확인하자면, 클라이언트가 Windows XP SP3 (!!) 이후면 문제가 없다. (DigiCert의 해당 안내문 ) 아직도 Windows XP SP2 + IE6를 쓴다면 이런 인증서를 써야할 수…는 있다.

RC4의 문제도 실제로 이게 선택되는 조합은 Qualys SSL LABS 결과엔 없다. (그럼 걍 빼지…).

SSL v3는 Windows XP + IE6 를 지원하려면 필요하긴 하다.

근데 이것 때문에 FS를 위한 키교환 알고리즘을 못 쓸 이유는 없는 점은 변명의 여지는 전혀 없음. 그리고 이런 사용자 (Windows XP + IE6)는 은행이 책임져 주지도 않는데 차단 할 일이지, 이걸 위해서 다른 사용자가 더 약한 암호화/인증서를 쓸 이유는 안된다.

기왕 모던 브라우저 지원하는 김에 버릴 건 버리고 가는게 맞지 않을까?