はじめに
RFC 9421 HTTP Message Signatures 仕様の実装である Java ライブラリ『authlete/http-message-signatures』をオープンソースで公開しているのですが、先月、「署名検証用シグネチャベースの再構築手順を示すテストがない」という指摘を受けました (Issue 1)。
諸般の事情で詳細ドキュメントの執筆を先延ばししているため、今のところ実装コードやテストコードを読まないとライブラリの使い方は分かりません。そこで、RFC 9421 の Appendix に載っている HTTP メッセージ署名を検証するテストコード RFC9421Test.java をライブラリに追加しました。
本記事では、このテストコードに沿って HTTP メッセージ署名を検証する手順を見ていきます。
RFC 9421 の概要については Authlete 社のウェブサイトにある文書『標準仕様による徹底的な API 保護』の『HTTP メッセージ署名』をご参照ください。
HTTP メッセージ署名検証手順
Signature HTTP フィールドをパースする
署名された HTTP メッセージには Signature HTTP フィールドが含まれています。RFC 9421 の B.2.4. Signing a Response Using ecdsa-p256-sha256 には次の例が載せられています。
Signature: sig-b24=:wNmSUAhwb5LxtOtOpNa6W5xj067m5hFrj0XQ4fvpaCLx0NK\
ocgPquLgyahnzDnDAUy5eCdlYUEkLIj+32oiasw==:
Signature HTTP フィールドの値のフォーマットはディクショナリ (RFC 8941 Section 3.2) です。個々のキー・バリューの組は、任意のラベルと、バイトシーケンス (RFC 8491 Section 3.3.5) で表現された署名です。
Signature: ラベル=:Base64エンコードされた署名:, ラベル=:Base64エンコードされた署名:, ...
http-message-signatures ライブラリには Signature HTTP フィールドを表すユーティリティクラス SignatureField があるので、それを用いてパースします。
// Signature HTTP フィールドの値
String signatureFieldValue =
"sig-b24=:wNmSUAhwb5LxtOtOpNa6W5xj067m5hFrj0XQ4fvpaCLx0NK" +
"ocgPquLgyahnzDnDAUy5eCdlYUEkLIj+32oiasw==:";
// Signature HTTP フィールドの値をパースする
SignatureField signatureField =
SignatureField.parse(signatureFieldValue);
Signature-Input HTTP フィールドをパースする
署名された HTTP メッセージには Signature-Input HTTP フィールドも含まれています。RFC 9421 の B.2.4. Signing a Response Using ecdsa-p256-sha256 には次の例が載せられています。
Signature-Input: sig-b24=("@status" "content-type" \
"content-digest" "content-length");created=1618884473\
;keyid="test-key-ecc-p256"
Signature-Input HTTP フィールドの値のフォーマットもディクショナリ (RFC 8941 Section 3.2) です。個々のキー・バリューの組は、任意のラベルと、内部リスト (RFC 8941 Section 3.1.1) で表現されたメタデータです。
Signature-Input: ラベル=(コンポーネント識別子群)任意パラメータ群, ラベル=(コンポーネント識別子群)任意パラメータ群, ...
http-message-signatures ライブラリには Signature-Input HTTP フィールドを表すユーティリティクラス SignatureInputField があるので、それを用いてパースします。
// Signature-Input HTTP フィールドの値
String signatureInputFieldValue =
"sig-b24=(\"@status\" \"content-type\" " +
"\"content-digest\" \"content-length\");created=1618884473" +
";keyid=\"test-key-ecc-p256\"";
// Signature-Input HTTP フィールドの値をパースする
SignatureInputField signatureInputField =
SignatureInputField.parse(signatureInputFieldValue);
署名・メタデータのペアを抽出する
署名とメタデータはそれぞれ、Signature HTTP フィールドと Signature-Input HTTP フィールドに分かれて置かれています。ペアとなっている署名とメタデータには同じラベルが付けられています。RFC 9421 B.2.4. の例では、ラベルとして sig-b24 が使われています。
Signature HTTP フィールドと Signature-Input HTTP フィールドの両方を調べ、署名・メタデータのペアを抽出処理は、SignatureEntry クラスの scan メソッドが担います。
Map<String, SignatureEntry> signatureEntries =
SignatureEntry.scan(signatureField, signatureInputField);
scan メソッドが返す Map インスタンスのキーはラベルです。ラベルを指定することで、特定の SignatureEntry インスタンスを取り出すことができます。
SignatureEntry signatureEntry = signatureEntries.get("sig-b24");
SignatureEntry クラスには、ラベル (String)、署名 (byte[])、メタデータ (SignatureMetadata) を表すプロパティがあり、それぞれ、getLabel()、getSignature()、getMetadata() メソッドでアクセスできます。
コンポーネント値を用意する
RFC 9421 B.2. Test Cases のテスト群では、HTTP レスポンスを使うテストでは共通して下記の HTTP メッセージを用います。
HTTP/1.1 200 OK
Date: Tue, 20 Apr 2021 02:07:56 GMT
Content-Type: application/json
Content-Digest: sha-512=:mEWXIS7MaLRuGgxOBdODa3xqM1XdEvxoYhvlCFJ41Q\
JgJc4GTsPp29l5oGX69wWdXymyU0rjJuahq4l5aGgfLQ==:
Content-Length: 23
{"message": "good dog"}
シグネチャベースを構築するのに先立ち、この HTTP メッセージに含まれる HTTP メッセージコンポーネント群 (RFC 9421, 2. HTTP Message Components) にアクセスするための SignatureContext を用意する必要があります。
ComponentValueProvider クラスを利用すると、比較的簡単に SignatureContext インターフェースの実装を用意できます。
class ResponseSignatureContext extends ComponentValueProvider
{
ResponseSignatureContext()
{
setStatus(200);
setHeaders(buildHeaders());
}
private static Map<String, List<String>> buildHeaders()
{
Map<String, List<String>> headers = new LinkedHashMap<>();
headers.put("Date", List.of("Tue, 20 Apr 2021 02:07:56 GMT"));
headers.put("Content-Type", List.of("application/json"));
headers.put("Content-Digest", List.of("sha-512=:mEWXIS7MaLRuGgxOBdODa3xqM1XdEvxoYhvlCFJ41QJgJc4GTsPp29l5oGX69wWdXymyU0rjJuahq4l5aGgfLQ==:"));
headers.put("Content-Length", List.of("23"));
return headers;
}
}
シグネチャベースを構築する
シグネチャベースを構築するためのユーティリティクラス SignatureBaseBuilder に、SignatureContext と SignatureMetadata を与えることで、シグネチャベースを表す SignatureBase インスタンスを生成できます。
SignatureBase signatureBase =
new SignatureBaseBuilder(new ResponseSignatureContext())
.build(signatureEntry.getMetadata());
署名検証鍵を用意する
B.2.4 の HTTP メッセージ署名は B.1.3. Example ECC P-256 Test Key の鍵を用いて生成されています。この鍵を元に署名検証用の鍵を用意します。
// RFC 9421, B.1.3. Example ECC P-256 Test Key の鍵
// (注: ただし "alg" を追加している)
String TEST_KEY_ECC_P256 =
"{\n" +
" \"kty\": \"EC\",\n" +
" \"alg\": \"ES256\",\n" +
" \"crv\": \"P-256\",\n" +
" \"kid\": \"test-key-ecc-p256\",\n" +
" \"d\": \"UpuF81l-kOxbjf7T4mNSv0r5tN67Gim7rnf6EFpcYDs\",\n" +
" \"x\": \"qIVYZVLCrPZHGHjP17CTW0_-D9Lfw0EkjqF7xB4FivA\",\n" +
" \"y\": \"Mc4nN9LTDOBhfoUeg8Ye9WedFRhnZXZJA12Qp0zZ6F0\"\n" +
"}";
// 署名検証用の鍵を用意する
// (注: JWK は、Nimbus JOSE + JWT ライブラリのもの)
JWK verificationKey = JWK.parse(TEST_KEY_ECC_P256).toPublicJWK();
署名検証器を用意する
署名検証器は HttpVerifier インターフェースで表されます。JWK インスタンスが用意できていれば、このインターフェースの実装である JoseHttpVerifier クラスのインスタンスを生成できます。
HttpVerifier verifier = new JoseHttpVerifier(verificationKey);
署名を検証する
SignatureBase クラスの verify メソッドに署名検証器 (HttpVerifier) と署名 (byte[]) を渡し、署名を検証します。署名にパスすれば、戻り値は true になります。
boolean verified = signatureBase.verify(verifier, signatureEntry.getSignature());
おわりに
HTTP メッセージ署名は API を保護する仕組みの一つとして利用できます。2025 年 10 月 29 日に開催したオンライン勉強会『OAuth・OpenID 標準仕様による徹底的な API 保護』でも紹介しておりますので、ご興味があれば録画をご視聴ください。
OAuth・OpenID 標準仕様による徹底的な API 保護 | HTTP メッセージ署名