HTTP Signatureの解析
ActivityPubで必須のHTTP Signature解析のためのサブルーチン
たぶん、ActivityPubを自作実装する時の最初のハードルがHTTP Signatureの作成と検証だと思う。
署名の作成や検証にはperlの優秀なモジュールを利用させていただいている。
「Crypt::Perl - Cryptography in pure Perl」
以下は、上記perlのモジュールに渡す署名対象文字列を作るための作業メモ。
「Fediverseに参加するための最低条件2」
↑当サイトの別ページにも何をやってるか書いたけど、こっちでは具体的なperlのコードを。
perlの「%ENV」で取得する環境変数データ
CONTENT_LENGTH 1796
CONTENT_TYPE application/activity+json
CONTEXT_DOCUMENT_ROOT /home/users/1/lolipop.jp/web
CONTEXT_PREFIX
DOCUMENT_ROOT /home/users/1/lolipop.jp/web
GATEWAY_INTERFACE CGI/1.1
HTTPS on
HTTP_DATE Fri, 09 Jan 2026 05:21:30 GMT
HTTP_DIGEST SHA-256=PaoEWV/zUadTRlY35xURS08pc0cNvWdv/SqFEh10rNg=
HTTP_HOST bookshelf.doncha.net
HTTP_SIGNATURE keyId="https://tokoroten.doncha.net/t2aki#main-key",algorithm="rsa-sha256",headers="(request-target) host date digest content-type",signature="N9ICYILzDulDOtRUquopLvTkm4nd1lGsPgl8OTFvOkL7XlQWkSFGl8lYNUnX2ct3p9IVCP7xhgbGBoBi89Z3MIzIMMiVS1AiEqm6OqZlL6+rM/Sg/3ECkSePMM9rHvWKc38vZSESpRd3ZLDXoNFPcz1h1/RJuqk6018CSj8ZS+xDCZFfLHLwKlQZSqmqd+wPMiIe3o+G/bG2eIofcRMaQnW1+CQ74NSrDVcje/C16c6uPLWFY1J9lBoHQ9C+Mkbk6Uy5eoGslZEOvFHIAAdf9PN1d1R9mZVREaetqGCfdJ4ee9hAlLUkAHTdXRZmdHKzvjNgRBlqJWjkYZWassXXwA=="
HTTP_USER_AGENT wwwlibperl (tokoroten; +https://tokoroten.doncha.net/)
HTTP_X_BACKEND lolipop.lan
HTTP_X_FORWARDED_FOR NNN.NNN.NNN.NNN
HTTP_X_FORWARDED_HOST bookshelf.doncha.net
HTTP_X_FORWARDED_PROTO https
NSS_SDB_USE_CACHE YES
PATH /usr/local/bin:/usr/bin:/bin
QUERY_STRING
REDIRECT_STATUS 200
REDIRECT_URL /librarian/inbox
REMOTE_ADDR NNN.NNN.NNN.NNN
REMOTE_PORT 56200
REQUEST_METHOD POST
REQUEST_SCHEME https
REQUEST_URI /librarian/inbox
SCRIPT_FILENAME /home/users/1/lolipop.jp/web/script.cgi
SCRIPT_NAME /script.cgi
SERVER_ADDR NNN.NNN.NNN.NNN
SERVER_ADMIN https://lolipop.jp/support/
SERVER_NAME bookshelf.doncha.net
SERVER_PORT 443
SERVER_PROTOCOL HTTP/1.1
SERVER_SIGNATURE
SERVER_SOFTWARE Apache
SSL_TLS_SNI bookshelf.doncha.net
UNIQUE_ID aWCQWn3aBn64anct-K1sQQAAAVU
ex) 「HTTP_DATE」がキー、「Fri, 09 Jan 2026 05:21:30 GMT」がその値
Signature(署名)は「HTTP_SIGNATURE」にある。
検証に必要なのは「date」だけど、「HTTP_DATE」とアルファベットが大文字で「HTTP_」というプレフィックスもついてる。
【手順その1】
- アルファベットをすべて小文字に置換
- プレフィックス「http_」「request_」を削除
- 「_」を「-」に置換
- アルファベット小文字をキーにしたhash(%$h)に値を入れておく
【手順その2】
- その1で用意したhashのうち「signature」を「,」でバラす
- バラした要素を「=」でキーと値にバラす
- 値についてる「"」は不要なので削除
- hash(%$sig)に入れておく
【手順その3】
headersから署名対象となる文字列を作る。
たぶん、ここが一番わかりにくいところ(わたしは)
「headers」に記載されている順番通りに文字列として組み立てる
ひとつずつ「キー[: ](コロンと半角空白)値」の文字列にして、最後に全部を「改行」で繋いだ文字列にしたものが、署名の対象文字列となる。
- その2で用意した「signature」のhashの「headers」を「 」(半角空白)でバラす
ex)「(request-target) host date digest content-type」
この例では対象になるのは5つだけど、サーバーによって違うので「headers」の確認は必須。
※ここにある要素名はそのままその1で用意したhashのキーと同じになる。 -
「(request-target)」だけ別扱い
その1で用意したhashの「method」と「uri」の値を半角空白で繋いで値に入れて文字列作成。 -
それ以外は、その1とその2で用意したhashでキーは同じ(headersのキー=%$hのキー)なのでそのまま%$hの値を入れて文字列作成。
文字列→「キー[: ](コロンと半角空白)値」 - push()を使って順番通りに配列に入れていく。
-
最後に配列の要素を「改行」で繋いだ文字列を作ったら完成
↓文字列
「キー[: ](コロンと半角空白)値[改行]」
「キー[: ](コロンと半角空白)値[改行]」
(「Fediverseに参加するための最低条件2」冒頭に記載した別ページに完成形の具体例があります)
sub parse_request{
my $self = shift;
my $args = shift;
my $h;
foreach (keys %ENV){
my $k = $_;
$k =~ tr/A-Z/a-z/; $k =~ s!http_!!; $k =~ s!request_!!; $k =~ s!_!-!g;
$h->{$k} = $ENV{$_};
}
my $sig;
foreach ( split(',', $h->{signature}) ){
my ( $k,$v ) = split('=', $_);
$v =~ s!\"!!g;
$sig->{$k} = $v;
}
my @sign_items;
foreach ( split(' ', $sig->{headers} ) ){
if( $_ eq '(request-target)'){
$h->{method} =~ tr/A-Z/a-z/;
push(@sign_items, sprintf( qq{%s: %s %s},$_ , $h->{method}, $h->{uri} ) );
}
else{
push(@sign_items, sprintf(qq{%s: %s}, $_, $h->{$_}));
}
}
my $sign_key = join("\n", @sign_items);
return {signature=>$sig->{signature}, sign_key=>$sign_key, key_id=>$sig->{keyId} };
}
署名対象文字列を作ってしまえば、後はありがたく使わせていただいているperlのモジュールに渡すだけ。
署名にあった「keyId」に、アカウント情報をリクエストして「公開鍵」を取得。
公開鍵を使って署名と対象文字列の検証をしてもらう。
このサブルーチンは呪文状態で使っていてコピペだけで、こと足りる。
sub verify_signature{
my $self = shift;
my $args = shift;
my $content = $self->get_actpb({url=>$args->{key_id}});
return if( ! $content );
my $json;
eval{
$json = decode_json($content);
};
if( $@ ){
printf qq{ERROR veri JSON %s}, $@;
printf qq{ url : %s}, $args->{key_id};
return;
}
my $pub = $json->{publicKey}->{publicKeyPem};
my $publickey = Crypt::Perl::RSA::Parse::public($pub);
my $decoded = decode_base64($args->{signature});
return $publickey->verify_RS256($args->{sign_key}, $decoded);
}
このへん、しょっちゅう忘れるのでメモ
[2026-01-11 07:34:42] v1.0.1
[2026-01-10 10:18:41] v1.0.0

