概要
- ACMEプロトコル導入の経緯と理由を説明
- 既存クライアントやライブラリへの不信感と独自実装の動機を記述
- 実装過程で遭遇した技術的課題と解決策を整理
- 主要な手順や必要な処理を箇条書きで解説
- ACMEの複雑さや感想についても触れる
ACMEプロトコル導入の経緯と背景
- 2023年初頭、「old-school cert」をhttpsサイトで使い続けていた理由を説明する投稿を執筆したことを振り返ること
- ACMEプロトコルの存在は2018年から認識していたが、仕様や既存クライアントの安全性・設計に強い不信感を持ち続けていたことを強調
- 既存クライアントはプライベートキーやroot権限へのアクセス権限を与えるには危険すぎると判断し、導入を拒否していた経緯を説明すること
- Gandiがプライベートエクイティに買収されサービス品質や価格が低下し、2025年以降の利用継続に疑問を持ったことが転機となったことを記載
- 既存サービスから離脱し、ACME導入を決断するに至った提案
独自実装への決意と初期ステップ
- 既存のACMEクライアントやライブラリの「コードの質」に疑念を持ち、自分自身で小さなユーティリティやライブラリをC++で実装し始めたことを説明すること
- JSON処理のためにjansson(Cライブラリ)をラップするなど、必要最小限の機能から段階的に構築すること
- JWKなどの生成ライブラリも検証したが、期待通りに機能せず何度も行き詰まりを経験した確認
- 問題を細分化し、少しずつ進展するアプローチを採用すること
- テスト用ACMEサーバ「pebble」を利用し、実際のCAを煩わせずに開発・検証を進めたことを強調すること
実装手順と主要処理
- RSA鍵(4096ビット)を作成し、ウェブサイト用のCSR(証明書署名要求)を作成すること
- CSRからCN(Common Name)とSAN(Subject Alternative Names)を抽出し、CNがSANに含まれることを検証すること
- ACMEサービスオペレーターから提供される<directory URL>にHTTP GETし、"newNonce"、"newAccount"、"newOrder"などの値をJSONから抽出すること
- RSA鍵ファイルからpublicExponent(通常65537)とmodulusを抽出し、バイト配列として保持すること
- publicExponentをビッグエンディアンのバイト列(例: 0x01, 0x00, 0x01)に変換し、base64web("+"→"-"、"/"→"_")エンコードすること
- JWK(JSON Web Key)オブジェクトを作成し、"e"(publicExponent)、"kty"("RSA")、"n"(modulus)を設定すること
- "termsOfServiceAgreed": true を含むJSONオブジェクト(payload)を作成すること
- "newNonce"のURLにHTTP HEADリクエストし、ヘッダーから"Replay-Nonce"の値を取得すること
- "newAccount"のURLを使い、"protected"(JWKやnonce等を含むJSON)、"payload"(上記JSON)、それらを連結した文字列をSHA256でダイジェストし、RSA署名・base64webエンコードした"signature"を作成すること
- これらをまとめてJOSE(JSON Object Signing and Encryption)形式でPOSTし、レスポンスヘッダーの"Location"からアカウント識別用URLを取得すること
- ここまででACMEアカウントの作成が完了することを確認すること
ACMEプロトコルの複雑さと感想
- RSA鍵、SHA256ダイジェスト、RSA署名、base64webエンコード、JSONのネスト、LocationヘッダーをIDとして利用、nonce取得のためのHEADリクエストなど、多数の複雑な処理が必要であることを強調すること
- オーダー作成、認証・チャレンジ、TXTレコード、キーサムプリントなど、さらに多くの手順が控えていることを認識すること
- 既存クライアントの一部にはpublicExponentのエンコードミス(10進と16進の混同)などのバグが存在することを指摘すること
- ACMEプロトコルの複雑さは「ジョブセキュリティ」のためかもしれないという皮肉な感想を述べること
- 仕様や実装の細部にこだわる場合、独自実装は大変だが学びも多い提案