1use sha2::Digest;
17use synta::{Decoder, Encoding, Tag, TagClass};
18use synta_certificate::oids::{self, CMS_SIGNED_DATA};
19use synta_certificate::{
20 BackendPublicKey, CertByteRanges, Certificate, KeyWrapAlgorithm, cert_byte_ranges,
21 default_signature_verifier, name::format_dn,
22};
23use tracing::warn;
24
25use crate::auth::{AuthMethod, AuthResult};
26use crate::error::KipukaError;
27
28#[derive(Debug, Clone)]
34pub struct CmsVerificationResult {
35 pub signer_cert_der: Vec<u8>,
41
42 pub signer_subject_dn: String,
46
47 pub payload: Vec<u8>,
51
52 pub signature_algorithm: String,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum SupportedContentEncryption {
66 Aes256Gcm,
68 Aes128Gcm,
70 Aes256Cbc,
72 Aes128Cbc,
74}
75
76pub fn validate_content_encryption(alg: &str) -> Result<SupportedContentEncryption, KipukaError> {
87 match alg.to_ascii_lowercase().as_str() {
88 "aes-256-gcm" | "aes256gcm" | "2.16.840.1.101.3.4.1.46" => {
89 Ok(SupportedContentEncryption::Aes256Gcm)
90 }
91 "aes-128-gcm" | "aes128gcm" | "2.16.840.1.101.3.4.1.6" => {
92 Ok(SupportedContentEncryption::Aes128Gcm)
93 }
94 "aes-256-cbc" | "aes256cbc" | "2.16.840.1.101.3.4.1.42" => {
95 Ok(SupportedContentEncryption::Aes256Cbc)
96 }
97 "aes-128-cbc" | "aes128cbc" | "2.16.840.1.101.3.4.1.2" => {
98 Ok(SupportedContentEncryption::Aes128Cbc)
99 }
100 _ => Err(KipukaError::BadRequest(format!(
101 "unsupported content encryption algorithm: {alg}"
102 ))),
103 }
104}
105
106pub fn verify_cms_signed_data(
135 signed_data_der: &[u8],
136 truststore: &[Vec<u8>],
137) -> Result<CmsVerificationResult, KipukaError> {
138 if signed_data_der.is_empty() {
140 return Err(KipukaError::BadRequest("CMS SignedData is empty".into()));
141 }
142
143 if signed_data_der.len() < 100 {
146 return Err(KipukaError::BadRequest(
147 "CMS SignedData is too short to be valid".into(),
148 ));
149 }
150
151 if truststore.is_empty() {
152 return Err(KipukaError::Auth(
153 "CMS truststore is empty — cannot verify signer certificate".into(),
154 ));
155 }
156
157 let mut outer_dec = Decoder::new(signed_data_der, Encoding::Ber);
159 let seq_tag = Tag::universal_constructed(16); let mut ci_dec = outer_dec
161 .enter_constructed(seq_tag)
162 .map_err(|e| KipukaError::BadRequest(format!("CMS ContentInfo parse error: {e:?}")))?;
163
164 let content_type: synta::ObjectIdentifier = ci_dec
165 .decode()
166 .map_err(|e| KipukaError::BadRequest(format!("CMS contentType OID parse error: {e:?}")))?;
167
168 if content_type.components() != CMS_SIGNED_DATA {
169 return Err(KipukaError::BadRequest(
170 "CMS ContentInfo contentType is not id-signedData".into(),
171 ));
172 }
173
174 let ctx0_tag = Tag::new(TagClass::ContextSpecific, true, 0);
176 let mut content_dec = ci_dec.enter_constructed(ctx0_tag).map_err(|e| {
177 KipukaError::BadRequest(format!("CMS [0] EXPLICIT wrapper parse error: {e:?}"))
178 })?;
179
180 let mut sd_dec = content_dec.enter_constructed(seq_tag).map_err(|e| {
182 KipukaError::BadRequest(format!("CMS SignedData SEQUENCE parse error: {e:?}"))
183 })?;
184
185 let _version: synta::RawDer = sd_dec.decode().map_err(|e| {
187 KipukaError::BadRequest(format!("CMS SignedData version parse error: {e:?}"))
188 })?;
189
190 let _digest_algs: synta::RawDer = sd_dec
192 .decode()
193 .map_err(|e| KipukaError::BadRequest(format!("CMS digestAlgorithms parse error: {e:?}")))?;
194
195 let encap_ci_tag = seq_tag;
197 let mut encap_dec = sd_dec
198 .enter_constructed(encap_ci_tag)
199 .map_err(|e| KipukaError::BadRequest(format!("CMS encapContentInfo parse error: {e:?}")))?;
200
201 let _econtent_type: synta::ObjectIdentifier = encap_dec
203 .decode()
204 .map_err(|e| KipukaError::BadRequest(format!("CMS eContentType parse error: {e:?}")))?;
205
206 let payload = if !encap_dec.is_empty() {
208 let ctx0_econtent = Tag::new(TagClass::ContextSpecific, true, 0);
209 let mut econtent_wrapper = encap_dec.enter_constructed(ctx0_econtent).map_err(|e| {
210 KipukaError::BadRequest(format!("CMS eContent [0] wrapper parse error: {e:?}"))
211 })?;
212
213 let octet: synta::RawDer = econtent_wrapper.decode().map_err(|e| {
214 KipukaError::BadRequest(format!("CMS eContent OCTET STRING parse error: {e:?}"))
215 })?;
216
217 let octet_bytes = octet.as_bytes();
219 let mut val_dec = Decoder::new(octet_bytes, Encoding::Ber);
220 val_dec
221 .read_tag()
222 .map_err(|e| KipukaError::BadRequest(format!("CMS eContent tag error: {e:?}")))?;
223 let val_len = val_dec
224 .read_length()
225 .map_err(|e| KipukaError::BadRequest(format!("CMS eContent length error: {e:?}")))?
226 .definite()
227 .map_err(|e| {
228 KipukaError::BadRequest(format!("CMS eContent indefinite length: {e:?}"))
229 })?;
230 let val_bytes = val_dec
231 .read_bytes(val_len)
232 .map_err(|e| KipukaError::BadRequest(format!("CMS eContent value error: {e:?}")))?;
233 val_bytes.to_vec()
234 } else {
235 return Err(KipukaError::BadRequest(
236 "CMS SignedData has no encapsulated content (eContent is absent)".into(),
237 ));
238 };
239
240 if payload.is_empty() {
241 return Err(KipukaError::BadRequest(
242 "CMS SignedData eContent is empty".into(),
243 ));
244 }
245
246 let mut signer_certs: Vec<Vec<u8>> = Vec::new();
249 let mut signer_infos_raw: Option<Vec<u8>> = None;
250
251 while !sd_dec.is_empty() {
252 let next_tag = sd_dec.peek_tag().map_err(|e| {
253 KipukaError::BadRequest(format!("CMS SignedData field peek error: {e:?}"))
254 })?;
255
256 if next_tag.class() == TagClass::ContextSpecific && next_tag.number() == 0 {
257 let ctx0_certs = Tag::new(TagClass::ContextSpecific, true, 0);
259 let mut cert_set = sd_dec.enter_constructed(ctx0_certs).map_err(|e| {
260 KipukaError::BadRequest(format!("CMS certificates field parse error: {e:?}"))
261 })?;
262
263 while !cert_set.is_empty() {
264 let cert_tag = cert_set.peek_tag().map_err(|e| {
265 KipukaError::BadRequest(format!("CMS cert entry peek error: {e:?}"))
266 })?;
267
268 if cert_tag.class() == TagClass::Universal && cert_tag.number() == 16 {
269 let raw: synta::RawDer = cert_set.decode().map_err(|e| {
270 KipukaError::BadRequest(format!("CMS cert parse error: {e:?}"))
271 })?;
272 signer_certs.push(raw.as_bytes().to_vec());
273 } else {
274 let _: synta::RawDer = cert_set.decode().map_err(|e| {
276 KipukaError::BadRequest(format!("CMS cert alt skip error: {e:?}"))
277 })?;
278 }
279 }
280 } else if next_tag.class() == TagClass::ContextSpecific && next_tag.number() == 1 {
281 let _: synta::RawDer = sd_dec
283 .decode()
284 .map_err(|e| KipukaError::BadRequest(format!("CMS crls skip error: {e:?}")))?;
285 } else if next_tag.class() == TagClass::Universal && next_tag.number() == 17 {
286 let raw: synta::RawDer = sd_dec.decode().map_err(|e| {
288 KipukaError::BadRequest(format!("CMS signerInfos parse error: {e:?}"))
289 })?;
290 signer_infos_raw = Some(raw.as_bytes().to_vec());
291 } else {
292 let _: synta::RawDer = sd_dec.decode().map_err(|e| {
294 KipukaError::BadRequest(format!("CMS unknown field skip error: {e:?}"))
295 })?;
296 }
297 }
298
299 let si_bytes = signer_infos_raw
300 .ok_or_else(|| KipukaError::BadRequest("CMS SignedData has no signerInfos SET".into()))?;
301
302 if signer_certs.is_empty() {
303 return Err(KipukaError::BadRequest(
304 "CMS SignedData has no certificates — signer cert is required".into(),
305 ));
306 }
307
308 let set_tag = Tag::universal_constructed(17); let mut si_set_dec = Decoder::new(&si_bytes, Encoding::Ber);
312 let mut si_inner = si_set_dec
313 .enter_constructed(set_tag)
314 .map_err(|e| KipukaError::BadRequest(format!("CMS signerInfos SET enter error: {e:?}")))?;
315
316 if si_inner.is_empty() {
317 return Err(KipukaError::BadRequest(
318 "CMS SignedData signerInfos is empty — at least one signer required".into(),
319 ));
320 }
321
322 let si_raw: synta::RawDer = si_inner
325 .decode()
326 .map_err(|e| KipukaError::BadRequest(format!("CMS SignerInfo parse error: {e:?}")))?;
327 let si_raw_bytes = si_raw.as_bytes();
328
329 let signer_info = synta_certificate::cms_2009_types::SignerInfo::from_der(si_raw_bytes)
330 .map_err(|e| {
331 KipukaError::BadRequest(format!("CMS SignerInfo structured parse error: {e:?}"))
332 })?;
333
334 let sig_alg_oid_str = {
336 let mut alg_dec = Decoder::new(signer_info.signature_algorithm.as_bytes(), Encoding::Der);
337 let mut alg_seq = alg_dec
338 .enter_constructed(seq_tag)
339 .map_err(|e| KipukaError::BadRequest(format!("CMS sigAlg SEQUENCE error: {e:?}")))?;
340 let oid: synta::ObjectIdentifier = alg_seq
341 .decode()
342 .map_err(|e| KipukaError::BadRequest(format!("CMS sigAlg OID error: {e:?}")))?;
343 oid.to_string()
344 };
345
346 let sig_bytes = signer_info.signature.as_bytes().to_vec();
348
349 let sid_raw = signer_info.sid.as_bytes();
353 let signer_cert_der = match_signer_cert_by_sid(sid_raw, &signer_certs)?;
354
355 let signer_ranges: CertByteRanges = cert_byte_ranges(signer_cert_der).ok_or_else(|| {
357 KipukaError::BadRequest("failed to parse signer certificate structure".into())
358 })?;
359
360 let signer_spki_der = &signer_cert_der[signer_ranges.subject_public_key_info.clone()];
361
362 let signer_subject_dn = {
364 let cert: synta_certificate::Certificate<'_> = Decoder::new(signer_cert_der, Encoding::Der)
365 .decode()
366 .map_err(|e| KipukaError::BadRequest(format!("signer cert decode error: {e:?}")))?;
367 let subject_raw = cert.tbs_certificate.subject.as_bytes();
368 format_dn(subject_raw)
369 };
370
371 super::certificate::valid_now(signer_cert_der).map_err(KipukaError::Auth)?;
372
373 let signer_cert_parsed = Certificate::from_der(signer_cert_der).map_err(|e| {
379 KipukaError::BadRequest(format!("failed to parse signer certificate: {e:?}"))
380 })?;
381 let cert_sig_bits = signer_cert_parsed.signature_value.as_bytes();
382
383 let verifier = default_signature_verifier();
384 let mut signer_trusted = false;
385 for ta_der in truststore {
386 let ta_ranges = match cert_byte_ranges(ta_der) {
387 Some(r) => r,
388 None => continue,
389 };
390 let ta_spki = &ta_der[ta_ranges.subject_public_key_info.clone()];
391
392 if verifier
394 .verify_certificate_signature_erased(
395 &signer_cert_der[signer_ranges.tbs.clone()],
396 &signer_cert_der[signer_ranges.signature_algorithm.clone()],
397 cert_sig_bits,
398 ta_spki,
399 )
400 .is_ok()
401 {
402 signer_trusted = true;
403 break;
404 }
405
406 if ta_der == signer_cert_der {
408 signer_trusted = true;
409 break;
410 }
411 }
412
413 if !signer_trusted {
414 return Err(KipukaError::Auth(
415 "CMS signer certificate does not chain to a trust anchor".into(),
416 ));
417 }
418
419 let verification_data = if let Some(ref signed_attrs_raw) = signer_info.signed_attrs {
426 let signed_attrs_bytes = signed_attrs_raw.as_bytes();
429
430 let digest_alg_oid = {
433 let mut da_dec = Decoder::new(signer_info.digest_algorithm.as_bytes(), Encoding::Der);
434 let mut da_seq = da_dec.enter_constructed(seq_tag).map_err(|e| {
435 KipukaError::BadRequest(format!("CMS digestAlg SEQUENCE error: {e:?}"))
436 })?;
437 let oid: synta::ObjectIdentifier = da_seq
438 .decode()
439 .map_err(|e| KipukaError::BadRequest(format!("CMS digestAlg OID error: {e:?}")))?;
440 oid.components().to_vec()
441 };
442
443 let payload_hash = compute_digest(&digest_alg_oid, &payload)?;
444 verify_message_digest_attribute(signed_attrs_bytes, &payload_hash)?;
445
446 let mut encoder = synta::Encoder::new(Encoding::Der);
449 encoder
450 .start_constructed_no_guard(Tag::universal_constructed(17))
451 .map_err(|e| KipukaError::BadRequest(format!("CMS signedAttrs SET error: {e}")))?;
452 encoder
453 .encode(&synta::RawDer(signed_attrs_bytes))
454 .map_err(|e| KipukaError::BadRequest(format!("CMS signedAttrs encoding error: {e}")))?;
455 encoder
456 .end_constructed()
457 .map_err(|e| KipukaError::BadRequest(format!("CMS signedAttrs encoding error: {e}")))?;
458 encoder
459 .finish()
460 .map_err(|e| KipukaError::BadRequest(format!("CMS signedAttrs encoding error: {e}")))?
461 } else {
462 payload.clone()
464 };
465
466 let verification_algorithm = if sig_alg_oid_str == "1.2.840.113549.1.1.1" {
470 let mut decoder = Decoder::new(signer_info.digest_algorithm.as_bytes(), Encoding::Der);
471 let mut algorithm = decoder
472 .enter_constructed(seq_tag)
473 .map_err(|e| KipukaError::BadRequest(format!("CMS digest algorithm: {e}")))?;
474 let oid: synta::ObjectIdentifier = algorithm
475 .decode()
476 .map_err(|e| KipukaError::BadRequest(format!("CMS digest OID: {e}")))?;
477 let hash = match oid.components() {
478 [2, 16, 840, 1, 101, 3, 4, 2, 1] => "sha256",
479 [2, 16, 840, 1, 101, 3, 4, 2, 2] => "sha384",
480 [2, 16, 840, 1, 101, 3, 4, 2, 3] => "sha512",
481 _ => return Err(KipukaError::Auth("unsupported CMS RSA digest".into())),
482 };
483 let rsa = synta::ObjectIdentifier::new(&[1, 2, 840, 113549, 1, 1, 1])
484 .map_err(|e| KipukaError::Internal(e.to_string()))?;
485 synta_certificate::signing_algorithm_der(&rsa, hash)
486 .ok_or_else(|| KipukaError::Auth("unsupported CMS RSA signature algorithm".into()))?
487 } else {
488 signer_info.signature_algorithm.as_bytes().to_vec()
489 };
490 let pub_key = BackendPublicKey::from_spki_der(signer_spki_der.to_vec());
491 pub_key
492 .verify_signature(&verification_data, &verification_algorithm, &sig_bytes)
493 .map_err(|e| {
494 KipukaError::Auth(format!("CMS SignedData signature verification failed: {e}"))
495 })?;
496
497 Ok(CmsVerificationResult {
498 signer_cert_der: signer_cert_der.clone(),
499 signer_subject_dn,
500 payload,
501 signature_algorithm: sig_alg_oid_str,
502 })
503}
504
505pub fn build_cms_enveloped_data(
540 payload: &[u8],
541 recipient_cert_der: &[u8],
542 content_encryption_alg: &str,
543) -> Result<Vec<u8>, KipukaError> {
544 if payload.is_empty() {
545 return Err(KipukaError::BadRequest(
546 "cannot encrypt empty payload".into(),
547 ));
548 }
549
550 if recipient_cert_der.is_empty() {
551 return Err(KipukaError::BadRequest(
552 "recipient certificate is empty".into(),
553 ));
554 }
555
556 if recipient_cert_der.len() < 100 {
558 return Err(KipukaError::BadRequest(
559 "recipient certificate is too short to be valid".into(),
560 ));
561 }
562
563 let _alg = validate_content_encryption(content_encryption_alg)?;
565
566 if matches!(
569 _alg,
570 SupportedContentEncryption::Aes256Gcm | SupportedContentEncryption::Aes128Gcm
571 ) {
572 let cipher = if matches!(_alg, SupportedContentEncryption::Aes256Gcm) {
573 openssl::symm::Cipher::aes_256_gcm()
574 } else {
575 openssl::symm::Cipher::aes_128_gcm()
576 };
577 let build = || -> Result<Vec<u8>, openssl::error::ErrorStack> {
578 let mut recipients = openssl::stack::Stack::new()?;
579 recipients.push(openssl::x509::X509::from_der(recipient_cert_der)?)?;
580 openssl::cms::CmsContentInfo::encrypt(
581 &recipients,
582 payload,
583 cipher,
584 openssl::cms::CMSOptions::BINARY,
585 )?
586 .to_der()
587 };
588 return build().map_err(|e| {
589 KipukaError::Internal(format!("CMS authenticated encryption failed: {e}"))
590 });
591 }
592 let content_enc_oid: &[u32] = match _alg {
593 SupportedContentEncryption::Aes256Cbc => synta_certificate::pkcs12_types::ID_AES256_CBC,
594 SupportedContentEncryption::Aes128Cbc => synta_certificate::pkcs12_types::ID_AES128_CBC,
595 _ => unreachable!("GCM handled above"),
596 };
597
598 let recipients: Vec<(&[u8], KeyWrapAlgorithm)> =
600 vec![(recipient_cert_der, KeyWrapAlgorithm::RsaOaepSha256)];
601
602 synta_certificate::default_create_enveloped_data(payload, &recipients, content_enc_oid)
603 .map_err(|e| KipukaError::Internal(format!("CMS EnvelopedData construction failed: {e}")))
604}
605
606pub fn extract_signer_identity(
625 cms_result: &CmsVerificationResult,
626) -> Result<AuthResult, KipukaError> {
627 if cms_result.signer_subject_dn.is_empty() {
628 return Err(KipukaError::Auth(
629 "CMS signer certificate has an empty subject DN".into(),
630 ));
631 }
632
633 Ok(AuthResult {
634 identity: cms_result.signer_subject_dn.clone(),
635 method: AuthMethod::Mtls,
639 client_cert_der: Some(cms_result.signer_cert_der.clone()),
640 subject_dn: Some(cms_result.signer_subject_dn.clone()),
641 subject_alt_names: Vec::new(),
642 extended_key_usage: Vec::new(),
643 })
644}
645
646fn match_signer_cert_by_sid<'a>(
652 sid_raw: &[u8],
653 certs: &'a [Vec<u8>],
654) -> Result<&'a Vec<u8>, KipukaError> {
655 if sid_raw.is_empty() {
656 return Err(KipukaError::BadRequest(
657 "CMS SignerInfo sid is empty".into(),
658 ));
659 }
660
661 let first_byte = sid_raw[0];
663
664 if first_byte == 0x30 {
665 let ias = synta_certificate::cms_2009_types::IssuerAndSerialNumber::from_der(sid_raw)
667 .map_err(|e| {
668 KipukaError::BadRequest(format!("CMS sid IssuerAndSerialNumber parse error: {e:?}"))
669 })?;
670
671 for cert_der in certs {
672 if let Ok(cert) = Certificate::from_der(cert_der)
673 && cert.tbs_certificate.issuer.as_bytes() == ias.issuer.as_bytes()
674 && cert.tbs_certificate.serial_number == ias.serial_number
675 {
676 return Ok(cert_der);
677 }
678 }
679 Err(KipukaError::BadRequest(
680 "CMS signer certificate not found by IssuerAndSerialNumber".into(),
681 ))
682 } else if first_byte == 0x80 {
683 let mut dec = Decoder::new(sid_raw, Encoding::Ber);
686 dec.read_tag()
687 .map_err(|e| KipukaError::BadRequest(format!("CMS sid SKI tag error: {e:?}")))?;
688 let len = dec
689 .read_length()
690 .map_err(|e| KipukaError::BadRequest(format!("CMS sid SKI length error: {e:?}")))?
691 .definite()
692 .map_err(|e| KipukaError::BadRequest(format!("CMS sid SKI indef error: {e:?}")))?;
693 let ski_value = dec
694 .read_bytes(len)
695 .map_err(|e| KipukaError::BadRequest(format!("CMS sid SKI value error: {e:?}")))?;
696
697 for cert_der in certs {
698 if let Ok(cert) = Certificate::from_der(cert_der) {
699 if let Some(ref exts_raw) = cert.tbs_certificate.extensions
702 && let Some(ext_value) = synta_certificate::find_extension_value(
703 exts_raw.as_bytes(),
704 oids::SUBJECT_KEY_IDENTIFIER,
705 )
706 {
707 let mut ev_dec = Decoder::new(ext_value, Encoding::Der);
709 if let Ok(tag) = ev_dec.read_tag()
710 && tag.number() == 4
711 {
712 if let Ok(ev_len) = ev_dec.read_length()
714 && let Ok(def_len) = ev_len.definite()
715 && let Ok(key_id) = ev_dec.read_bytes(def_len)
716 && key_id == ski_value
717 {
718 return Ok(cert_der);
719 }
720 }
721 }
722 }
723 }
724 Err(KipukaError::BadRequest(
725 "CMS signer certificate not found by SubjectKeyIdentifier".into(),
726 ))
727 } else {
728 warn!(
730 first_byte = format_args!("0x{first_byte:02x}"),
731 "CMS SignerIdentifier has unexpected tag, falling back to first certificate"
732 );
733 Ok(&certs[0])
734 }
735}
736
737fn compute_digest(alg_oid: &[u32], data: &[u8]) -> Result<Vec<u8>, KipukaError> {
741 if alg_oid == oids::ID_SHA256 {
742 Ok(sha2::Sha256::digest(data).to_vec())
743 } else if alg_oid == oids::ID_SHA384 {
744 Ok(sha2::Sha384::digest(data).to_vec())
745 } else if alg_oid == oids::ID_SHA512 {
746 Ok(sha2::Sha512::digest(data).to_vec())
747 } else if alg_oid == oids::ID_SHA1 {
748 Err(KipukaError::BadRequest(
749 "CMS digest algorithm SHA-1 is not permitted — use SHA-256 or stronger".into(),
750 ))
751 } else {
752 Err(KipukaError::BadRequest(format!(
753 "unsupported CMS digest algorithm OID: {alg_oid:?}"
754 )))
755 }
756}
757
758fn verify_message_digest_attribute(
764 signed_attrs_bytes: &[u8],
765 expected_digest: &[u8],
766) -> Result<(), KipukaError> {
767 let mut attrs = Decoder::new(signed_attrs_bytes, Encoding::Der);
769 let mut found = false;
770 while !attrs.is_empty() {
771 let mut attr = attrs
772 .enter_constructed(Tag::universal_constructed(16))
773 .map_err(|e| KipukaError::BadRequest(format!("CMS attr SEQUENCE error: {e}")))?;
774 let oid: synta::ObjectIdentifier = attr
775 .decode()
776 .map_err(|e| KipukaError::BadRequest(format!("CMS attr OID error: {e}")))?;
777 if oid.components() == oids::PKCS9_MESSAGE_DIGEST {
778 if found {
779 return Err(KipukaError::BadRequest(
780 "duplicate CMS message-digest attribute".into(),
781 ));
782 }
783 let mut values = attr
784 .enter_constructed(Tag::universal_constructed(17))
785 .map_err(|e| {
786 KipukaError::BadRequest(format!("CMS messageDigest SET error: {e}"))
787 })?;
788 let digest: synta::OctetStringRef = values.decode().map_err(|e| {
789 KipukaError::BadRequest(format!("CMS messageDigest OCTET STRING error: {e}"))
790 })?;
791 if !values.is_empty() || !attr.is_empty() {
792 return Err(KipukaError::BadRequest(
793 "CMS message-digest requires exactly one value".into(),
794 ));
795 }
796 if digest.as_bytes() != expected_digest {
797 return Err(KipukaError::Auth(
798 "CMS message-digest attribute does not match eContent hash".into(),
799 ));
800 }
801 found = true;
802 } else {
803 while !attr.is_empty() {
804 let _: synta::RawDer = attr
805 .decode()
806 .map_err(|e| KipukaError::BadRequest(format!("CMS attr value error: {e}")))?;
807 }
808 }
809 }
810 if !found {
811 return Err(KipukaError::BadRequest(
812 "CMS signedAttrs is missing the required message-digest attribute".into(),
813 ));
814 }
815 Ok(())
816}
817
818#[cfg(test)]
819mod tests {
820 use super::*;
821
822 #[test]
823 fn validate_content_encryption_accepts_aes256gcm() {
824 let result = validate_content_encryption("aes-256-gcm");
825 assert!(result.is_ok());
826 assert_eq!(result.unwrap(), SupportedContentEncryption::Aes256Gcm);
827 }
828
829 #[test]
830 fn validate_content_encryption_accepts_oid() {
831 let result = validate_content_encryption("2.16.840.1.101.3.4.1.46");
832 assert!(result.is_ok());
833 assert_eq!(result.unwrap(), SupportedContentEncryption::Aes256Gcm);
834 }
835
836 #[test]
837 fn validate_content_encryption_rejects_unknown() {
838 let result = validate_content_encryption("triple-des-cbc");
839 assert!(result.is_err());
840 }
841
842 #[test]
843 fn validate_content_encryption_case_insensitive() {
844 let result = validate_content_encryption("AES-128-GCM");
845 assert!(result.is_ok());
846 assert_eq!(result.unwrap(), SupportedContentEncryption::Aes128Gcm);
847 }
848
849 #[test]
850 fn verify_rejects_empty_input() {
851 let result = verify_cms_signed_data(&[], &[vec![0u8; 200]]);
852 assert!(matches!(result, Err(KipukaError::BadRequest(_))));
853 }
854
855 #[test]
856 fn verify_rejects_short_input() {
857 let result = verify_cms_signed_data(&[0u8; 50], &[vec![0u8; 200]]);
858 assert!(matches!(result, Err(KipukaError::BadRequest(_))));
859 }
860
861 #[test]
862 fn verify_rejects_empty_truststore() {
863 let result = verify_cms_signed_data(&[0u8; 200], &[]);
864 assert!(matches!(result, Err(KipukaError::Auth(_))));
865 }
866
867 #[test]
868 fn build_enveloped_rejects_empty_payload() {
869 let result = build_cms_enveloped_data(&[], &[0u8; 200], "aes-256-gcm");
870 assert!(matches!(result, Err(KipukaError::BadRequest(_))));
871 }
872
873 #[test]
874 fn build_enveloped_rejects_empty_cert() {
875 let result = build_cms_enveloped_data(&[1, 2, 3], &[], "aes-256-gcm");
876 assert!(matches!(result, Err(KipukaError::BadRequest(_))));
877 }
878
879 #[test]
880 fn build_enveloped_rejects_bad_algorithm() {
881 let result = build_cms_enveloped_data(&[1, 2, 3], &[0u8; 200], "rc4");
882 assert!(matches!(result, Err(KipukaError::BadRequest(_))));
883 }
884
885 #[test]
886 fn extract_identity_rejects_empty_dn() {
887 let cms = CmsVerificationResult {
888 signer_cert_der: vec![0u8; 100],
889 signer_subject_dn: String::new(),
890 payload: vec![1, 2, 3],
891 signature_algorithm: "sha256WithRSAEncryption".into(),
892 };
893 let auth = extract_signer_identity(&cms);
894 assert!(auth.is_err());
895 }
896
897 #[test]
898 fn extract_identity_produces_valid_auth_result() {
899 let cms = CmsVerificationResult {
900 signer_cert_der: vec![0u8; 100],
901 signer_subject_dn: "CN=client.example.com".into(),
902 payload: vec![1, 2, 3],
903 signature_algorithm: "sha256WithRSAEncryption".into(),
904 };
905 let auth = extract_signer_identity(&cms).unwrap();
906 assert_eq!(auth.identity, "CN=client.example.com");
907 assert_eq!(auth.method, AuthMethod::Mtls);
908 assert!(auth.client_cert_der.is_some());
909 assert_eq!(auth.subject_dn.as_deref(), Some("CN=client.example.com"));
910 }
911}