Skip to main content

HTTP Signatures

HTTP Signatures provide authentication for ActivityPub federation. When a server sends an activity to another server's inbox, it signs the request to prove authenticity.

Specification​

How It Works​

1Sender creates signature with private key2Signature included in HTTP Signature header3Receiver fetches sender's public key from actor4Receiver verifies signature5If valid, process request✓ Authenticated

Signature Header Format​

Signature: keyId="https://example.com/users/alice#main-key",
algorithm="rsa-sha256",
headers="(request-target) host date digest",
signature="base64EncodedSignature..."

Parameters​

ParameterDescription
keyIdURL of the public key
algorithmSigning algorithm (usually rsa-sha256)
headersSpace-separated list of signed headers
signatureBase64-encoded signature

Signed Headers​

Minimum Required​

(request-target) host date
(request-target) host date digest

Header Meanings​

HeaderDescription
(request-target)Method + path (e.g., post /inbox)
hostTarget hostname
dateRequest timestamp
digestSHA-256 hash of body

Creating a Signature​

Step 1: Build Signing String​

function buildSigningString(method, path, headers) {
const lines = [];

for (const header of headers) {
if (header === '(request-target)') {
lines.push(`(request-target): ${method.toLowerCase()} ${path}`);
} else {
lines.push(`${header}: ${headers[header]}`);
}
}

return lines.join('\n');
}

Example signing string:

(request-target): post /users/bob/inbox
host: server-b.com
date: Sun, 15 Jan 2024 10:30:00 GMT
digest: SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=

Step 2: Create Signature​

const crypto = require('crypto');

function createSignature(signingString, privateKey) {
const signer = crypto.createSign('RSA-SHA256');
signer.update(signingString);
return signer.sign(privateKey, 'base64');
}

Step 3: Build Signature Header​

function buildSignatureHeader(keyId, signedHeaders, signature) {
return [
`keyId="${keyId}"`,
'algorithm="rsa-sha256"',
`headers="${signedHeaders.join(' ')}"`,
`signature="${signature}"`
].join(',');
}

Complete Signing Function​

const crypto = require('crypto');

function signRequest(method, url, body, privateKey, keyId) {
const urlObj = new URL(url);
const date = new Date().toUTCString();

// Create digest of body
const digestHash = crypto.createHash('sha256').update(body).digest('base64');
const digest = `SHA-256=${digestHash}`;

// Headers to sign
const signedHeaders = ['(request-target)', 'host', 'date', 'digest'];

// Build signing string
const signingString = [
`(request-target): ${method.toLowerCase()} ${urlObj.pathname}`,
`host: ${urlObj.host}`,
`date: ${date}`,
`digest: ${digest}`
].join('\n');

// Create signature
const signer = crypto.createSign('RSA-SHA256');
signer.update(signingString);
const signature = signer.sign(privateKey, 'base64');

// Build header
const signatureHeader = [
`keyId="${keyId}"`,
'algorithm="rsa-sha256"',
`headers="${signedHeaders.join(' ')}"`,
`signature="${signature}"`
].join(',');

return {
Date: date,
Digest: digest,
Signature: signatureHeader
};
}

Verifying a Signature​

Step 1: Parse Signature Header​

function parseSignatureHeader(header) {
const params = {};
const regex = /(\w+)="([^"]+)"/g;
let match;

while ((match = regex.exec(header)) !== null) {
params[match[1]] = match[2];
}

return params;
}

Step 2: Fetch Public Key​

async function fetchPublicKey(keyId) {
// Key ID format: https://example.com/users/alice#main-key
const actorUrl = keyId.split('#')[0];

const response = await fetch(actorUrl, {
headers: { 'Accept': 'application/activity+json' }
});

const actor = await response.json();

// Verify key belongs to actor
if (actor.publicKey.id !== keyId) {
throw new Error('Key not found');
}

return actor.publicKey.publicKeyPem;
}

Step 3: Verify Signature​

async function verifySignature(req) {
const sig = parseSignatureHeader(req.headers.signature);

// Fetch public key
const publicKey = await fetchPublicKey(sig.keyId);

// Rebuild signing string
const signingString = sig.headers.split(' ').map(header => {
if (header === '(request-target)') {
return `(request-target): ${req.method.toLowerCase()} ${req.path}`;
}
return `${header}: ${req.headers[header.toLowerCase()]}`;
}).join('\n');

// Verify
const verifier = crypto.createVerify('RSA-SHA256');
verifier.update(signingString);

const valid = verifier.verify(publicKey, sig.signature, 'base64');

if (!valid) {
throw new Error('Invalid signature');
}

return true;
}

Complete Verification Middleware​

async function verifyHttpSignature(req, res, next) {
try {
// Check required headers
if (!req.headers.signature) {
return res.status(401).json({ error: 'Missing Signature header' });
}

if (!req.headers.date) {
return res.status(401).json({ error: 'Missing Date header' });
}

// Check date freshness (±5 minutes)
const date = new Date(req.headers.date);
const now = new Date();
const diff = Math.abs(now - date);

if (diff > 5 * 60 * 1000) {
return res.status(401).json({ error: 'Request too old' });
}

// Verify digest if present
if (req.headers.digest) {
const body = JSON.stringify(req.body);
const expectedDigest = `SHA-256=${crypto
.createHash('sha256')
.update(body)
.digest('base64')}`;

if (req.headers.digest !== expectedDigest) {
return res.status(401).json({ error: 'Invalid Digest' });
}
}

// Verify signature
await verifySignature(req);

next();
} catch (error) {
res.status(401).json({ error: error.message });
}
}

// Use middleware
app.post('/inbox', verifyHttpSignature, handleInbox);

Key Management​

Generating Keys​

const { generateKeyPairSync } = require('crypto');

function generateActorKeys() {
const { publicKey, privateKey } = generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});

return { publicKey, privateKey };
}

Including Key in Actor​

{
"type": "Person",
"id": "https://example.com/users/alice",
"publicKey": {
"id": "https://example.com/users/alice#main-key",
"owner": "https://example.com/users/alice",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
}
}

Common Issues​

Clock Skew​

Servers reject requests with dates too far from current time.

Solution: Use NTP to sync server clocks.

Algorithm Mismatch​

Some servers only support specific algorithms.

Solution: Use rsa-sha256 (most compatible).

Key Format​

Keys must be in PEM format.

Solution: Verify key encoding is correct.

Path vs Full URL​

The (request-target) should use path only, not full URL.

Correct: (request-target): post /inbox Wrong: (request-target): post https://example.com/inbox

Debugging​

Log Signing String​

console.log('Signing string:', JSON.stringify(signingString));
console.log('Signature:', signature);

Test with curl​

# Generate signature manually and test
curl -X POST https://example.com/inbox \
-H "Date: $(date -u +"%a, %d %b %Y %H:%M:%S GMT")" \
-H "Digest: SHA-256=..." \
-H "Signature: ..." \
-H "Content-Type: application/activity+json" \
-d '{"type": "Follow", ...}'

Next Steps​