Expand description
Custodial signing keys for agent accounts.
This personal data server holds an agent’s signing key the way any hosted
atproto PDS holds keys for its users. Agents never hold long-lived key
material; they ask the server to sign, and the server decides whether to.
SigningKey::to_bytes and SigningKey::from_bytes exist for that
custody, not for handing keys out.
§Why secp256k1 and nothing else
atproto accepts two signing curves, secp256k1 (0xE7) and P-256
(0x1200), but it requires every ECDSA signature to be low-S: of the two
mathematically valid s values for a signature, only the one below half
the curve order is accepted. This removes the trivial malleability where
(r, n - s) verifies just as well as (r, s) and would give the same
record two different valid signatures.
The k256 crate enforces that for us in both directions. Its
SignPrimitive normalizes s before returning a signature, and its
VerifyPrimitive rejects a signature whose s is high. The p256 crate
does neither: it emits whichever s the math produced, so around half of
the signatures a P-256 implementation would produce here are malleable and
a relay would reject them. Getting P-256 right means normalizing by hand on
every sign and re-checking on every verify, with a silent, intermittent,
half-the-time failure as the cost of forgetting once.
So this crate signs with secp256k1 only, and the low-S property is
covered by a test rather than by this paragraph. It can still read a
P-256 key and judge a P-256 signature, which is a different thing: see
secp256r1, where the normalization this crate refuses to do on the way
out is done by hand on the way in. A server that will one day verify
another server’s commit needs both curves; nothing here can mint the
second one.
§Encoding
Public keys leave this crate as did:key:z..., or as the bare multibase
string that goes in a DID document’s publicKeyMultibase. atproto requires
the 33-byte SEC1 compressed point encoding there. Nothing in the stack
compresses on your behalf — k256 will happily hand back a 65-byte
uncompressed point, and jacquard-common accepts both on decode — so
VerifyingKey::to_multibase asks for compression explicitly and
VerifyingKey::from_multibase refuses anything else.
use didbot_key::{SigningKey, VerifyingKey};
let key = SigningKey::generate();
let sig = key.sign(b"record");
assert_eq!(sig.to_bytes().len(), 64);
let did = key.verifying_key().to_did_key();
assert!(did.starts_with("did:key:z"));
VerifyingKey::from_did_key(&did)?.verify(b"record", &sig)?;Modules§
- secp256r1
- P-256 verification, and deliberately no P-256 signing.
Structs§
- Signature
- A secp256k1 ECDSA signature over SHA-256, in 64-byte compact
r || sform. - Signing
Key - A secp256k1 private key held on behalf of an agent.
- Verifying
Key - The public half of a
SigningKey.
Enums§
- KeyError
- Anything that can go wrong handling a key or a signature.
Constants§
- COMPRESSED_
LEN 🔒 - Length of a SEC1 compressed secp256k1 point.
- DID_
KEY_ 🔒PREFIX - URI scheme prefix for the
did:keymethod. - SECP256
K1_ 🔒CODEC - Multicodec code for a secp256k1 public key.