本記事は生成AIと共同で執筆しています。事実関係は可能な範囲で公式ドキュメント等と照合していますが、誤りが含まれている可能性があります。重要な判断を行う前にご自身でも一次情報をご確認ください。

前の記事証明書の鎖は、署名の繰り返しにすぎないで扱ったのは、サーバ証明書でした。証明書が結びつけていたのは、人ではなくホスト名です。

本記事では、もう一方の向き、つまり接続を始めた側が出す証明書を見ます。手元で認証局(前の記事の CA)を立てて実際に動かし、そのうえでマイナンバーカードの中身を読み解きます。以降は、後半で扱う公的個人認証サービスの用語に合わせて「認証局」と表記します。

結びつける相手は、記事をまたいで移り変わっています。

前の記事   www.example.com(ホスト名) ↔ 公開鍵   ── 接続を受ける側が出す
本記事     Alice Archivist(人)      ↔ 公開鍵   ── 接続を始める側が出す

ただし「接続を始める側」の中身が人とは限らない、というのが次節の要点です。

まず呼び名を整理する

この分野は同じものを違う切り口で呼ぶため、用語が揺れます。3 つ並べると整理できます。

呼び名何による名前か意味
クライアント証明書役割TLS(Transport Layer Security、通信を保護する規格)の通信で、接続を始めた側が出す証明書
個人認証用証明書対象人に対して発行される証明書
S/MIME(Secure/Multipurpose Internet Mail Extensions)証明書用途メールの署名・暗号化に使う証明書

重なりますが、一致はしません。とくに 1 つ目が誤解のもとで、クライアント証明書の中身は人とは限りません

人がブラウザから使う          → クライアント証明書(中身は人)
サーバ同士が通信する          → クライアント証明書(中身はサーバ)

「クライアント」は接続を始めた側という意味であって、人かどうかとは別の軸です。

手元で認証局を立てる

言葉より動かすほうが早いので、自前の認証局を作ります。使うのは openssl だけです。

mkdir -p certs && cd certs

# 1. 認証局
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
  -keyout ca.key -out ca.crt -days 3650 \
  -subj "/C=JP/O=Example University/CN=Example University CA"

# 2. サーバ証明書(localhost 用)
openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
  -keyout server.key -out server.csr \
  -subj "/C=JP/O=Example University/CN=localhost"
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out server.crt -days 365 \
  -extfile <(printf "subjectAltName=DNS:localhost,IP:127.0.0.1\nextendedKeyUsage=serverAuth")

# 3. クライアント証明書(アリス用)
openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
  -keyout alice.key -out alice.csr \
  -subj "/C=JP/O=Example University/OU=Library/CN=Alice Archivist"
openssl x509 -req -in alice.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out alice.crt -days 365 \
  -extfile <(printf "extendedKeyUsage=clientAuth")

サーバ用とクライアント用の違いは、extendedKeyUsageserverAuthclientAuth かという用途の指定と、サーバ側にだけ必要な subjectAltName(ホスト名の指定)です。鍵を作って認証局に署名させる手順そのものは同じです。

subjectAltName はサーバ証明書では省けません。現代のクライアントはホスト名の照合にこの欄を使い、CN は見ないためです。

なお、このラボの認証局は 1 段だけです。前の記事で扱った「ルートと中間を分ける」構成にはしていません。中間を分けるのはルートの秘密鍵を使わずに済ませるための運用上の工夫で、仕組みを見るうえでは省いても差し支えないためです。

比較用に、別の認証局とそこが発行した証明書も作っておきます。

# 別の認証局
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
  -keyout other-ca.key -out other-ca.crt -days 3650 \
  -subj "/C=JP/O=Somewhere Else/CN=Somewhere Else CA"

# その認証局が発行したクライアント証明書
openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
  -keyout mallory.key -out mallory.csr \
  -subj "/C=JP/O=Somewhere Else/CN=Mallory"
openssl x509 -req -in mallory.csr -CA other-ca.crt -CAkey other-ca.key -CAcreateserial \
  -out mallory.crt -days 365 \
  -extfile <(printf "extendedKeyUsage=clientAuth")

cd ..

-CA other-ca.crt -CAkey other-ca.key の部分が要点です。ここを ca.crt のままにすると、マロリーの証明書も大学の認証局が発行したことになり、後の③が受理されてしまいます。

サーバ側の設定は 3 行

import { createServer } from 'node:https';
import { readFileSync as R } from 'node:fs';

