HTTPメッセージに署名をするSignatureヘッダの標準化

HTTPメッセージに署名をするSignatureヘッダを定義する「Signing HTTP Messages」という仕様が提案されています。

HTTPメッセージへの署名は、様々なところで行われていますが、それぞれ独自の方式で行われています。例えば、AWS APIを叩く際に利用する「Signature Version 4 Signing」や、OAuth2.0 DPoPなどでHTTPメッセージへの署名が行われています。

その他にも、WebPackagingで使用される「Signed HTTP Exchanges」といったところでも署名が行われています。

一方でTLSではだめなのかという話もありますが、TLSを用いることでHTTPメッセージの機密性および完全性は保護されますが、TLS終端プロキシなどを経由するとそれ移行の部分で通信の完全性は担保されません。クライアントとサーバ間でHTTPメッセージの完全性を保つために、HTTPメッセージに署名する方法が必要です。

HTTPはProxyなどを通ると、viaヘッダやX-Forwarded-Forヘッダが足されたり、順序が入れわかったり、同名のヘッダが結合されたりされます。そのような場合でもちゃんと署名および検証をできるようにする標準的な署名方式を定義するのが「Signing HTTP Messages」です。

もともとは、Mark Cavage氏によって2013年頃より標準化が進められていましたが、昨年12月にその仕様を基に改めてAnnabelle Backman氏やJustin Richer氏のもと再出発となっています。同名の仕様ですので注意が必要です。背景については、[IETF106の資料]および、[MLの議論]をご覧ください

Signing HTTP Messages

Signing HTTP Messages」の仕様は主に3つの部分に別れます

  • HTTPメッセージの正規化
  • HTTPメッセージの署名および検証
  • 署名のメタデータを付与する方法

今回は正規化方法を説明し、残りふたつは合わせて説明します。

HTTPメッセージの正規化

Proxyによって、HTTPヘッダは順番が入れ替わったり、同名のヘッダが結合されたりします。

その他にも実装によって、大文字小文字の変換、HTTP/1.1とHTTP/2での変換、obs-fold(行頭空白を用いた改行)の追加・削除、などさまざまな変形が行われる可能性があります。

そのため、正規化処理を行います。

例として、obs-foldや同名のヘッダを持つ下記のHTTPレスポンスは

   HTTP/1.1 200 OK
   Server: www.example.com
   Date: Tue, 07 Jun 2014 20:51:35 GMT
   X-OWS-Header:   Leading and trailing whitespace.
   X-Obs-Fold-Header: Obsolete
       line folding.
   X-Empty-Header:
   Cache-Control: max-age=60
   Cache-Control:    must-revalidate

次のようにソート、正規化されます。
f:id:ASnoKaze:20200107005553p:plain

また、HTTPリクエストの場合はメソッドやパスも署名対象であり、リクエストターゲットも正規化されます。その例は次のとおりです。
f:id:ASnoKaze:20200107005836p:plain

HTTPボディはどうするのかというと、digestヘッダを用いてボディのダイジェストをHTTPヘッダに組み入れることでボディも合わせて署名対象に入れることができます。

Signatureヘッダ

Signatureヘッダ

   Signature: keyId="test-key-b", algorithm="rsa-sha256",
       created=1402170695, expires=1402170995,
       headers="(request-target) (created) host date cache-control
           x-emptyheader x-example",
       signature="T1l3tWH2cSP31nfuvc3nVaHQ6IAu9YLEXg2pCeEOJETXnlWbgKtBTa
           XV6LNQWtf4O42V2DZwDZbmVZ8xW3TFW80RrfrY0+fyjD4OLN7/zV6L6d2v7uB
           puWZ8QzKuHYFaRNVXgFBXN3VJnsIOUjv20pqZMKO3phLCKX2/zQzJLCBQvF/5
           UKtnJiMp1ACNhG8LF0Q0FPWfe86YZBBxqrQr5WfjMu0LOO52ZAxi9KTWSlceJ
           2U361gDb7S5Deub8MaDrjUEpluphQeo8xyvHBoNXsqeax/WaHyRYOgaW6krxE
           GVaBQAfA2czYZhEA05Tb38ahq/gwDQ1bagd9rGnCHtAg=="

それぞれのパラメータの意味は次のとおりです

  • keyId: 検証で用いる鍵を示す識別子
  • algorithm: 署名アルゴリズム。デフォルトは”hs2019” (RSASSA-PSS and SHA-512)、 "rsa-sha256" が指定できる
  • created: 署名の作成時刻
  • expires: 失効時刻
  • headers: 署名の対象となるメタデータ及びヘッダを指定する
  • signature: 署名値

署名の対象はheadersで指定された、正規化されたメタデータ及びヘッダ値を改行(\n)で連結したものです。
例えばこんな感じのものが署名アルゴリズムのインプットになります

(request-target): get /foo
(created): 1402170695
host: example.org
date: Tue, 07 Jun 2014 20:51:35 GMT
cache-control: max-age=60, must-revalidate
x-emptyheader:
x-example: Example header with some whitespace.

検証する側は同様に、HTTPメッセージの正規化を行い署名値を計算することによって検証を行います。