概要
1.1 インターネットの標準セキュリティ・プロトコル: TLS
このシリーズでは、製品開発に携わるソフトウェアエンジニアが、製品組み込みのためのセキュリティ機能に関するプログラミングを進めるにあたり必要となる知識について、整理して具体的にまとめます。一口にセキュリティといっても最近は非常に幅広い分野を含みます。ここでは、主にネットワークのセキュリティプロトコルや暗号アルゴリズムなど、製品に直接組み込んで利用するような機能を中心に取りあげて行きます。
今回は、その第一回としてインターネットのセキュリティプロトコルの代表選手TLS(Transport Layer Security)の製品組み込みに関してです。TLSはもともとSSL(Secure Socket Layer)と呼ばれていたデファクト標準をインターネットの標準化団体IETF(Internet Engineering Task Force)が標準化し推進しています。プロトコルの正式名称はTLSですが、現在でもSSLという呼び方も広く使われています。この記事ではどちらも同義語として扱うことにします。
TLSはインターネット上の通信メッセージを安全に伝えるために広く使われています。もちろん、従来TCP/IPの上で生の情報をそのままやりとりしていたアプリケーションのセキュリティを実現するためにも使われますが、WebのHTTPやメールのSMTPなどのアプリケーション層プロトコルのセキュリティ実現のためにも広く使われています。
下の図はインターネットプロトコルの階層構造の概要を示したものです。この図のようにTLSはTCPとIPレイヤーの上に位置します。ですので、もし今の自社製品がTCPで通信しているならば、そのプログラム構造を比較的そのまま維持しながら、TLS層を追加することができます。そのようなプログラミングについては後半で紹介してきます。
TLSと似た位置付けで同じトランスポート層のセキュリティプロトコルとしてDTLS(Datagram Transport Layer Security)があります。こちらは、UDP(User Datagram Protocol)のような不安定な通信の上でセキュリティを実現するためのプロトコルです。こちらについても、このシリーズの中で説明していきたいと思っています。
ここでは説明を簡単にするためにTLSやDTLSは「TCP/IPやUDP」の上のプロトコルと書きました。しかし、それぞれの階層は独立しているのでTLSやDTLSはTCP/IPやUDP以外でも動作させることができ、現実にそうした事例もたくさん存在しています。
1.2 最新のTLS1.3。でも、古いバージョンの扱いは?
インターネットの普及に伴って、TLSは非常に広範囲なセキュリティの脅威についても経験を積んでいて都度改善されてきています。プロトコルのバージョンもTLS1.0からはじまって現在は2018年に発行されたTLS1.3が最新版となっています。TLS1.3は従来のセキュリティ上のさまざまな問題を整理したバージョンで、整理されたおかげで性能的な面でもよくなっている点もあり、おすすめです。自社製品の今後を考えれば、まずはTLS1.3のサポートは必須です。
しかし、今でもTLS1.2までしかサポートしていない通信相手が少なからず存在している現実があります。セキュリティの観点からは新しいプロトコルを採用するのが望ましいのはもちろんですが、自社の製品がおかれた環境と考え合わせて決めなければいけません。現在、TLS1.0以前はセキュリティ上のリスクがあり推奨されません。TLS1.1もできれば使用しないことが推奨されます。TLS1.2については、現在のところ使用方法に注意をして安全な機能、安全な暗号アルゴリズムだけに限定するようにすれば、ほぼ安心して使えると考えていいでしょう。
そういうことから現状の現実的な解としては、製品としてはTLS1.3, 1.2をサポート。それ以外は除外としておきましょう。このようにしておくと、TLS1.3のプロトコル規定では相手方がTLS1.3をサポートしている場合は、必ずTLS1.3を使って接続することが義務付けられているので、現時点で最高の安全性を確保することができます。相手がTLS1.2しかサポートしていない場合だけ、TLS1.2で接続することになります。使用するTLSのライブラリをTLS1.3と1.2だけでコンフィグレーションしておけば、自動的にそのような動作となるので、コンフィグレーションさえしっかり選択しておけば心配ありません。
1.3 TLSってどんな通信?
さて、セキュリティ・プロトコルといえば暗号化ですね。大切なメーセージをどうやって相手に送り届けるのかが気になります。しかし、実際のソフトウェアの役割分担からいうとメッセージの秘匿性(意図しない第三者に見られない)とか、受け取ったメッセージの真正性(改竄されたメッセージではない)などは、プロトコルを取り扱うTLSライブラリがほとんどの部分を実現してくれます。TLSを利用するアプリケーションは次のような操作だけでTLSの通信を行うことができます。
1) 通信の相手側への接続を指示する
2) 接続が完了したら送信したい生のメッセージをライブラリに渡す
3) または受信した暗号メッセージを解読した状態で受け取る
4) 通信が終了したら接続を解除する
ですので、TLSライブラリを使って普通のTLS通信を実現する限り、具体的なTLSプロトコルや詳細については、あまり気にする必要はありません。とはいえ、気になるプロトコルなので、その内容について少しだけ紹介しておきます。
下の図は、Wiresharkというネットワーク上のパケットの様子をモニターする(パケットキャプチャー)ツールで簡単なTLS通信の様子を見たところです。
TLS通信はクライアントからサーバへのClientHelloメッセージで開始されます。このメッセージを受け取ったサーバはServerHelloを返信し、その後、両者でTLS通信開始のための準備のメッセージを交換します。この部分のやりとりをTLSハンドシェークと呼びます。TLSハンドシェークでは、その後の暗号化されたアプリケーションメッセージのやり取りに使う暗号アルゴリズムや暗号鍵を合意したり、正しい相手かどうか確認(ピア認証)をしたりします。
それが正常に完了するとアプリケーションメッセージの通信が始まりました。この例ではハンドシェークの後の一往復のメッセージがそれにあたります。もちろん、この部分はアプリケーションの必要に応じて、自由にメッセージをやり取りすることができます。
アプリケーションメッセージの通信が終了したら、最後に正常に終了したことを互いに確認する終了アラートを交換しTLSのセッションは正常に終了します。
1.4 大事なのは、成りすましを見破ること
このように、TLSによるセキュアな通信に必要なプロトコルは概ねのところTLSライブラリが実現してくれるので、アプリケーションプログラムを作るエンジニアは、あまり細かい内容について意識する必要はありません。しかし、一つだけライブラリを使うエンジニアが強く意識してきちんと組み込んでおかなければならない事があります。それは、信頼できるCAの公開鍵証明書(SSL証明書ともいう)です。
ネットワークのセキュリティプロトコルの役目は概ね次の三つにまとめられます。一つ目は、通信メッセージが他人に漏れないようにする秘匿性です。これは、普通の人が一般的に持っているセキュリティプロトコルのイメージそのものかもしれません。メッセージを送る前に暗号化したり、受け取ったメッセージを復号したりすることで実現します。
二つ目は、通信メッセージが途中で改ざんされたりせず、送信者の意図した通りのものであること(真正性)の保証です。単純に暗号化されたメッセージを復号できたからといって、それがもともと送信したメッセージそのものである保証はありません。TLSのようなセキュリティプロトコルはその点も保証します。
三つ目は、通信の相手方が成りすましなどではない、意図した通りの正しい相手であることを確認(ピア認証)することです。そもそも通信相手が成りすまし相手だったりしたら、秘匿性や真正性を保証しても、まったく無意味となってしまいます。ピア認証は三つのなかでも一番重要な要素ともいえるかと思います。
ピア認証には、クライアントからみて正しいサーバであることを確認するサーバ認証と、サーバからみて正しいクライアントであることを確認するクライアント認証の双方向があります。Webのようなクライアント側が不特定多数に公開が原則のものではサーバ認証のみを行います。最近のIoTの利用シナリオでは、デバイス(クライアント)側の成りすましのリスクが重大となる場合もあります。そのような場合はクライアント認証も行います。TLSプロトコル規定では、サーバ認証は必須、クライアント認証はオプションとなっています。両方を行う場合を相互認証とも呼びます。
サーバ認証
それでは、そのサーバ認証について少し詳しくみていきましょう。TLSのピア認証は公開鍵署名、検証の技術とそれを利用した公開鍵証明書によって行います。
サーバは認証を行なおうとするクライアントに対して公開鍵証明書を送って自分が正しいサーバであることを証明します。そういう風に聞くと、クライアントは何か正しいサーバの名簿のようなものを持っていてサーバの送ってきた証明書が正しいかどうか記載内容と名簿を照らし合わせるようなことをイメージするかもしれません。しかし、正確なコピーがいくらでも作れてしまうデジタルの世界では、そういう確認方法では不正を見破ることはできません。
近代のネットワークプロトコルでは、公開鍵署名という特別な方法で署名が正しいかどうかを判定します。公開鍵署名の詳しい、正確なメカニズムについては多数の参考資料があるので、ここではその動作について直感的な説明にとどめておきます。
公開鍵署名では、署名をするための署名鍵と署名を検証するための検証鍵の二つが対になっています。署名鍵は署名をする人だけの秘密鍵です。ある署名鍵による署名はそれと対になっている検証鍵でのみ正しく検証できるので、署名が正しく検証できるということは、その署名は正しい署名鍵を持った署名者によるものだということが判定できます。
署名は署名対象となる適当なデジタルデータ(テキスト)に対して生成します。テキストが異なれば異なる署名となります。
TLSでは、この公開鍵署名を使ってサーバ認証(必要に応じてクライアント認証)を行います。その様子を図2-1に示しています。
1)サーバ認証の準備
サーバ側では自分が正しいサーバであることを証明できるように、自分のための署名鍵(秘密鍵)と署名検証用の鍵(公開鍵)を一組生成しておきます。署名検証用の公開鍵の方は、TLS通信のときにクライアント側に送ることができるように公開鍵証明書の形にして、それに対して認証局(CA)に署名してもらっておきます。
クライアント側では、認証局(CA)のCA証明書を認証局からダウンロードしておきます。
2)サーバ認証のプロトコル
TLS接続の準備の中で、サーバは自分の公開鍵証明書(サーバ証明書)をクライアントに送ります。証明書を受け取ったクライアントは、用意してあるCA証明書(の中の公開鍵)を使って、サーバ証明書の署名がCA局として署名した正しい署名であることを検証します。これによって、まず送られてきたサーバ証明書が偽造でないこと(CA局が正しく署名したもの)であることを確認します。
サーバはそれと同時に、TLSのハンドシェークメッセージに対する署名を自分の署名鍵(秘密鍵)を使って生成し、それをクライアントに送ります。
次にクライアント側では、送られてきたサーバ証明書に格納されている公開鍵(署名検証用の鍵)を使って、同じくサーバから送られてきたハンドシェークメッセージに対する署名が正しいか検証します。署名が正しければ、このサーバは送られてきたサーバ証明書に格納されていた公開鍵に対応する秘密鍵(署名鍵)で署名していることが確認できます。
このようにして、クライアント側は今通信しているサーバが、正当なサーバであることを確認します。
TLSプログラミング
3.1 TCPソケットのプログラムにTLSを加える
それでは、TLSによるメッセージ通信のプログラムはどのようになるのか、ソケットを使ったTCPによるプレーンテキストのメッセージ通信と比較しながら見ていきましょう。
動作可能なサンプルプログラムはこちらにあるので併せて参照してください。
TCPクライアント、サーバ
TLSクライアント、サーバ
図3-1ではクライアントとサーバの間のソケットによる簡単なTCP通信のプログラムの主要部分だけを抜き出してみました。サーバは適当なタイミングで`accept`による接続要求待ちに入ります。実際の通信の始まりはクライアントからサーバへのTCP接続要求`connect`です。接続が成立すると、クライアントから`send`にてアプリケーションメッセージを送信します。サーバ側ではこれを`recv`で受信します。必要な通信が終わったら`close`にて切断処理をします。
これをベースに、図3-2ではTLS通信プログラムに拡張します。TLSはTCP上のプロトコルですから、TCP接続が完了したらサーバ側では`wolfSSL_accept`、クライアント側では`wolfSSL_connect`によってTLS接続処理を行います。TLS接続に必要なハンドシェークプロトコルは全てこれらの関数の中で行われます。TLS接続が完了したら、アプリケーションメッセージの送信、受信のための関数`wolfSSL_write`、`wolfSSL_read`を呼び出します。これらの関数内では、メッセージの暗号化、復号などのTLSに必要な処理を行なった上でそれをTCPパケットに乗せて送信、受信します。
3.2 準備処理
実際のTLSプログラムではこれに加えて次にような若干の準備処理、後処理が必要です。
- ヘッダーファイルとライブラリ初期化
- コンテクストの確保
- サーバ認証のための証明書のロード
- TLS接続ディスクリプターの確保
- 後処理
では、それらを一つずつみていきましょう。
1) ヘッダーファイルとライブラリ初期化
wolfSSLのビルドオプションの格納されたヘッダーファイル wolfssl/ssl.h をインクルードします。mainの冒頭でライブラリの初期化関数 wolfSSL_Init()を呼び出します。
#includeint main(int argc, char** argv) { wolfSSL_Init();
2)コンテクストの確保
TLSを使用時の動的パラメータをおさえておくためのコンテクスト WOLFSSL_CTX を一つ確保します。
WOLFSSL_CTX* ctx; ... if ((ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())) == NULL) { fprintf(stderr, "ERROR: failed to create WOLFSSL_CTX\n"); return -1; }
第一アーギュメントで接続に使用するTLSプロトコルのバージョンを指定します。wolfSSLv23_client_method を指定するとクライアント、サーバ間で合意できる最新のプロトコルで接続します。
バージョン | API名 |
---|---|
TLS 1.1 | wolfTLSv1_1_client_method |
TLS 1.2 | wolfTLSv1_2_client_method |
TLS 1.3 | wolfTLSv1_3_client_method |
最新バージョン | wolfSSLv23_client_method |
3)証明書のロード
確保したコンテクストにサーバ認証のためのPEM形式のCA証明書を登録します。一つの証明書ファイルのみの場合は、ファイルパスを第二アーギュメントに、複数の証明書ファイルの場合は格納しているディレクトリパスを第三アーギュメントに指定します。使用しないアーギュメントはNULLとします。
#define CERT_FILE "../certs/ca-cert.pem" ... if (wolfSSL_CTX_load_verify_locations(ctx, CERT_FILE, NULL) != SSL_SUCCESS) { fprintf(stderr, "ERROR: failed to load %s, please check the file.\n", CERT_FILE); return -1; }
4)TLS接続ディスクリプター
次に、TLS接続のためのディスクリプタを一つ確保し、確保してあるソケットを登録します。wolfSSLのライブラリがハンドシェークなどのTCP通信を行う時にはこのソケットが使用されます。
WOLFSSL* ssl; if ((ssl = wolfSSL_new(ctx)) == NULL) { fprintf(stderr, "ERROR: failed to create WOLFSSL object\n"); return -1; } wolfSSL_set_fd(ssl, sockfd);
5. 後処理
通信が終わったら資源を開放します。
wolfSSL_free(ssl); wolfSSL_CTX_free(ctx); wolfSSL_Cleanup(); close(sockfd);
3.3 コンパイル、実行させてみる
wolfSSLライブラリの正式版はwolfSSLサイトから、最新版はGithubからダウンロードすることができます。
正式リリース版:
wolfSSLダウンロードページからwolfssl-x.x.0.zipを選択してダウンロード、解凍します。
最新版:
wolfSSL Githubサイトからcloneします。cloneの後、wolfsslデイレクトリでsh autogen.shを実行しconfigureなど必要ファイルを生成します。
$ git clone https://github.com/wolfssl/wolfssl Cloning into 'wolfssl'... remote: Enumerating objects: 5, done. ... Resolving deltas: 100% (64503/64503), done. $ cd wolfssl $ sh autogen.sh autoreconf: Entering directory `.' ... parallel-tests: installing 'build-aux/test-driver' autoreconf: Leaving directory `.'
1)ビルド
次に、ライブラリとサンプルプログラム一式をビルドします。configureコマンドでMakefileを生成し、makeコマンドにてビルドします。configureコマンドは、とりあえずアーギュメントなしのデフォルトでビルドします。make checkのように指定するとコンパイル完了後、ローカルテストを実行するので、すべてのテストが正常終了することを確認します。
$ ./configure checking for gcc... gcc checking whether the C compiler works... yes checking for C compiler default output file name... a.out ... * Linux AF_ALG: no * Linux devcrypto: no * Crypto callbacks: no --- $ make check /Applications/Xcode.app/Contents/Developer/usr/bin/make -j17 check-am CC wolfcrypt/benchmark/benchmark.o CC wolfcrypt/src/src_libwolfssl_la-hmac.lo ... PASS: scripts/tls13.test PASS: tests/unit.test ============================================================================ Testsuite summary for wolfssl 4.4.1 ============================================================================ # TOTAL: 7 # PASS: 7 # SKIP: 0 # XFAIL: 0 # FAIL: 0 # XPASS: 0 # ERROR: 0 ============================================================================
2)インストール
最後にライブラリを所定の場所にインストールします。
$ sudo make install Password:
3)サンプルコードを入手、ビルド
wolfSSLのGithubからサンプルコード一式をダウンロードし、TLS関連のサンプルのある wolfssl-examples/tls の下に移動します。makeコマンドを実行すると、TLSに関するサンプルプログラム一式がビルドされます。
$ git clone https://github.com/wolfssl/wolfssl-examples $ cd wolfssl-examples/tls $ make
まとめ
以上、組み込みシステムの環境で利用するTLSについて、その仕組みから利用方法まで解説してきました。wolfSSLのオープンソース版は、検証用に無料で使えるので、計画段階、研究段階でぜひ実際の環境で試してみてください。機能要求を満たすことが確認できたら、そのまま商用版ライセンスへの移行が可能ですので、無駄なく開発を進めることができます。今後発生する脆弱性対応などは、wolfSSLが提供するサポートパッケージに含まれますので、安心してビジネスユースでお使いいただけます。
wolfSSLは、wolfSSL Inc. が独自に開発し、権利関係の一切を有するTLSライブラリです。wolfSSLに関するご質問、お問い合わせは info@wolfssl.jp宛お送りください。