AT Protocol Cryptography Notes#
JWT Service Authentication#
AT Protocol uses signed JWTs for inter-service authentication. When a service (like a feed generator) receives a request from Bluesky, the request includes a JWT in the Authorization: Bearer <token> header.
JWT Structure#
Required fields:
iss: issuer - the requester's DID (e.g.,did:plc:abc123)aud: audience - the service DID (e.g.,did:web:zig-bsky-feed.fly.dev)exp: expiration - UNIX timestamp (~60 seconds recommended)
Optional fields:
iat: issued at timestamplxm: lexicon method (NSID of the endpoint being called)jti: unique nonce to prevent replay attacks
Signing Algorithms#
The JWT alg header indicates the key type:
ES256Kfor secp256k1 (k256) keysES256for P-256 (p256) keys
The signature is computed over base64url(header).base64url(payload) using the account's signing key (same key used for repo commits).
DID Document and Public Keys#
To verify a JWT signature, the receiving service must:
- Parse the JWT to extract the issuer DID
- Resolve the DID document from plc.directory (for did:plc) or .well-known/did.json (for did:web)
- Extract the public key from the
#atprotoverification method - Verify the signature
Example DID document:
{
"id": "did:plc:abc123",
"verificationMethod": [{
"id": "did:plc:abc123#atproto",
"type": "Multikey",
"publicKeyMultibase": "zQ3sh..."
}]
}
Multibase and Multicodec Encoding#
Public keys are encoded using:
- Multibase: prefix indicating encoding (e.g.,
z= base58btc) - Multicodec: prefix indicating key type (varint encoded)
Multicodec Key Type Prefixes#
Multicodec uses unsigned LEB128 (varint) encoding:
| Key Type | Decimal | Varint Bytes |
|---|---|---|
| secp256k1-pub | 231 (0xe7) | 0xe7 0x01 |
| p256-pub | 4608 (0x1200) | 0x80 0x24 |
Important: Values >= 128 require 2+ bytes in varint encoding. The high bit indicates continuation.
Decoding a Multibase Key#
Example: zQ3shpqvKe7jKxYdPBoJHq9VVNLoqaDkL7GTtsX16eqFKXFjS
- Strip
zprefix (base58btc) - Base58 decode the rest
- First 2 bytes:
0xe7 0x01= secp256k1-pub - Remaining 33 bytes: compressed SEC1 public key (starts with 0x02 or 0x03)
Bug We Fixed#
The original code assumed secp256k1's multicodec prefix was 1 byte (0xe7), but it's actually 2 bytes (0xe7 0x01) because 231 >= 128 requires varint continuation.
This caused signature verification to fail because we passed a 34-byte "key" (with the extra 0x01 byte prepended) instead of the correct 33-byte compressed public key.