こんにちは、サーバーサイドグループのマネージャーの鬼木です。
今回、直近でSSL証明書を取り扱うタスクを担当し、HTTPS通信について学びになることが多かったのでその知見共有の記事になります。
SSLの仕組み
本題に入る前に簡単にSSLの仕組みについて説明したいと思います。
HTTPS通信をする際、TLSプロトコルを用いてクライアント・サーバー間で通信を行います。HTTPS通信はHTTP通信に比べて暗号化通信ができることが特長ですが、暗号化通信成立までにいくつかの工程を挟みます。
その中でクライアント側は通信するサーバーの正当性検証を行います。
正当性検証
サーバー側はHTTPS通信を行うにあたって必要なSSL証明書を持っており、リクエストがあった場合にクライアント側にそのSSL証明書を送付し、受け取ったクライアント側ではSSL証明書の正当性検証が行われます。
SSL証明書の仕組み
SSL証明書は信頼性のある証明書から別の証明書を署名することで、その署名された証明書の正当性を担保するという仕組みによって成り立っています。 署名する側の大元として、ルート認証局というものが存在します。
ルート認証局は業界基準に基づき、定期的に第三者監査を受けることで信頼性を確保している組織です。ルート認証局が持つルートCA証明書でSSL証明書を署名することで、そのSSL証明書を使用しているウェブサイトやサービスが正当なものであることを証明します。
また、ルート認証局の下位には中間認証局という認証局があります。中間認証局も中間CA証明書と呼ばれる自身の証明書を持っており、これはルートCA証明書で署名されています。
特定のウェブサイトなどが新しいSSL証明書を取得する際はこの中間CA証明書によって署名されます。この中間CA証明書によって署名された各ウェブサイトなどのSSL証明書はエンドエンティティ証明書と呼ばれます。
エンドエンティティ証明書はルートCA証明書に直接署名されていないですが、ルートCA証明書によって署名された中間CA証明書によって署名されることで、ルートCA証明書によって署名された証明書であることの正当性を確保しています。
クライアント側
SSL証明書はこのような署名の仕組みで成り立っています。実際にサーバー側とクライアント側で通信を行う際、クライアント側でどのようにしてその正当性検証を行うかというと、クライアント側で保持しているルートCA証明書の公開鍵を使用してサーバーから送られてきたSSL証明書の正当性を検証します。
後半で詳細な部分にも触れていきますが基本的に広く使われているOS(Windows、macOS、Linuxなど)はルートCA証明書の公開鍵を持っており、HTTPS通信を行う際の正当性検証ではその公開鍵が使用されています。
特定のウェブサイトにアクセスしたときにそのサイトのエンドエンティティ証明書がサーバーから送付されますが、SSL証明書の仕組みとして大元の署名元(ルートCA証明書)を辿ることができるため、クライアント側で保持しているルートCA証明書の公開鍵を用いてそのルートCA証明書の正当性検証をすることで、送付されたエンドエンティティ証明書の正当性を確認することができます。
このSSL証明書の署名元はブラウザでも確認することができます。Google ChromeであればURLの隣のアイコンから証明書チェーン1を確認可能です。
上記はtech.yappli.ioの証明書の階層を表しており、E6が中間CA証明書、ISRG Root X1がルートCA証明書になっています。「発行元」を選択することでその証明書の発行元が下部に表示されます。tech.yappli.ioの署名元はE6となっていますが、ルートCA証明書は自分自身を署名しているので以下のように「証明書の階層」でISRG Root X1を選択して「発行元」を確認すると署名元はISRG Root X1自身となっています。
この証明書チェーンの仕組みにより、ウェブサイトが新しいSSL証明書(エンドエンティティ証明書)を取得してもその大元の署名がルートCA証明書によるものであれば、新しいエンドエンティティ証明書の公開鍵がクライアント側に無くてもルートCA証明書の公開鍵があるため、正当性検証を行うことができます。
またSSL証明書は自己署名で作成する方法もありますが(オレオレ証明書と言われたりします)、署名元の大元がルートCA証明書ではないため、クライアント側にあるルートCA証明書の公開鍵で正当性検証を行うことができません。そのためクライアント側に自己署名で作成したSSL証明書の公開鍵がない限り通信時の正当性検証に失敗し、ブラウザからのアクセスであればアドレスバーに赤や黄色の文字で警告が出ます。
SSL証明書の公開鍵を用いてHTTPS通信を行う
前提となるHTTPS通信における正当性検証の仕組みについて説明しました。ここからは実際にSSL証明書を用いてサーバー・クライアント間で通信することでその仕組みについて深掘りしていきたいと思います。
今回は検証のため以下のようなprojectを用意しました。Dockerでサーバー側とクライアント側を立て、サーバー側はnginxを使用してHTTPS通信できるようにし、クライアント側からサーバー側にリクエストしてSSL証明書の正当性検証の挙動を確認します。 使用するSSL証明書は自己署名証明書を作成しました。自己署名証明書(certs/*)は以下を参考に作成しました。
project構成
project/ ├── certs/ │ └── app/ │ ├── ca-crt.pem │ ├── server-crt.pem │ └── server-privatekey.pem ├── docker-compose.yaml ├── Dockerfile.client ├── Dockerfile.server └── nginx.conf
【docker-compose.yaml】
version: '3.8' services: server: build: context: . dockerfile: Dockerfile.server container_name: https-server volumes: - ./certs:/certs ports: - "443:443" client: build: context: . dockerfile: Dockerfile.client container_name: https-client depends_on: - server volumes: - ./certs:/certs stdin_open: true tty: true
【Dockerfile.client】
FROM alpine:latest WORKDIR /app RUN apk add --no-cache curl bash openssl ca-certificates CMD ["/bin/sh"]
【Dockerfile.server】
FROM nginx:alpine WORKDIR /etc/nginx EXPOSE 443 COPY nginx.conf /etc/nginx/conf.d/default.conf CMD ["nginx", "-g", "daemon off;"]
【nginx.conf】
server { listen 443 ssl; server_name localhost; ssl_certificate /certs/server-crt.pem; ssl_certificate_key /certs/server-privatekey.pem; location / { return 200 'Hello, SSL over Docker!'; add_header Content-Type text/plain; } }
検証
まずはdocker-compose upでContainerを立ち上げた後、別のセッションでクライアント側(https-client
)のContainerに入ります。
docker exec -it ${Container ID} bash
curlコマンドでの検証
クライアント側からはHost名server
でサーバー側(https-server
)にアクセスすることができます。しかし今の時点では通常のcurlコマンドでのHTTPS通信は失敗します。
# curl https://server:443 curl: (60) SSL certificate problem: unable to get local issuer certificate More details here: https://curl.se/docs/sslcerts.html curl failed to verify the legitimacy of the server and therefore could not establish a secure connection to it. To learn more about this situation and how to fix it, please visit the webpage mentioned above.
SSL証明書の公開鍵はdocker-compose.yamlのvolumesで指定している通りクライアント側の/certs
に存在していますが、SSL通信でのSSL証明書の正当性検証にその公開鍵を使用することを明示的に指定していないため通信が失敗します。
curlコマンドでは--cacert
optionによってSSL証明書の公開鍵を指定することができます。
# curl --cacert /certs/ca-crt.pem https://server:443 Hello, SSL over Docker!
このようにSSL証明書の公開鍵を指定し、接続しようとしているサーバー側のSSL証明書の正当性検証を行える場合、通信が成功します。
opensslコマンドでの検証
次にopensslコマンドを使用して検証します。opensslコマンドを使用することでSSL証明書の情報の詳細を確認できたり、SSL証明書の検証失敗時のエラーの詳細を確認することができます。
# openssl s_client -connect server:443 -showcerts // 中略 Verify return code: 21 (unable to verify the first certificate)
こちらも先ほどと同じくSSL証明書の公開鍵を指定していないので失敗します。
opensslコマンドでは-CAfile
optionによってSSL証明書の公開鍵を指定することができます。
# openssl s_client -connect server:443 -CAfile /certs/ca-crt.pem // 中略 Verify return code: 0 (ok)
こちらもSSL証明書の公開鍵を指定することで証明書の正当性検証を行うことができました。
またopensslコマンドではSSL証明書の署名元などの詳細を確認することもできるので、前半で説明したルートCA証明書や中間CA証明書などについても確認することができます。以下のようにしてtech.yappli.ioをopensslコマンドで検証すると、ルートCA証明書や中間CA証明書の情報が出力されます。
# openssl s_client -connect tech.yappli.io:443 Connecting to 35.75.255.9 CONNECTED(00000006) depth=2 C=US, O=Internet Security Research Group, CN=ISRG Root X1 verify return:1 depth=1 C=US, O=Let's Encrypt, CN=E6 verify return:1 depth=0 CN=tech.yappli.io verify return:1 // 略
Trust StoreにSSL証明書を登録し、証明書指定無しで通信できるようにする
これまではcurlコマンド、opensslコマンドでSSL証明書の公開鍵を指定してHTTPS通信を行いました。しかし実際の本番環境などではサーバー上で動いているアプリケーションからHTTPS通信をするに当たってSSL証明書を指定するのは現実的ではないため、これを証明書指定無しでHTTPS通信を行えるようにします。
Dockerfile.clientのapk addでca-certificates
というパッケージを指定していますが、このパッケージをインストールすることにより、/usr/local/share/ca-certificates/というディレクリが作成されます。このディレクリにSSL証明書を配置し、update-ca-certificates
を実行することで証明書指定無しで通信できるようになります。
# cp /certs/ca-crt.pem /usr/local/share/ca-certificates/ # update-ca-certificates # openssl s_client -connect server:443 -showcerts // 中略 Verify return code: 0 (ok)
上記で行っていることは、クライアント側のTrust Storeの更新です。Trust Storeは信頼できるSSL証明書を保管してる機構で、HTTPS通信時にはこのTrust StoreのSSL証明書を参照して正当性検証を行います。
alpineではTrust Storeは/etc/ssl/certs/にあたり、ルート証明書などが保管されています
この/etc/ssl/certs/配下にca-certificates.crt
というファイルがあります。ca-certificates.crt
はこのTrust Store内の証明書をまとめたバンドルファイルであり、このサーバー上でHTTPS通信を行う時の証明書正当性検証の際に参照されるのはこのファイルになります。先ほどのupdate-ca-certificates
は/usr/local/share/ca-certificates/
、/etc/ssl/certs/
配下の証明書をca-certificates.crt
に反映するということを行っているため、証明書をTrust Store内に配置するだけでなく、この更新も行って初めてHTTPS通信で追加した証明書が使われるようになります。
ca-certificates.crt
はTrust Store内の証明書の内容がそのままテキストで保存されている形になるため、update-ca-certificates
を行った後にca-certificates.crt
を見るとca-crt.pem
の内容が末尾に追加されていることが確認できます。
このようにTrust Storeに登録された証明書をSSL通信時に参照して証明書の正当性検証を行うことで、HTTPS通信が成り立っています。
またSSL証明書には有効期限がありますが、ルートCA証明書の有効期限が切れる前にOSアップデートなどにより新しいルートCA証明書の公開鍵がTrust Storeに登録されることで、期限が切れる前にルートCA証明書の切り替えを行うことができています。
最後に
SSL証明書の仕組みやTrust Storeの仕組みなど各partはすで世の中にある情報ですが、一からどういう仕組みで成り立っているのかなどが共有できればと思い、今回アドベントカレンダーの記事として書いてみました。
ヤプリでは来年も引き続きサーバーサイドエンジニアを募集しています。
この記事を読んで弊社に興味を持たれた方はぜひカジュアル面談にお越しください!
- クライアント、サーバなどの証明書から、ルート認証局のルートCA証明書までの連なりのこと↩