mockforge_platform_signing/
signer.rs1use async_trait::async_trait;
10use thiserror::Error;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
19#[serde(rename_all = "kebab-case")]
20pub enum SigningAlgorithm {
21 EcdsaSha256P256,
23 EcdsaSha384P384,
25}
26
27impl SigningAlgorithm {
28 pub fn as_str(self) -> &'static str {
30 match self {
31 Self::EcdsaSha256P256 => "ecdsa-sha256-p256",
32 Self::EcdsaSha384P384 => "ecdsa-sha384-p384",
33 }
34 }
35}
36
37#[async_trait]
44pub trait PlatformSigner: Send + Sync {
45 fn key_id(&self) -> &str;
48
49 fn algorithm(&self) -> SigningAlgorithm;
51
52 async fn public_key_der(&self) -> Result<Vec<u8>, SignerError>;
55
56 async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, SignerError>;
60}
61
62#[derive(Debug, Error)]
64pub enum SignerError {
65 #[error("signer backend error: {0}")]
68 Backend(String),
69
70 #[error("invalid key id: {0}")]
72 InvalidKeyId(String),
73
74 #[error("missing environment variable: {0}")]
76 MissingEnv(&'static str),
77
78 #[error("unexpected public-key encoding from backend: {0}")]
80 UnexpectedPublicKey(String),
81}
82
83pub struct MockSigner {
89 key_id: String,
90 keypair: ring::signature::EcdsaKeyPair,
91 public_key_der: Vec<u8>,
92 algorithm: SigningAlgorithm,
93}
94
95impl std::fmt::Debug for MockSigner {
96 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97 f.debug_struct("MockSigner")
98 .field("key_id", &self.key_id)
99 .field("algorithm", &self.algorithm)
100 .finish_non_exhaustive()
101 }
102}
103
104impl MockSigner {
105 pub fn generate(key_id: impl Into<String>) -> Result<Self, SignerError> {
107 let rng = ring::rand::SystemRandom::new();
108 let pkcs8 = ring::signature::EcdsaKeyPair::generate_pkcs8(
109 &ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING,
110 &rng,
111 )
112 .map_err(|e| SignerError::Backend(format!("ring pkcs8 generate failed: {e}")))?;
113 let keypair = ring::signature::EcdsaKeyPair::from_pkcs8(
114 &ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING,
115 pkcs8.as_ref(),
116 &rng,
117 )
118 .map_err(|e| SignerError::Backend(format!("ring keypair load failed: {e}")))?;
119 let raw_pub = ring::signature::KeyPair::public_key(&keypair).as_ref().to_vec();
123 let public_key_der = wrap_p256_spki(&raw_pub);
124 Ok(Self {
125 key_id: key_id.into(),
126 keypair,
127 public_key_der,
128 algorithm: SigningAlgorithm::EcdsaSha256P256,
129 })
130 }
131}
132
133#[async_trait]
134impl PlatformSigner for MockSigner {
135 fn key_id(&self) -> &str {
136 &self.key_id
137 }
138
139 fn algorithm(&self) -> SigningAlgorithm {
140 self.algorithm
141 }
142
143 async fn public_key_der(&self) -> Result<Vec<u8>, SignerError> {
144 Ok(self.public_key_der.clone())
145 }
146
147 async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, SignerError> {
148 let rng = ring::rand::SystemRandom::new();
149 let sig = self
150 .keypair
151 .sign(&rng, message)
152 .map_err(|e| SignerError::Backend(format!("ring sign failed: {e}")))?;
153 Ok(sig.as_ref().to_vec())
154 }
155}
156
157fn wrap_p256_spki(raw_uncompressed_point: &[u8]) -> Vec<u8> {
161 const ALG_PREFIX: &[u8] = &[
173 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, ];
177 let bitstring_len = 1 + raw_uncompressed_point.len(); let mut bitstring = Vec::with_capacity(2 + bitstring_len);
179 bitstring.push(0x03); bitstring.push(bitstring_len as u8);
181 bitstring.push(0x00); bitstring.extend_from_slice(raw_uncompressed_point);
183 let body_len = ALG_PREFIX.len() + bitstring.len();
184 let mut out = Vec::with_capacity(2 + body_len);
185 out.push(0x30); out.push(body_len as u8);
187 out.extend_from_slice(ALG_PREFIX);
188 out.extend_from_slice(&bitstring);
189 out
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 #[tokio::test]
197 async fn mock_signer_round_trips() {
198 let signer = MockSigner::generate("test-key-1").unwrap();
199 assert_eq!(signer.key_id(), "test-key-1");
200 assert_eq!(signer.algorithm(), SigningAlgorithm::EcdsaSha256P256);
201
202 let msg = b"hello mockforge";
203 let sig = signer.sign(msg).await.unwrap();
204 let pub_der = signer.public_key_der().await.unwrap();
205
206 let raw_point = extract_p256_point_from_spki(&pub_der).expect("valid spki");
210 let pubkey = ring::signature::UnparsedPublicKey::new(
211 &ring::signature::ECDSA_P256_SHA256_ASN1,
212 &raw_point,
213 );
214 pubkey.verify(msg, &sig).expect("signature should verify");
215 }
216
217 #[tokio::test]
218 async fn mock_signer_rejects_tampered_message() {
219 let signer = MockSigner::generate("test-key-2").unwrap();
220 let sig = signer.sign(b"original").await.unwrap();
221 let pub_der = signer.public_key_der().await.unwrap();
222 let raw_point = extract_p256_point_from_spki(&pub_der).unwrap();
223 let pubkey = ring::signature::UnparsedPublicKey::new(
224 &ring::signature::ECDSA_P256_SHA256_ASN1,
225 &raw_point,
226 );
227 assert!(pubkey.verify(b"tampered", &sig).is_err());
228 }
229
230 #[test]
231 fn signing_algorithm_wire_form_is_stable() {
232 assert_eq!(SigningAlgorithm::EcdsaSha256P256.as_str(), "ecdsa-sha256-p256");
236 assert_eq!(SigningAlgorithm::EcdsaSha384P384.as_str(), "ecdsa-sha384-p384");
237 }
238
239 fn extract_p256_point_from_spki(spki: &[u8]) -> Option<Vec<u8>> {
242 const HEADER_LEN: usize = 26; if spki.len() <= HEADER_LEN {
247 return None;
248 }
249 Some(spki[HEADER_LEN..].to_vec())
250 }
251}