SSLサーバ証明書の中間CA証明書集めを自動化した

# SSLサーバ証明書の設定に必要なもの
取得したSSLサーバ証明書を実際に設定する際には通常以下の３点が必要になります。

- 秘密鍵（CSRを作るために使ったもの）
- サーバ証明書（認証局から発行されたもの）
- 中間CA証明書（基本は認証局が配布してるんだが、探すのが大変なことがある）

# 中間CA証明書がよくわからん！
必要なもののうち、秘密鍵とサーバ証明書は１つずつしか無いので混乱は無いのですが、中間CA証明書というのは認証局によってはどこで配布されてるのかあるのか分かりづらいことも多いです。今まで見たパターンには以下のようなものがありました。

- メールで送られてくるパターン
    - サーバ証明書と一緒に本文に書いてあるパターン（即コピペで済むので楽ちん系）
    - サーバ証明書と一緒にzip添付されてくるパターン（zipの中にcrtがいっぱい入っててどれをどう使えばよいかよくわからん！）
- サイトから落とすパターン
    - サイトに行ってみたらサーバ証明書の種類に応じて何種類も中間CA証明書がおいてあってどれを使えばよいのか分からない！
    - 必要な中間CA証明書が１つじゃなくて複数必要らしい、どう設定すればよいか分からない！
    - PEMというbese64テキスト形式じゃなくDERというバイナリ形式しかなくってどう扱えばよいか分からない！
- 英語が分からなくて困るパターン
    - 中間CA証明書（=Intermediate Certificate）という英語の呼び方を知らなくて中間CA証明書の配布場所を探すとっかかりが掴めない！

などなど、SSLの設定素人にとってはなかなかに悩む部分だと思います。（えぇ、これらはもちろん僕自身がコレまでに悩んだポイントですが何かｗ）


# 認証局によって配布方法が違うのが元凶、これだけ覚えとけば良いってのが欲しい→自動化したい→自動化出来た！
実は巷で発行されている大抵のSSLサーバ証明書内には、X509v3の拡張として親CA証明書の鍵識別子(AuthorityKeyIdentifier)と一緒に、`CA Issuer - URI`という情報が入っている。これは親CAの証明書がダウンロード出来るURL、つまりお目当ての中間CA証明書がこっから引っ張れるのである。
ただしそこからDLしたものが本当に真正な証明書なのか？とかRootCAまで辿り尽くせてしまった場合は、RootCAはクライアントがプリインストールで持ってるべきもので中間CAに含めるものではないから無視する、とかの条件を加えて証明書チェーンを探索していく。
こうして自分のサーバ証明書とRootCA証明書の間に存在している、まさに中間CA証明書だけを全て抽出してやればそれが求めていたものなのだ！

