Authentication and Security
Security is critical in federated systems. This guide covers how ActivityPub handles authentication, the role of HTTP Signatures, and best practices for securing your implementation.
Security Model Overview
ActivityPub uses a decentralized trust model. There's no central authority — servers verify each other using cryptographic signatures.
HTTP Signatures
HTTP Signatures prove that a request came from a specific actor. They're required for all inbox deliveries.
How Signatures Work
- Sign: Create a signature using your private key
- Send: Include signature in request headers
- Verify: Recipient fetches your public key and verifies
Signature Header Format
Signature: keyId="https://example.com/users/alice#main-key",
algorithm="rsa-sha256",
headers="(request-target) host date digest",
signature="Base64EncodedSignature..."
| Parameter | Description |
|---|---|
keyId | URL of the public key (usually {actor}#main-key) |
algorithm | Signing algorithm (rsa-sha256) |
headers | Which headers are signed |
signature | Base64-encoded signature |
Creating a Signature
const crypto = require('crypto');
function createSignature(privateKey, method, url, headers, body) {
const urlObj = new URL(url);
// Create digest of body (if present)
let digest = null;
if (body) {
const hash = crypto.createHash('sha256').update(body).digest('base64');
digest = `SHA-256=${hash}`;
}
// Build the signing string
const date = new Date().toUTCString();
const signedHeaders = body
? '(request-target) host date digest'
: '(request-target) host date';
const signingString = [
`(request-target): ${method.toLowerCase()} ${urlObj.pathname}`,
`host: ${urlObj.host}`,
`date: ${date}`,
body ? `digest: ${digest}` : null
].filter(Boolean).join('\n');
// Create signature
const signer = crypto.createSign('RSA-SHA256');
signer.update(signingString);
const signature = signer.sign(privateKey, 'base64');
return {
date,
digest,
signature: [
`keyId="${headers.keyId}"`,
'algorithm="rsa-sha256"',
`headers="${signedHeaders}"`,
`signature="${signature}"`
].join(',')
};
}
Verifying a Signature
async function verifySignature(req) {
// Parse the Signature header
const sigHeader = req.headers.signature;
const params = parseSignatureHeader(sigHeader);
// Fetch the public key
const actor = await fetchActor(params.keyId.split('#')[0]);
const publicKey = actor.publicKey.publicKeyPem;
// Reconstruct the signing string
const signingString = params.headers.split(' ').map(header => {
if (header === '(request-target)') {
return `(request-target): ${req.method.toLowerCase()} ${req.path}`;
}
return `${header}: ${req.headers[header]}`;
}).join('\n');
// Verify
const verifier = crypto.createVerify('RSA-SHA256');
verifier.update(signingString);
return verifier.verify(publicKey, params.signature, 'base64');
}
Key Management
Key Generation
Generate an RSA key pair for each actor:
# Generate private key
openssl genrsa -out private.pem 2048
# Extract public key
openssl rsa -in private.pem -outform PEM -pubout -out public.pem
Or in Node.js:
const { generateKeyPairSync } = require('crypto');
const { publicKey, privateKey } = generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});
Key Format in Actor
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1"
],
"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-----\nMIIBIjANBg...\n-----END PUBLIC KEY-----"
}
}
Key Storage
Private keys must be stored securely. Never expose them in logs, APIs, or client-side code.
Best practices:
- Store encrypted at rest
- Use environment variables or secrets manager
- Rotate keys periodically
- Backup keys securely
Key Rotation
When rotating keys:
- Generate new key pair
- Update actor's
publicKeywith new key - Keep old key available briefly (for in-flight requests)
- Remove old key after 24-48 hours
Content Verification
Object Attribution
Verify that activities come from the actor they claim:
function verifyAttribution(activity) {
// The activity's actor must match the signing key's owner
const signerActor = extractActorFromKeyId(req.headers.signature);
const activityActor = activity.actor;
if (signerActor !== activityActor) {
throw new Error('Actor mismatch');
}
// For Create activities, the object's attributedTo must match
if (activity.type === 'Create') {
const objectAuthor = activity.object.attributedTo;
if (objectAuthor !== activityActor) {
throw new Error('Object author mismatch');
}
}
return true;
}
ID Verification
Object IDs must be on the same domain as the actor:
function verifyObjectId(activity) {
const actorDomain = new URL(activity.actor).hostname;
if (activity.object && activity.object.id) {
const objectDomain = new URL(activity.object.id).hostname;
// Object ID must be on same domain as actor (for Creates)
if (activity.type === 'Create' && objectDomain !== actorDomain) {
throw new Error('Object ID domain mismatch');
}
}
return true;
}
Common Attack Vectors
Spoofed Activities
Attack: Malicious server sends activity claiming to be from another actor.
Defense:
- Verify HTTP Signature
- Check keyId domain matches actor domain
- Fetch actor to confirm key ownership
Replay Attacks
Attack: Attacker captures and re-sends a valid signed request.
Defense:
- Check
Dateheader is within ±5 minutes - Store activity IDs and reject duplicates
function checkFreshness(req) {
const date = new Date(req.headers.date);
const now = new Date();
const diff = Math.abs(now - date);
// Reject if older than 5 minutes
if (diff > 5 * 60 * 1000) {
throw new Error('Request too old');
}
}
Activity Injection
Attack: Sending activities with forged object IDs.
Defense:
- Verify object ID domains match actor domain
- Fetch objects from original source when in doubt
- Don't trust embedded objects for deletion/updates
Content Spoofing
Attack: Claiming to delete/update objects you don't own.
Defense:
- Only accept Delete/Update from original author
- Verify
attributedTomatches actor
Rate Limiting
Protect your server from abuse:
const rateLimit = require('express-rate-limit');
// Global rate limit
app.use('/inbox', rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 100, // 100 requests per minute per IP
message: { error: 'Too many requests' }
}));
// Per-actor rate limit
const actorLimits = new Map();
function checkActorLimit(actor) {
const now = Date.now();
const limit = actorLimits.get(actor) || { count: 0, reset: now + 60000 };
if (now > limit.reset) {
limit.count = 0;
limit.reset = now + 60000;
}
limit.count++;
actorLimits.set(actor, limit);
if (limit.count > 50) {
throw new Error('Actor rate limited');
}
}
Input Validation
Validate all incoming data:
function validateActivity(activity) {
// Required fields
if (!activity.type) throw new Error('Missing type');
if (!activity.actor) throw new Error('Missing actor');
// Type validation
const validTypes = ['Create', 'Update', 'Delete', 'Follow', 'Accept', 'Reject', 'Like', 'Announce', 'Undo'];
if (!validTypes.includes(activity.type)) {
throw new Error('Invalid activity type');
}
// URL validation
if (typeof activity.actor === 'string') {
const url = new URL(activity.actor);
if (url.protocol !== 'https:') {
throw new Error('Actor must be HTTPS URL');
}
}
// Size limits
const size = JSON.stringify(activity).length;
if (size > 1024 * 1024) { // 1MB
throw new Error('Activity too large');
}
return true;
}
Content Sanitization
Sanitize HTML content to prevent XSS:
const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');
const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);
function sanitizeContent(html) {
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['p', 'br', 'a', 'span', 'strong', 'em', 'code', 'pre'],
ALLOWED_ATTR: ['href', 'class', 'rel'],
ALLOW_DATA_ATTR: false
});
}
Security Checklist
Before Launch
- HTTPS only (no HTTP fallback)
- HTTP Signatures implemented
- Rate limiting in place
- Input validation on all endpoints
- Content sanitization for HTML
- Private keys stored securely
- Logging without sensitive data
Ongoing
- Monitor for unusual activity patterns
- Keep dependencies updated
- Review and rotate keys periodically
- Test signature verification regularly
- Maintain block lists for bad actors
Next Steps
- HTTP Signatures Reference - Detailed specification
- Building an Actor - Implement secure actors
- Content Moderation - Handle abuse