lys_core/ca/extensions.rs
1//! Custom X.509 extension transport.
2//!
3//! These helpers encode and extract opaque DER payloads carried in custom
4//! certificate extensions. The crate owns only the extension *structure* —
5//! encoding bytes into an extension and reading them back out by OID. It does
6//! not interpret the payload: the meaning of the bytes is the consumer's
7//! concern.
8//!
9//! Extensions are created non-critical so that generic X.509 parsers, which
10//! do not recognise the custom OID, still accept certificates that carry them.
11
12use x509_parser::oid_registry::Oid;
13use x509_parser::prelude::{FromDer, X509Certificate};
14
15use crate::error::{TrustError, TrustResult};
16
17/// The rcgen extension type produced by [`encode_extension`] and consumed by
18/// [`crate::ca::CertificateAuthority::issue_certificate`].
19pub use rcgen::CustomExtension;
20
21/// Private-enterprise OID arc reserved for lys custom extensions.
22///
23/// Rooted at the IANA Private Enterprise Numbers arc (`1.3.6.1.4.1`). The
24/// final component, `66364`, is the Private Enterprise Number IANA assigned
25/// to lys — permanent, publicly registered, and never renumbered: extensions
26/// are signed under OIDs beneath this arc, so changing it would invalidate
27/// every certificate ever issued. Consumers append their own sub-components
28/// to this arc to distinguish individual extension kinds.
29pub const LYS_OID_ARC: &[u64] = &[1, 3, 6, 1, 4, 1, 66364];
30
31/// Encodes an opaque DER payload into a non-critical custom X.509 extension.
32///
33/// The returned extension carries `payload_bytes` verbatim under `oid`. The
34/// payload is treated as opaque transport — no structure is imposed on or
35/// read from it here. Criticality is set to `false` so parsers that do not
36/// recognise `oid` will not reject the certificate.
37pub fn encode_extension(oid: &[u64], payload_bytes: impl Into<Vec<u8>>) -> CustomExtension {
38 let mut extension = CustomExtension::from_oid_content(oid, payload_bytes.into());
39 extension.set_criticality(false);
40 extension
41}
42
43/// Extracts the opaque payload of a custom extension identified by `oid`.
44///
45/// Parses `cert_der`, then looks up the extension by OID, rejecting
46/// certificates that carry the same OID more than once. Returns the raw
47/// extension value bytes unchanged, or `None` when the certificate does not
48/// carry an extension with that OID.
49///
50/// # The returned payload is UNAUTHENTICATED
51///
52/// This function performs **no signature check and no issuer check**. It
53/// parses bytes out of a certificate that anybody can construct, and the
54/// extensions it reads are non-critical, so a parser that does not recognise
55/// the OID accepts the certificate regardless of what the payload says.
56///
57/// Capability claims read this way are therefore **claims, not permissions**.
58/// They become trustworthy only once
59/// [`verify_certificate_chain`](crate::ca::verify_certificate_chain) — or
60/// [`verify_certificate_chain_at`](crate::ca::verify_certificate_chain_at)
61/// for an explicit instant — has succeeded against the public key of an
62/// issuer you already trust. Verifying the certificate *after* acting on a
63/// payload read from it is not equivalent, and treating the return value of
64/// this function as an authorisation decision is a privilege-escalation bug:
65/// the caller chooses the certificate, so the caller chooses the claims.
66///
67/// The CLI's `lys inspect cert` models the intended discipline — it prints
68/// these payloads under an explicit `UNVERIFIED CLAIMS` marker naming the
69/// command that does verify.
70///
71/// # Errors
72///
73/// Returns [`TrustError::CertificateParsing`] if `cert_der` is not a valid
74/// certificate, `oid` is not a well-formed object identifier, or the
75/// certificate carries the OID more than once.
76pub fn decode_extension(cert_der: &[u8], oid: &[u64]) -> TrustResult<Option<Vec<u8>>> {
77 let (_, certificate) =
78 X509Certificate::from_der(cert_der).map_err(|e| TrustError::CertificateParsing {
79 reason: format!("failed to parse certificate DER: {e:?}"),
80 })?;
81
82 let target = Oid::from(oid).map_err(|_err| TrustError::CertificateParsing {
83 reason: "extension lookup OID is not a well-formed object identifier".to_string(),
84 })?;
85
86 match certificate.tbs_certificate.get_extension_unique(&target) {
87 Ok(Some(extension)) => Ok(Some(extension.value.to_vec())),
88 Ok(None) => Ok(None),
89 Err(e) => Err(TrustError::CertificateParsing {
90 reason: format!("duplicate or malformed extension for OID lookup: {e}"),
91 }),
92 }
93}
94
95#[cfg(test)]
96#[path = "extensions_tests.rs"]
97mod tests;