158
145

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

[Unity/C#]WWW/HttpWebRequestにおける中間者攻撃の危険性を考慮した通信プログラムまとめ

Last updated at Posted at 2015-12-01

#1.はじめに
最近ではサーバーと通信をするゲームやアプリが増えてきました.
そのため,通信のセキュリティについて考えていかないといけないですね
私が調べた限り,App Storeのランキング上位に載っているアプリケーションでも,意外に通信部分はセキュリティが甘いものが多かったです.そこで今回は,今後Unityで通信系プログラムを書く人向けの簡単な通信プログラムと,セキュリティを高めるための工夫を紹介したいと思います.

前置きはいいからソースコード見せろって人は[通信プログラム[セキュリティ編]] (#4%E9%80%9A%E4%BF%A1%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0%E3%82%BB%E3%82%AD%E3%83%A5%E3%83%AA%E3%83%86%E3%82%A3%E7%B7%A8)へ!

##目的
Unityの通信プログラムを様々なウェブサイトで拝見していたところ,
セキュリティ的にいかがなものかというものを何点か発見したので,
皆さんに通信のセキュリティを知っていただきたいという思いで書きました.

間違っている理解や表現などがございましたらご指摘の程お願い致します.

##対象者
専門用語はなんとなく聞きかじっているけども結局ネットワーク・セキュリティてどうすんだ?って人向けです.

SSLの仕組みや中間者攻撃などについて少し触れていますが,抽象化されていますので
詳しい仕組みなどを知りたい方は別途調べてください.

##構成
構成としては,良くある/見かける実装→セキュリティのお話→よく見かける実装の修正→デバッグの仕方です.

#2.通信プログラム[基礎編]
Unityで皆さんが良く用いられいてるであろう通信プログラムを以下に紹介します.
Unityで通信プログラムを開発するには主にWWWクラスとHttpWebRequestを使う2つの方法があります.

基本的に,以下の通信プログラムをそのまま利用していればセキュリティ的にはそこそこ安全です.

##WWWクラス
まずは,Unityオリジナル関数のWWWクラスのサンプルコード.

Test.cs

using UnityEngine;
using System.Text;
using System.Collections;
using System.Collections.Generic;

public class Test : MonoBehaviour {
	
	void Start () {
		// WWWクラスでTestWWW関数を呼ぶ
		StartCoroutine(TestWWW("YOUR_URL"));
	}
	
	IEnumerator TestWWW (string url) {
		
		// 送信開始
		WWW www = new WWW (url);
		yield return www;
		
		// 取得した内容の表示
		Debug.Log (www.text);
		
		// 成功
		if (www.error == null) {
			Debug.Log("Success");
		}
		// 失敗
		else{
			Debug.Log("Error");           
		}
	}	
}

参考:
[Unity | WWWクラス ] (http://docs.unity3d.com/ja/current/ScriptReference/WWW.html)
[Unity | C# Coroutine for WWW ] (http://forum.unity3d.com/threads/c-coroutine-for-www.19584/)

##HttpWebRequestクラス
次は.NET系で使われているHttpWebRequestのサンプルコード.
一応.NET4.系を参考にしたつもり.

Test.cs
using UnityEngine;
using System.Net;
using System.IO;
using System.Text;
using System.Reflection;
using System.Collections;
using System;

public class Test : MonoBehaviour {
  static string URL = "YOUR_URL"; 
  void Start ()
  {
    try{
      HttpWebRequest request = (HttpWebRequest)WebRequest.Create (URL);
      HttpWebResponse response = (HttpWebResponse)request.GetResponse ();
      Stream stream = response.GetResponseStream ();
      string webContext = new StreamReader (stream).ReadToEnd ();
      Debug.Log (webContext);
    }
    catch (WebException webException)
    {
      var ex = webException as Exception;
      while (ex != null)
      {
        Debug.Log(ex.ToString());
        ex = ex.InnerException;
      }
    }
    
  }

}

参考:
[Microsoft | HttpWebRequest クラス ] (https://msdn.microsoft.com/ja-jp/library/system.net.httpwebrequest(v=vs.110).aspx)

#3.セキュリティについて
上記のありきたりな実装を踏まえてセキュリティのお話をしたいと思います.
流れとしては,http,https,httpsの問題についてです.

まず良く利用する通信にhttpとhttpsがあります.
前者が暗号化されていない通信,後者が暗号化された通信となっています.
身近な違いとしてはURLがhttp://かhttps://で始まるかですね.

通信のセキュリティを考える上で,この2つの通信の違いを理解しないといけません.

https使っておけばセキュリティ的に問題ないでしょって言うのは間違いです.
いくらhttpsを利用していようが,正しく利用できなていなければ意味がないのです.
このhttpsを正しく利用するっていうのが簡単そうで,意外に難しいのです.

それをこれから説明します.

##httpについて

http通信とは簡単に言うと暗号化されていないデータ交換です.
こんな感じ.
1.png

実際の通信内容はこんな感じ.
2.png


telnet yahoo.co.jp 80
Trying 183.79.135.206...
Connected to yahoo.co.jp.
Escape character is '^]'.
GET / http/1.0

http/1.1 200 OK
Server: nginx
Date: Mon, 30 Nov 2015 13:42:32 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
P3P: policyref="http://privacy.yahoo.co.jp/w3c/p3p_jp.xml", CP="CAO DSP COR CUR ADM DEV TAI PSA PSD IVAi IVDi CONi TELo OTPi OUR DELi SAMi OTRi UNRi PUBi IND PHY ONL UNI PUR FIN COM NAV INT DEM CNT STA POL HEA PRE GOV"
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN
Cache-Control: private, no-cache, no-store, must-revalidate
Expires: -1
Pragma: no-cache
X-XRDS-Location: https://open.login.yahooapis.jp/openid20/www.yahoo.co.jp/xrds
Vary: Accept-Encoding
X-Frame-Options: SAMEORIGIN

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta http-equiv="content-style-type" content="text/css">
<meta http-equiv="content-script-type" content="text/javascript">
<meta name="description" content="日本最大級のポータルサイト.検索,オークション,ニュース,メール,コミュニティ,ショッピング,など80以上のサービスを展開.あなたの生活をより豊かにする「ライフ・エンジン」を目指していきます.">
...
...
...
...
...
...
...
</html>

<!-- p12.f1.top.kks.yahoo.co.jp Mon Nov 25 22:42:32 JST 2015 -->
Connection closed by foreign host.

これはYahooに繋いだ際の情報.
最初の方にHTTP/1.1と書かれています.これはClientのリクエストを指しています.
次にHTTP/1.1 200と帰ってきています.これはServerのレスポンスを指しています.
すなわち,これはクライアントとヤフーのサーバーで通信をしている内容だということが分かります.

http/1.1はお互いの通信の規約です.その次に帰ってきた200と書いてあるのはステータスコードといって,
通信が上手くいっているといったことを指しています.
興味がある場合は調べて見て下さい.
[wiki | ステータスコード ] (https://ja.wikipedia.org/wiki/http%E3%82%B9%E3%83%86%E3%83%BC%E3%82%BF%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%89)

そして,このhttpは実は大きな問題を抱えています.
httpは暗号化されていないので下記の画像みたいに悪い奴(中間者)が通信の間に介入し,
通信の情報を書き換えてしまうことができるわけです.
3.png

これでは困りますよね.
それで,なのかは知りませんが・・・
ということで生まれたのがhttpsなのです.

httpsとは,通信の情報を傍受(覗き見)されてしまっても大丈夫なように,
通信の情報を暗号化(SSL/TLS)したプロトコルのことです.

参考:
[ゆとり世代のブログ運営論 | http通信とは ] (http://viral-community.com/other-it/http-1873/)
[The Strange Storage | http通信とはなんでしょう? ] (http://www.storange.jp/2012/04/http.html)

##httpsについて
httpsはhttpと違い仕組みがとても難解です.
かなり省いて説明します.

簡単に言うと,httpの通信に+暗号化と証明書っていうものを追加したのがhttpsです.
まずは,この暗号化について説明します.次に,証明書について説明します.

###https - 暗号化

httpsの通信は以下のSSL Handshakeが行われた後に,接続が確立されます.

4.png
※証明書の中に公開鍵は含まれている.
6.png

大まかには以下の流れとなっている.
1.クライアントはサーバーから証明書(+公開鍵)を貰う.
2.クライアントは共通鍵を生成し,サーバーから貰った公開鍵で暗号化し,サーバーに送る
3.サーバーは自身の秘密鍵で共通鍵を復号化できたら,受け取った合図を出し,データ通信を開始する
※公開鍵で暗号化された情報は,公開鍵と対になる秘密鍵でしか復号化できない.この場合は,サーバーが秘密鍵と公開鍵を持っており,公開鍵のみをクライアントに送信している.

詳しくはこちらから:
[Atmarckit | Webの表示速度を遅くする「SSLハンドシェイク」とは ] (http://www.atmarkit.co.jp/ait/articles/1002/09/news120.html)

基本的に共通鍵の交換が終わってしまえば,それ以降はClient-Server間の通信は,その共通鍵で通信が暗号化されるため,httpのように中間者が通信を傍受しても,通信内容を特定することはできません.
7.png

因みに,実際の証明書の中身はこんな情報です.
Googleの証明書(chrome等から確認できる)
8.png

###https - 証明書
httpの流れからの説明でしたので暗号化の話ばかりでしたが,httpsは証明書自体も重要です.

サーバーが送ってきた証明書の中には,サーバーの公開鍵以外にも「私はこういう者です」といった情報が入っています.これをSubjectといいます.

このサーバーの「私はこういう者です」といった情報に対し,「偽りがない」と,それを証明(間違いないと署名)する者をCA(認証局)といいます.これもまた,Issuerという名前で証明書の中に入っています.

署名までの流れはこんな感じです(詳しくは調べてください)
13.png

因みに,申請書をCSRといい,証明書をCRTと言います.

これでようやく証明書が手に入り,先程のような通信ができるわけですね.

後は,通信開始時に,クライアントはサーバーから証明書が送られてくるので,その証明書がちゃんと署名されているか確認すれば良い訳です.これを証明書の検証と言います.

証明書の発行から通信,公開鍵・秘密鍵など全体的な構成は以下のようになっているはずです.
21.png

証明書の検証は,端的に言うと,証明書の署名を復号化できるかできないかです.
復号化に必要な鍵(CAの公開鍵)は既にクライアントのブラウザなどにインストールされています.この鍵の事をルート証明書といいます.

証明書の検証手順は,以下となっています.
1.証明書の署名を復号化する
2. 署名を復号化すると,その証明書のハッシュ値がでてくる
3. 証明書のハッシュ値を再計算する.
4. 署名から出てきたハッシュ値と,再計算したハッシュ値を照合する

ブラウザにインストールされているルート証明書は,特定のCAの公開鍵しかありません.
特定のCAとは,証明書申請をちゃんとチェックし,証明書をちゃんと発行することができる企業のことです.

これは,テキトーなCAの署名を防ぐためです.

そのため,自身で証明書申請(CSR)と認証局(CA)を用意し,署名済みの証明書(CRT)を用意しても,ブラウザは,その署名を復号化する鍵を持っていないので,不明な署名がされていると判断します.

この自身で証明書を作ることをオレオレ認証局/証明書といいます.よく聞きますね.

一般的に特定のCAは,証明書を発行して貰うのにお金が掛かります.それはちゃんとしたチェックをしているからです.

そのため,オレオレ証明書だとお金が掛からないので,オレオレ証明書を使う場合もあります.テスト開発時などですね.

その場合は,どのようにブラウザに証明書の検証をして貰っているのかというと,オレオレ認証局の公開鍵(ルート証明書)をブラウザにインポートさせているか,証明書の検証をオフにしています.

後者の実装を良く見ます.

因みに,証明書の検証に失敗(=不明な証明がされている)した場合でも,証明書の中にあるサーバーの公開鍵が取り出され,通信は暗号化されます.

証明書の検証に失敗したら通信を破棄することも設定次第でできます.Certificate Errorなどで.

今回はブラウザで説明をしましたが,基本的にゲームなどでも仕組みは同じです.

詳しくはこちらから:
[IT PRO | SSL/TLS (1/3) ] (http://itpro.nikkeibp.co.jp/article/COLUMN/20060731/244715/?ST=selfup&P=1)
[IT PRO | 相手が信頼できることを確かめる「サーバー証明書」とは? ] (http://itpro.nikkeibp.co.jp/article/COLUMN/20071012/284426/)

##httpsへの攻撃(中間者攻撃)
今までhttpsの暗号化や証明書の仕組みについて書いてきました.これらの仕組みは一見安全かのように見えます.

しかしながら,このhttpsへも攻撃が可能なのです.

例えば,httpsで想定される通信は以下となります.
17.png

この場合,正常にSSL handshakeが行われ,通信網が暗号化されています.第三者が通信を傍受・改竄するのは難しいと思われます.

しかしながら,以下の場合だとどうでしょうか.
18.png

ClientがServerだと思って通信している相手が実は中間者(悪者)で,
ServerがClientだと思って通信している相手が実は中間者(悪者)だった場合です.

Client-中間者の間の通信がいくら暗号化されていようが,中間者で一度復号化されてしまいますので,実質httpと同じように平文のデータを得ることができてしまいます.

このような中間者が通信を中継し,通信の情報を傍受/改竄することを中間者攻撃といいます.
[Wiki | 中間者攻撃 ] (https://ja.wikipedia.org/wiki/%E4%B8%AD%E9%96%93%E8%80%85%E6%94%BB%E6%92%83)

この攻撃は非常に強力で,対策していないと気付かない間に,第三者に情報を傍受/改竄されてしまいます.

もちろんスマートフォンの様々なアプリにも有効な攻撃手法の1つです.

##httpsに対する中間者攻撃の検知
では,どのように,この中間者攻撃を検知するのかということですが
まず,証明書が変わっていることに着目します.

19.png

この図でいう中間者の証明書は大凡の場合,オレオレ証明書であることが多いです.
そのため,クライアント側で証明書の検証をしっかりしていれば検知できるはずです.

因みに,プログラムの仕様によっては,証明書の検証が失敗しても通信を開始してしまうことがあるため,その辺はしっかりと仕様を確認して下さい.

では,証明書の検証をしっかりしていれば,中間者攻撃は防げるの?ってことですが,
これは何とも言えません.

例えば,クライアントが中間者の証明書の検証に失敗するのは,
クライアントに中間者の証明書の検証を行うのに必要なルート証明書(認証局の公開鍵)を持っていないからですよね.

では,クライアントに中間者の証明書を検証するのに必要なルート証明書をインストールさせてしまえばどうなるかですが,この場合,問題なく中間者の証明書を検証できるようになってしまうのです.

さらに,証明書の検証や,ルート証明書の管理などはクライアントのOS側で管理されていることが多いため,アプリ側から不正なルート証明書のインストールをさせないなどを強制させることができません.

そのため,対策としては,これだけでは不十分です.

では,どうするかというと,通信開始時にクライアント(アプリ)がサーバーからどのような証明書を渡されたかを確認すれば良いのです.

手法としては,クライアントは事前に通信する予定のサーバーの証明書のハッシュ値を持っておき,通信開始時に渡される証明書のハッシュ値と比較すれば良いということです.

ハッシュ値が一致しない場合は,通信を破棄します.(SSL handshake error)

これによって,特定の証明書でのみしか通信しないことを強制させることができます.
すなわち,Serverと通信する場合は,Serverの公開鍵でしか暗号化せず,それ以外は通信を破棄するため,中間者に通信の情報を中継される恐れがなくなります.

なぜなら,Serverの公開鍵で暗号化された情報は,Serverの秘密鍵を持っていない限り復号化できないからです.一般的にはServerの秘密鍵はServer以外が持っていることはありません.

##httpsを用いる上での運用上の注意
HTTPSには多くのバージョンがあります.この中で,RC4やSSL 2.0 / 3.0に対応していると暗号強度が弱いため,通信が解析されてしまう恐れがあります.これらはダウングレード攻撃やPOODLE Attackと呼ばれ,既に攻撃手法として確立されています.そのため,HTTPS,すなわちSSL/TLSを用いる際は,RC4やSSL 2.0 / 3.0へ対応しないようにしましょう.

#4.通信プログラム[セキュリティ編]
通信プログラム[基礎編]で紹介したWWWクラス,HttpWebRequestクラス,この両方のクラスはデフォルトの状態で利用すると比較的安全で,不明な証明書を利用した通信をはじいてくれます.

言い換えると,中間者攻撃などで利用される不明な証明書を検知し,通信を破棄することで,中間者が通信内容を解析できないようにしてくれます.

しかし,クライアントに中間者のルート証明書をインストールしてしまうと,不明な証明書ではなくなってしまうので,中間者の証明書で通信ができるよう(=中間者攻撃が可能)になってしまいます.

何らかの理由で証明書の検証をオフにしていた場合も同様に,中間者攻撃などを可能にしてしまいます.

そこで,どうにかして,

  1. アプリケーション側で通信時に利用する証明書を選定し,中間者攻撃を防ぎたいと考えます.
  2. また,開発環境などに合わせて自己証明書(別名:オレオレ証明書)なども利用したいとします.

以上の2点を満たすには,証明書の検証を行うクラスの一部をオーバーライドして利用することで実現できます.

まずWWWクラスでの実装方法ですが,これは調べたところ
自己署名の証明書が利用できなかったり,そもそもクラスのカスタマイズができなかったりと,いまいちカスタマイズ性が優れていないみたいでした.

公式サイトからのキャプチャ画像
20.png

Unity | WWW-クラスで自己署名証明書は使用できない

WWW-クラスで自己署名証明書は使用できないということなので,セキュリティ的には中々良いです.
しかしながら,自己署名証明書を使えないのは不便ですし,クライアントにルート証明書のインストールをした場合に実質的に自己署名証明書の通信ができてしまいます.

すなわち,クライアントにルート証明書のインストールをさせるタイプの中間者攻撃にはWWWクラスでは対応できません.

ということで,今回はWWWクラスでの実装を見送ります.

因みに,リファレンスでもそれらしきものは見当たらなかったです.
[Unity | WWW ] (http://docs.unity3d.com/ja/current/ScriptReference/WWW.html)

一方のHttpWebRequestクラスは,UnityオリジナルのWWWクラスと異なり,.Net系ということもあり,クラスのカスタマイズだとかその辺はできるようです.

ということで今回は.Net4.0のHttpWebRequestクラスを用いて,以下2点を満たす通信プログラムを実装します.

  1. 通信時に利用する証明書を選定し,中間者攻撃を防ぐ
  2. 自己証明書(別名:オレオレ証明書)も利用できるようにする

##HttpWebRequestクラス
以下が実装したプログラムになります.

Test.cs
using UnityEngine;
using System.Net;
using System.IO;
using System.Text;
using System.Reflection;
using System.Collections;
using System;
using System.Security.Cryptography.X509Certificates;

public class Test : MonoBehaviour {

  static string URL = "YOUR_URL";
  static string FINGER_PRINT = "YOUR_FINGER_PRINT";
  
  void Start ()
  {
    try{
      ServicePointManager.ServerCertificateValidationCallback = new System.Net.Security.RemoteCertificateValidationCallback (ValidateServerCertificate);
      HttpWebRequest request = (HttpWebRequest)WebRequest.Create (URL);
      HttpWebResponse response = (HttpWebResponse)request.GetResponse ();
      Stream stream = response.GetResponseStream ();
      string webContext = new StreamReader (stream).ReadToEnd ();
      Debug.Log (webContext);
    }
    catch (WebException webException)
    {
      var ex = webException as Exception;
      while (ex != null)
      {
        Debug.Log(ex.ToString());
        ex = ex.InnerException;
      }
    }
    
  }

  public static bool ValidateServerCertificate (
    object sender,
    System.Security.Cryptography.X509Certificates.X509Certificate certificate,
    X509Chain chain,
    System.Net.Security.SslPolicyErrors sslPolicyErrors)
  {
    Debug.Log ("SSL Policy Error: " + sslPolicyErrors.ToString ());
    Debug.Log ("Cert. Hash: " + certificate.GetCertHashString ());

    if (certificate.GetCertHashString () == FINGER_PRINT) {
      Debug.Log ("Correct!");
      return true;
    }
    return false;
  }

}

主に以下を参考にしました.
[MSDN | RemoteCertificateValidationCallback] (https://msdn.microsoft.com/ja-jp/library/system.net.security.remotecertificatevalidationcallback(v=vs.110).aspx)

[MSDN | x509certificate] (https://msdn.microsoft.com/ja-jp/library/system.security.cryptography.x509certificates.x509certificate(v=vs.110).aspx)

[MSDN | servercertificatevalidationcallback] (https://msdn.microsoft.com/ja-jp/library/system.net.servicepointmanager.servercertificatevalidationcallback(v=vs.110).aspx)

使い方:

  1. ソースコードのYOUR_URLに接続したいURLを入れて実行を押す
30.png Cert Hashにハッシュ値が書き出されるので,これをコピー

2.ソースコードのYOUR_FINGER_PRINTにコピーしたハッシュ値を入力
32.png
Googleの例だとこんな感じになりました

3.再度実行ボタンを押す.
31.png
Correctと表示される.

この状態で中間者攻撃などをすると証明書が変わってしまうのでエラーがでて通信できなくなります.
Unity側のエラー内容としては1になります.

因みに中間者攻撃を行っている某ツールではSSL handshake errorが表示されます.
4.png

##実装の説明
証明書の検証時に特殊な処理を割り込ませるためにコールバック用関数を登録しています.
コールバックの登録は以下でできます.

ServicePointManager.ServerCertificateValidationCallback = new System.Net.Security.RemoteCertificateValidationCallback (ValidateServerCertificate);

以下が登録した関数になります.

public static bool ValidateServerCertificate (
  object sender,
  System.Security.Cryptography.X509Certificates.X509Certificate certificate,
  X509Chain chain,
  System.Net.Security.SslPolicyErrors sslPolicyErrors)
{
  Debug.Log ("SSL Policy Error: " + sslPolicyErrors.ToString ());
  Debug.Log ("Cert. Hash: " + certificate.GetCertHashString ());

  if (certificate.GetCertHashString () == FINGER_PRINT) {
    Debug.Log ("Correct!");
    return true;
  }
  return false;
}

この関数内でfalseを返すとSSL handshake errorが起き,通信を破棄したことになります.
この関数内でtrueを返すと,証明書の検証に失敗していても,通信を開始してしまいます.

因みに,sslPolicyErrorsには証明書の検証結果が入っています.

Screen Shot 2015-11-11 at 6.28.40 AM.png [MSDN | SslPolicyErrors 列挙体] (https://msdn.microsoft.com/ja-jp/library/ms145055(v=vs.110).aspx)

そのため,クライアントに中間者攻撃をするとSslPolicyErrorsにRemoteCertificateChainErrorsが入ります.詳しいエラー内容はChainStatusを参照することで確認できます.

以下のようにSslPolicyErrorsにエラーがあれば云々といった処理も書けます.

if (sslPolicyErrors != System.Net.Security.SslPolicyErrors.None) {
    // todo   
}

しかし,中間者のルート証明書がクライアントにインストールされていると,中間者攻撃をされてもSslPolicyErrorsにはNoneが格納されてしまうため,これだけでは中間者攻撃を対策できません.

また,証明書のIssuerやSubjectを確認するといったことが考えられますが,オレオレ証明書の場合,SubjectやIssuerは自身で値を入力できるため,類似の証明書を偽装できてしまいます.

そのため,IssuerやSubjectの確認をしただけでは,正当な証明書か検証できません.

そこで,証明書のフィンガープリントの照合を行います.フィンガープリントとは,証明書の公開鍵を含む全ての情報をハッシュ化したものです.

例え,攻撃者からハッシュ値の一致する正当な証明書のコピーを送られてきたとしても,その場合は,正当な証明書の公開鍵で暗号化されるため,攻撃者は,その情報を復号化することができません.

そのため,証明書のハッシュ値をアプリ側で照合することで,中間者攻撃は防げると考えられます.

これらによって,証明書の検証だけでは防げなかった,クライアントにルート証明書をインストールするタイプの中間者攻撃も
利用する証明書の正当性を確認することで防げるようになります.

ハッシュ値の照合は以下のプログラムで行っています.

if (certificate.GetCertHashString () == FINGER_PRINT) {
  Debug.Log ("Correct!");
  return true;
}

[MSDN | getcerthashstring] (https://msdn.microsoft.com/ja-jp/library/system.security.cryptography.x509certificates.x509certificate.getcerthashstring(v=vs.110).aspx)

この手法の問題点として,証明書を更新する場合にプログラム側も更新しなければなりません.
そのため,定期的に残りの期限をチェックする必要があります.

また,ハッシュ値をプログラムに埋め込んだ理由としては,プログラムがコンパイルされるため,逆アセンブルが困難になるのではないかと考えたからです.
ファイルに埋め込んだ場合は,差し替えられてしまう恐れがあると思われます.
といっても,プログラムのコードも書き換えられてしまいそうな気がしますが.

##処理速度

処理速度を測定してみました.処理速度に使用したコードは以下になります.

Test.cs
using UnityEngine;
using System.Net;
using System.IO;
using System.Text;
using System.Reflection;
using System.Collections;
using System;
using System.Security.Cryptography.X509Certificates;

public class SSLTLS : MonoBehaviour {
	
	static string URL = "****";
	static string FINGER_PRINT = "****";
	void Start ()
	{
		float check_time = Time.realtimeSinceStartup;
		try {
				// on / off
				//ServicePointManager.ServerCertificateValidationCallback = new System.Net.Security.RemoteCertificateValidationCallback (ValidateServerCertificate);
				HttpWebRequest request = (HttpWebRequest)WebRequest.Create (URL);
				HttpWebResponse response = (HttpWebResponse)request.GetResponse ();
		} catch (WebException webException) {
				var ex = webException as Exception;
				while (ex != null) {
						Debug.Log (ex.ToString ());
						ex = ex.InnerException;
				}
		}

		// The real time in seconds since the game started (Read Only).
		check_time = Time.realtimeSinceStartup - check_time;
		Debug.Log( "check time : " + check_time.ToString("0.00000") + "s" );
	}
	

public static bool ValidateServerCertificate (
		object sender,
		System.Security.Cryptography.X509Certificates.X509Certificate certificate,
		X509Chain chain,
		System.Net.Security.SslPolicyErrors sslPolicyErrors)
	{
		if (certificate.GetCertHashString () == FINGER_PRINT) {
			return true;
		}
		return false;
	}
	
}

コールバック登録ありとなしで,計15回実行し観測した結果以下のようになりました.
100.png

コールバック登録なしよりも,ありの方が0.00286s遅かったですが,ネットワークの遅延なども考えると,ほぼ誤差だと思われます.

また,ValidateServerCertificate関数内の処理を測定しましたが,処理時間は0.0000sでした.
文字列比較はかなり高速なようです.コールバックを呼ぶ処理の方が重い・・?

#5.デバッグやその他の実装
証明書の検証などに関してのデバッグや他の実装方法について軽く触れます.
デバッグのソースコードなので本番では利用しないでください.

ステータスの確認

どのような証明書で通信をしているのか,どのようなエラーで通信が破棄されたのか,
こういったログを出力するのは意外に役に立ちます.revocation modeの確認などもできます.

[MSDN | X509ChainPolicy] (https://msdn.microsoft.com/ja-jp/library/System.Security.Cryptography.X509Certificates.X509ChainPolicy(v=vs.90).aspx)

こんな感じで表示されます.
26.png

以下サンプルコード

Test.cs

using UnityEngine;
using System.Collections;
using System.Net;
using System.IO;
using System.Text;
using System.Reflection;
using System.Collections;
using System;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;



public class Test : MonoBehaviour {
	
	static string URL = "YOUR_URL";
	
	void Start ()
	{
		try{
			ServicePointManager.ServerCertificateValidationCallback = new System.Net.Security.RemoteCertificateValidationCallback (ValidateServerCertificate);
			HttpWebRequest request = (HttpWebRequest)WebRequest.Create (URL);
			HttpWebResponse response = (HttpWebResponse)request.GetResponse ();
			Stream stream = response.GetResponseStream ();
			string webContext = new StreamReader (stream).ReadToEnd ();
			Debug.Log (webContext);
		}
		catch (WebException webException)
		{
			var ex = webException as Exception;
			while (ex != null)
			{
				Debug.Log(ex.ToString());
				ex = ex.InnerException;
			}
		}
		
	}

	public static bool ValidateServerCertificate (
		object sender,
		System.Security.Cryptography.X509Certificates.X509Certificate certificate,
		X509Chain chain,
		System.Net.Security.SslPolicyErrors sslPolicyErrors)
	{
		Debug.Log("Chain: " + chain.ChainStatus.Length);
		Debug.Log("Certificate error: " + sslPolicyErrors);
		Debug.Log(string.Format("Chain revocation flag: {0}", chain.ChainPolicy.RevocationFlag));
		Debug.Log(string.Format("Chain revocation mode: {0}", chain.ChainPolicy.RevocationMode));
		Debug.Log(string.Format("Chain verification flag: {0}", chain.ChainPolicy.VerificationFlags));
		Debug.Log(string.Format("Chain verification time: {0}", chain.ChainPolicy.VerificationTime));
		Debug.Log(string.Format("Chain status length: {0}", chain.ChainStatus.Length));
		Debug.Log(string.Format("Chain application policy count: {0}", chain.ChainPolicy.ApplicationPolicy.Count));
		Debug.Log(string.Format("Chain certificate policy count: {0} {1}", chain.ChainPolicy.CertificatePolicy.Count, Environment.NewLine));
		for(int i=0; i<chain.ChainStatus.Length; i++) {
			Debug.Log("Chain status: " + chain.ChainStatus[i].Status);
			foreach (X509ChainElement element in chain.ChainElements)
			{
				Debug.Log (string.Format("Element issuer name: {0}", element.Certificate.Issuer));
				Debug.Log (string.Format("Element certificate valid until: {0}", element.Certificate.NotAfter));
				Debug.Log (string.Format("Element certificate is valid: {0}", element.Certificate.Verify ()));
				Debug.Log (string.Format("Element error status length: {0}", element.ChainElementStatus.Length));
				Debug.Log (string.Format("Element information: {0}", element.Information));
				Debug.Log (string.Format("Number of element extensions: {0}{1}", element.Certificate.Extensions.Count, Environment.NewLine));
				if (chain.ChainStatus.Length > 1)
				{
					for (int index = 0; index < element.ChainElementStatus.Length; index++)
					{
						Debug.Log ("Status:" + element.ChainElementStatus[index].Status);
						Debug.Log ("Status:" + element.ChainElementStatus[index].StatusInformation);
					}
				}
			}
		}
		// Environment: Development
		return true;	
	}
}

因みにchainの設定は以下のように変更できます.
chainのクラスはX509Chainです.

chain.ChainPolicy.RevocationFlag = X509RevocationFlag.EntireChain;
chain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
chain.ChainPolicy.UrlRetrievalTimeout = new TimeSpan(0, 1, 0);
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllFlags;

Revocationの列挙体などは以下から確認できます.
[MSDN | x509revocationmode] (https://msdn.microsoft.com/ja-jp/library/system.security.cryptography.x509certificates.x509revocationmode(v=vs.90).aspx)

Storeの選択

Storeの選択は以下のように実装できます.
以下は,特定のStoreをReadWriteモードで開き,新たな証明書をaddし,再表示している実装となります.

var cert = new X509Certificate2(YOUR_CERT_PATH);
var store = new X509Store(StoreName.TrustedPublisher, StoreLocation.LocalMachine); 
store.Open(OpenFlags.ReadWrite);
store.Add(cert);
var certificates = store.Certificates;
foreach (var certificate in certificates)
{
  var friendlyName = certificate.FriendlyName;
  var name = certificate.GetName();
  Debug.Log("friendName:"+friendlyName);
  Debug.Log("name:"+name);

}
store.Close();

詳しくは以下から.
[MSDN | x509certificates.x509store.certificates] (https://msdn.microsoft.com/ja-jp/library/system.security.cryptography.x509certificates.x509store.certificates(v=vs.110).aspx)
[MSDN | x509certificates.x509store] (https://msdn.microsoft.com/ja-jp/library/system.security.cryptography.x509certificates.x509store(v=vs.90).aspx)
[MSDN | 証明書の使用] (https://msdn.microsoft.com/ja-jp/library/ms731899(v=vs.110).aspx)

iOSやAndroidでは挙動が不明です.

再検証

パラメータを自身で設定して再検証を行うこともできます.
例えば,RevocationFlagを立てた場合や,自身のCAを追加した場合などです.
Revocationとは失効リストとの照合です.

Store(検証を行う際のルート証明書の倉庫)の選択などもできますが,これはiOSやAndroidでの挙動が不明なので,使用していません.
[MSDN | x509certificates.x509store] (https://msdn.microsoft.com/ja-jp/library/system.security.cryptography.x509certificates.x509store(v=vs.90).aspx)
[MSDN | 証明書の使用] (https://msdn.microsoft.com/ja-jp/library/ms731899(v=vs.110).aspx)

検証を行う際のルート証明書の追加としてはX509形式の鍵を読み込みExtraStore.Addで鍵を追加しています.
Revocationの設定などはChainPolicyに適宜行うものとします.

実装では以下を参考にしました.
[MSDN | x509certificates.x509chain.build] (https://msdn.microsoft.com/ja-jp/library/system.security.cryptography.x509certificates.x509chain.build(v=vs.110).aspx)

以下実装.

Test.cs

using UnityEngine;
using System.Collections;
using System.Net;
using System.IO;
using System.Text;
using System.Reflection;
using System.Collections;
using System;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;

public class SSLTLS : MonoBehaviour {
	static string URL = "YOUR_URL";	
	void Start ()
	{
		
		try{
			ServicePointManager.ServerCertificateValidationCallback = new System.Net.Security.RemoteCertificateValidationCallback (ValidateServerCertificate);
			HttpWebRequest request = (HttpWebRequest)WebRequest.Create (URL);
			HttpWebResponse response = (HttpWebResponse)request.GetResponse ();
			Stream stream = response.GetResponseStream ();
			string webContext =new StreamReader (stream).ReadToEnd ();
			Debug.Log (webContext); 
		}
		catch (WebException we)
		{
			var ex = we as Exception;
			while (ex != null)
			{
				Debug.Log(ex.ToString());
				ex = ex.InnerException;
			}
		}
		
	}
	
	
	public static bool ValidateServerCertificate (
		object sender,
		System.Security.Cryptography.X509Certificates.X509Certificate certificate,
		X509Chain chain,
		System.Net.Security.SslPolicyErrors sslPolicyErrors)
	{		
/*		byte[] data = System.Text.Encoding.ASCII.GetBytes("-----BEGIN CERTIFICATE-----\n"+
		                                                  "****************************\n"+
		                                                  "-----END CERTIFICATE-----");		
		X509Certificate2 xc2 = new X509Certificate2(data);
*/
	
		X509Chain ch = new X509Chain();
//		ch.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
//		ch.ChainPolicy.ExtraStore.Add(xc2);
		ch.ChainPolicy.ExtraStore.Add(new X509Certificate2 ("YOUR_CA_KEY_PATH"));
		ch.Build((X509Certificate2)certificate);
	
		for(int i=0; i<ch.ChainStatus.Length; i++) {
			Debug.Log("Chain status: " + chain.ChainStatus[i].Status);
			foreach (X509ChainElement element in chain.ChainElements)
			{
				Debug.Log (string.Format("Element issuer name: {0}", element.Certificate.Issuer));
				Debug.Log (string.Format("Element certificate valid until: {0}", element.Certificate.NotAfter));
				Debug.Log (string.Format("Element certificate is valid: {0}", element.Certificate.Verify ()));
				Debug.Log (string.Format("Element error status length: {0}", element.ChainElementStatus.Length));
				Debug.Log (string.Format("Element information: {0}", element.Information));
				Debug.Log (string.Format("Number of element extensions: {0}{1}", element.Certificate.Extensions.Count, Environment.NewLine));
				if (chain.ChainStatus.Length > 1)
				{
					for (int index = 0; index < element.ChainElementStatus.Length; index++)
					{
						Debug.Log ("Status:" + element.ChainElementStatus[index].Status);
						Debug.Log ("Status:" + element.ChainElementStatus[index].StatusInformation);
					}
				}
			}
		}

		// Environment: Development
		return true;	
	}	
}

158
145
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
158
145

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?