An easy-to-host PDS on the ATProtocol, MacOS. Grandma-approved.

docs(crypto): update CLAUDE.md with build_did_plc_genesis_op contracts (MM-89)

authored by malpercio.dev and committed by

Tangled fa15689a 807f6b54

+82 -9
+82 -9
crates/crypto/CLAUDE.md
··· 1 1 # Crypto Crate 2 2 3 - Last verified: 2026-03-12 3 + Last verified: 2026-03-13 4 4 5 5 ## Purpose 6 6 Provides cryptographic primitives for the ezpds workspace: P-256 key generation, ··· 9 9 This is a pure functional core -- no I/O, no database, no config. 10 10 11 11 ## Contracts 12 - - **Exposes**: `generate_p256_keypair() -> Result<P256Keypair, CryptoError>`, `encrypt_private_key(&[u8; 32], &[u8; 32]) -> Result<String, CryptoError>`, `decrypt_private_key(&str, &[u8; 32]) -> Result<Zeroizing<[u8; 32]>, CryptoError>`, `split_secret(&[u8; 32]) -> Result<[ShamirShare; 3], CryptoError>`, `combine_shares(&ShamirShare, &ShamirShare) -> Result<Zeroizing<[u8; 32]>, CryptoError>`, `P256Keypair`, `ShamirShare`, `CryptoError` 13 - - **P256Keypair fields**: `key_id` (full `did:key:z...` URI), `public_key` (multibase base58btc compressed point, no did:key: prefix), `private_key_bytes` (`Zeroizing<[u8; 32]>` -- zeroized on drop) 14 - - **ShamirShare fields**: `index` (u8, 1/2/3 -- not secret), `data` (`Zeroizing<[u8; 32]>` -- zeroized on drop) 15 - - **Encryption format**: `base64(nonce(12) || ciphertext(32) || tag(16))` = 80 base64 chars. Fresh 12-byte nonce from OS RNG per call. 16 - - **did:key format**: P-256 multicodec varint `[0x80, 0x24]` + compressed public key, multibase base58btc encoded 17 - - **CryptoError variants**: `KeyGeneration`, `Encryption`, `Decryption`, `SecretSharing`, `SecretReconstruction` 12 + 13 + ### Public API functions 14 + 15 + **`generate_p256_keypair`** 16 + ```rust 17 + pub fn generate_p256_keypair() -> Result<P256Keypair, CryptoError> 18 + ``` 19 + - Generates a fresh P-256 keypair from OS RNG 20 + - Returns `key_id` (full `did:key:z...` URI), `public_key` (multibase base58btc, no prefix), `private_key_bytes` (zeroized) 21 + 22 + **`encrypt_private_key`** 23 + ```rust 24 + pub fn encrypt_private_key(&[u8; 32], &[u8; 32]) -> Result<String, CryptoError> 25 + ``` 26 + - Encrypts a 32-byte secret with a 32-byte master key using AES-256-GCM 27 + - Fresh 12-byte nonce per call; returns `base64(nonce(12) || ciphertext(32) || tag(16))` (80 base64 chars) 28 + 29 + **`decrypt_private_key`** 30 + ```rust 31 + pub fn decrypt_private_key(&str, &[u8; 32]) -> Result<Zeroizing<[u8; 32]>, CryptoError> 32 + ``` 33 + - Decrypts a base64-encoded ciphertext with a master key 34 + - Returns opaque `CryptoError::Decryption` on all failure modes (no oracle) 35 + 36 + **`split_secret`** 37 + ```rust 38 + pub fn split_secret(&[u8; 32]) -> Result<[ShamirShare; 3], CryptoError> 39 + ``` 40 + - Shamir secret sharing (2-of-3 scheme) with fresh OS RNG polynomial coefficients 41 + - Information-theoretic security: a single share reveals nothing 42 + 43 + **`combine_shares`** 44 + ```rust 45 + pub fn combine_shares(&ShamirShare, &ShamirShare) -> Result<Zeroizing<[u8; 32]>, CryptoError> 46 + ``` 47 + - Reconstructs secret from 2 distinct shares (indices [1,3]) 48 + - Returns `CryptoError::SecretReconstruction` if indices are duplicate or out of range 49 + 50 + **`build_did_plc_genesis_op`** (new, MM-89) 51 + ```rust 52 + pub fn build_did_plc_genesis_op( 53 + rotation_key: &DidKeyUri, // user's root rotation key (rotationKeys[0]) 54 + signing_key: &DidKeyUri, // relay's signing key (rotationKeys[1] + verificationMethods.atproto) 55 + signing_private_key: &[u8; 32], // raw P-256 private key scalar for signing_key 56 + handle: &str, // e.g. "alice.example.com" 57 + service_endpoint: &str, // e.g. "https://relay.example.com" 58 + ) -> Result<PlcGenesisOp, CryptoError> 59 + ``` 60 + - Constructs a signed did:plc genesis operation 61 + - Returns `PlcGenesisOp { did, signed_op_json }` 62 + - `did` matches `^did:plc:[a-z2-7]{24}$`; derived from SHA-256 of CBOR-encoded signed op, base32-lowercase, first 24 chars 63 + - `signed_op_json` is ready to POST to `https://plc.directory/{did}` 64 + - Deterministic: same inputs → same DID (RFC 6979 ECDSA + SHA-256 + base32) 65 + - Errors: `CryptoError::PlcOperation` if `signing_private_key` is an invalid P-256 scalar 66 + 67 + ### Public types 68 + 69 + **`P256Keypair`** 70 + - `key_id`: full `did:key:z...` URI 71 + - `public_key`: multibase base58btc compressed point (no prefix) 72 + - `private_key_bytes`: `Zeroizing<[u8; 32]>` (zeroized on drop) 73 + 74 + **`PlcGenesisOp`** (new, MM-89) 75 + - `did`: `"did:plc:xxxx..."` (28 chars total) 76 + - `signed_op_json`: contains `type`, `rotationKeys`, `verificationMethods`, `alsoKnownAs`, `services`, `prev` (null), `sig` 77 + 78 + **`ShamirShare`** 79 + - `index`: u8 in [1, 3] (not secret) 80 + - `data`: `Zeroizing<[u8; 32]>` (zeroized on drop) 81 + 82 + **`CryptoError`** variants: 83 + - `KeyGeneration`, `Encryption`, `Decryption`, `SecretSharing`, `SecretReconstruction`, `PlcOperation` (new, MM-89) 84 + 85 + ### Format guarantees 86 + 87 + - **did:key**: P-256 multicodec varint `[0x80, 0x24]` + compressed point, multibase base58btc encoded 88 + - **Encryption**: `base64(nonce(12) || ciphertext(32) || tag(16))` = 80 base64 chars; fresh nonce per call 89 + - **did:plc genesis op sig**: base64url (no padding) decoding to exactly 64 bytes (r‖s, big-endian, low-S canonical) 18 90 19 91 ## Dependencies 20 - - **Uses**: p256 (ECDSA/key generation), aes-gcm (AES-256-GCM), multibase (base58btc encoding), rand_core (OS RNG), base64 (storage encoding), zeroize (secret cleanup) 21 - - **Used by**: `crates/relay/` (key generation endpoint) 92 + - **Uses**: p256 (ECDSA/key generation), aes-gcm (AES-256-GCM), multibase (base58btc encoding), rand_core (OS RNG), base64 (storage encoding), zeroize (secret cleanup), ciborium (CBOR serialization for did:plc), data-encoding (base32-lowercase), sha2 (SHA-256), serde/serde_json (struct serialization) 93 + - **Used by**: `crates/relay/` (key generation, did:plc genesis endpoint) 22 94 23 95 ## Invariants 24 96 - Private key bytes are always wrapped in `Zeroizing` -- callers must not copy them into non-zeroizing storage ··· 32 104 ## Key Files 33 105 - `src/lib.rs` - Re-exports public API 34 106 - `src/keys.rs` - P-256 key generation, AES-256-GCM encrypt/decrypt 107 + - `src/plc.rs` - did:plc genesis operation builder (MM-89) 35 108 - `src/shamir.rs` - Shamir Secret Sharing (split/combine, GF(2^8) arithmetic) 36 109 - `src/error.rs` - CryptoError enum