createServer({
  key: R('certs/server.key'),
  cert: R('certs/server.crt'),

  ca: [R('certs/ca.crt')],   // ← 誰が発行した証明書を信じるか
  requestCert: true,         // ← クライアントにも証明書を求める
  rejectUnauthorized: false, // 拒否理由を表示したいので、判定は自分で行う
}, (req, res) => {
  const cert = req.socket.getPeerCertificate();
  // authorized が true でも cert が空のことがある(後述)
  const ok = req.socket.authorized && cert && cert.subject
             && !req.socket.isSessionReused();
  if (!ok) {
    res.writeHead(401, { 'Content-Type': 'text/plain; charset=utf-8' });
    return res.end(`✗ 拒否\n理由: ${req.socket.authorizationError || '証明書が提示されなかった'}`);
  }
  res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
  res.end(`✓ 受理\nあなたは: ${cert.subject.CN}\n発行者  : ${cert.issuer.CN}`);
}).listen(9401);

通常の HTTPS と違うのは carequestCert、そして rejectUnauthorized の 3 行です。3 行目を false にしているのは、拒否の理由を画面に出したいからで、本番では true にして TLS の層で切るのが普通です。

ok の判定で証明書の中身と isSessionReused() まで見ているのには理由があります。Node.js のドキュメントが、まさにこの書き方に対して警告しています。TLS 1.3 では、証明書を出さずに接続した相手がセッションを再開すると、authorizedtrue になり、getPeerCertificate() は空のオブジェクトを返します。rejectUnauthorized: false にして自分で判定するなら、セッションの再利用と証明書の存在も確かめる必要があります。

この記事を書く過程で、そこを見ていなかったために実際にサーバが落ちました。

自前の認証局がサーバとアリスの両方に証明書を発行する構成を示した図。中央上に Example University CA が置かれ、そこから左の server.crt(CN=localhost、extendedKeyUsage は serverAuth)と右の alice.crt(CN=Alice Archivist、extendedKeyUsage は clientAuth)へ署名の矢印が伸びている。左はサーバ側が出すもの、右はクライアント側が出すもので、両者が互いに証明書を出し合うことが mTLS(相互 TLS)であると示されている。下部に、サーバの設定は ca: [ca.crt] の 1 行だけであり、ふだんの HTTPS で使われるルート認証局の一覧はここでは 1 つも使われず、誰を信じるかを決めているのはブラウザを配る側ではなくサーバの管理者である、と注記されている。

そして ca の行が本記事の要点になります。ここで指定しているのは、自分で作った認証局 1 つだけです。公開の認証局の一覧は、ここでは使われません

ca を指定すると、その値が既定の一覧を置き換えます。置き換えられるのは、正確には Node.js に同梱されている一覧のほうです(ブラウザの一覧とは別物ですが、公開の認証局が並んでいる点は同じです)。

3 通り試す

certs/ の 1 つ上のディレクトリで node server.mjs として起動し、別の端末から次を実行します。

curl -sS --cacert certs/ca.crt https://localhost:9401/
curl -sS --cacert certs/ca.crt --cert certs/alice.crt   --key certs/alice.key   https://localhost:9401/
curl -sS --cacert certs/ca.crt --cert certs/mallory.crt --key certs/mallory.key https://localhost:9401/

結果です。

① 証明書を出さない
✗ 拒否
理由: UNABLE_TO_GET_ISSUER_CERT

② アリスの証明書を出す
✓ 受理
あなたは: Alice Archivist
発行者  : Example University CA

③ 別の認証局が発行した証明書を出す
✗ 拒否
理由: UNABLE_TO_VERIFY_LEAF_SIGNATURE

同じサーバに 3 通りで接続した結果を示した図。①証明書を出さない場合は拒否で理由は UNABLE_TO_GET_ISSUER_CERT、②アリスの証明書を出した場合は受理で Example University CA が発行したもの、③別の認証局の証明書を出した場合は拒否で理由は UNABLE_TO_VERIFY_LEAF_SIGNATURE。下部に、③ の証明書は壊れておらず形式も署名も正しく、その発行元を信じるように設定されたサーバから見れば有効であること、このサーバが信じる認証局が発行していないというだけで拒否されること、が注記されている。

①と③のエラー文言は、いずれも「発行者にたどり着けない」という意味です。①は証明書そのものが提示されておらず、③は提示された証明書の発行者がこのサーバの一覧に無い、という違いです。

① は証明書を出していないのに「発行者証明書を取得できない」と返ります。Node は相手の証明書が無いときも検証エラーの形で理由を返す実装になっていて、ソースには「ピア証明書が無いときの UNABLE_TO_GET_ISSUER_CERT は疑問の残る挙動だが、従来との互換のため」という趣旨のコメントが付いています。そのため、上のコードに書いたフォールバックの文字列はこの経路では使われません。それが出るのは、先に触れた TLS 1.3 のセッション再開のときです。③ はこれとは別で、証明書は届いているものの、信頼する認証局まで鎖をたどれない、というエラーです。

