HTTP Signatures in Misskey ActivityPub#
Misskey uses HTTP Signatures to authenticate outbound and inbound ActivityPub requests. All signing primitives live in ApRequestService.ts under packages/backend/src/core/activitypub/. The @peertube/http-signature library handles parsing and verification on the inbound side .
Signing Outbound Requests#
The static class ApRequestCreator exposes two signing methods used throughout the stack:
createSignedPost— signsPOSTrequests (activity delivery). Signed headers:(request-target),date,host,digest.createSignedGet— signsGETrequests (AP object fetching). Signed headers:(request-target),date,host.
Both delegate to the private #signToRequest method, which:
- Builds the signing string via
#genSigningString. - Signs it with RSA-SHA256 using the
slacclibrary'sRsaKeyPair. - Produces a
Signatureheader of the form:
keyId="…",algorithm="rsa-sha256",headers="…",signature="…".
(request-target) Construction#
#genSigningString builds the signing string by iterating over the included headers. For the special (request-target) pseudo-header, it emits (request-target): <method_lowercase> <pathname> — e.g., (request-target): post /inbox — where the pathname comes from new URL(request.url).pathname . All other header names are lower-cased and their values taken verbatim from the request object. The lines are joined with \n.
Digest Header#
For POST requests, a Digest header is computed as SHA-256=<base64(sha256(body))> via createDigest. If not pre-computed by the caller, createSignedPost generates it automatically .
Key Identity#
The keyId is always {instanceUrl}/users/{userId}#main-key . The corresponding RSA keypair is fetched—with Redis + in-memory caching—from UserKeypairService.getUserKeypair().
The injectable ApRequestService wraps ApRequestCreator with NestJS DI, providing signedPost() and signedGet() methods that look up the user's keypair and execute the HTTP request via HttpRequestService.
Verifying Inbound Requests#
Verification is split across two stages:
Stage 1 — HTTP layer (ActivityPubServerService.inbox()):
- Parses the
Signatureheader withhttpSignature.parseRequest(), requiring(request-target),host, anddate. - Rejects requests where
hostis absent or doesn't match the configured hostname . - Requires a
digestheader and validatesSHA-256=<hash>against the raw request body . - On success, enqueues the activity body + parsed signature for async processing .
Stage 2 — Queue layer (InboxProcessorService.process()):
- Resolves the signer's public key via
apDbResolverService.getAuthUserFromKeyId(signature.keyId). - Calls
httpSignature.verifySignature(signature, authUser.key.keyPem). - Also asserts that
authUser.user.uri === activity.actor— the signing key owner must be the declared actor .
LD-Signature Fallback#
If HTTP Signature verification fails but the activity contains an embedded signature object, Misskey falls back to Linked Data Signatures (RsaSignature2017) . Verification is handled by JsonLdService.verifyRsaSignature2017(), which JSON-LD-canonicalizes the activity before verifying. Outbound LD signatures are attached by ApRendererService.attachLdSignature().
Key Files#
| File | Role |
|---|---|
ApRequestService.ts | ApRequestCreator (signing) + ApRequestService (DI wrapper) |
ActivityPubServerService.ts | Stage 1 inbound validation (parse, digest check, enqueue) |
InboxProcessorService.ts | Stage 2 inbound verification (key lookup, signature verify, LD fallback) |
UserKeypairService.ts | Cached keypair retrieval |
JsonLdService.ts | LD-Signature sign/verify |
ApRendererService.ts | attachLdSignature() for outbound LD signatures |