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
83#[async_trait]
91impl PlatformSigner for Box<dyn PlatformSigner> {
92 fn key_id(&self) -> &str {
93 (**self).key_id()
94 }
95
96 fn algorithm(&self) -> SigningAlgorithm {
97 (**self).algorithm()
98 }
99
100 async fn public_key_der(&self) -> Result<Vec<u8>, SignerError> {
101 (**self).public_key_der().await
102 }
103
104 async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, SignerError> {
105 (**self).sign(message).await
106 }
107}
108
109pub struct MockSigner {
115 key_id: String,
116 keypair: ring::signature::EcdsaKeyPair,
117 public_key_der: Vec<u8>,
118 algorithm: SigningAlgorithm,
119}
120
121impl std::fmt::Debug for MockSigner {
122 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123 f.debug_struct("MockSigner")
124 .field("key_id", &self.key_id)
125 .field("algorithm", &self.algorithm)
126 .finish_non_exhaustive()
127 }
128}
129
130impl MockSigner {
131 pub fn generate(key_id: impl Into<String>) -> Result<Self, SignerError> {
133 let rng = ring::rand::SystemRandom::new();
134 let pkcs8 = ring::signature::EcdsaKeyPair::generate_pkcs8(
135 &ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING,
136 &rng,
137 )
138 .map_err(|e| SignerError::Backend(format!("ring pkcs8 generate failed: {e}")))?;
139 let keypair = ring::signature::EcdsaKeyPair::from_pkcs8(
140 &ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING,
141 pkcs8.as_ref(),
142 &rng,
143 )
144 .map_err(|e| SignerError::Backend(format!("ring keypair load failed: {e}")))?;
145 let raw_pub = ring::signature::KeyPair::public_key(&keypair).as_ref().to_vec();
149 let public_key_der = wrap_p256_spki(&raw_pub);
150 Ok(Self {
151 key_id: key_id.into(),
152 keypair,
153 public_key_der,
154 algorithm: SigningAlgorithm::EcdsaSha256P256,
155 })
156 }
157}
158
159#[async_trait]
160impl PlatformSigner for MockSigner {
161 fn key_id(&self) -> &str {
162 &self.key_id
163 }
164
165 fn algorithm(&self) -> SigningAlgorithm {
166 self.algorithm
167 }
168
169 async fn public_key_der(&self) -> Result<Vec<u8>, SignerError> {
170 Ok(self.public_key_der.clone())
171 }
172
173 async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, SignerError> {
174 let rng = ring::rand::SystemRandom::new();
175 let sig = self
176 .keypair
177 .sign(&rng, message)
178 .map_err(|e| SignerError::Backend(format!("ring sign failed: {e}")))?;
179 Ok(sig.as_ref().to_vec())
180 }
181}
182
183fn wrap_p256_spki(raw_uncompressed_point: &[u8]) -> Vec<u8> {
187 const ALG_PREFIX: &[u8] = &[
199 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, ];
203 let bitstring_len = 1 + raw_uncompressed_point.len(); let mut bitstring = Vec::with_capacity(2 + bitstring_len);
205 bitstring.push(0x03); bitstring.push(bitstring_len as u8);
207 bitstring.push(0x00); bitstring.extend_from_slice(raw_uncompressed_point);
209 let body_len = ALG_PREFIX.len() + bitstring.len();
210 let mut out = Vec::with_capacity(2 + body_len);
211 out.push(0x30); out.push(body_len as u8);
213 out.extend_from_slice(ALG_PREFIX);
214 out.extend_from_slice(&bitstring);
215 out
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 #[tokio::test]
223 async fn mock_signer_round_trips() {
224 let signer = MockSigner::generate("test-key-1").unwrap();
225 assert_eq!(signer.key_id(), "test-key-1");
226 assert_eq!(signer.algorithm(), SigningAlgorithm::EcdsaSha256P256);
227
228 let msg = b"hello mockforge";
229 let sig = signer.sign(msg).await.unwrap();
230 let pub_der = signer.public_key_der().await.unwrap();
231
232 let raw_point = extract_p256_point_from_spki(&pub_der).expect("valid spki");
236 let pubkey = ring::signature::UnparsedPublicKey::new(
237 &ring::signature::ECDSA_P256_SHA256_ASN1,
238 &raw_point,
239 );
240 pubkey.verify(msg, &sig).expect("signature should verify");
241 }
242
243 #[tokio::test]
244 async fn mock_signer_rejects_tampered_message() {
245 let signer = MockSigner::generate("test-key-2").unwrap();
246 let sig = signer.sign(b"original").await.unwrap();
247 let pub_der = signer.public_key_der().await.unwrap();
248 let raw_point = extract_p256_point_from_spki(&pub_der).unwrap();
249 let pubkey = ring::signature::UnparsedPublicKey::new(
250 &ring::signature::ECDSA_P256_SHA256_ASN1,
251 &raw_point,
252 );
253 assert!(pubkey.verify(b"tampered", &sig).is_err());
254 }
255
256 #[test]
257 fn signing_algorithm_wire_form_is_stable() {
258 assert_eq!(SigningAlgorithm::EcdsaSha256P256.as_str(), "ecdsa-sha256-p256");
262 assert_eq!(SigningAlgorithm::EcdsaSha384P384.as_str(), "ecdsa-sha384-p384");
263 }
264
265 fn extract_p256_point_from_spki(spki: &[u8]) -> Option<Vec<u8>> {
268 const HEADER_LEN: usize = 26; if spki.len() <= HEADER_LEN {
273 return None;
274 }
275 Some(spki[HEADER_LEN..].to_vec())
276 }
277}