Public key cryptography is used in several aspects of atproto ("AT Protocol"). This short, informal, not-very-official document clarifies some details of which specific systems, keys, serialization formats, and signing/verification procedures are used.
For folks who are already familiar with atproto, the quick things to note are:
- yes, it is expected for everybody to support two curve types
- take the SHA-256 hash of repo commit nodes before signing/verifying them
- depending on library/language used,
p256
may have quirks with "compressed" encoding - yes, DID documents do require two similar-but-different encodings for public keys (
did:web
andmultibase
)
Two elliptic curves are currently supported in the protocol, with full support in Bluesky's implementations:
p256
elliptic curve: aka “NIST P-256”, akasecp256r1
(note ther
), akaprime256v1
- NOTE: supported in WebCrypto
k256
elliptic curve: aka “NIST K-256”, akasecp256k1
(note thek
)- NOTE: used in Bitcoin, other cryptocurrencies. Not supported in in WebCrypto
- NOTE: Bluesky's Typescript PDS implementation defaults to this curve type
Because of the subtle visual distinction when the full curve names are written out, we often refer to them as p256
or k256
. It can be helpful to add comments in source code or clarifications in discussions to reduce confusion.\
Key points for both systems have loss-less "compressed" representations, which are useful when sharing the public keys. This is usually supported natively for k256
, but sometimes requires extra methods or jumping through hoops for p256
. You can read more about this at:
02, 03 or 04? So What Are Compressed and Uncompressed Public Keys?.
The signing key indicated in the DID doc is used for signing and verification of repo "commit nodes". These are IPLD objects, encoded in DAG-CBOR.
The actual bytes used for signing (and verification) are the SHA-256 of the DAG-CBOR representation of the un-signed version of the commit node. To generate these bytes:
- if starting with a signed "commit node", create an "unsigned commit node" which has all the fields except for the
sig
field. this should not be an object (CBOR, struct, etc) with thesig
field set to "null"; the sig field should not exist at all on an "unsigned commit node" - encode the "unsigned commit node" as DAG-CBOR (the strictly-defined subset of CBOR used by IPLD) to get an array of bytes
- take the SHA-256 hash of those bytes, and keep the binary output of the hash function. you should get 32-bytes (256 bits) of SHA-256 output. this should not be a 64-character hex-encoded string
- sign or verify the resulting hash output bytes
The repo signature itself is stored and transmitted as bytes. In DAG-CBOR, this is the "bytes string" type. In other contexts (like a CLI tool printing to screen), you might want to use something like base64.
Note that neither the signature itself nor the "commit node" object indicate either the type of key used (curve type), or the specific public key used. That information must be fetched from the relevant DID document. With key rotation, verification of older commit signatures can become ambiguous. The most recent commit should always be possible to verify using the most recent DID document. When the signing key is rotated, a new commit should have been be created to ensure the signature is verifiable.
Both p256
or k256
can be specified in did:plc operations, and used to sign the genesis block (creating the did:plc itself). Signing and recovery keys can be any combination of the two.
Refer to the DID standard (from the W3C) for details about the overall structure and contents of DID documents.
The W3C-standardized did:key
encoding is used to represent public keys in both did:plc operations and in DID documents. This encoding includes metadata about the type of key, so they can be parsed and used unambiguously. The encoding process is:
- encode the public key “point” as bytes. be sure to use the smaller "compact" or "compressed" representation. This is usually easy for
k256
, but might require extra research forp256
(see above) - prepend the appropriate per-key-type multicode indicator bytes in front of the key bytes:
p256
: [0x80, 0x24]k256
: [0xE7, 0x01]
- encode the combined bytes as
base58btc
, yielding a string - add
did:key:
as a prefix
For inclusion in DID Documents, the keys also need to be encoded in “multibase” format. This goes under the verificationMethod
object, in the field publicKeyMultibase
. The process for "multibase" encoding is:
base58btc
encoding of the key bytes. Do not use the "compressed" or "compact" representation in this context (unlike fordid:key
)- add the character
z
as a prefix, and no other codec indicator
The sibling JSON keys contain the context of which key type is being indicated:
p256
:EcdsaSecp256r1VerificationKey2019
k256
:EcdsaSecp256k1VerificationKey2019
Having multiple curve types supported in the protocol does increase complexity and can lead to bugs compared to a single-allowed-curve system. By supporting multiple curves from the start, we have forced this complexity to be tackled up-front, which should make future migrations easier. But we don't want to proliferate complexity by adding additional curves to the supported set without very persuasive reasons.
We are considering adding support for the Ed25519
elliptic curve at some point. This curve has undergone extensive review and has broad adoption. It is not currently included in WebCrypto. But to make it explicit, Ed25519
is not supported in atproto (or did:plc), as of 2023-05-17.