ちなみに今どきの大抵のPCブラウザは上記手順による中間CA証明書の自動取得機能を備えているので、中間CA証明書がちゃんと設定されていなくてもエラーにならずに正常に見れてしまう。便利なもんだが、これが中間CA証明書の設定ミスに気づかない原因にもなっていて「ブラウザで警告でてないのでOK」と言えないのでサーバ管理者目線では逆に困りものでもある(^^;
この辺りの外部チェック手順については[別記事に書いた](http://qiita.com/kawaz/items/93e06eeb3f858be4f743)のでそっちを参照のこと…。

## 中間CA証明の自動取得スクリプト
ちょっと解説が脇道にそれたが、さっき出来た自動取得スクリプトが[コレ](https://github.com/kawaz/ssl-cert-script/blob/master/bin/collectInCACert.php)です！

```php:https://github.com/kawaz/ssl-cert-script/blob/master/bin/collectInCACert.php
#!/bin/env php
<?php
$file = (count($argv) == 2) ? $argv[1] : "php://stdin";
echo collectInCACert($file);

function collectInCACert($file, $depth=0, $subjectKeyIdentifier_verifier=null) {
  list($cert, $pem) = read_cert($file);
  $inca = "";
  $extensions = aget($cert, "extensions");
  $basicConstraints_CA = aget(text2hash(aget($extensions, "basicConstraints"), '/, */'), "CA");
  $subjectKeyIdentifier = aget($extensions, "subjectKeyIdentifier");
  $authorityKeyIdentifier_keyid = aget(text2hash(aget($extensions, "authorityKeyIdentifier")), "keyid");
  $authorityInfoAccess_CAIssuersURI = aget(text2hash(aget($extensions, "authorityInfoAccess")), "CA Issuers - URI");
  if(0) {//DEBUG
    echo "FILE: $file\n";
    echo "subjectKeyIdentifier: $subjectKeyIdentifier\n";
    echo "authorityKeyIdentifier: $authorityKeyIdentifier_keyid\n";
    $cert["purposes"] = array_map(function($a){return intval($a[0])." ".intval($a[1])." ".$a[2];},array_values($cert["purposes"])); //purposes情報って細かくて見難いので表示整理
    echo json_encode($cert, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE)."\n";
  }
  //KeyIdentifierチェック(サブジェクトキーIDが子の言ってる親のキーIDと一致するかチェック)
  if(!empty($subjectKeyIdentifier_verifier) && $subjectKeyIdentifier != $subjectKeyIdentifier_verifier) {
    fputs(STDERR, "[ERROR] CA証明書のサブジェクトキーIDが一致しません。配布サーバ側の問題か偽物を掴まされている可能性があります。errorfile=$file\n");
    return false; //この中間CA証明書、間違いor偽物じゃね？？
  }
  //親CAが無いので探索終了。RootCAは中間CAではないのでPEM出力もしない。
  if(empty($authorityKeyIdentifier_keyid)) {
    return $inca;
  }
  //中間CAのPEMを出力。中間CAを探しているのでdepth==0はスキップ。
  if($basicConstraints_CA == "TRUE" && 0 < $depth) {
    $inca .= $pem;
  }
  //親CAを辿ってPEMを連結する
  if(preg_match('/^https?:/', $authorityInfoAccess_CAIssuersURI)) {
    $authorityInCaCert = collectInCACert($authorityInfoAccess_CAIssuersURI, $depth + 1, $authorityKeyIdentifier_keyid);
    if($authorityInCaCert === false) {
      exit(1);
    }
    $inca .= $authorityInCaCert;
  } else {
    fputs(STDERR, "[ERROR] 証明書チェーンの途中に親CA証明書の配布URLが埋められていないものがあるため自動化出来ませんでした。認証局のドキュメントに従って手動で中間CAを集めてください。errorfile=$file\n");
    return false;
  }
  return $inca;
}

//配列操作でissetチェックとかする手間を省く関数
function aget($arr, $key, $default=null) {
  if(!is_array($arr)) {
    return $default;
  }
  return isset($arr[$key]) ? $arr[$key] : $default;
}

//X509には "KEY:hoge, foo:bar" みたいな分解されつくしてない文字列フィールドが結構あるのでそれらをパースする関数
function text2hash($text, $spliter='/[\r\n]+/') {
  $hash = array();
  if(is_string($text)) {
    foreach(preg_split($spliter, $text) as $line) {
      $kv = preg_split('/:/', $line, 2);
      if(count($kv) == 2) {
        $hash[$kv[0]] = $kv[1];
      }
    }
  }
  return $hash;
}

//DER形式でもPEM形式でも気にせず読み込んでX509情報を取得する
function read_cert($file="php://stdin") {
  $pem = file_get_contents($file);
  $cert = openssl_x509_parse($pem);
  if($cert === false) {
    $pem = der2pem($pem);
    $cert = openssl_x509_parse($pem);
  }
  return array($cert, $pem);
}

//DER形式データをPEM形式に変換する
function der2pem($der_data) {
  return "-----BEGIN CERTIFICATE-----\n" . chunk_split(base64_encode($der_data), 64, "\n") . "-----END CERTIFICATE-----\n";
}
```

最初は `openssl` コマンドを使ってシェル芸で解決しようと思ったんだけど、opensslの出力がすっげーパースしにくい糞な感じなので途中で投げて、適当にPHPででっち上げてみました。


## 使い方
普通です。第一引数か、標準入力からサーバ証明書を渡してやると、標準出力に中間CA証明書が出力されます。

```bash:
./collectInCACert.php server.crt > server.inca.crt
```

ワンライナーで使えるようにしたので↓これでいける！

```bash:
php <(curl -sL https://raw.githubusercontent.com/kawaz/ssl-cert-script/master/bin/collectInCACert.php) server.crt
```

## 試してみて駄目なケースがあったら教えてください
それなりに基本に忠実にチェックしながら集めてるつもりで、手元にある何種類かのCAで発行したサーバ証明書からは問題なく動作することは確認しました。
ですが、もしこの証明書だと上手く中間CA証明書集められなかったー、ていう人があれば対応できればしたいので教えて貰えると助かります。