igvmfilegen/corim_signature/
envelope.rs1use anyhow::Context;
34use corim::types::signed::CORIM_CONTENT_TYPE;
35use corim::types::signed::CoseAlgorithm;
36use corim::types::signed::decode_signed_corim;
37use corim::types::signed::encode_signed_corim;
38use crypto::HashAlgorithm;
39use crypto::x509::X509Certificate;
40
41#[derive(Debug)]
44pub struct DetachedCorim {
45 pub document: Vec<u8>,
47 pub signature: Vec<u8>,
50}
51
52pub fn detach_payload(data: &[u8]) -> anyhow::Result<DetachedCorim> {
77 let mut signed = decode_signed_corim(data).context("Signed CoRIM: decode failed")?;
78
79 let document = signed.payload.take().ok_or_else(|| {
80 anyhow::anyhow!(
81 "Signed CoRIM: payload is nil (already detached); pass the detached \
82 signature directly instead of splitting it"
83 )
84 })?;
85
86 let signature = encode_signed_corim(&signed)
88 .context("Signed CoRIM: failed to encode detached signature")?;
89
90 tracing::debug!(
91 input_size = data.len(),
92 document_size = document.len(),
93 detached_signature_size = signature.len(),
94 "Split signed CoRIM into document payload and detached COSE_Sign1 signature"
95 );
96
97 Ok(DetachedCorim {
98 document,
99 signature,
100 })
101}
102
103pub fn verify_corim_signature(signature: &[u8], document: &[u8]) -> anyhow::Result<()> {
142 let signed = decode_signed_corim(signature).context("CoRIM signature: decode failed")?;
143
144 if !signed.is_detached() {
145 anyhow::bail!(
146 "CoRIM signature: payload must be nil for a detached signature; \
147 embedded payloads must be split first"
148 );
149 }
150
151 if signed.signature.is_empty() {
152 anyhow::bail!("CoRIM signature: COSE signature bytes must be non-empty");
153 }
154
155 if let Some(ct) = &signed.protected.content_type
156 && ct != CORIM_CONTENT_TYPE
157 {
158 anyhow::bail!(
159 "CoRIM signature: protected content-type is {ct:?}, expected {CORIM_CONTENT_TYPE:?}"
160 );
161 }
162
163 let issuer_cert_der: &[u8] = signed
168 .protected
169 .x5chain
170 .as_ref()
171 .or(signed.protected.x5bag.as_ref())
172 .map(|x| x.end_entity())
173 .ok_or_else(|| {
174 anyhow::anyhow!(
175 "CoRIM signature: protected header carries neither x5chain (key 33) \
176 nor x5bag (key 32); cannot identify the issuer certificate"
177 )
178 })?;
179
180 let hash = match signed.protected.alg {
184 CoseAlgorithm::Ps384 => HashAlgorithm::Sha384,
185 other => anyhow::bail!(
186 "CoRIM signature: unsupported COSE algorithm {other} ({}). \
187 Only PS384 (-38) is supported.",
188 other.to_i64(),
189 ),
190 };
191
192 let cert = X509Certificate::from_der(issuer_cert_der)
193 .context("CoRIM signature: failed to parse issuer certificate (expected DER)")?;
194
195 let pubkey = cert
196 .public_key()
197 .context("CoRIM signature: failed to extract public key from issuer certificate")?
198 .rsa()
199 .context("CoRIM signature: issuer certificate public key is not an RSA key")?;
200
201 let tbs = signed
202 .to_be_signed_detached(document, &[])
203 .context("CoRIM signature: failed to construct Sig_structure1 TBS bytes")?;
204
205 let valid = pubkey
206 .pss_verify(&tbs, &signed.signature, hash)
207 .context("CoRIM signature: RSA-PSS verification primitive returned an error")?;
208
209 if !valid {
210 anyhow::bail!(
211 "CoRIM signature: cryptographic verification failed; signature \
212 does not match the supplied document under the issuer's public key"
213 );
214 }
215
216 tracing::debug!(
217 signature_size = signature.len(),
218 document_size = document.len(),
219 tbs_size = tbs.len(),
220 issuer_cert_size = issuer_cert_der.len(),
221 alg = %signed.protected.alg,
222 "CoRIM signature cryptographically verified against issuer certificate from x5chain/x5bag"
223 );
224
225 Ok(())
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use corim::cbor::value::Value;
232 use corim::types::signed::CwtClaims;
233 use corim::types::signed::SignedCorimBuilder;
234 use test_with_tracing::test;
235
236 const TEST_PAYLOAD: &[u8] = &[0xAA, 0xBB, 0xCC, 0xDD];
237
238 fn make_bundled(payload: &[u8], signature: Vec<u8>) -> Vec<u8> {
241 SignedCorimBuilder::new(-7_i64, payload.to_vec())
242 .set_cwt_claims(CwtClaims::new("test"))
243 .build_with_signature(signature)
244 .unwrap()
245 }
246
247 fn make_detached(payload: &[u8], signature: Vec<u8>) -> Vec<u8> {
250 SignedCorimBuilder::new(-7_i64, payload.to_vec())
251 .set_cwt_claims(CwtClaims::new("test"))
252 .build_detached_with_signature(signature)
253 .unwrap()
254 }
255
256 #[test]
257 fn split_basic_round_trip() {
258 let signature = vec![0xDE; 32];
259 let bundled = make_bundled(TEST_PAYLOAD, signature.clone());
260
261 let detached = detach_payload(&bundled).unwrap();
262 assert_eq!(detached.document, TEST_PAYLOAD);
263
264 let decoded = decode_signed_corim(&detached.signature).unwrap();
267 assert_eq!(decoded.signature, signature);
268 assert!(decoded.payload.is_none());
269 }
270
271 #[test]
272 fn split_preserves_signed_bytes() {
273 let signature = vec![0x01; 64];
277 let bundled = make_bundled(&[0xCA, 0xFE, 0xBA, 0xBE], signature.clone());
278
279 let original = decode_signed_corim(&bundled).unwrap();
280 let detached = detach_payload(&bundled).unwrap();
281 let after = decode_signed_corim(&detached.signature).unwrap();
282
283 assert_eq!(
284 after.protected_header_bytes,
285 original.protected_header_bytes
286 );
287 assert_eq!(after.signature, original.signature);
288 assert!(after.payload.is_none());
289 }
290
291 #[test]
292 fn split_already_detached_errors() {
293 let detached = make_detached(TEST_PAYLOAD, vec![0xDE; 32]);
294 let err = detach_payload(&detached).unwrap_err();
295 assert!(
296 err.to_string().contains("already detached"),
297 "Error should mention already detached: {err}"
298 );
299 }
300
301 #[test]
302 fn split_empty_errors() {
303 assert!(detach_payload(&[]).is_err());
304 }
305
306 #[test]
307 fn split_large_payload_round_trip() {
308 let payload: Vec<u8> = (0..256).map(|i| (i & 0xFF) as u8).collect();
309 let signature = vec![0xAB; 64];
310 let bundled = make_bundled(&payload, signature.clone());
311
312 let detached = detach_payload(&bundled).unwrap();
313 assert_eq!(detached.document, payload);
314
315 let decoded = decode_signed_corim(&detached.signature).unwrap();
316 assert_eq!(decoded.signature, signature);
317 assert!(decoded.payload.is_none());
318 }
319
320 use crate::corim_signature::test_helpers::SIGNER;
323 use crate::corim_signature::test_helpers::sign_envelope_for;
324 use crate::corim_signature::test_helpers::sign_envelope_no_cert;
325 use crate::corim_signature::test_helpers::sign_envelope_with;
326 use corim::types::signed::COSE_HEADER_ALG;
327 use corim::types::signed::COSE_HEADER_CONTENT_TYPE;
328 use corim::types::signed::COSE_HEADER_CWT_CLAIMS;
329 use corim::types::signed::COSE_HEADER_X5CHAIN;
330 use corim::types::signed::cwt::CWT_CLAIM_ISS;
331 use corim::types::tags::TAG_SIGNED_CORIM;
332 use crypto::rsa::RsaKeyPair;
333
334 #[test]
335 fn verify_ps384_round_trip() {
336 let document = b"corim-document-bytes";
337 let envelope = sign_envelope_for(document, "test");
338
339 verify_corim_signature(&envelope, document).expect("valid PS384 signature should verify");
340 }
341
342 #[test]
343 fn verify_rejects_tampered_document() {
344 let document = b"original-document";
345 let tampered = b"tampered-document";
346 let envelope = sign_envelope_for(document, "test");
347
348 let err = verify_corim_signature(&envelope, tampered).unwrap_err();
349 let msg = format!("{err:#}");
350 assert!(
351 msg.contains("cryptographic verification failed"),
352 "Error should report verification failure: {msg}"
353 );
354 }
355
356 #[test]
357 fn verify_rejects_wrong_issuer() {
358 let document = b"corim-document";
362 let signer_key = RsaKeyPair::generate(2048).expect("signer keygen");
363 let envelope = sign_envelope_with(&signer_key, document, &SIGNER.cert_der, "test");
364
365 let err = verify_corim_signature(&envelope, document).unwrap_err();
366 let msg = format!("{err:#}");
367 assert!(
368 msg.contains("cryptographic verification failed"),
369 "Error should report verification failure: {msg}"
370 );
371 }
372
373 #[test]
374 fn verify_rejects_unsupported_algorithm() {
375 let envelope = SignedCorimBuilder::new(CoseAlgorithm::Es256, b"corim-document".to_vec())
377 .set_cwt_claims(CwtClaims::new("test"))
378 .add_protected(COSE_HEADER_X5CHAIN, Value::Bytes(b"dummy".to_vec()))
379 .build_detached_with_signature(vec![0xDE; 64])
380 .unwrap();
381 let err = verify_corim_signature(&envelope, b"corim-document").unwrap_err();
382 let msg = format!("{err:#}");
383 assert!(
384 msg.contains("unsupported COSE algorithm"),
385 "Error should report unsupported algorithm: {msg}"
386 );
387 }
388
389 #[test]
390 fn verify_rejects_malformed_cert() {
391 let document = b"corim-document";
394 let envelope = sign_envelope_with(
395 &SIGNER.key,
396 document,
397 b"not-a-der-encoded-x509-cert",
398 "test",
399 );
400
401 let err = verify_corim_signature(&envelope, document).unwrap_err();
402 let msg = format!("{err:#}");
403 assert!(
404 msg.contains("issuer certificate"),
405 "Error should mention issuer certificate: {msg}"
406 );
407 }
408
409 #[test]
410 fn verify_rejects_missing_issuer_cert() {
411 let envelope = sign_envelope_no_cert(&SIGNER.key, b"doc");
414 let err = verify_corim_signature(&envelope, b"doc").unwrap_err();
415 let msg = format!("{err:#}");
416 assert!(
417 msg.contains("x5chain") && msg.contains("x5bag"),
418 "Error should mention x5chain/x5bag: {msg}"
419 );
420 }
421
422 #[test]
423 fn verify_rejects_attached_payload() {
424 let bundled = make_bundled(b"doc", vec![0xDE; 32]);
425 let err = verify_corim_signature(&bundled, b"doc").unwrap_err();
426 let msg = format!("{err:#}");
427 assert!(
428 msg.contains("nil") || msg.contains("embedded"),
429 "Error should mention nil or embedded: {msg}"
430 );
431 }
432
433 #[test]
434 fn verify_rejects_empty_signature() {
435 let sig = make_detached(b"doc", vec![]);
436 let err = verify_corim_signature(&sig, b"doc").unwrap_err();
437 assert!(
438 err.to_string().contains("non-empty"),
439 "Error should mention non-empty: {err}"
440 );
441 }
442
443 #[test]
444 fn verify_rejects_empty_input() {
445 assert!(verify_corim_signature(&[], b"doc").is_err());
446 }
447
448 #[test]
449 fn verify_rejects_untagged_envelope() {
450 let cose = Value::Array(vec![
453 Value::Bytes(vec![]),
454 Value::Map(vec![]),
455 Value::Null,
456 Value::Bytes(vec![0xFF; 32]),
457 ]);
458 let buf = corim::cbor::encode(&cose).unwrap();
459 assert!(verify_corim_signature(&buf, b"doc").is_err());
460 }
461
462 #[test]
463 fn verify_rejects_wrong_content_type() {
464 let protected_map = Value::Map(vec![
468 (
469 Value::Integer(COSE_HEADER_ALG.into()),
470 Value::Integer(CoseAlgorithm::Ps384.to_i64().into()),
471 ),
472 (
473 Value::Integer(COSE_HEADER_CONTENT_TYPE.into()),
474 Value::Text("application/x-other".into()),
475 ),
476 (
477 Value::Integer(COSE_HEADER_CWT_CLAIMS.into()),
478 Value::Map(vec![(
479 Value::Integer(CWT_CLAIM_ISS.into()),
480 Value::Text("test".into()),
481 )]),
482 ),
483 ]);
484 let protected_bytes = corim::cbor::encode(&protected_map).unwrap();
485 let cose = Value::Tag(
486 TAG_SIGNED_CORIM,
487 Box::new(Value::Array(vec![
488 Value::Bytes(protected_bytes),
489 Value::Map(vec![]),
490 Value::Null,
491 Value::Bytes(vec![0xAB; 16]),
492 ])),
493 );
494 let buf = corim::cbor::encode(&cose).unwrap();
495 let err = verify_corim_signature(&buf, b"doc").unwrap_err();
496 let msg = format!("{err:#}");
497 assert!(
498 msg.contains("content-type"),
499 "Error should mention content-type: {msg}"
500 );
501 }
502}