CLI app for developers prototyping atproto functionality
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(identity): add AnySigningKey newtype with ES256/ES256K low-s signing

Mirrors AnyVerifyingKey for the signing side. Provides:
- verifying_key() to derive the public key
- jwt_alg() to get the algorithm identifier (ES256K or ES256)
- sign() to hash and sign a message with low-s normalization
- sign_prehash() to sign a precomputed SHA-256 digest
- signature_to_jws_bytes() to serialize signatures for JWS compact form

Includes 4 unit tests verifying round-trip signing/verification for both
K256 and P256 curves, JWT algorithm selection, and JWS byte serialization.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

authored by

Jack Grigg
Claude Haiku 4.5
and committed by
Tangled
6e562354 3b9286c1

+124
+124
src/common/identity.rs
··· 145 145 } 146 146 } 147 147 148 + /// A private signing key that may be one of several supported curves. 149 + /// 150 + /// Mirrors `AnyVerifyingKey` for the signing side. Signatures produced by 151 + /// `sign` are always low-s normalized to match the atproto convention 152 + /// already established by `AnySignature`. 153 + #[derive(Debug, Clone)] 154 + pub enum AnySigningKey { 155 + /// secp256k1 signing key. 156 + K256(k256::ecdsa::SigningKey), 157 + /// P-256 signing key. 158 + P256(p256::ecdsa::SigningKey), 159 + } 160 + 161 + impl AnySigningKey { 162 + /// Returns the corresponding verifying key. 163 + pub fn verifying_key(&self) -> AnyVerifyingKey { 164 + match self { 165 + AnySigningKey::K256(k) => AnyVerifyingKey::K256(*k.verifying_key()), 166 + AnySigningKey::P256(k) => AnyVerifyingKey::P256(*k.verifying_key()), 167 + } 168 + } 169 + 170 + /// Returns the JWT `alg` header identifier for this key's curve 171 + /// ("ES256K" for secp256k1, "ES256" for P-256). 172 + pub fn jwt_alg(&self) -> &'static str { 173 + match self { 174 + AnySigningKey::K256(_) => "ES256K", 175 + AnySigningKey::P256(_) => "ES256", 176 + } 177 + } 178 + 179 + /// Signs the SHA-256 prehash of `msg` and returns the signature in 180 + /// low-s normalized form. 181 + /// 182 + /// The returned `AnySignature` is guaranteed to satisfy 183 + /// `AnyVerifyingKey::verify_prehash` against the corresponding 184 + /// verifying key when given the same prehash bytes. 185 + pub fn sign(&self, msg: &[u8]) -> AnySignature { 186 + use sha2::{Digest, Sha256}; 187 + let prehash: [u8; 32] = Sha256::digest(msg).into(); 188 + self.sign_prehash(&prehash) 189 + } 190 + 191 + /// Signs a precomputed 32-byte SHA-256 prehash directly. 192 + pub fn sign_prehash(&self, prehash: &[u8; 32]) -> AnySignature { 193 + use k256::ecdsa::signature::hazmat::PrehashSigner as K256PrehashSigner; 194 + use p256::ecdsa::signature::hazmat::PrehashSigner as P256PrehashSigner; 195 + match self { 196 + AnySigningKey::K256(k) => { 197 + // k256's sign_prehash already returns a low-s normalized 198 + // signature (BIP-0062 enforcement is built in). Returns an 199 + // ecdsa::Signature. 200 + let sig: k256::ecdsa::Signature = K256PrehashSigner::sign_prehash(k, prehash) 201 + .expect("SHA-256 output is always 32 bytes"); 202 + AnySignature::K256(sig) 203 + } 204 + AnySigningKey::P256(k) => { 205 + // p256's sign_prehash may return a high-s signature; 206 + // normalize explicitly. 207 + let sig: p256::ecdsa::Signature = P256PrehashSigner::sign_prehash(k, prehash) 208 + .expect("SHA-256 output is always 32 bytes"); 209 + let normalized = sig.normalize_s().unwrap_or(sig); 210 + AnySignature::P256(normalized) 211 + } 212 + } 213 + } 214 + 215 + /// Serializes the signature bytes for JWS compact form: raw `r || s` 216 + /// big-endian concatenation (NOT DER). 217 + /// 218 + /// For both ES256 and ES256K this is a 64-byte fixed-length array. 219 + pub fn signature_to_jws_bytes(sig: &AnySignature) -> [u8; 64] { 220 + match sig { 221 + AnySignature::K256(s) => s.to_bytes().into(), 222 + AnySignature::P256(s) => s.to_bytes().into(), 223 + } 224 + } 225 + } 226 + 148 227 /// A signature that may be one of several supported curves. 149 228 #[derive(Debug, Clone)] 150 229 pub enum AnySignature { ··· 804 883 use k256::ecdsa::SigningKey as K256SigningKey; 805 884 use k256::ecdsa::signature::hazmat::PrehashSigner; 806 885 use p256::ecdsa::SigningKey as P256SigningKey; 886 + use sha2::Digest; 807 887 use std::collections::HashMap; 808 888 809 889 /// Response variant for FakeHttpClient. ··· 1386 1466 } 1387 1467 e => panic!("Expected DidResolutionFailed with status 0, got {e:?}"), 1388 1468 } 1469 + } 1470 + 1471 + #[test] 1472 + fn any_signing_key_k256_round_trip() { 1473 + let key = AnySigningKey::K256(K256SigningKey::from_slice(&[1u8; 32]).expect("valid seed")); 1474 + let vkey = key.verifying_key(); 1475 + let msg = b"test message"; 1476 + let sig = key.sign(msg); 1477 + assert!(vkey.verify_prehash(&[0u8; 32], &sig).is_err()); // Wrong prehash should fail. 1478 + 1479 + // Sign the same message and verify. 1480 + let sig2 = key.sign(msg); 1481 + let hash: [u8; 32] = sha2::Sha256::digest(msg).into(); 1482 + assert!(vkey.verify_prehash(&hash, &sig2).is_ok()); 1483 + } 1484 + 1485 + #[test] 1486 + fn any_signing_key_p256_round_trip() { 1487 + let key = AnySigningKey::P256(P256SigningKey::from_slice(&[2u8; 32]).expect("valid seed")); 1488 + let vkey = key.verifying_key(); 1489 + let msg = b"test message"; 1490 + let sig = key.sign(msg); 1491 + let hash: [u8; 32] = sha2::Sha256::digest(msg).into(); 1492 + assert!(vkey.verify_prehash(&hash, &sig).is_ok()); 1493 + } 1494 + 1495 + #[test] 1496 + fn any_signing_key_jwt_alg() { 1497 + let k256_key = 1498 + AnySigningKey::K256(K256SigningKey::from_slice(&[1u8; 32]).expect("valid seed")); 1499 + let p256_key = 1500 + AnySigningKey::P256(P256SigningKey::from_slice(&[2u8; 32]).expect("valid seed")); 1501 + 1502 + assert_eq!(k256_key.jwt_alg(), "ES256K"); 1503 + assert_eq!(p256_key.jwt_alg(), "ES256"); 1504 + } 1505 + 1506 + #[test] 1507 + fn any_signing_key_signature_to_jws_bytes() { 1508 + let key = AnySigningKey::K256(K256SigningKey::from_slice(&[1u8; 32]).expect("valid seed")); 1509 + let msg = b"test"; 1510 + let sig = key.sign(msg); 1511 + let jws_bytes = AnySigningKey::signature_to_jws_bytes(&sig); 1512 + assert_eq!(jws_bytes.len(), 64); 1389 1513 } 1390 1514 }