synta_certificate/reader.rs
1//! Format-agnostic PKI data reader.
2//!
3//! [`read_pki_blocks`] inspects raw file bytes, detects the encoding
4//! (PEM, PKCS#7/CMS, PKCS#12, or raw DER), and returns every PKI object
5//! as a `(label, der)` pair — the same shape as [`pem_blocks`][crate::pem_blocks].
6
7use synta::{Decoder, Encoding, TagClass};
8
9use crate::crypto::Pkcs12Decryptor;
10use crate::pkcs12::{pki_from_pkcs12, Pkcs12Error};
11use crate::pkcs7::{certs_from_pkcs7, Pkcs7Error};
12
13// ── Object-safe decryptor facade ─────────────────────────────────────────────
14
15/// Object-safe decryptor used by [`read_pki_blocks`].
16///
17/// A blanket implementation is provided for every [`Pkcs12Decryptor`], so any
18/// existing decryptor (e.g. `OpensslDecryptor`) can
19/// be passed directly as `Some(&my_decryptor)` without additional boilerplate.
20pub trait PkiDecryptor {
21 /// Decrypt a PKCS#12 encrypted bag.
22 ///
23 /// Arguments mirror [`Pkcs12Decryptor::decrypt`]; the error is erased to
24 /// `Box<dyn Error>` to keep this trait object-safe.
25 fn decrypt_pkcs12(
26 &self,
27 algorithm_der: &[u8],
28 ciphertext: &[u8],
29 password: &[u8],
30 ) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>>;
31}
32
33impl<T: Pkcs12Decryptor> PkiDecryptor for T {
34 fn decrypt_pkcs12(
35 &self,
36 algorithm_der: &[u8],
37 ciphertext: &[u8],
38 password: &[u8],
39 ) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
40 Pkcs12Decryptor::decrypt(self, algorithm_der, ciphertext, password)
41 .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
42 }
43}
44
45// ── Error type ───────────────────────────────────────────────────────────────
46
47/// Error returned by [`read_pki_blocks`] on structural or decryption failure.
48#[derive(Debug)]
49pub enum ReadAnyError {
50 /// PKCS#7 parse or structure error.
51 Pkcs7(Pkcs7Error),
52 /// PKCS#12 ASN.1 structural parse error.
53 Pkcs12Parse(synta::Error),
54 /// PKCS#12 decryption failed (wrong password or unsupported algorithm).
55 Pkcs12Crypto(Box<dyn std::error::Error + Send + Sync>),
56 /// PKCS#12 unsupported format variant.
57 Pkcs12Format(&'static str),
58}
59
60impl std::fmt::Display for ReadAnyError {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 match self {
63 ReadAnyError::Pkcs7(e) => write!(f, "PKCS#7 error: {}", e),
64 ReadAnyError::Pkcs12Parse(e) => write!(f, "PKCS#12 parse error: {}", e),
65 ReadAnyError::Pkcs12Crypto(e) => write!(f, "PKCS#12 decryption error: {}", e),
66 ReadAnyError::Pkcs12Format(s) => write!(f, "PKCS#12 unsupported format: {}", s),
67 }
68 }
69}
70
71impl std::error::Error for ReadAnyError {}
72
73// ── Public API ────────────────────────────────────────────────────────────────
74
75/// Detect the encoding of `data` and return every PKI object as a
76/// `(label, der)` pair, matching the output style of
77/// [`pem_blocks`][crate::pem_blocks].
78///
79/// # Supported formats
80///
81/// | Format | How detected | Labels returned |
82/// |--------|-------------|-----------------|
83/// | PEM | `-----BEGIN` marker | `"CERTIFICATE"` per cert for PKCS#7 blocks; original label for all other block types (e.g. `"PRIVATE KEY"`) |
84/// | PKCS#7 / CMS SignedData | first inner tag is OID | `"CERTIFICATE"` per cert |
85/// | PKCS#12 | first inner tag is INTEGER | `"CERTIFICATE"` per cert, `"PRIVATE KEY"` per key |
86/// | Raw DER | everything else | `"CERTIFICATE"` |
87///
88/// PEM blocks whose DER payload is a PKCS#7 `ContentInfo` (outer SEQUENCE
89/// containing an OID as its first field) are transparently expanded: the
90/// embedded certificates are extracted and returned with the `"CERTIFICATE"`
91/// label, exactly as if the same data had been supplied in binary form.
92/// Other PEM block types (e.g. `CERTIFICATE`, `PRIVATE KEY`) pass through
93/// with their original label. Malformed PEM blocks (bad base64, truncated
94/// header) are silently skipped.
95///
96/// # Decryption
97///
98/// `password` and `decryptor` apply to PKCS#12 archives:
99///
100/// - `decryptor = Some(d)` — encrypted bags are decrypted with `d` and
101/// `password`; a decryption failure returns `Err`.
102/// - `decryptor = None` — encrypted bags are silently skipped; unencrypted
103/// certificates in the same archive are still returned.
104///
105/// For PEM input the `password` and `decryptor` arguments are currently
106/// ignored (PEM-encrypted private keys are not supported).
107///
108/// # Errors
109///
110/// | Variant | Trigger |
111/// |---------|---------|
112/// | [`ReadAnyError::Pkcs7`] | A PKCS#7 block (binary or PEM-wrapped) has malformed DER, or its `contentType` OID is not `id-signedData`. |
113/// | [`ReadAnyError::Pkcs12Parse`] | The PKCS#12 input has a structural ASN.1 error (truncated, wrong tag, etc.). |
114/// | [`ReadAnyError::Pkcs12Crypto`] | `decryptor` is `Some` and decryption of an encrypted SafeBag fails. Never returned when `decryptor` is `None`. |
115/// | [`ReadAnyError::Pkcs12Format`] | The PKCS#12 authSafe uses an unsupported `ContentInfo` content type. |
116///
117/// # Example
118///
119/// ```rust,ignore
120/// use synta_certificate::{read_pki_blocks, NoCrypto};
121///
122/// let data = std::fs::read("bundle.p7b").unwrap();
123/// let blocks = read_pki_blocks(&data, b"", None::<&NoCrypto>).unwrap();
124/// for (label, der) in &blocks {
125/// println!("{}: {} bytes", label, der.len());
126/// }
127/// ```
128pub fn read_pki_blocks(
129 data: &[u8],
130 password: &[u8],
131 decryptor: Option<&dyn PkiDecryptor>,
132) -> Result<Vec<(String, Vec<u8>)>, ReadAnyError> {
133 // PEM: textual `-----BEGIN` marker somewhere in the data.
134 if data.windows(11).any(|w| w == b"-----BEGIN ") {
135 let pem = crate::pem::pem_blocks(data);
136 let mut out = Vec::with_capacity(pem.len());
137 for (label, der) in pem {
138 // PEM-wrapped PKCS#7: the first element inside the outermost
139 // SEQUENCE is an OID (ContentInfo.contentType). Recurse once
140 // into the binary path to extract the embedded certificates.
141 if matches!(
142 peek_inner_tag(&der),
143 Some(tag) if tag.class() == TagClass::Universal && tag.number() == 6
144 ) {
145 let certs = certs_from_pkcs7(&der).map_err(ReadAnyError::Pkcs7)?;
146 out.extend(label_as_certificates(certs));
147 } else {
148 out.push((label, der));
149 }
150 }
151 return Ok(out);
152 }
153
154 // Binary: probe the tag of the first element inside the outermost SEQUENCE.
155 match peek_inner_tag(data) {
156 // INTEGER → PKCS#12 PFX (version INTEGER is the first field of PFX).
157 Some(tag) if tag.class() == TagClass::Universal && tag.number() == 2 => {
158 read_pkcs12_blocks(data, password, decryptor)
159 }
160 // OID → PKCS#7 / CMS ContentInfo (contentType OID is the first field).
161 Some(tag) if tag.class() == TagClass::Universal && tag.number() == 6 => {
162 certs_from_pkcs7(data)
163 .map_err(ReadAnyError::Pkcs7)
164 .map(label_as_certificates)
165 }
166 // Anything else — hand back the whole buffer as a single raw DER object.
167 _ => Ok(vec![("CERTIFICATE".to_string(), data.to_vec())]),
168 }
169}
170
171// ── Internal helpers ──────────────────────────────────────────────────────────
172
173/// Peek at the tag of the first element inside the outermost SEQUENCE TLV.
174///
175/// Returns `None` if `data` is shorter than two bytes, does not start with a
176/// SEQUENCE tag, or has a malformed length encoding.
177fn peek_inner_tag(data: &[u8]) -> Option<synta::Tag> {
178 let mut d = Decoder::new(data, Encoding::Ber);
179 d.read_tag().ok()?; // outer SEQUENCE tag
180 d.read_length().ok()?; // outer SEQUENCE length
181 d.peek_tag().ok() // first inner tag
182}
183
184/// Wrap a list of DER blobs with the `"CERTIFICATE"` label.
185fn label_as_certificates(certs: Vec<Vec<u8>>) -> Vec<(String, Vec<u8>)> {
186 certs
187 .into_iter()
188 .map(|der| ("CERTIFICATE".to_string(), der))
189 .collect()
190}
191
192// ── PKCS#12-specific logic ────────────────────────────────────────────────────
193
194/// Concrete error type that wraps a type-erased box so it can satisfy the
195/// `std::error::Error` bound required by [`Pkcs12Decryptor::Error`].
196///
197/// (`Box<dyn Error>` itself is `!Sized` and does not implement `Error` in
198/// current Rust versions, so a newtype is needed.)
199struct BoxedError(Box<dyn std::error::Error + Send + Sync + 'static>);
200
201impl std::fmt::Debug for BoxedError {
202 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203 self.0.fmt(f)
204 }
205}
206
207impl std::fmt::Display for BoxedError {
208 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
209 self.0.fmt(f)
210 }
211}
212
213impl std::error::Error for BoxedError {
214 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
215 self.0.source()
216 }
217}
218
219/// Thin `Pkcs12Decryptor` wrapper around a `dyn PkiDecryptor`, so we can pass
220/// a trait-object into the generic `certs_from_pkcs12` function.
221struct DynWrap<'a>(&'a dyn PkiDecryptor);
222
223impl Pkcs12Decryptor for DynWrap<'_> {
224 type Error = BoxedError;
225
226 fn decrypt(
227 &self,
228 algorithm_der: &[u8],
229 ciphertext: &[u8],
230 password: &[u8],
231 ) -> Result<Vec<u8>, BoxedError> {
232 self.0
233 .decrypt_pkcs12(algorithm_der, ciphertext, password)
234 .map_err(BoxedError)
235 }
236}
237
238/// A decryptor that "succeeds" by returning an empty SafeContents SEQUENCE.
239///
240/// Used when `decryptor` is `None`: encrypted bags yield zero certificates
241/// rather than an error, so unencrypted bags in the same archive are still
242/// collected.
243struct SkipEncrypted;
244
245impl Pkcs12Decryptor for SkipEncrypted {
246 type Error = std::convert::Infallible;
247
248 fn decrypt(
249 &self,
250 _algorithm_der: &[u8],
251 _ciphertext: &[u8],
252 _password: &[u8],
253 ) -> Result<Vec<u8>, std::convert::Infallible> {
254 // `30 00` = SEQUENCE { } — an empty SafeContents with no SafeBags.
255 Ok(vec![0x30, 0x00])
256 }
257}
258
259fn read_pkcs12_blocks(
260 data: &[u8],
261 password: &[u8],
262 decryptor: Option<&dyn PkiDecryptor>,
263) -> Result<Vec<(String, Vec<u8>)>, ReadAnyError> {
264 let pki = if let Some(d) = decryptor {
265 pki_from_pkcs12(data, password, &DynWrap(d)).map_err(|e| match e {
266 Pkcs12Error::Parse(e) => ReadAnyError::Pkcs12Parse(e),
267 Pkcs12Error::Crypto(e) => ReadAnyError::Pkcs12Crypto(e.0),
268 Pkcs12Error::UnsupportedFormat(s) => ReadAnyError::Pkcs12Format(s),
269 })?
270 } else {
271 pki_from_pkcs12(data, password, &SkipEncrypted).map_err(|e| match e {
272 Pkcs12Error::Parse(e) => ReadAnyError::Pkcs12Parse(e),
273 // SkipEncrypted::Error = Infallible — this branch is unreachable.
274 Pkcs12Error::Crypto(never) => match never {},
275 Pkcs12Error::UnsupportedFormat(s) => ReadAnyError::Pkcs12Format(s),
276 })?
277 };
278
279 let mut out = label_as_certificates(pki.certs);
280 out.extend(
281 pki.keys
282 .into_iter()
283 .map(|der| ("PRIVATE KEY".to_string(), der)),
284 );
285 Ok(out)
286}