Skip to main content

x509_info/
mod.rs

1#![doc = include_str!("../README.md")]
2//! Owned X.509 certificate inspection, independent of applets and transport.
3//!
4//! This extracts the reusable DER/PEM inspection responsibility from Console's
5//! Rust `api/crypto.rs`. It uses the same `x509-parser` dependency, with owned
6//! typed results and bounded, strict single-certificate inputs. This crate is
7//! licensed under Apache-2.0.
8//!
9//! Parsing is not signature verification, chain building, trust, revocation, or
10//! application policy. No system clock or randomness is read. The caller supplies
11//! a timestamp to [`Validity::contains`] if it wants a validity-period check.
12//! Unknown algorithm/extension OIDs and their encoded data remain available.
13//!
14//! [`CertificateInfo::summary`] provides owned application details, common decoded
15//! extensions and a SHA-256 fingerprint without raw certificate/key/signature copies.
16//! Enable `serde` for the versioned [`CertificateSummary`] serialization contract.
17//! Full-result serialization remains available for diagnostics (raw bytes are octet
18//! arrays). JSON belongs to callers; FRB can map these fields to its own DTOs.
19//! No public result borrows backend types or requires a registry/handle lifecycle.
20//!
21//! Enable `schema` for Schemars schemas of the library's Serde result types.
22//! These describe source representations. The optional CLI's `--schema` command
23//! additionally accounts for report field names, Base64 and UUID formatting.
24//!
25//! See [Ecosystem and dependency choices](#ecosystem-and-dependency-choices)
26//! for comparisons with other X.509 libraries.
27//!
28//! # Example
29//!
30//! ```
31//! use x509_info::{parse_pem, ParseOptions};
32//! # let pem = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/inspection.pem"));
33//! let info = parse_pem(pem, ParseOptions::default())?;
34//! assert_eq!(info.public_key.key_size_bits, Some(256));
35//! assert_eq!(info.public_key.algorithm.oid, "1.2.840.10045.2.1");
36//! let details = info.summary();
37//! drop(info);
38//! let subject = details.subject; // Owned independently of parser input/results.
39//! assert!(subject.display.contains("libcanokey test certificate"));
40//! # Ok::<(), x509_info::Error>(())
41//! ```
42#![doc = include_str!("../docs/x509-ecosystem.md")]
43#![deny(missing_docs)]
44#![forbid(unsafe_code)]
45
46mod decoding;
47mod extensions;
48mod keys;
49mod names;
50mod oids;
51mod summary;
52#[cfg(test)]
53mod tests;
54
55pub use decoding::{DecodeDiagnostic, DecodeIssue, IntegerValue};
56pub use extensions::additional::{
57    DirectoryAttribute, NetscapeCertificateType, PolicyConstraints, PolicyMapping,
58    PrivateKeyUsagePeriod,
59};
60pub use extensions::constraints::{ConstraintName, GeneralSubtree, NameConstraints};
61pub use extensions::device::{CertificateTemplate, FidoTransports};
62pub use extensions::locations::{
63    AccessDescription, AuthorityKeyIdentifier, DistributionPoint, DistributionPointName,
64};
65pub use extensions::policies::{
66    CertificatePolicy, NoticeReference, PolicyQualifier, PolicyQualifierDetails, UserNotice,
67};
68pub use extensions::transparency::{SctEntry, SignedCertificateTimestamp};
69pub use extensions::{ExtensionDetails, ExtensionInfo, GeneralName, KeyPurpose, KeyUsage};
70pub use keys::public_key::PublicKeyDetails;
71pub use keys::{AlgorithmInfo, KeyDataStatus, ParameterStatus, PssParameters};
72pub use names::details::NameDetails;
73pub use names::{DistinguishedName, NameAttribute};
74pub use oids::{InvalidOid, OidNames};
75pub use summary::{
76    AlgorithmSummary, CertificateSummary, ExtensionSummary, NameSummary, PublicKeySummary,
77};
78
79use sha2::{Digest, Sha256};
80use x509_parser::nom::Parser;
81
82impl CertificateInfo {
83    /// SHA-256 digest of the retained certificate DER; no trust decision is implied.
84    /// This recomputes the digest from the current bytes, without caching or global state.
85    pub fn sha256_fingerprint(&self) -> [u8; 32] {
86        Sha256::digest(&self.der).into()
87    }
88}
89
90/// Limits for a single certificate input; defaults to 1 MiB.
91#[derive(Clone, Copy, Debug)]
92pub struct ParseOptions {
93    /// Maximum input bytes, also checked against decoded DER for PEM input.
94    /// PEM limits include its armor, base64 and surrounding whitespace.
95    pub max_input_bytes: usize,
96}
97impl Default for ParseOptions {
98    fn default() -> Self {
99        Self {
100            max_input_bytes: 1024 * 1024,
101        }
102    }
103}
104
105/// Typed local parsing failures, distinct from card status and transport errors.
106#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
107#[non_exhaustive]
108pub enum Error {
109    /// A zero parsing budget was supplied.
110    #[error("certificate input limit must be nonzero")]
111    InvalidOptions,
112    /// Encoded input or decoded DER exceeds the caller's budget.
113    #[error("certificate input limit exceeded")]
114    LimitExceeded,
115    /// PEM armor/base64 is invalid, or input contains extra text/blocks.
116    #[error("invalid certificate PEM")]
117    InvalidPem,
118    /// The PEM label is not CERTIFICATE.
119    #[error("expected a CERTIFICATE PEM block")]
120    UnexpectedPemLabel,
121    /// Input could not be decoded as an X.509 certificate.
122    #[error("invalid X.509 certificate")]
123    InvalidCertificate,
124    /// Bytes follow the single DER certificate.
125    #[error("trailing data after certificate")]
126    TrailingData,
127    /// Legacy error retained for compatibility; parsing now preserves both identifiers.
128    #[error("certificate signature algorithm identifiers disagree")]
129    InconsistentSignatureAlgorithm,
130}
131
132/// Certificate validity interval as signed Unix seconds, independent of a clock.
133#[derive(Clone, Copy, Debug, PartialEq, Eq)]
134#[cfg_attr(feature = "serde", derive(serde::Serialize))]
135#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
136pub struct Validity {
137    /// Inclusive notBefore timestamp, measured from 1970-01-01T00:00:00Z.
138    pub not_before_unix: i64,
139    /// Inclusive notAfter timestamp, measured from 1970-01-01T00:00:00Z.
140    pub not_after_unix: i64,
141}
142impl Validity {
143    /// Check only the inclusive time interval at an explicit caller timestamp.
144    /// This does not establish certificate trust, role, revocation or signature validity.
145    pub fn contains(self, unix_seconds: i64) -> bool {
146        self.not_before_unix <= unix_seconds && unix_seconds <= self.not_after_unix
147    }
148}
149
150/// Owned public-key algorithm and encoding, without performing key validation.
151#[derive(Clone, Debug, PartialEq, Eq)]
152#[cfg_attr(feature = "serde", derive(serde::Serialize))]
153#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
154#[non_exhaustive]
155pub struct PublicKeyInfo {
156    /// Algorithm name, OID and parameter inspection.
157    pub algorithm: AlgorithmInfo,
158    /// Whether supported key encoding was decoded; no mathematical validation is performed.
159    pub key_data_status: KeyDataStatus,
160    /// Common named-curve label, when recognized.
161    pub curve_name: Option<String>,
162    /// Named-curve parameter OID when encoded as an OID; otherwise absent.
163    pub curve_oid: Option<String>,
164    /// RSA modulus bit length or nominal size of a recognized EC/EdDSA curve.
165    /// None for unknown algorithms/curves or undecodable key data. This does not
166    /// validate the key and is never replaced with the encoded bit-string length.
167    pub key_size_bits: Option<usize>,
168    /// Number of significant bits in the encoded subjectPublicKey BIT STRING.
169    /// Distinct from a curve size, modulus size or security-strength estimate.
170    pub encoded_key_bits: usize,
171    /// Complete SubjectPublicKeyInfo DER, including algorithm parameters.
172    pub spki_der: Vec<u8>,
173    /// SubjectPublicKey BIT STRING bytes without its unused-bit count prefix.
174    pub key_bytes: Vec<u8>,
175}
176
177/// Owned certificate inspection result; no input lifetime or device state.
178///
179/// Fields describe parsed data, not a trusted identity. The original DER remains
180/// available for consumers that need richer policy/extension processing.
181#[derive(Clone, PartialEq, Eq)]
182#[cfg_attr(feature = "serde", derive(serde::Serialize))]
183#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
184#[non_exhaustive]
185pub struct CertificateInfo {
186    /// Outer signature algorithm with decoded parameters where supported.
187    pub signature_algorithm: AlgorithmInfo,
188    /// Signature algorithm from TBSCertificate, retained independently of the outer field.
189    pub tbs_signature_algorithm: AlgorithmInfo,
190    /// Complete certificate DER, with no trailing object metadata or other certificate.
191    pub der: Vec<u8>,
192    /// One-based X.509 version (1, 2 or 3 for standard versions).
193    pub version: u32,
194    /// Subject distinguished name.
195    pub subject: DistinguishedName,
196    /// Issuer distinguished name; not evidence of a verified issuer relationship.
197    pub issuer: DistinguishedName,
198    /// Encoded validity period; evaluating it requires a caller timestamp.
199    pub validity: Validity,
200    /// Original serial INTEGER content bytes, retaining any leading sign-padding byte.
201    pub serial_number: Vec<u8>,
202    /// Signature BIT STRING bytes without its unused-bit count prefix.
203    pub signature_value: Vec<u8>,
204    /// Number of unused bits in the final signature byte.
205    pub signature_unused_bits: u8,
206    /// Encoded public-key information and any recognized key size.
207    pub public_key: PublicKeyInfo,
208    /// Extensions in encoded order, with common decoded values; no policy is enforced.
209    pub extensions: Vec<ExtensionInfo>,
210}
211impl std::fmt::Debug for CertificateInfo {
212    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213        f.debug_struct("CertificateInfo")
214            .field("version", &self.version)
215            .field("signature_algorithm_oid", &self.signature_algorithm.oid)
216            .finish_non_exhaustive()
217    }
218}
219
220fn check_limit(bytes: &[u8], options: ParseOptions) -> Result<(), Error> {
221    if options.max_input_bytes == 0 {
222        return Err(Error::InvalidOptions);
223    }
224    if bytes.len() > options.max_input_bytes {
225        return Err(Error::LimitExceeded);
226    }
227    Ok(())
228}
229
230/// Inspect exactly one DER certificate, copying its fields into an owned result.
231///
232/// PIV callers pass `certificate.der()` after the applet has unwrapped its object;
233/// this function does not interpret PIV tags, decompress gzip or access a device.
234///
235/// # Errors
236/// Returns InvalidOptions for a zero budget, LimitExceeded before parsing an
237/// oversized input, InvalidCertificate for decoding failures, TrailingData if any
238/// bytes follow the certificate. Inner/outer signature algorithms are retained independently.
239/// Syntactic parsing does not verify signatures, key points, extensions or trust.
240pub fn parse_der(bytes: &[u8], options: ParseOptions) -> Result<CertificateInfo, Error> {
241    check_limit(bytes, options)?;
242    parse_der_with_names(bytes, options, &OidNames::default())
243}
244
245/// Inspect one DER certificate using a caller-owned OID name table.
246/// Results copy names and do not retain the table. Overrides affect presentation
247/// only, never decoder selection, key sizes or validation.
248///
249/// # Errors
250/// Returns the same input/syntax errors as [`parse_der`].
251pub fn parse_der_with_names(
252    bytes: &[u8],
253    options: ParseOptions,
254    names: &OidNames,
255) -> Result<CertificateInfo, Error> {
256    check_limit(bytes, options)?;
257    let (rest, cert) = x509_parser::certificate::X509CertificateParser::new()
258        .with_deep_parse_extensions(false)
259        .parse(bytes)
260        .map_err(|_| Error::InvalidCertificate)?;
261    if !rest.is_empty() {
262        return Err(Error::TrailingData);
263    }
264    let spki = cert.public_key();
265    let encoded_key_bits = spki
266        .subject_public_key
267        .data
268        .len()
269        .checked_mul(8)
270        .and_then(|bits| bits.checked_sub(usize::from(spki.subject_public_key.unused_bits)))
271        .ok_or(Error::InvalidCertificate)?;
272    let algorithm_oid = spki.algorithm.algorithm.to_id_string();
273    let curve_oid = if algorithm_oid == "1.2.840.10045.2.1" {
274        spki.algorithm
275            .parameters
276            .as_ref()
277            .filter(|parameters| {
278                parameters.class() == x509_parser::asn1_rs::Class::Universal
279                    && parameters.tag() == x509_parser::asn1_rs::Tag::Oid
280            })
281            .and_then(|parameters| parameters.as_oid().ok())
282            .map(|oid| oid.to_id_string())
283    } else {
284        None
285    };
286    let (key_size_bits, key_data_status) = keys::key_details(
287        &algorithm_oid,
288        curve_oid.as_deref(),
289        &spki.subject_public_key.data,
290        spki.subject_public_key.unused_bits,
291    );
292    let mut counts = std::collections::BTreeMap::new();
293    for extension in cert.extensions() {
294        *counts.entry(extension.oid.to_id_string()).or_insert(0usize) += 1;
295    }
296    Ok(CertificateInfo {
297        signature_algorithm: keys::inspect(&cert.signature_algorithm, true, names)?,
298        tbs_signature_algorithm: keys::inspect(&cert.tbs_certificate.signature, true, names)?,
299        der: bytes.to_vec(),
300        version: cert
301            .version()
302            .0
303            .checked_add(1)
304            .ok_or(Error::InvalidCertificate)?,
305        subject: names::inspect_name(cert.subject(), names),
306        issuer: names::inspect_name(cert.issuer(), names),
307        validity: Validity {
308            not_before_unix: cert.validity().not_before.timestamp(),
309            not_after_unix: cert.validity().not_after.timestamp(),
310        },
311        serial_number: cert.raw_serial().to_vec(),
312        signature_value: cert.signature_value.data.to_vec(),
313        signature_unused_bits: cert.signature_value.unused_bits,
314        public_key: PublicKeyInfo {
315            algorithm: keys::inspect(&spki.algorithm, false, names)?,
316            curve_name: curve_oid
317                .as_deref()
318                .and_then(|oid| names.get(oid))
319                .map(str::to_owned),
320            curve_oid,
321            key_size_bits,
322            key_data_status,
323            encoded_key_bits,
324            spki_der: spki.raw.to_vec(),
325            key_bytes: spki.subject_public_key.data.to_vec(),
326        },
327        extensions: cert
328            .extensions()
329            .iter()
330            .map(|extension| ExtensionInfo {
331                oid: extension.oid.to_id_string(),
332                critical: extension.critical,
333                duplicate: counts[&extension.oid.to_id_string()] > 1,
334                details: extensions::decode_with_names(
335                    &extension.oid.to_id_string(),
336                    extension.value,
337                    names,
338                ),
339                value_der: extension.value.to_vec(),
340            })
341            .collect(),
342    })
343}
344
345/// Inspect one CERTIFICATE PEM block, permitting only surrounding ASCII whitespace.
346///
347/// Input is bounded before base64 decoding. Bundles, unrelated labels, and text
348/// before/after the block are rejected rather than silently selecting one entry.
349/// Decoded DER is checked with [`parse_der`]. No file or system I/O occurs.
350///
351/// # Errors
352/// Returns InvalidOptions/LimitExceeded for budgets, InvalidPem for invalid armor,
353/// UnexpectedPemLabel for another block type, InvalidPem for extra PEM blocks/data,
354/// or an error from [`parse_der`] for the decoded certificate.
355pub fn parse_pem(bytes: &[u8], options: ParseOptions) -> Result<CertificateInfo, Error> {
356    check_limit(bytes, options)?;
357    parse_pem_with_names(bytes, options, &OidNames::default())
358}
359
360/// Inspect one PEM certificate with caller-owned presentation names.
361/// The input and name table are borrowed only for this call; output owns all data.
362///
363/// # Errors
364/// Returns the same budget, armor and certificate errors as [`parse_pem`].
365pub fn parse_pem_with_names(
366    bytes: &[u8],
367    options: ParseOptions,
368    names: &OidNames,
369) -> Result<CertificateInfo, Error> {
370    check_limit(bytes, options)?;
371    let trimmed = bytes.trim_ascii();
372    // RFC 7468 decoding validates matching boundaries and full consumption.
373    // Unlike Console's former PEM helper, no extra block/text is silently ignored.
374    let (label, der) = pem_rfc7468::decode_vec(trimmed).map_err(|_| Error::InvalidPem)?;
375    if label != "CERTIFICATE" {
376        return Err(Error::UnexpectedPemLabel);
377    }
378    parse_der_with_names(&der, options, names)
379}