MightyNetwork::Doc::HTTP-signatures
Documentation about HTTP-signatures.
The ActivityPub specification asked every application implementing the protocol to ensure users’ safety:
prevent identity theft
prevent modifications of sent messages
However, the specification does not suggest a solution for that. The de facto solution is HTTP requests signature.
Here’s the keys to understand and use HTTP signatures.
Every actor will have a public and a private key, provided by its instance.
The keys are created with the RSA algorithm, with a length of 4096 bits.
You need to create the Digest
header before signing your HTTP response.
In order to do that, you need to transform the content of your response to base64, then calculate its SHA256 hashsum.
use Mojo::Util qw(b64_encode);
use Digest::SHA qw(sha256_base64);
my $content = 'foobarbaz';
my $base64 = b64_encode $content;
my $digest = 'SHA-256=' . sha256_base64($base64);
You will sign the concatenation of different headers: request-target
(like POST /foo
), Host
, Date
, Digest
.
See https://tools.ietf.org/id/draft-cavage-http-signatures-10.html#canonicalization for details.
use Mojo::Date;
use Crypt::OpenSSL::RSA;
use Mojo::Util qw(b64_encode);
my $date = Mojo::Date->new(time)->to_string; # HTTP date format
my $host = 'foo.org';
my $concatenated_headers = <<EOF;
(request-target): post /
host: $host
date: $date
digest: $digest
EOF
my $rsa = Crypt::OpenSSL::RSA->new_private_key($actor->private_key)
->use_sha256_hash();
my $signing = b64_encode $rsa->sign($concatenated_headers);
my $header = sprintf('Signature keyId="%s",algorithm="rsa-sha256",headers="request-target host date digest",signature="%s"',
$actor->url.'#main-key', $signing);
Then use $date
, $digest
and $signing
in your request headers (respectively: Date
, Digest
and Authorization
headers).
Luckily, there is module to sign requests more easily: Authen::HTTP::Signature.
use Authen::HTTP::Signature;
use HTTP::Request::Common;
use Mojo::Date;
my $date = Mojo::Date->new(time)->to_string; # HTTP date format
my $url = 'https://foo.org';
my $signer = Authen::HTTP::Signature->new(
key => $actor->private_key;
key_id => $actor->url.'#main-key'
);
my $req = POST($url,
Date => $date,
Digest => $digest,
Content => $body
);
my $signed_req = $signer->sign($req);
Then use $date
, $digest
and $signed_req
in your request headers (respectively: Date
, Digest
and Authorization
headers).
Before verifying the signature of the request, recreate its digest and verify it’s the same as the one sent in the Digest
header.
That’s quite easy: fetch the public key of the actor if you don’t already have it (it’s in the actor object that you got with WebFinger).
Recreate the concatenation of the headers listed in the Authorization
header (the headers
part) and verify the signature with the public key.
use Crypt::OpenSSL::RSA;
use Mojo::Util qw(b64_decode);
my $host = $request->headers->host;
my $date = $request->headers->date;
my $digest = $request->headers->digest;
my $body = $request->body;
my $concatenated_headers = <<EOF;
POST / HTTP/1.1
Host: $host
Date: $date
Digest: $digest
$body
EOF
my $signature = $request->headers->authorization;
$signature =~ s/.*signature="(.*?)".*/$1/;
my $rsa = Crypt::OpenSSL::RSA->new_public_key($actor->public_key);
if ($rsa->verify($concatenated_headers, b64_decode($signature))) {
say "Request is valid!"
} else {
say "Request isn’t valid";
}
Once again, Authen::HTTP::Signature make it easier.
use Authen::HTTP::Signature::Parser;
use HTTP::Request::Common;
my $req = POST($request->url,
Date => $request->headers->date,
Digest => $request->headers->digest,
Authorization => $request->headers->authorization,
Content => $request->body
);
my $p;
try {
$p = Authen::HTTP::Signature::Parser->new($req)->parse();
} catch {
die "Parse failed: $_\n";
};
$p->key($actor->public_key);
if ($p->verify()) {
say "Request is valid!"
} else {
say "Request isn’t valid";
};
MightyNetwork::Doc, Authen::HTTP::Signature, HTTP::Request::Common