mockforge_platform_signing/
verifier.rs1use base64::Engine;
9use chrono::Utc;
10use thiserror::Error;
11
12use crate::rotation::RotationEvent;
13use crate::signer::SigningAlgorithm;
14
15pub fn verify_rotation_event(event: &RotationEvent) -> Result<(), VerifyError> {
30 let payload = &event.payload;
31
32 if payload.version != 1 {
33 return Err(VerifyError::UnsupportedVersion(payload.version));
34 }
35 if payload.transition_until <= payload.issued_at {
36 return Err(VerifyError::MalformedTimestamps);
37 }
38 let now = Utc::now();
41 if payload.issued_at > now + chrono::Duration::minutes(5) {
42 return Err(VerifyError::FutureIssuedAt);
43 }
44
45 let signed_bytes = RotationEvent::signed_bytes(payload)
46 .map_err(|e| VerifyError::Reencoding(format!("{e}")))?;
47
48 let sig_der = base64::engine::general_purpose::STANDARD
49 .decode(event.handover_signature_b64.as_bytes())
50 .map_err(|e| VerifyError::InvalidSignatureBase64(e.to_string()))?;
51
52 let from_spki = base64::engine::general_purpose::STANDARD
53 .decode(payload.from_public_key_b64.as_bytes())
54 .map_err(|e| VerifyError::InvalidPublicKeyBase64(e.to_string()))?;
55
56 let raw_point = extract_p256_or_p384_point(&from_spki)?;
57 let alg: &dyn ring::signature::VerificationAlgorithm = match payload.from_algorithm {
58 SigningAlgorithm::EcdsaSha256P256 => &ring::signature::ECDSA_P256_SHA256_ASN1,
59 SigningAlgorithm::EcdsaSha384P384 => &ring::signature::ECDSA_P384_SHA384_ASN1,
60 };
61 let pubkey = ring::signature::UnparsedPublicKey::new(alg, raw_point);
62 pubkey
63 .verify(&signed_bytes, &sig_der)
64 .map_err(|e| VerifyError::SignatureMismatch(format!("{e}")))?;
65
66 Ok(())
67}
68
69fn extract_p256_or_p384_point(spki: &[u8]) -> Result<Vec<u8>, VerifyError> {
77 if spki.len() < 4 || spki[0] != 0x30 {
87 return Err(VerifyError::MalformedPublicKey("not a SEQUENCE"));
88 }
89 let (_seq_len, after_seq_hdr) = read_der_length(&spki[1..])?;
90 if after_seq_hdr.is_empty() || after_seq_hdr[0] != 0x30 {
92 return Err(VerifyError::MalformedPublicKey("missing AlgorithmIdentifier"));
93 }
94 let (alg_len, after_alg_hdr) = read_der_length(&after_seq_hdr[1..])?;
95 if after_alg_hdr.len() < alg_len {
96 return Err(VerifyError::MalformedPublicKey("truncated AlgorithmIdentifier"));
97 }
98 let after_alg = &after_alg_hdr[alg_len..];
99 if after_alg.is_empty() || after_alg[0] != 0x03 {
101 return Err(VerifyError::MalformedPublicKey("missing BIT STRING"));
102 }
103 let (bs_len, after_bs_hdr) = read_der_length(&after_alg[1..])?;
104 if after_bs_hdr.len() < bs_len || bs_len == 0 {
105 return Err(VerifyError::MalformedPublicKey("truncated BIT STRING"));
106 }
107 if after_bs_hdr[0] != 0 {
110 return Err(VerifyError::MalformedPublicKey("BIT STRING has unused bits"));
111 }
112 Ok(after_bs_hdr[1..bs_len].to_vec())
113}
114
115fn read_der_length(input: &[u8]) -> Result<(usize, &[u8]), VerifyError> {
117 if input.is_empty() {
118 return Err(VerifyError::MalformedPublicKey("missing length octet"));
119 }
120 let first = input[0];
121 if first & 0x80 == 0 {
122 return Ok((first as usize, &input[1..]));
123 }
124 let n_octets = (first & 0x7F) as usize;
125 if n_octets == 0 || n_octets > 4 || input.len() < 1 + n_octets {
126 return Err(VerifyError::MalformedPublicKey("bad long-form length"));
127 }
128 let mut len = 0usize;
129 for &byte in &input[1..=n_octets] {
130 len = (len << 8) | byte as usize;
131 }
132 Ok((len, &input[1 + n_octets..]))
133}
134
135#[derive(Debug, Error)]
137pub enum VerifyError {
138 #[error("unsupported rotation-event version: {0}")]
140 UnsupportedVersion(u32),
141
142 #[error("malformed timestamps: transition_until must be after issued_at")]
144 MalformedTimestamps,
145
146 #[error("issued_at is in the future beyond clock-skew tolerance")]
148 FutureIssuedAt,
149
150 #[error("re-encoding failed: {0}")]
152 Reencoding(String),
153
154 #[error("handover signature is not valid base64: {0}")]
156 InvalidSignatureBase64(String),
157
158 #[error("from public key is not valid base64: {0}")]
160 InvalidPublicKeyBase64(String),
161
162 #[error("from public key is not a valid SubjectPublicKeyInfo: {0}")]
164 MalformedPublicKey(&'static str),
165
166 #[error("handover signature did not verify against from public key: {0}")]
168 SignatureMismatch(String),
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174 use crate::rotation::{RotationEvent, RotationEventPayload, RotationStateMachine};
175 use crate::signer::MockSigner;
176 use chrono::Duration;
177
178 #[tokio::test]
179 async fn happy_path_verifies() {
180 let cur = MockSigner::generate("from").unwrap();
181 let next = MockSigner::generate("to").unwrap();
182 let sm = RotationStateMachine::new(cur);
183 let event = sm.begin_handover(&next, Duration::days(30)).await.unwrap();
184 verify_rotation_event(&event).expect("verifies");
185 }
186
187 #[tokio::test]
188 async fn tampered_to_key_id_fails() {
189 let cur = MockSigner::generate("from").unwrap();
190 let next = MockSigner::generate("to").unwrap();
191 let sm = RotationStateMachine::new(cur);
192 let mut event = sm.begin_handover(&next, Duration::days(30)).await.unwrap();
193 event.payload.to_key_id = "attacker-key".into();
194 let err = verify_rotation_event(&event).unwrap_err();
195 assert!(matches!(err, VerifyError::SignatureMismatch(_)));
196 }
197
198 #[tokio::test]
199 async fn tampered_transition_until_fails() {
200 let cur = MockSigner::generate("from").unwrap();
201 let next = MockSigner::generate("to").unwrap();
202 let sm = RotationStateMachine::new(cur);
203 let mut event = sm.begin_handover(&next, Duration::days(30)).await.unwrap();
204 event.payload.transition_until += Duration::days(365);
205 let err = verify_rotation_event(&event).unwrap_err();
206 assert!(matches!(err, VerifyError::SignatureMismatch(_)));
207 }
208
209 #[tokio::test]
210 async fn replayed_signature_against_different_payload_fails() {
211 let cur = MockSigner::generate("from").unwrap();
214 let next1 = MockSigner::generate("to1").unwrap();
215 let sm = RotationStateMachine::new(cur);
216 let event1 = sm.begin_handover(&next1, Duration::days(30)).await.unwrap();
217 let mut event2 = event1.clone();
221 event2.payload.to_key_id = "to2".into();
222 let err = verify_rotation_event(&event2).unwrap_err();
224 assert!(matches!(err, VerifyError::SignatureMismatch(_)));
225 }
226
227 #[test]
228 fn rejects_version_mismatch() {
229 let payload = RotationEventPayload {
230 version: 99,
231 from_algorithm: SigningAlgorithm::EcdsaSha256P256,
232 from_key_id: "a".into(),
233 from_public_key_b64: "AAAA".into(),
234 to_algorithm: SigningAlgorithm::EcdsaSha256P256,
235 to_key_id: "b".into(),
236 to_public_key_b64: "BBBB".into(),
237 issued_at: Utc::now(),
238 transition_until: Utc::now() + Duration::days(30),
239 };
240 let event = RotationEvent {
241 payload,
242 handover_signature_b64: "AAAA".into(),
243 };
244 let err = verify_rotation_event(&event).unwrap_err();
245 assert!(matches!(err, VerifyError::UnsupportedVersion(99)));
246 }
247
248 #[test]
249 fn rejects_inverted_timestamps() {
250 let now = Utc::now();
251 let payload = RotationEventPayload {
252 version: 1,
253 from_algorithm: SigningAlgorithm::EcdsaSha256P256,
254 from_key_id: "a".into(),
255 from_public_key_b64: "AAAA".into(),
256 to_algorithm: SigningAlgorithm::EcdsaSha256P256,
257 to_key_id: "b".into(),
258 to_public_key_b64: "BBBB".into(),
259 issued_at: now,
260 transition_until: now - Duration::days(1),
261 };
262 let event = RotationEvent {
263 payload,
264 handover_signature_b64: "AAAA".into(),
265 };
266 let err = verify_rotation_event(&event).unwrap_err();
267 assert!(matches!(err, VerifyError::MalformedTimestamps));
268 }
269
270 #[test]
271 fn rejects_future_issued_at() {
272 let now = Utc::now();
273 let payload = RotationEventPayload {
274 version: 1,
275 from_algorithm: SigningAlgorithm::EcdsaSha256P256,
276 from_key_id: "a".into(),
277 from_public_key_b64: "AAAA".into(),
278 to_algorithm: SigningAlgorithm::EcdsaSha256P256,
279 to_key_id: "b".into(),
280 to_public_key_b64: "BBBB".into(),
281 issued_at: now + Duration::hours(1),
282 transition_until: now + Duration::days(30),
283 };
284 let event = RotationEvent {
285 payload,
286 handover_signature_b64: "AAAA".into(),
287 };
288 let err = verify_rotation_event(&event).unwrap_err();
289 assert!(matches!(err, VerifyError::FutureIssuedAt));
290 }
291}