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
- Draft: draft-cavage-http-signatures
- Note: This is a draft spec, but widely implemented in the Fediverse
How It Works
Signature Header Format
Signature: keyId="https://example.com/users/alice#main-key",
algorithm="rsa-sha256",
headers="(request-target) host date digest",
signature="base64EncodedSignature..."
Parameters
| Parameter | Description |
|---|---|
keyId | URL of the public key |
algorithm | Signing algorithm (usually rsa-sha256) |
headers | Space-separated list of signed headers |
signature | Base64-encoded signature |
Signed Headers
Minimum Required
(request-target) host date
Recommended
(request-target) host date digest
Header Meanings
| Header | Description |
|---|---|
(request-target) | Method + path (e.g., post /inbox) |
host | Target hostname |
date | Request timestamp |
digest | SHA-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
- Server-to-Server - Using signatures
- Delivery - Signed delivery
- Security Guide - Best practices