Skip to main content

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}