100日チャレンジ day46 (PKI基盤とACMEのシュミレーター)

昨日
100日チャレンジに感化されたので、アレンジして自分でもやってみます。
やりたいこと
- 世の中のさまざまなドメインの簡易実装をつくり、バックエンドの実装に慣れる(dbスキーマ設計や、関数の分割、使いやすいインターフェイスの切り方に慣れる
- 設計力(これはシステムのオーバービューを先に自分で作ってaiに依頼できるようにする
- 生成aiをつかったバイブコーティングになれる
- 実際にやったことはzennのスクラップにまとめ、成果はzennのブログにまとめる(アプリ自体の公開は必須ではないかコードはgithubにおく)
できたもの

PKI基盤とACME をやる

はい、承知いたしました。「ACME対応 簡易認証局 (CA) シミュレーター」の仕様を具体化していきましょう。
アプリケーション名
Day46 - ACME対応 簡易認証局シミュレーター
デザインテーマ
グラスモーフィズム x ミニマリズム
概要
ユーザーが認証局 (CA) の運営と、ACMEプロトコルによる証明書発行プロセスを体験できるシミュレーションアプリケーション。
従来の証明書発行フローに加え、ACMEサーバーとして振る舞い、ACMEクライアント (UIで操作) からの要求に応じてドメイン検証を行い、証明書を発行する機能を持つ。
機能一覧
1. CA管理機能 (従来型)
-
ルートCA設定 (アプリ内仮想)
- アプリケーション起動時に、固定のルートCA情報(秘密鍵、公開鍵、証明書)を内部的に持つ (DBには保存せず、毎回同じものを使用するか、起動時に生成)。
- 表示用として、ルートCAの証明書情報をUIで確認できる。
-
証明書発行要求 (CSR) の手動受付と発行
- UIから以下の情報を入力してCSRを擬似的に作成、またはユーザーが用意したCSR (PEM形式テキストエリア) を貼り付け。
- コモンネーム (ドメイン名など)
- 組織名
- 国コード など (主要なものに絞る)
- 公開鍵 (テキストエリア、PEM形式)
- CAがCSRに署名し、X.509証明書 (PEM形式もどき) を発行。
- 発行された証明書をテキスト表示し、コピーできるようにする。
- UIから以下の情報を入力してCSRを擬似的に作成、またはユーザーが用意したCSR (PEM形式テキストエリア) を貼り付け。
-
発行済み証明書管理
- 発行した証明書 (従来型・ACME経由双方) の一覧を表示 (コモンネーム, 発行日, 有効期限, 発行方法[手動/ACME], ステータス[有効/失効])。
- 証明書の詳細情報を表示。
- 証明書を失効させる機能。失効した証明書は一覧でステータスが変更される。
-
証明書失効リスト (CRL) の表示 (簡易版)
- 失効した証明書のシリアル番号と失効日の一覧を表示する。
2. ACMEサーバー機能 (シミュレーション)
-
ACMEエンドポイント (Next.js Route Handlersで実装)
- POST
/api/acme/new-account
- リクエストボディ:
{ "publicKey": "クライアントの公開鍵 (JWK形式文字列)" }
(簡略化のため) - レスポンス:
{ "accountId": "一意のアカウントID", "status": "valid" }
- リクエストボディ:
- POST
/api/acme/new-order
- リクエストヘッダー:
Authorization: Bearer {accountId}
(簡易認証) - リクエストボディ:
{ "identifiers": [{ "type": "dns", "value": "ドメイン名" }] }
- レスポンス:
{ "orderId": "一意のオーダーID", "status": "pending", "authorizations": ["{authorizationUrl1}"], // /api/acme/authz/{authzId} "finalize": "{finalizeUrl}" // /api/acme/finalize/{orderId} }
- リクエストヘッダー:
- GET
/api/acme/authz/{authzId}
- レスポンス:
{ "identifier": { "type": "dns", "value": "ドメイン名" }, "status": "pending", // or "valid" "challenges": [ { "type": "http-01", "url": "{challengeUrl}", // /api/acme/challenge/{challengeId} "token": "ランダムなトークン文字列" } // dns-01 はオプション ] }
- レスポンス:
- POST
/api/acme/challenge/{challengeId}
(クライアントがチャレンジ準備完了を通知)- リクエストボディ:
{}
(空でOK) - レスポンス: チャレンジの現在のステータスを返す (サーバー側で検証処理を実行するきっかけ)
- リクエストボディ:
- POST
/api/acme/finalize/{orderId}
- リクエストボディ:
{ "csr": "PEM形式のCSR文字列" }
- レスポンス:
{ "status": "processing", // or "valid" if already processed "certificate": "{certificateUrl}" // /api/acme/certificate/{certId} (証明書取得URL、statusがvalidの場合) }
- リクエストボディ:
- GET
/api/acme/certificate/{certId}
- レスポンス: PEM形式の証明書文字列 (text/plain)
- POST
-
ドメイン検証 (HTTP-01 チャレンジのシミュレーション)
- ACMEサーバーはチャレンジとしてトークンと期待されるパス (
/.well-known/acme-challenge/{token}
) をクライアントに提示。 - UI側 (ACMEクライアントシミュレーター): ユーザーが「トークンを指定パスに配置しました」というボタンをクリックすると、対応するチャレンジIDの検証フラグをDB上でONにする。
- ACMEサーバーは、チャレンジのステータス確認時またはクライアントからの準備完了通知時に、このフラグを見て検証成功とみなす。
- ACMEサーバーはチャレンジとしてトークンと期待されるパス (
-
ACMEクライアント操作UI
-
アカウント作成:
- 「ACMEアカウント作成」ボタン。内部でキーペアを生成 (表示はしない) し、公開鍵を
/new-account
に送信。アカウントIDを保持。
- 「ACMEアカウント作成」ボタン。内部でキーペアを生成 (表示はしない) し、公開鍵を
-
証明書発行フロー:
- ドメイン名を入力し、「証明書発行開始 (New Order)」ボタン。
/new-order
にリクエスト。 - サーバーから返された
authorizations
URL にアクセスし、チャレンジ情報 (トークン、期待されるパス) を表示。 - 「HTTP-01チャレンジ準備完了」ボタン。
/challenge/{challengeId}
にリクエスト。- (このボタン押下で、裏では「サーバーが外部からトークンを確認できた」というシミュレーションを行う)
- サーバーがチャレンジを
valid
と判断したら (ポーリング or 通知)、CSR (クライアント側で生成またはユーザー入力) を用意。 - 「CSR送信 (Finalize)」ボタン。
/finalize/{orderId}
にCSRを送信。 - サーバーが証明書を発行したら (ポーリング or 通知)、
certificate
URL から証明書を取得し表示。ダウンロードできるようにする。
- ドメイン名を入力し、「証明書発行開始 (New Order)」ボタン。
-
アカウント作成:
lib/db.ts
)
3. データモデル (SQLite - -
TraditionalCertificates
: 手動発行された証明書-
id
(INTEGER PRIMARY KEY AUTOINCREMENT) -
commonName
(TEXT) -
organizationName
(TEXT) -
countryCode
(TEXT) -
publicKeyPem
(TEXT) -
certificatePem
(TEXT) -
serialNumber
(TEXT UNIQUE) -
issuedAt
(DATETIME) -
expiresAt
(DATETIME) -
status
(TEXT DEFAULT 'valid') -- 'valid', 'revoked' -
issuedBy
(TEXT) -- ルートCAの識別子など
-
-
AcmeAccounts
: ACMEクライアントのアカウント-
id
(TEXT PRIMARY KEY) -- アカウントID (UUIDなど) -
publicKeyJwk
(TEXT) -- クライアントの公開鍵 (JWK形式文字列) -
createdAt
(DATETIME)
-
-
AcmeOrders
: ACME証明書発行オーダー-
id
(TEXT PRIMARY KEY) -- オーダーID (UUIDなど) -
accountId
(TEXT, FOREIGN KEY REFERENCES AcmeAccounts(id)) -
domain
(TEXT) -
status
(TEXT DEFAULT 'pending') -- 'pending', 'ready', 'processing', 'valid', 'invalid' -
expires
(DATETIME) -- Orderの有効期限 -
certificateId
(TEXT UNIQUE NULL) -- 発行された証明書のID (AcmeCertificates.id) -
finalizeUrl
(TEXT) -- /api/acme/finalize/{orderId} -
createdAt
(DATETIME)
-
-
AcmeAuthorizations
: ACMEドメイン認証-
id
(TEXT PRIMARY KEY) -- 認証ID (UUIDなど) -
orderId
(TEXT, FOREIGN KEY REFERENCES AcmeOrders(id)) -
domain
(TEXT) -
status
(TEXT DEFAULT 'pending') -- 'pending', 'valid', 'invalid' -
expires
(DATETIME) -- Authorizationの有効期限
-
-
AcmeChallenges
: ACMEチャレンジ-
id
(TEXT PRIMARY KEY) -- チャレンジID (UUIDなど) -
authorizationId
(TEXT, FOREIGN KEY REFERENCES AcmeAuthorizations(id)) -
type
(TEXT) -- 'http-01' -
status
(TEXT DEFAULT 'pending') -- 'pending', 'processing', 'valid', 'invalid' -
token
(TEXT) -- http-01用トークン -
validatedAt
(DATETIME NULL) -- (シミュレーション用) UIで「準備完了」を押した時刻
-
-
AcmeCertificates
: ACME経由で発行された証明書-
id
(TEXT PRIMARY KEY) -- 証明書ID (UUIDなど) -
orderId
(TEXT, FOREIGN KEY REFERENCES AcmeOrders(id)) -
csrPem
(TEXT) -
certificatePem
(TEXT) -
serialNumber
(TEXT UNIQUE) -
issuedAt
(DATETIME) -
expiresAt
(DATETIME) -
status
(TEXT DEFAULT 'valid') -- 'valid', 'revoked'
-
4. 技術スタック
- 言語: TypeScript
- フレームワーク: Next.js (App Router)
- DB: SQLite
- DBアクセス: better-sqlite3
- API: Next.js Route Handlers
- スタイリング: Tailwind CSS
- コード品質: Biome
作業順序と主要ファイル
-
プロジェクト初期化 (0.5h)
-
template
からday46_acme_ca_simulator
をコピー。 -
package.json
のname
を変更。 -
README.md
にこの仕様の概要を記載。 -
PROGRESS.md
に作業工程を記載。 - 基本レイアウト作成 (
app/layout.tsx
,app/page.tsx
の初期設定)。 -
app/globals.css
にTailwindCSS import。
-
-
データモデリングとDB設定 (1.5h)
-
lib/db.ts
: 上記のテーブルスキーマをinitializeSchema
に定義。 - DBユーティリティ関数 (CRUD操作ヘルパーなど) を適宜作成。
-
db/dev.db
を削除してスキーマ反映。
-
-
CA管理機能実装 (3h)
-
UIコンポーネント (
components/ca/
)-
CertificateRequestForm.tsx
: 手動CSR発行フォーム。 -
CertificateList.tsx
: 発行済み証明書一覧 (手動・ACME共通で使えるように)。 -
CertificateDetails.tsx
: 証明書詳細表示。 -
RevocationList.tsx
: CRL表示。
-
-
API (
app/api/ca/
)-
POST /api/ca/issue-certificate
: 手動CSR発行処理。 -
GET /api/ca/certificates
: 発行済み証明書一覧取得。 -
PUT /api/ca/certificates/{serial}/revoke
: 証明書失効処理。 -
GET /api/ca/crl
: CRL取得。
-
-
ページ (
app/(pages)/ca-management/page.tsx
): 上記コンポーネントを配置。 - ルートCAの概念を
lib/ca.ts
などに定義 (秘密鍵、証明書のハードコードまたは起動時生成)。
-
UIコンポーネント (
-
ACMEサーバー API実装 (4h)
-
app/api/acme/[...slug]/route.ts
: 上記ACMEエンドポイント仕様に基づき実装。- アカウント作成、オーダー作成、認証情報取得、チャレンジ応答、証明書発行要求、証明書取得のロジック。
- 各処理でDBへの適切な読み書きを行う。
- 暗号処理は簡略化 (例: JWS検証はスキップ、CSRの署名は行う)。
- チャレンジ検証はDBのフラグ (
AcmeChallenges.validatedAt
) を見る。
-
-
ACMEクライアント UI実装 (3h)
-
UIコンポーネント (
components/acme/
)-
AcmeClientDashboard.tsx
: ACME操作のメインUI。 -
AccountCreator.tsx
: アカウント作成ボタンと状態表示。 -
OrderForm.tsx
: ドメイン入力とオーダー開始。 -
ChallengeDisplay.tsx
: チャレンジ情報表示と「準備完了」ボタン。 -
CsrSubmitter.tsx
: CSR入力とFinalizeボタン。 -
CertificateDownloader.tsx
: 発行された証明書表示とダウンロード。
-
-
ページ (
app/(pages)/acme-client/page.tsx
): 上記コンポーネントを配置し、ACMEフローを制御。- 状態管理 (Zustandなど簡易的なものでも可、またはReact Context)。
- 各ステップで対応するACME APIを呼び出す。
-
UIコンポーネント (
-
主要業務フロー実装とテスト (2.5h)
- 手動での証明書発行・一覧表示・失効・CRL表示フローのテスト。
- ACMEクライアントUIからのアカウント作成、オーダー、チャレンジ、証明書発行、証明書取得フローのテスト。
- APIエンドポイントを
curl
などで個別にテスト。 - 発行された証明書が証明書一覧に反映されることを確認。
-
ドキュメント作成と仕上げ (0.5h)
-
README.md
の更新 (使い方、API仕様の概要など)。 -
.cursor/rules/knowledge.mdc
に今回のアプリ情報を追記。 - 不要なファイルやコードの削除。
-
合計見積もり時間: 15時間 (複数日にまたがる可能性あり)
各ファイルのおおよその行数:
各コンポーネント、APIルートファイルは300行以内を目指します。必要に応じてファイルを分割します。
lib/db.ts
はスキーマ定義が主なので長くなる可能性がありますが、ロジックはシンプルに保ちます。
テストについて:
- 各APIエンドポイント実装後、
curl
や Postman のようなツールで基本的な動作確認を行います。 - UIコンポーネント実装後、ブラウザで手動テストを行い、期待通りに動作することを確認します。
- 特にACMEフローはステップが多く、状態遷移が複雑なため、各ステップでのAPI通信とUIの反応を丁寧に確認します。
- PlaywrightによるE2Eテストは、主要なフロー (手動発行、ACME発行) に対して作成します。
コミット戦略:
上記の作業順序の各項目完了時にコミットします。 (例: day46: step 2/7 Data modeling and DB setup
)
PROGRESS.md も都度更新します。
この仕様と作業順序で進めてよろしいでしょうか? 特に簡略化する部分 (暗号処理、外部アクセスシミュレーション) について、ご意見があれば調整します。

ACMEとは、Automated Certificate Management Environment の略で、SSL/TLS証明書の取得、更新、失効といった管理作業を自動化するためのプロトコルです。
もう少し詳しく説明しますね。
ウェブサイトを安全に運営するためには、HTTPS通信が不可欠です。HTTPS通信を実現するためには、SSL/TLS証明書という電子的な証明書が必要になります。この証明書は、認証局(CA: Certificate Authority)と呼ばれる信頼できる第三者機関によって発行されます。
従来、この証明書の取得や更新は、多くの場合、手作業で行われていました。
- 証明書発行の申請
- ドメイン所有権の確認
- 発行された証明書のサーバーへのインストール
- 有効期限が切れる前の更新作業
これらの作業は手間がかかり、特に多数のウェブサイトを管理している場合には大きな負担となっていました。また、手作業によるミスで証明書の有効期限が切れてしまい、ウェブサイトが表示できなくなるという問題も起こりがちでした。
ACMEプロトコルは、これらの問題を解決するために登場しました。
主な特徴は以下の通りです。
- 自動化: 証明書の申請、ドメイン所有権の検証、証明書の取得とインストール、そして定期的な更新まで、一連のプロセスを自動化できます。
- 標準化: ACMEは標準化されたプロトコル (RFC 8555) であり、様々な認証局やクライアントソフトウェアが対応しています。これにより、特定のベンダーに依存することなく利用できます。
- 無料化の促進: ACMEプロトコルを推進する代表的な認証局である Let's Encrypt は、無料でSSL/TLS証明書を提供しています。ACMEによる自動化と無料化により、HTTPSの普及が大きく進みました。
ACMEプロトコルの基本的な仕組みは以下のようになります。
- ウェブサーバー上で動作するACMEクライアントソフトウェアが、認証局(ACMEサーバー)に対して証明書の発行を要求します。
- 認証局は、クライアントに対してドメインの所有権を確認するためのチャレンジ(課題)を出します。
- 例えば、「指定された情報を、あなたのドメインの特定の場所に配置してください(HTTP-01チャレンジ)」や「指定された情報を、あなたのドメインのDNSレコードに設定してください(DNS-01チャレンジ)」といった具合です。
- クライアントは、そのチャレンジをクリアします。
- 認証局は、チャレンジが正しくクリアされたことを確認し、ドメインの所有権を検証します。
- 検証が成功すると、認証局は証明書を発行し、クライアントに送信します。
- クライアントは、受け取った証明書をウェブサーバーに自動的にインストールします。
- 証明書の有効期限が近づくと、クライアントは同様のプロセスで証明書を自動的に更新します。
現在作成している day46_acme_ca_simulator
は、このACMEプロトコルの一連の流れを体験するためのシミュレーションツールというわけです。
より詳細な情報が必要な場合は、お気軽にお尋ねください。