③ が要点です。マロリーの証明書は壊れていません。形式も署名も正しく、発行元の Somewhere Else CA を信じるように設定されたサーバから見れば有効です。このサーバが信じる認証局が発行していないというだけで拒否されています。

サーバ側が何を出しているかも見ておきます。

echo | openssl s_client -connect localhost:9401 -CAfile certs/ca.crt 2>/dev/null \
  | grep -E "^(subject|issuer)=|Verify return code"
subject=C=JP, O=Example University, CN=localhost
issuer=C=JP, O=Example University, CN=Example University CA
Verify return code: 0 (ok)

双方が証明書を出し合っています。これを mTLS(mutual TLS、相互 TLS)と呼びます。このラボでは片方の中身が人ですが、両方がサーバでも構いません。ただし、既存のサーバ証明書をそのまま流用することはできません。接続する側が出す証明書の extendedKeyUsage には clientAuth が要ります。serverAuth だけの証明書を出すと、このサーバは INVALID_PURPOSE で拒否します(serverAuth,clientAuth と両方書けば 1 枚で兼用できます)。用途の指定は飾りではなく、検証する側が実際に見ています。

信頼の起点は、サーバの管理者が決めている

サーバ証明書との一番の違いはここです。

サーバ証明書クライアント証明書
誰が検証するか世界中のブラウザそのサーバだけ
ルート認証局ブラウザや OS に同梱された一覧その組織が自分で立てた認証局
誰が信頼を決めるかブラウザや OS を配る側サーバの管理者
中身ホスト名人でもサーバでもよい

サーバ証明書は世界中の誰でも検証できる必要があるため、世界共通の認証局が要ります。クライアント証明書は、検証する相手が限られるので、組織が自前で認証局を立てれば足ります。

ただし「必ず自前」ではありません。次に見るマイナンバーカードは、国の機関が発行した証明書を多くのサービスが検証する形で、自前ではありません。要は、検証する側がその認証局を信じると決めていればよい、ということです。

上のラボで ca: [R('certs/ca.crt')] と書いた 1 行が、その「誰を信じるか」の指定です。

マイナンバーカードには 2 種類入れられる

身近な例がマイナンバーカードです。カードには 2 種類の電子証明書を付与できます(希望しなければ付きませんし、15 歳未満と成年被後見人には原則、署名用電子証明書は発行されません)。

署名用電子証明書利用者証明用電子証明書
記録される情報氏名・住所・生年月日・性別これらは記載されない
暗証番号6 文字から 16 文字(英数字の混在が必要)4 桁の数字
ロックまでの回数5 回3 回
有効期間原則として発行から 5 回目の誕生日まで同じ

