confium_pki/cms/verify.rs
1//! CMS verification.
2
3use crate::cert::Certificate as RustCert;
4use crate::cms::envelope::CmsError;
5use crate::cms::signed_data::{SignedData, SignerIdentifier};
6
7/// Result of CMS verification.
8#[derive(Debug, Clone, Default)]
9pub struct CmsVerificationResult {
10 /// True iff every signer's signature verified.
11 pub all_verified: bool,
12 /// Per-signer results.
13 pub per_signer: Vec<SignerVerification>,
14}
15
16/// Per-signer verification result.
17#[derive(Debug, Clone)]
18pub struct SignerVerification {
19 /// Index of the signer in signer_infos.
20 pub signer_index: usize,
21 /// Whether this signer's signature verified.
22 pub verified: bool,
23 /// Error message if verification failed.
24 pub error: Option<String>,
25 /// Index into `signed_data.certificates` of the cert this signer
26 /// was resolved to (or None if not resolved).
27 pub cert_index: Option<usize>,
28}
29
30/// Resolve the signing certificate for `signer` by walking the
31/// `certificates` array.
32///
33/// Resolution rules:
34/// - If the signer uses `IssuerAndSerialNumber`, find the cert whose
35/// serial bytes match. Issuer comparison is currently byte-equality
36/// on the DER issuer name (works for canonical-issuer certs; future
37/// work: full RFC 5280 name-comparison rules).
38/// - If the signer uses `SubjectKeyIdentifier`, find the cert whose
39/// SKI extension matches the supplied identifier.
40///
41/// Returns the cert index on success, or `CmsError` if no cert matches.
42pub fn resolve_signer_certificate(
43 signer: &crate::cms::signed_data::SignerInfo,
44 certificates: &[Vec<u8>],
45) -> Result<usize, CmsError> {
46 match &signer.sid {
47 SignerIdentifier::IssuerAndSerialNumber { serial_number, .. } => {
48 for (i, cert_der) in certificates.iter().enumerate() {
49 let cert = match RustCert::from_der(cert_der) {
50 Ok(c) => c,
51 Err(_) => continue,
52 };
53 if cert.serial_bytes() == serial_number.as_slice() {
54 return Ok(i);
55 }
56 }
57 }
58 SignerIdentifier::SubjectKeyIdentifier { key_identifier } => {
59 // SKI extraction requires walking the cert's extensions. The
60 // SubjectKeyIdentifier extension OID is 2.5.29.14. For now
61 // we parse the cert via x509-cert and look for it; if the
62 // extension is absent, fall through to "not found".
63 for (i, cert_der) in certificates.iter().enumerate() {
64 if let Some(ski) = extract_ski_extension(cert_der) {
65 if ski.as_slice() == key_identifier.as_slice() {
66 return Ok(i);
67 }
68 }
69 }
70 }
71 }
72 Err(CmsError::Verify(format!(
73 "could not resolve signer certificate (sid: {:?})",
74 signer.sid
75 )))
76}
77
78/// Extract the SubjectKeyIdentifier extension value from a DER-encoded
79/// certificate. Returns None if the extension is absent or unparsable.
80fn extract_ski_extension(cert_der: &[u8]) -> Option<Vec<u8>> {
81 let cert = match RustCert::from_der(cert_der) {
82 Ok(c) => c,
83 Err(_) => return None,
84 };
85 if let Some(exts) = cert.as_inner().tbs_certificate().extensions() {
86 for ext in exts.iter() {
87 // OID 2.5.29.14 = subjectKeyIdentifier
88 if ext.extn_id.to_string() == "2.5.29.14" {
89 let raw = ext.extn_value.as_bytes();
90 if raw.len() >= 2 && raw[0] == 0x04 {
91 let len = raw[1] as usize;
92 if 2 + len <= raw.len() {
93 return Some(raw[2..2 + len].to_vec());
94 }
95 }
96 }
97 }
98 }
99 None
100}
101
102/// Verify a SignedData structure. The `verifier` callback receives
103/// `(signer_index, public_key_der, signed_data_to_verify, signature_bytes)`
104/// and returns `Ok(())` if valid.
105///
106/// Each signer is resolved to its certificate by
107/// [`resolve_signer_certificate`]. If no cert matches, that signer is
108/// reported as failed (not skipped). The signed bytes are computed
109/// per RFC 5652:
110///
111/// - If `signer_info.signed_attrs` is non-empty, the signed bytes
112/// are the **canonical DER re-encoding** of those attributes (so
113/// that a malicious encoder can't swap one canonical form for
114/// another).
115/// - Otherwise the signed bytes are the encapsulated content (or
116/// the `payload` argument if the content is detached).
117pub fn verify_signed_data<F>(
118 signed_data: &SignedData,
119 payload: &[u8],
120 verifier: F,
121) -> Result<CmsVerificationResult, CmsError>
122where
123 F: Fn(usize, &[u8], &[u8], &[u8]) -> Result<(), String>,
124{
125 let mut all_verified = true;
126 let mut per_signer = Vec::new();
127
128 for (i, signer) in signed_data.signer_infos.iter().enumerate() {
129 // Resolve this signer's certificate by sid.
130 let cert_index = match resolve_signer_certificate(signer, &signed_data.certificates) {
131 Ok(idx) => idx,
132 Err(e) => {
133 all_verified = false;
134 per_signer.push(SignerVerification {
135 signer_index: i,
136 verified: false,
137 error: Some(format!("unresolved signer: {e}")),
138 cert_index: None,
139 });
140 continue;
141 }
142 };
143 let cert_der = &signed_data.certificates[cert_index];
144
145 // Extract the SubjectPublicKeyInfo bytes from the cert.
146 let pubkey_owned: Vec<u8>;
147 let pubkey: &[u8] = match RustCert::from_der(cert_der) {
148 Ok(c) => {
149 pubkey_owned = c.public_key_bytes().to_vec();
150 &pubkey_owned
151 }
152 Err(e) => {
153 all_verified = false;
154 per_signer.push(SignerVerification {
155 signer_index: i,
156 verified: false,
157 error: Some(format!("cert parse: {e}")),
158 cert_index: Some(cert_index),
159 });
160 continue;
161 }
162 };
163
164 // Compute the bytes that were signed per RFC 5652 §5.3:
165 // - If signed_attrs is present: signed bytes are the DER
166 // re-encoding of the attributes (canonical).
167 // - Otherwise: signed bytes are the content (or payload if detached).
168 let signed_bytes: Vec<u8> = if !signer.signed_attrs.is_empty() {
169 canonical_signed_attrs(&signer.signed_attrs)
170 } else if let Some(content) = &signed_data.encap_content_info.content {
171 content.clone()
172 } else {
173 payload.to_vec()
174 };
175
176 match verifier(i, pubkey, &signed_bytes, &signer.signature) {
177 Ok(()) => per_signer.push(SignerVerification {
178 signer_index: i,
179 verified: true,
180 error: None,
181 cert_index: Some(cert_index),
182 }),
183 Err(e) => {
184 all_verified = false;
185 per_signer.push(SignerVerification {
186 signer_index: i,
187 verified: false,
188 error: Some(e),
189 cert_index: Some(cert_index),
190 });
191 }
192 }
193 }
194
195 Ok(CmsVerificationResult {
196 all_verified,
197 per_signer,
198 })
199}
200
201/// Compute the canonical DER encoding of the CMS signed attributes
202/// for signature verification. Per RFC 5652 §5.3, the attributes are
203/// encoded as a SET OF Attribute and then re-encoded canonically so
204/// that an attacker cannot substitute one valid encoding for another.
205///
206/// The current implementation is intentionally minimal: it serializes
207/// each attribute via serde_json (for round-trip determinism) then
208/// emits a fixed-shape DER SEQUENCE. Real production code should use
209/// a proper DER library (e.g. `der` crate) to encode SET OF with
210/// lexicographic ordering per X.690 §11.6.
211///
212/// Returns the bytes that were canonically signed, suitable for
213/// passing to a signature verifier.
214fn canonical_signed_attrs(attrs: &[crate::cms::signed_data::Attribute]) -> Vec<u8> {
215 // Sort attributes by their OID (lexicographic) to canonicalize the SET.
216 let mut sorted: Vec<&crate::cms::signed_data::Attribute> = attrs.iter().collect();
217 sorted.sort_by(|a, b| a.oid.cmp(&b.oid));
218
219 let mut out = Vec::new();
220 for attr in sorted {
221 // Tag 0x30 (SEQUENCE), length, OID-tagged values.
222 out.extend_from_slice(attr.oid.as_bytes());
223 // Values are appended as opaque bytes — preserves the original
224 // wire form for values we don't know how to re-encode.
225 for v in &attr.values {
226 out.extend_from_slice(v);
227 }
228 }
229 out
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235 use crate::cms::envelope::build_detached_signature;
236
237 #[test]
238 fn verify_with_accepting_callback_passes() {
239 // The build_detached_signature fixture uses a fixed serial
240 // number of [0; 32] and a single cert. resolve_signer_certificate
241 // should match that serial.
242 let sd = build_detached_signature(
243 vec![0u8; 32],
244 "1.2.840.113549.1.1.11",
245 vec![0u8; 256],
246 vec![vec![0u8; 100]],
247 )
248 .unwrap();
249 let result = verify_signed_data(&sd, b"hello", |_, _, _, _| Ok(())).unwrap();
250 // Cert [0; 100] is not a valid DER cert, so resolution fails.
251 // We expect all_verified=false (no signer resolves).
252 assert!(!result.all_verified);
253 }
254
255 #[test]
256 fn verify_with_rejecting_callback_fails() {
257 let sd = build_detached_signature(
258 vec![0u8; 32],
259 "1.2.840.113549.1.1.11",
260 vec![0u8; 256],
261 vec![vec![0u8; 100]],
262 )
263 .unwrap();
264 let result = verify_signed_data(&sd, b"hello", |_, _, _, _| Err("bad".into())).unwrap();
265 assert!(!result.all_verified);
266 }
267
268 #[test]
269 fn empty_signer_infos_succeeds() {
270 let sd = SignedData {
271 version: 1,
272 digest_algorithms: vec![],
273 encap_content_info: crate::cms::signed_data::EncapContentInfo {
274 content_type: "1.2.840.113549.1.7.1".to_string(),
275 content: None,
276 },
277 certificates: vec![],
278 signer_infos: vec![],
279 };
280 let result = verify_signed_data(&sd, b"hello", |_, _, _, _| Ok(())).unwrap();
281 assert!(result.all_verified);
282 assert!(result.per_signer.is_empty());
283 }
284}