Skip to main content

Crate didbot_key

Crate didbot_key 

Source
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 || s form.
SigningKey
A secp256k1 private key held on behalf of an agent.
VerifyingKey
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:key method.
SECP256K1_CODEC 🔒
Multicodec code for a secp256k1 public key.