(出典: 公的個人認証サービス ポータルサイト

発行しているのは J-LIS(地方公共団体情報システム機構)で、申請の受付と本人確認は市区町村の窓口が行います。前の記事で見た「確認する人と署名する人は別」がここでも成り立っていて、市区町村の窓口が RA(登録局)にあたります。こちらは規程上もその名前で、署名用認証局の運用規程は 1.3.2「登録局」の節で、市区町村長が発行申請・更新申請・失効申請の受付と申請者の本人確認を行うと定めています。

マイナンバーカードの証明書が 2 種類に分かれている理由を示した図。左は利用者証明用電子証明書で、記録されるのはシリアル番号と有効期限のみ、氏名・住所・生年月日・性別は入らず、入館証をかざすようなもので受付は分かるが書類には何も残らない。右は署名用電子証明書で、氏名・住所・生年月日・性別が記録され、どこにも照会せずに名前が読め、書類に押印し印鑑証明を添えるようなもので書類自体が誰の意思かを示し続ける。下部に、軸1 は名前が証明書に入っているか、軸2 は文書に貼り付いて残るかであること、暗証番号が 4 桁と 6〜16 桁で結果の重さに応じて守りの強さを変えてあることが示されている。

2 枚に分かれている理由は 2 つの軸で説明できる

軸 1: 名前が証明書に入っているか

署名用には氏名が書かれているので、その証明書を受け取った相手は、どこにも照会せずに名前を読めます。前の記事で見た O=The University of Tokyo と同じで、証明書に書いてある属性は開けば読めます。

利用者証明用には氏名がありません。持ち主の欄に入るのは個人名ではない識別子と国名だけで、相手が個人を見分ける手がかりは電子証明書の発行番号(シリアル番号)だけです。名前に辿り着けるのは、照会する経路を持つ相手に限られます。

軸 2: 文書に貼り付いて残るか

署名用は文書に署名を付けるので、その文書と一緒に残ります。後からその文書を受け取った人が、誰の署名かを確認できます。

利用者証明用はその場の確認で終わり、文書には残りません。

利用者証明用   入館証をかざす。受付は分かるが、書類には何も残らない
署名用         書類に押印し、印鑑証明を添えて渡す。書類自体が誰の意思かを示し続ける

必要以上の情報を渡さないために、証明書を 2 枚に分けている、という設計です。

4 桁でよい理由

利用者証明用の暗証番号は 4 桁です。短く見えますが、守りはそれだけではありません。

  • カード、またはスマホ用電子証明書を搭載した端末の物理的な所持が必要で、鍵はカードの IC チップから取り出せない
  • 3 回間違えるとロックされ、時間が経っても自動的には解除されない

なお、公的個人認証サービスは 2023 年 5 月に Android、2025 年 6 月に iPhone で「スマホ用電子証明書」の搭載が始まり、カードがなくても端末だけで使えます(搭載の手続きにはカードが要ります)。スマホ用も署名用が英数字 6〜16 文字、利用者証明用が数字 4 桁で、カード用と同じ認証局から発行されます。以下ではカードの場合の回数制限で説明します。

1 本目の記事公開鍵と署名の仕組みを、実際の数字で追うで、鍵ファイルを入手した相手はオフラインで何度でもパスフレーズを試せるため、長さが要ると書きました。マイナンバーカードは 3 回で止まるので、必要な長さが変わります。

鍵ファイルのパスフレーズ    試行が無制限     → 長さが要る
カードの暗証番号            3 回で止まる     → 4 桁で成立する

とはいえ、カードや端末を盗まれ、かつ暗証番号を推測された場合には通ります。誕生日のような値を設定していれば、3 回でも当たる可能性はあります。安全だから 4 桁でよい、ということではなく、物理的な所持と回数制限があるからその水準まで下げられる、という割り切りです。

証明書を使わない選択もある

クライアント証明書には配布の手間があります。利用者全員の端末に入れる必要があり、端末を追加するたびに作業が増えます。

そのため、利用者が多い場面では証明書を使わない構成も一般的です。たとえば東京大学の VPN サービスは、公開されている案内によると、クライアントに Cisco Secure Client を使い、認証は UTokyo Account で行う方式です。多要素認証の設定が必須とされています。クライアント証明書ではなく、ID による認証と多要素認証の組み合わせです。

判断の目安はこうなります。

相手が少なく、失効も手で管理できる   →  公開鍵を直接登録すれば足りる(SSH の authorized_keys など)
相手が多い、または失効を一括したい   →  証明書
利用者が非常に多く、端末も多様       →  ID による認証と多要素認証

人かサーバかではなく、数と、失効をどう管理するかで決まります。

触れていないもの

  • 失効の確認。証明書が期限前に失効させられていないかを確かめる手段(CRL、OCSP)には触れていません。なお、失効を発行者に問い合わせる方式では、その問い合わせ自体によって発行者が利用を知り得ます
  • 署名用電子証明書の失効情報の提供を受けられる者(署名検証者)の要件。証明書を受け取って署名を暗号的に検証すること自体に制度上の資格は要りませんが、その証明書が失効していないかを発行元に確認できるのは、公的個人認証法第 17 条第 1 項に基づき機構に届け出た者に限られます。対象は民間事業者だけでなく行政機関等や裁判所も含み、民間事業者の場合は主務大臣の認定などが要ります。手続きの詳細は本記事では扱いません
  • 無線 LAN での利用(EAP-TLS)。仕組みは同じですが、具体的な導入例は確認していません
  • クライアント証明書をブラウザに導入する手順。本記事では curl で確かめています

まとめ

  • 「クライアント証明書」は接続を始めた側が出す証明書という意味で、中身が人とは限らない
  • 作り方はサーバ証明書とほぼ同じで、違うのは用途を示す extendedKeyUsage と、サーバ側にだけ必要な subjectAltName
  • 誰を信じるかはサーバの管理者が決める。ブラウザに入っているルート認証局は使われない
  • 別の認証局が発行した証明書は、壊れていなくても拒否される
  • 双方が証明書を出し合う構成を mTLS と呼び、サーバ同士でも使える
  • マイナンバーカードには 2 種類の電子証明書を入れられ、「名前が読めるか」「文書に残るか」の 2 軸で使い分けられている
  • 4 桁の暗証番号が成立するのは、物理的な所持と 3 回のロックがあるため
  • 利用者が多い場面では、証明書を使わずに ID による認証と多要素認証を選ぶ構成もある