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}