HTTP 署名の付加
curl -POST -H "Date: $(date -uR | head -c -6 | sed -e 's/$/GMT/')" -d '{"foo";""bar}' http://ホニャララ
とすると
http://ホニャララ
に
POST / HTTP/1.1
Host: ホニャララ
User-Agent: curl/バージョン
Accept: */*
Date: Fri, 05 Jan 2024 10:38:24 GMT
Content-Length: 13
{"foo";""bar}
というリクエストが送られるわけですが、HTTP 署名が求められるエンドポイントだと受け入れられず 401 などが返ってきます。
ここでは例として、内容のダイジェストと日付を SHA-256 秘密鍵で署名したものを Signature タグにしてヘッダに付けて送ると、(秘密鍵から導出された)公開鍵で検証して受け入れるエンドポイントを仮定します。ActivityPub なんですけど。
まず、秘密鍵・公開鍵を作ります。
openssl genrsa 2048 > private.key
cat private.key | openssl rsa -pubout > public.key
で、秘密鍵・private.key 、公開鍵・public.key が作られます。
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAtv+pCzDqmiLTAWi5c1WcuilPmQ9PrO+tLuYDKuzrMmKzBC6A
2cIa2YGtxH4YQv436PjFi8QroFFjuVtF1AcJfv9+UzV73TIFgIQvst+O3/hlvZXv
ZFzISXj+cusJcrIbYTZEI3685C6AP4O3mLwuskQ8ExG41DwQT3j1kSSJWCnfAUkO
saFA41qOf8OAIVYPquosPxvcHYEyVZvvI2P+ltSF5XF+02Z7PwcoNSAWAFF4iRhH
WmUvGen7WfkVNjlIa+xqKyjYnq4zER0rLHo4FFeMxJ/tTDVcGZVnCU7TjgO1rUFn
Yf74L0fhWnP9pWT8H/dDOAp/0yGU9ODYL5C2CwIDAQABAoIBAAzJMiNxCIM6eam4
inSPf8LWDhSwqC16FYyYT5JZOVms4bsiEKimUj/uOpjnAoTzxC5H622HiFDMPv59
bRSSZUx1R3tC2mOrEg1Xrwl9azsk3N7xMee+P9Q6WvTmjSNxZE5Xf01HlqUOxrEp
X9ORGmYkNFpUu6hAhhc3aVj5x4rcS9GO5Pqs/S9Q20cDLs9S7Ph1h9ZKuiBHi3o/
6uXCDVi/82re64VtyIIfa6GLIZmfXuSiGq4rGV+pjUA6045kkXU6/7VdHysG0yeh
yJDblxXl+ZA2Z7FUaoETMZbuVI79n6VpFvdMtpAVrRqlTBuiMYBj4bSiVRp/lmXT
S9mOKbECgYEA3pxV4cryUYVSsoTs0TF6zcpa7Y9BAGIGdxnJBanVUHn6BPSjwwdS
HyWw/MfgJKM0YCogliro6wkRBBsoo6oSQ9v//Ak4UR2ALV1bXqMtLQN+ZfT0I0mf
ptMz0LTCeeJEhl3jqmf3VWc8WuGYT8j4P8ISQIB03OCsYqCGmMsc2/MCgYEA0nJT
68sCzc5KWx7fhtKwBkIlVvXuZw2gM/MCphGSiCmjws89hscH5VteAqwaXXJN35/u
nhsXbz1anrND2TACkxTYh9RnzlS72WXhp7aiFODSBorLfnAoQC1oUJtU3N4b9Jr3
vfYUDhQ7w0gHTPo04HNJIqrV44eUgrOGJzFaO4kCgYEAoITIpNUroD7r39Bb14i/
TY8hu/U1YOpOUSllu9C4AZzC/TnOsD4iKFWMZupVpPWOOd2Gu+HbLEfQXk6bIlDr
dMU5s+qOEmecWpnb3cT6OWAwj6JvPNbE6Y2X6mG9bhgLY3xmQyVOwuV7LulHZBi9
cXQmpx45pl/XzhKEm76yY6MCgYAw9oiwA7vTRnvmV3iKVdq+mgU2BmJBd9oePBwR
p2UAaS/A05btFMGxi2CEHqbDtySHMx7BimwZZZz/75WJorH2ppL6h2DfkLdkDWBD
NSrg0K4M954A/PdgdzAeEQXdnY/DiYq6l9ZysCJ3fPq13kEPN8N8XWMGXTUgc8Ry
ufjUgQKBgEtmZv4WQCO2WeGkuGe9xSDJmXxhkNdlp0pNCYbtjLLN4yQWIa5xX2Hj
azqGMUPJbYP1b66KSXPQPjs2Fn4QBMkQ1qfipyLl7O1UtLtQqc4ZkfGZk/OyJCYV
O1UkgRpIgRcWtCj2gFmt+PGNNeM3TE+k/BjloX9wHgZKRoqc4VuL
-----END RSA PRIVATE KEY-----
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtv+pCzDqmiLTAWi5c1Wc
uilPmQ9PrO+tLuYDKuzrMmKzBC6A2cIa2YGtxH4YQv436PjFi8QroFFjuVtF1AcJ
fv9+UzV73TIFgIQvst+O3/hlvZXvZFzISXj+cusJcrIbYTZEI3685C6AP4O3mLwu
skQ8ExG41DwQT3j1kSSJWCnfAUkOsaFA41qOf8OAIVYPquosPxvcHYEyVZvvI2P+
ltSF5XF+02Z7PwcoNSAWAFF4iRhHWmUvGen7WfkVNjlIa+xqKyjYnq4zER0rLHo4
FFeMxJ/tTDVcGZVnCU7TjgO1rUFnYf74L0fhWnP9pWT8H/dDOAp/0yGU9ODYL5C2
CwIDAQAB
-----END PUBLIC KEY-----
次に
{"foo";"bar"}
のダイジェストは
echo -n '{"foo";"bar"}' | sha256sum | xxd -r -p | base64
で求まります。echo の -n オプションで '{"foo";"bar"}' の最後に改行を付けないようにするのが大事です。
求まった
PHw9D0DTPKH8GWPj1QRsRdh2ENZSDdlTZvNpNoHn728=
を
Digest: SHA-256=PHw9D0DTPKH8GWPj1QRsRdh2ENZSDdlTZvNpNoHn728=
と、"SHA-256=" を加えてタグにし、ヘッダに付加します。
Signature タグは
Signature: keyId="public.keyを公開している場所(サイトによって色々)",algorithm="rsa-sha256",headers="date digest",signature="署名"
で良いということにします。
問題は署名なのですが、Signature タグの headers="〜〜" に並んでいるように date digest の順に、頭文字を小文字にして 並べ、間を \n でつないだものを秘密鍵で署名したものです。
Date: Fri, 05 Jan 2024 10:38:24 GMT
Digest: SHA-256=PHw9D0DTPKH8GWPj1QRsRdh2ENZSDdlTZvNpNoHn728=
のままならば
echo -en "date: Fri, 05 Jan 2024 10:38:24 GMT\ndigest: SHA-256=PHw9D0DTPKH8GWPj1QRsRdh2ENZSDdlTZvNpNoHn728="
の結果を秘密鍵で署名したものです。echo に -e オプションを付けることで \n を有効にし、-n オプションで最後に改行を付けないようにします。また、文字列はクォーテーションで括らないと -e オプションが上手く発動しません。
実際にはこの文字列を秘密鍵で署名した上で base64 でエンコードしたものを Signature タグの signature="〜〜" の 〜〜 に加えます。
順にパイプでつないで実行すると
echo -en "date: Fri, 05 Jan 2024 10:38:24 GMT\ndigest: SHA-256=PHw9D0DTPKH8GWPj1QRsRdh2ENZSDdlTZvNpNoHn728=" | openssl dgst -binary -sign private.key -sha256 | openssl enc -A -base64
で、返ってくる値は
SD7/LvW9e0c48M37BGWRPKyQg9bDlPCo5/1xq3BV7eG5vxLd5PhinyeWqtpkiVTuROEn0SlFL4kEJncDchnMiVt5+nwlYefzu/3lXeCMA45jXftoZoid9CV/brKTOAHSvZpnaD2B6vjPo0tpBZ1zerDjFhZqbGWtJdYHJfkA7Slgg16LBPwU5Lqwq6lK8ltyMGHrZ5LoId1AjPb0QwJR7Lv0okexOJPgje9h++2bd8PKLprNsnZXHM6lSPVI8WSkCXu1J5fxB747RHVWE6qw1ZlB99SFxGHKWxK++8BFxxVacQa61bHR89N8PuVo7uF1HVtvr0X+5hKbb9ae5FS/xg==
なので、これをそのまま加えた
Signature: keyId="public.keyを公開している場所(サイトによって色々)",algorithm="rsa-sha256",headers="date digest",signature="SD7/LvW9e0c48M37BGWRPKyQg9bDlPCo5/1xq3BV7eG5vxLd5PhinyeWqtpkiVTuROEn0SlFL4kEJncDchnMiVt5+nwlYefzu/3lXeCMA45jXftoZoid9CV/brKTOAHSvZpnaD2B6vjPo0tpBZ1zerDjFhZqbGWtJdYHJfkA7Slgg16LBPwU5Lqwq6lK8ltyMGHrZ5LoId1AjPb0QwJR7Lv0okexOJPgje9h++2bd8PKLprNsnZXHM6lSPVI8WSkCXu1J5fxB747RHVWE6qw1ZlB99SFxGHKWxK++8BFxxVacQa61bHR89N8PuVo7uF1HVtvr0X+5hKbb9ae5FS/xg=="
が Signature タグです。
そのまま curl にします。ただし、「 -H "" 」 のダブルクォーテーションの中に Sifnature タグ内のダブルクォーテーションが入るので、\ でエスケープします。
curl -POST -H "Date: Fri, 05 Jan 2024 10:38:24 GMT" -H "Digest: SHA-256=PHw9D0DTPKH8GWPj1QRsRdh2ENZSDdlTZvNpNoHn728=" -H "Signature: keyId=\"public.keyを公開している場所(サイトによって色々)\",algorithm=\"rsa-sha256\",headers=\"date digest\",signature=\"SD7/LvW9e0c48M37BGWRPKyQg9bDlPCo5/1xq3BV7eG5vxLd5PhinyeWqtpkiVTuROEn0SlFL4kEJncDchnMiVt5+nwlYefzu/3lXeCMA45jXftoZoid9CV/brKTOAHSvZpnaD2B6vjPo0tpBZ1zerDjFhZqbGWtJdYHJfkA7Slgg16LBPwU5Lqwq6lK8ltyMGHrZ5LoId1AjPb0QwJR7Lv0okexOJPgje9h++2bd8PKLprNsnZXHM6lSPVI8WSkCXu1J5fxB747RHVWE6qw1ZlB99SFxGHKWxK++8BFxxVacQa61bHR89N8PuVo7uF1HVtvr0X+5hKbb9ae5FS/xg==\"" -d '{"foo";""bar}' http://ホニャララ
これで送られる
POST / HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.47.0
Accept: */*
Date: Fri, 05 Jan 2024 10:38:24 GMT
Digest: SHA-256=PHw9D0DTPKH8GWPj1QRsRdh2ENZSDdlTZvNpNoHn728=
Signature: keyId="public.keyを公開している場所(サイトによって色々)",algorithm="rsa-sha256",headers="date digest",signature="SD7/LvW9e0c48M37BGWRPKyQg9bDlPCo5/1xq3BV7eG5vxLd5PhinyeWqtpkiVTuROEn0SlFL4kEJncDchnMiVt5+nwlYefzu/3lXeCMA45jXftoZoid9CV/brKTOAHSvZpnaD2B6vjPo0tpBZ1zerDjFhZqbGWtJdYHJfkA7Slgg16LBPwU5Lqwq6lK8ltyMGHrZ5LoId1AjPb0QwJR7Lv0okexOJPgje9h++2bd8PKLprNsnZXHM6lSPVI8WSkCXu1J5fxB747RHVWE6qw1ZlB99SFxGHKWxK++8BFxxVacQa61bHR89N8PuVo7uF1HVtvr0X+5hKbb9ae5FS/xg=="
Content-Length: 13
Content-Type: application/x-www-form-urlencoded
{"foo";""bar}
が HTTP 署名が付加されたリクエストです。
HTTP 署名の検証
ではこのリクエストを、公開されている public.key で検証します。
まずはリクエストに付加された署名、
SD7/LvW9e0c48M37BGWRPKyQg9bDlPCo5/1xq3BV7eG5vxLd5PhinyeWqtpkiVTuROEn0SlFL4kEJncDchnMiVt5+nwlYefzu/3lXeCMA45jXftoZoid9CV/brKTOAHSvZpnaD2B6vjPo0tpBZ1zerDjFhZqbGWtJdYHJfkA7Slgg16LBPwU5Lqwq6lK8ltyMGHrZ5LoId1AjPb0QwJR7Lv0okexOJPgje9h++2bd8PKLprNsnZXHM6lSPVI8WSkCXu1J5fxB747RHVWE6qw1ZlB99SFxGHKWxK++8BFxxVacQa61bHR89N8PuVo7uF1HVtvr0X+5hKbb9ae5FS/xg==
をファイル signature に貼り付けます。
この署名の値は base64 でエンコードされたものなので、検証の前にデコードして戻します。
cat signature | openssl enc -A -d -base64 > decode
このデコードされた decode を、(http://ホニャララに)送られた(はずの)リクエストのヘッダの Signature タグにある通り、Date タグと Digest タグを、頭文字を小文字にして並べ改行させたもの(echo -en "date: Fri, 05 Jan 2024 10:38:24 GMT\ndigest: SHA-256=PHw9D0DTPKH8GWPj1QRsRdh2ENZSDdlTZvNpNoHn728=")と比べます。
echo -en "date: Fri, 05 Jan 2024 10:38:24 GMT\ndigest: SHA-256=PHw9D0DTPKH8GWPj1QRsRdh2ENZSDdlTZvNpNoHn728=" | openssl dgst -sha256 -verify public.key -signature decode
Verified OK
が出れば検証成功です。
シェルスクリプトにする
これで終わりなら楽なのですが、一つ問題があります。
curl の -H で Date タグの値を指定して送りましたが、この方法だと現実に送る時間とのラグが大きすぎます。たとえば Mastodon のインスタンスではタイムラグが 30 秒以内でないと有効とみなされないそうです。(https://blog.joinmastodon.org/2018/06/how-to-implement-a-basic-activitypub-server/ )
なのでシェルスクリプトにして時刻取得・ダイジェストと署名の計算・送信をほぼ同時に行えるようにします。
#!/bin/bash
post_to=$1 # 第一引数で送り先のアドレスを指定
body=$(cat $2) # 第二引数で送る内容が記されたファイルを指定
date_now=$(date -uR | head -c -6 | sed -e 's/$/GMT/') # 今の時刻を取得
digest=$(echo -n "$body" | sha256sum | xxd -r -p | base64) # ダイジェストの計算
sig=$(echo -en "date: $date_now\ndigest: SHA-256=$digest" | openssl dgst -binary -sign private.key -sha256 | openssl enc -A -base64) # 署名の計算
curl -POST -H "Date: $date_now" -H "Digest: SHA-256=$digest" -H "Signature: keyId=\"public.keyを公開している場所(サイトによって色々)\",algorithm=\"rsa-sha256\",headers=\"date digest\",signature=\"$sig\"" -d "$body" "$1"
bash send_req.sh http://ホニャララ 送るファイル
で HTTP 署名付きのリクエストが送れます。
send_req.sh の curl のヘッダに -H "Content-Type: application/activity+json" を加えると、Mastodon 等にも通るようになります。
おまけ HTTP 署名の検証用シェルスクリプト
bash verify.sh 検証対象のファイル
で実行。検証対象を署名した秘密鍵から作られた公開鍵を public.key にしておく。
#!/bin/bash
[ -e 4_verify ] && rm 4_verify
while read -r line; do
[ -z "$line" ] && break
Tag=$(awk '{print $1}' <<< "$line")
tag=$(echo ${Tag,,})
cont=$(cut -d ' ' -f 2- <<< "$line" )
if [ $tag = "signature:" ]; then cont_sig_comma="$cont"; fi # Signature の値を取る
echo "$tag" "$cont" >> 4_verify # ヘッダを、タグを小文字にして4_verifyファイルに
done < $1
echo -e ${cont_sig_comma//,/\\n} > cont_sig
# Signature の値をカンマ区切りで改行してcont_sigファイルに
. ./cont_sig # cont_sig ファイルを読み込み
for head in $headers; do
if [ $head = "(request-target)" ]; then
target=$(grep -m1 "post" 4_verify | cut -d ' ' -f 2)
sig_target+=$(echo "(request-target): post $target\n")
else
sig_target+=$(grep -m1 "$head" 4_verify)"\n"
fi
# headers の値に対応するヘッダのタグの値を \n でつないでいく
# ただし (request-target) の場合はヘッダの POST の行の2番め(指定ディレクトリ)を取得
done
sig_target=$(echo $sig_target | head -c -3) # sig_target から最後の改行文字と改行を除去
echo $signature | openssl enc -A -d -base64 > decode # signature の値をデコード
echo -en $sig_target | openssl dgst -sha256 -verify public.key -signature decode # 検証
HTTP 署名と検証に Dino Chiesa (https://github.com/DinoChiesa ) 氏にご助言と励ましを頂きました。ありがとうございます。