HTTP Signatureの解析 RFC9421版

ActivityPubで必須のHTTP Signature解析のためのサブルーチンをRFC9421に対応させた。

たぶん、ActivityPubを自作実装する時の最初のハードルがHTTP Signatureの作成と検証だと思う。

署名の作成や検証にはperlの優秀なモジュールを利用させていただいている。
「Crypt::Perl - Cryptography in pure Perl」

以下は、上記perlのモジュールに渡す署名対象文字列を作るための作業メモ。

「Fediverseに参加するための最低条件2」
↑当サイトの別ページにも何をやってるか書いたけど、こっちでは具体的なperlのコードを。

perlの「%ENV」で取得する環境変数データ


CONTENT_LENGTH	3872
CONTENT_TYPE	application/activity+json
CONTEXT_DOCUMENT_ROOT	/home/users/tokoroten
CONTEXT_PREFIX	
DOCUMENT_ROOT	/home/users/tokoroten
GATEWAY_INTERFACE	CGI/1.1
HTTPS	on
HTTP_ACCEPT	*/*
HTTP_ACCEPT_ENCODING	br, gzip, deflate
HTTP_ACCEPT_LANGUAGE	*
HTTP_CONTENT_DIGEST	sha-256=:p3C3HGJMgCYeUWZdh3/G2m6EPv1rQpQIfkadCrUMalQ=:
HTTP_DATE	Sun, 11 Jan 2026 14:02:13 GMT
HTTP_HOST	tokoroten.doncha.net
HTTP_SEC_FETCH_MODE	cors
HTTP_SIGNATURE	sig1=:jY3kZnZQTuEItQ1p+PzvNHzogyWKAdGjAF+XsWdD+KbGlCnuNZtA1zE/3+awSqWGzOg1zPnGwfsCXJ3g9N39dPQY0KTrRYNgCHrbVLjbRY5ijtRPbq8npBEAom9zKoHSJeKkGBdYA9gf2bzK7OaHm9zDEwfpuhYnlg/5mSZDwPMotYHk+iSQ1XLWWt0PnJofM0h9g2NRUJNrQF7ZM1UHr9CxLXvYnLd1j52qehtAMpTfewkLgSBR7sl14OtnRbRYVzhp+hE9EdSrd6YhrfxUK3QfCJTpb6ik6vru8xRuhS0YlNaE5qzF2GBqR+zmBD4VRqDoqHYcCL2Zap4aKZ4zZugiHNkr+gkrjdL6rjSzPsvK+wwo4YYABWM0+Ct4P7e2Gf47MhNv4f4g3JEGrafqBCobH4cPxM9Rs2FYom6gQNWsWGKUz9wDPlaktRuJmbFI0lKDY3b+eNcoFAgWviw5uFODfY8oIKuT7xDm1IjtMSCHT/oZeNvHJsZg/XRMnV7EhFdEoTTKsYGrj5HD+POki/NQCCcudFAFOvDMEtrGMy7zyIU4vmZz/PDoXgApzO+NiwMsI3BfRURAB+GFTp1FKRVVaoRiCZnCIeen2ABv6HTuipJ0hj76moedySwOe1DP7YdJ630/oLKb7VHoUmp2Ge2TPtrAN5gF/Sox2JB69mU=:
HTTP_SIGNATURE_INPUT	sig1=("@method" "@target-uri" "@authority" "host" "date" "content-digest");alg="rsa-v1_5-sha256";keyid="https://hollo.example.net/username#main-key";created=1768140133
HTTP_USER_AGENT	node
HTTP_X_BACKEND	lolipop.lan
HTTP_X_FORWARDED_FOR	NNN.NNN.NNN.NNN
HTTP_X_FORWARDED_HOST	tokoroten.doncha.net
HTTP_X_FORWARDED_PROTO	https
NSS_SDB_USE_CACHE	YES
PATH	/usr/local/bin:/usr/bin:/bin
QUERY_STRING	re=inbox
REMOTE_ADDR	NNN.NNN.NNN.NNN
REMOTE_PORT	55230
REQUEST_METHOD	POST
REQUEST_SCHEME	https
REQUEST_URI	/inbox
SCRIPT_FILENAME	/home/users/script.cgi
SERVER_ADDR	NNN.NNN.NNN.NNN
SERVER_ADMIN	https://lolipop.jp/support/
SERVER_NAME	tokoroten.doncha.net
SERVER_PORT	443
SERVER_PROTOCOL	HTTP/1.1
SERVER_SIGNATURE	
SERVER_SOFTWARE	Apache
	

ex) 「REQUEST_METHOD」がキー、「POST」がその値

Signature(署名)は「HTTP_SIGNATURE」
署名の対象は「HTTP_SIGNATURE_INPUT」
「HTTP_」や「REQUEST_」とアルファベットが大文字で「HTTP_」「REQUEST_」などプレフィックスもついてる。

【手順その1】

  1. アルファベットをすべて小文字に置換
  2. プレフィックス「http_」「request_」を削除
  3. 「_」を「-」に置換
  4. アルファベット小文字をキーにしたhash(%$h)に値を入れておく

【手順その2】

  1. その1で用意したhashのうち「signature-input」を「;」でバラす
  2. 先頭の(対象リスト)
    例では「("@method" "@target-uri" "@authority" "host" "date" "content-digest")」を半角空白でバラす。
  3. 値についてる「"」は不要なので削除
  4. hash(%$sig)に入れておく

【手順その3】

「(対象リスト)」に記載されている順番通りに文字列として組み立てる

ひとつずつ「キー[: ](コロンと半角空白)値」の文字列にして、最後に全部を「改行」で繋いだ文字列にしたものが、署名の対象文字列となる。

  1. 「@」で始まるものは「Derived Components(派生コンポーネント)」、HTTPの環境変数で取得できるものから組み立てる必要がある(環境変数の名前そのままでは使えない)
    https://datatracker.ietf.org/doc/html/rfc9421
    ↑RFC公式ページに実例が掲載されてます。
  2. それ以外は、その1とその2で用意したhashでキーは同じ(headersのキー=%$hのキー)なのでそのまま%$hの値を入れて文字列作成。
    文字列→「キー[: ](コロンと半角空白)値」
  3. push()を使って順番通りに配列に入れていく。
  4. 最後に「Signature-Input」の識別子(sig1=)を除いたものを「@signature-params」として配列に追加。
    配列の要素を「改行」で繋いだ文字列を作ったら完成
    ↓文字列
    「キー[: ](コロンと半角空白)値[改行]」
    「キー[: ](コロンと半角空白)値[改行]」
    (「Fediverseに参加するための最低条件2」冒頭に記載した別ページに完成形の具体例があります)

%ENVから署名対象をハッシュに設定


sub parse_http_env{
  my $self = shift;
  my $args = shift;

  my $env = $args->{env};
  my $http;
  foreach (keys %{$env}){
    my $k = $_;
    $k =~ s!^HTTP_!!; $k =~ s!^REQUEST_!!; $k =~ tr/A-Z/a-z/; $k =~ s!_!-!g;
    $http->{$k} = $env->{$_};
  }
  my $sig;
  foreach my $k (keys %{$http} ){
    if( $k eq 'signature' ){
      $sig->{sign} = $http->{$k};
    }
    elsif( $k eq 'signature-input' ){
      $sig->{base} = $http->{$k};
    }
  }
  if( ! $sig->{base} ){
    my @buf = split(',', $sig->{sign});
    foreach (@buf){
      my ($k,$v) = split('='); $v =~ s!^"!!; $v =~ s!"$!!;
      if( $k =~ m!keyId!i ){
        $sig->{keyid} = $v;
      }
      elsif( $k =~ m!^alg! ){
        $sig->{alg} = $v;
      }
      elsif( $k =~ m!signature! ){
        $sig->{signature} = $v;
      }
      elsif( $k =~ m!headers! ){
        $sig->{headers} = $v;
      }
    }
    foreach my $k ( split(' ', $sig->{headers}) ){
      $sig->{$k} = $http->{$k};
    }
  }
  else{
    my @buf = split(';', $sig->{base});
    foreach (@buf){
      my($k,$v) = split('='); $v =~ s!^"!!; $v =~ s!"$!!;
      if( $k =~ m!keyid!i ){
        $sig->{keyid} = $v;
      }
      elsif( $k =~ m!alg! ){
        $sig->{alg}  = $v;
      }
      elsif( $k =~ m!created! ){
        $sig->{created} = $v;
      }
      elsif( $k =~ m!sig\d! ){
        $sig->{params} = $v;
      }
    }
    # legacyに合わせて%$sigのsignatureに入れておく
    $sig->{signature} = $http->{'signature'}; $sig->{signature} =~ s!sig\d=:!!; $sig->{signature} =~ s!:$!!;
  }
  return {sig=>$sig, http=>$http};
}
	

署名対象を収めたハッシュから署名対象の文字列を作る


sub _make_signature_base{
  my $self = shift;
  my $args = shift;

  my $base;
  if( $args->{sig}->{headers} ){
    my @buf;
    foreach (split(' ', $args->{sig}->{headers})){
      if( $_ eq '(request-target)' ){
        $args->{http}->{method} =~ tr/A-Z/a-z/;
        push(@buf, sprintf(qq{%s: %s %s},$_, $args->{http}->{method}, $args->{http}->{uri}));
      }
      else{
        push(@buf, sprintf(qq{%s: %s},$_, $args->{http}->{$_}));
      }
    }
    $base = join("\n", @buf);
  }
  else{
    my @buf;
    $args->{sig}->{params} =~ s!^\(!!; $args->{sig}->{params} =~ s!\)$!!;
    my @params = split(' ',$args->{sig}->{params});
    foreach my $k ( @params ){
      $k =~ s!^"!!; $k =~ s!"$!!;
      if($args->{http}->{$k}){
        push(@buf, sprintf(qq{"%s": %s},$k, $args->{http}->{$k}));
        next;
      }
      push(@buf, sprintf(qq{"%s": %s},$k, $args->{http}->{method})) , next if $k eq '@method';
      push(@buf, sprintf(qq{"%s": %s},$k, $args->{http}->{host})), next if $k eq '@authority';
      push(@buf, sprintf(qq{"%s": %s://%s%s},$k, $args->{http}->{scheme},$args->{http}->{host}, $args->{http}->{uri})), next if $k eq '@target-uri';
      push(@buf, sprintf(qq{"%s": %s://%s%s},$k, $args->{http}->{scheme},$args->{http}->{host}, $args->{http}->{uri})), next if $k eq '@request-target';
    }
    if(scalar(@buf) != scalar(@params)){
      printf qq{_make signature base:  buf:%s params:%s\n}, scalar(@buf), scalar(@params);
      return;
    }
    my $sig_base = $args->{sig}->{base}; $sig_base =~ s!sig\d=!!;
    push(@buf, sprintf( qq{"\@signature-params": %s}, $sig_base));
    $base = join("\n", @buf);
  }
  return $base;
}
	

これで署名対象文字列を作ったら、「Signature-Input」にあった「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-17 11:14:01] v1.0.0

Menu