Personal ATProto tools.
1//! # Crypto
2//! Sign messages using the secp256k1 elliptic curve algorithm.
3
4use secp256k1::{Secp256k1, Message, SecretKey, PublicKey};
5use secp256k1::hashes::{sha256, Hash};
6use std::str::FromStr;
7
8use crate::types::{AssignedLabelResponse, RetrievedLabelResponse, SignatureBytes, SignatureEnum};
9
10
11/// Cryptographic signing and verification.
12pub struct Crypto {
13 private_key: SecretKey,
14 _public_key: PublicKey,
15}
16impl Default for Crypto {
17 fn default() -> Self {
18 Self::new()
19 }
20}
21impl Crypto {
22 /// Create a new `Crypto` instance.
23 pub fn new() -> Self {
24 let secp = Secp256k1::new();
25 drop(dotenvy::dotenv().expect("Failed to load .env file"));
26 let private_key_hex = std::env::var("PRIVATE_KEY_HEX").expect("Expected to be able to get a private key from the environment, but failed");
27 let private_key_vec = hex::decode(private_key_hex).expect("Expected to be able to decode a hex string, but failed");
28 let private_key_array: [u8; 32] = private_key_vec.as_slice().try_into().expect("Expected 32 bytes, within curve order, but failed");
29 let private_key = SecretKey::from_slice(&private_key_array).expect("Expected 32 bytes, within curve order, but failed");
30 let public_key = PublicKey::from_secret_key(&secp, &private_key);
31 Self {
32 private_key,
33 _public_key: public_key,
34 }
35 }
36 /// Create a new `Crypto` instance from a slice.
37 pub fn from_slice(slice: &[u8]) -> Self {
38 let secp = Secp256k1::new();
39 let private_key = SecretKey::from_slice(slice).expect("Expected 32 bytes, within curve order, but failed");
40 let public_key = PublicKey::from_secret_key(&secp, &private_key);
41 Self {
42 private_key,
43 _public_key: public_key,
44 }
45 }
46 /// Sign a message.
47 #[expect(clippy::cognitive_complexity)]
48 pub fn sign(&self, label: &mut AssignedLabelResponse) {
49 let secp = Secp256k1::new();
50 let label_for_serialization = RetrievedLabelResponse {
51 cts: label.cts.clone(),
52 neg: label.neg,
53 src: label.src.clone(),
54 uri: label.uri.clone(),
55 val: label.val.clone(),
56 ver: label.ver,
57 };
58 tracing::debug!("Label for serialization: {:?}", label_for_serialization);
59 label.sig = None;
60 // let label_json = serde_json::to_string(&label_for_serialization).unwrap();
61 // let digest = sha256::Hash::hash(msg.as_bytes());
62 // let message = Message::from_digest(digest.to_byte_array());
63 let label_cbor = serde_cbor::to_vec(&label_for_serialization).expect("Expected to be able to serialize a label, but failed");
64 tracing::debug!("Label CBOR: {:?}", label_cbor);
65 // decode the cbor we just made
66 let label_decoded: RetrievedLabelResponse = serde_cbor::from_slice(&label_cbor).expect("Expected to be able to deserialize a label, but failed");
67 tracing::debug!("Label decoded: {:?}", label_decoded);
68 let digest = sha256::Hash::hash(&label_cbor);
69 let message = Message::from_digest(digest.to_byte_array());
70 let sig = secp.sign_ecdsa(&message, &self.private_key);
71 // verify the sig we just made:
72 let verified = secp.verify_ecdsa(&message, &sig, &self._public_key);
73 assert!(verified.is_ok());
74 tracing::debug!("Verified: {:?}", verified);
75 tracing::debug!("Message: {:?}", message);
76 tracing::debug!("Signature: {:?}", sig);
77 tracing::debug!("Public key: {:?}", self._public_key);
78 // let serialized_sig = sig.serialize_der();
79 // return raw 64 byte sig not DER-encoded
80 let serialized_sig: [u8; 64] = sig.serialize_compact();
81 // serialized_sig.to_vec()
82 label.sig = Some(SignatureEnum::Bytes(SignatureBytes::from_bytes(serialized_sig)));
83 }
84 /// Consume a label response and validate the signature.
85 #[expect(clippy::cognitive_complexity)]
86 pub fn validate(&self,
87 label: RetrievedLabelResponse,
88 sig: &str,
89 public_key_string: &str, // multibase-encoded string
90 ) -> bool {
91 tracing::debug!("Retrieved label: {:?}", label);
92 // let public_key_vec = hex::decode(public_key_string).unwrap();
93 // When encoding public keys as strings, the preferred representation uses multibase (with base58btc specifically) and a multicode prefix to indicate the specific key type. By embedding metadata about the type of key in the encoding itself, they can be parsed unambiguously.
94 // The process for encoding a public key in this format is:
95 // Encode the public key curve "point" as bytes. Be sure to use the smaller "compact" or "compressed" representation.
96 // Prepend the appropriate curve multicodec value, as varint-encoded bytes, in front of the key bytes
97 // p256 (compressed, 33 byte key length): p256-pub, code 0x1200, varint-encoded bytes: [0x80, 0x24]
98 // k256 (compressed, 33 byte key length): secp256k1-pub, code 0xE7, varint bytes: [0xE7, 0x01]
99 // Encode the combined bytes with with base58btc, and prefix with a z character, yielding a multibase-encoded string
100 // The decoding process is the same in reverse, using the identified curve type as context.
101 let public_key_string = public_key_string.strip_prefix("z").expect("Expected to be able to strip a prefix, but failed");
102 let public_key_vec = bs58::decode(public_key_string).into_vec().expect("Expected to be able to decode a base58 string, but failed");
103 // // Remove the multicodec prefix
104 // let public_key_vec = public_key_vec[2..].to_vec();
105 // Determine which curve the key is for
106 match public_key_vec[0] {
107 0x80 => {
108 tracing::debug!("p256");
109 // p256
110 },
111 0xE7 => {
112 tracing::debug!("k256");
113 // k256
114 },
115 _ => {
116 panic!("Unknown curve");
117 },
118 };
119 let public_key_vec = public_key_vec[2..].to_vec();
120
121 let public_key_array: [u8; 33] = public_key_vec.as_slice().try_into().expect("Expected 33 bytes, within curve order, but failed");
122 let public_key = PublicKey::from_slice(&public_key_array).expect("Expected 33 bytes, within curve order, but failed");
123 // use of the "low-S" signature variant is required
124 // let secp = Secp256k1::new();
125 let secp = Secp256k1::verification_only();
126 // let label_json = serde_json::to_string(&label).unwrap();
127 // tracing::debug!("Label JSON: {:?}", label_json);
128 let label_cbor = serde_cbor::to_vec(&label).expect("Expected to be able to serialize a label, but failed");
129 let digest = sha256::Hash::hash(&label_cbor);
130 tracing::debug!("Digest: {:?}", digest);
131 let message = Message::from_digest(digest.to_byte_array());
132 tracing::debug!("Signature: {:?}", sig);
133 let sig = SignatureBytes::from_str(sig).expect("Expected to be able to parse a signature from a string, but failed");
134 tracing::debug!("Signature bytes: {:?}", sig);
135 let signature = secp256k1::ecdsa::Signature::from_compact(&sig.as_vec()).expect("Expected to be able to parse a signature from a byte array, but failed");
136 tracing::debug!("Message: {:?}", message);
137 tracing::debug!("Signature: {:?}", signature);
138 tracing::debug!("Public key: {:?}", public_key);
139 secp.verify_ecdsa(&message, &signature, &public_key).is_ok()
140 }
141
142}