はじめに
当記事執筆時点では Web アプリケーションフレームワークによる Content-Digest HTTP フィールドの自動検証は一般的ではないので、検証コードを自分で書いていきます。
Content-Digest HTTP フィールド
RFC 9530 Digest Fields の Section 2. The Content-Digest Field で定義されている Content-Digest HTTP フィールドは、メッセージボディのダイジェスト値を含んでいます。このダイジェスト値と、実際のメッセージボディのダイジェスト値が一致しなければ、メッセージボディが改竄されたと判断することができます。
ただし、メッセージボディと Content-Digest HTTP フィールドの内容が同時に改竄された場合、ダイジェスト値の比較だけでは改竄を検出できません。Content-Digest HTTP フィールド改竄の可能性も考慮する場合は、HTTP メッセージ署名 (RFC 9421 HTTP Message Signatures) も併せて利用するなどの追加対策が必要です。
Content-Digest HTTP フィールドの値のフォーマットは RFC 8941 Structured Field Values for HTTP の Section 3.2. Dictionaries で定義されているディクショナリです。ディクショナリの形式は、キー・バリューの組をカンマ区切りで列挙したものです。
sf-dictionary = dict-member *( OWS "," OWS dict-member )
dict-member = member-key ( parameters / ( "=" member-value ))
member-key = key
member-value = sf-item / inner-list
Content-Digest HTTP フィールドの場合、ディクショナリのキーはハッシュアルゴリズムの名前、バリューはそのハッシュアルゴリズムを用いて計算したメッセージボディのダイジェスト値です。下記は RFC 9530 から抜粋した Content-Digest HTTP フィールドの例です。
Content-Digest: \
sha-256=:d435Qo+nKZ+gLcUHn7GQtQ72hiBVAgqoLsZnZPiTGPk=:,\
sha-512=:YMAam51Jz/jOATT6/zvHrLVgOYTGFy1d6GJiOHTohq4yP+pgk4vf2aCs\
yRZOtw8MjkM7iw7yZ/WkppmM44T3qg==:
この例にはキー・バリューの組が二つ含まれています。一つ目のキーは sha-256 で、二つ目のキーは sha-512 です。これらのキーはハッシュアルゴリズムを表しており、有効な値は IANA Hash Algorithms for HTTP Digest Fields に登録されています。
IANA Hash Algorithms for HTTP Digest Fields に登録されているアルゴリズムのうち、本記事執筆時点で Status=Active となっているのは sha-256 と sha-512 のみです。
ダイジェスト値は RFC 8941 Structured Field Values for HTTP の Section 3.3.5. Byte Sequences で定義されているバイトシーケンス形式で表現されます。バイトシーケンスの形式は、データの Base64 表現の両端にコロンを置いたものです。
sf-binary = ":" *(base64) ":"
base64 = ALPHA / DIGIT / "+" / "/" / "="
Content-Digest の検証
Content-Digest の検証処理の簡易実装を下記に示します。処理の流れが分かりやすくなるよう、エラー処理や詳細情報の返却などは行なっていません。
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Map;
import org.greenbytes.http.sfv.ByteSequenceItem;
import org.greenbytes.http.sfv.Dictionary;
import org.greenbytes.http.sfv.ListElement;
import org.greenbytes.http.sfv.Parser;
public class ContentDigestVerification
{
public static void main(String[] args) throws Exception
{
// Content-Digest HTTP フィールドの値 (RFC 9530 Appendix D より)
String contentDigest = """
sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+\
AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:,\
sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:""";
// メッセージボディの値 (RFC 9530 Appendix D より)
byte[] content = "{\"hello\": \"world\"}".getBytes(StandardCharsets.UTF_8);
// Content-Digest を検証する。
boolean passed = verify(contentDigest, content);
// 検証結果を出力する。
System.out.println(passed);
}
private static boolean verify(String contentDigest, byte[] content) throws Exception
{
// Content-Digest HTTP フィールドの値をディクショナリとしてパースする。
Dictionary dictionary = Parser.parseDictionary(contentDigest);
// ディクショナリ内の各キー・バリューの組を一つ一つ処理していく。
for (Map.Entry<String, ListElement<?>> member : dictionary.get().entrySet())
{
// ハッシュアルゴリズム (キー)
String algorithm = member.getKey();
// ダイジェスト値 (バリューをバイトシーケンスとして解釈してからバイト配列として取り出す)
byte[] digest = ((ByteSequenceItem)member.getValue()).get().array();
// ハッシュアルゴリズムを用いてメッセージボディのダイジェスト値を計算する。
byte[] computedDigest = MessageDigest.getInstance(algorithm).digest(content);
// ダイジェスト値が一致しなければ
if (!MessageDigest.isEqual(digest, computedDigest))
{
// 検証をパスできなかった。
return false;
}
}
// 検証をパスした。
return true;
}
}
ソースコード内の org.greenbytes.http.sfv.* クラス群は structured-fields ライブラリのものです。
このプログラムを実行すると、
java -cp structured-fields.jar ContentDigestVerification.java
検証をパスしたことを示す true という文字列が出力されます。
true
おわりに
Content-Digest の仕様 (RFC 9530) と HTTP メッセージ署名の仕様 (RFC 9421) はお互いを参照しており、同時並行で仕様策定作業が進められたことを示唆しています。
FAPI 2.0 Http Signatures はこれらの仕様を土台とし、HTTP メッセージの完全性 (integrity) や真正性 (authenticity) を保証します。詳細については、2024 年のアドベントカレンダーの記事『FAPI 2.0 HTTP Signingの紹介』や、Authlete 社のウェブサイト上の記事『標準仕様による徹底的な API 保護』をご参照ください。