Skip to main content

liminal_server/
link_certificate.rs

1//! A capability certificate proves a key and a holder, never a permission.
2
3use crate::link_handshake::MachineIdentity;
4use lys_core::ca::{
5    LYS_OID_ARC, certificate_signing_key, decode_extension, verify_certificate_chain_at,
6};
7use std::time::{Duration, UNIX_EPOCH};
8
9#[path = "link_certificate/offer.rs"]
10pub mod offer;
11#[path = "link_certificate/service_key.rs"]
12pub mod service_key;
13#[path = "link_certificate/setup.rs"]
14pub mod setup;
15#[path = "link_certificate/standing.rs"]
16pub mod standing;
17#[path = "link_certificate/status.rs"]
18pub mod status;
19#[cfg(test)]
20#[path = "link_certificate_tests.rs"]
21mod tests;
22
23/// Stable refusal names exclude certificate bytes and untrusted parser text.
24#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
25pub enum CertificateRefusal {
26    /// Certificate allocation or encoding exceeded its configured limit.
27    #[error("certificate_too_large")]
28    Bound,
29    /// The exact DER, including its outer length, could not be read.
30    #[error("certificate_encoding_refused")]
31    Encoding,
32    /// The configured service key did not verify the certificate.
33    #[error("certificate_signature_refused")]
34    Signature,
35    /// The certificate no longer stands at the operation's time.
36    #[error("certificate_expired")]
37    Expired,
38    /// The certificate cannot yet stand at the operation's time.
39    #[error("certificate_not_yet_valid")]
40    NotYetValid,
41    /// Signed capability claims name another holder.
42    #[error("certificate_holder_mismatch")]
43    HolderMismatch,
44    /// The certified key differs from the Noise offer's static identity.
45    #[error("certificate_key_mismatch")]
46    KeyMismatch,
47    /// Signed capability claims were absent or malformed.
48    #[error("certificate_claims_refused")]
49    Claims,
50    /// No trusted current withdrawal observation exists.
51    #[error("certificate_status_unavailable")]
52    StatusUnavailable,
53    /// The issuer's current observation withdrew this certificate.
54    #[error("certificate_withdrawn")]
55    Withdrawn,
56    /// The configured issuer route, response or service key was refused.
57    #[error("certificate_service_key_refused")]
58    ServiceKey,
59    /// The configured certificate cannot be read.
60    #[error("certificate_path_refused")]
61    Path,
62    /// The current proof was malformed, stale or signed with another key.
63    #[error("certificate_proof_refused")]
64    Proof,
65}
66
67/// The result of signature, lifetime, holder and Noise-key checks.
68/// Withdrawal and current grants are separate decisions at admission.
69pub struct VerifiedCertificate {
70    machine: MachineIdentity,
71    digest: blake3::Hash,
72    not_after: u64,
73}
74
75impl std::fmt::Debug for VerifiedCertificate {
76    fn fmt(&self, out: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        out.debug_struct("VerifiedCertificate")
78            .field("digest", &self.digest)
79            .field("not_after", &self.not_after)
80            .finish_non_exhaustive()
81    }
82}
83
84impl VerifiedCertificate {
85    /// The static identity the signed certificate binds.
86    #[must_use]
87    pub const fn machine(&self) -> MachineIdentity {
88        self.machine
89    }
90    /// A digest identifies the certificate without retaining its payload.
91    #[must_use]
92    pub const fn digest(&self) -> &blake3::Hash {
93        &self.digest
94    }
95    /// The last valid Unix second, for the open link's expiry fence.
96    #[must_use]
97    pub const fn not_after(&self) -> u64 {
98        self.not_after
99    }
100}
101
102/// Verifies only with the service key fetched from the trusted API base.
103/// Historical capability grants are deliberately never inspected for rights.
104///
105/// # Errors
106/// A named bound, encoding, signature, validity, holder or key refusal.
107pub fn verify(
108    der: &[u8],
109    service_key: &[u8; 32],
110    holder: &str,
111    offered: MachineIdentity,
112    now: u64,
113    bound: usize,
114) -> Result<VerifiedCertificate, CertificateRefusal> {
115    if bound == 0 || der.len() > bound {
116        return Err(CertificateRefusal::Bound);
117    }
118    exact_der(der)?;
119    let key = certificate_signing_key(der).map_err(|_| CertificateRefusal::Encoding)?;
120    if i128::from(now) < i128::from(key.not_before) {
121        return Err(CertificateRefusal::NotYetValid);
122    }
123    if i128::from(now) > i128::from(key.not_after) {
124        return Err(CertificateRefusal::Expired);
125    }
126    let at = UNIX_EPOCH
127        .checked_add(Duration::from_secs(now))
128        .ok_or(CertificateRefusal::Encoding)?;
129    verify_certificate_chain_at(der, service_key, at.into())
130        .map_err(|_| CertificateRefusal::Signature)?;
131    let mut oid = LYS_OID_ARC.to_vec();
132    oid.push(1);
133    let claims = decode_extension(der, &oid)
134        .map_err(|_| CertificateRefusal::Claims)?
135        .ok_or(CertificateRefusal::Claims)?;
136    let claims: serde_json::Value =
137        serde_json::from_slice(&claims).map_err(|_| CertificateRefusal::Claims)?;
138    if claims["agent"].as_str() != Some(holder) {
139        return Err(CertificateRefusal::HolderMismatch);
140    }
141    if key.public_key != *offered.ed25519_public() {
142        return Err(CertificateRefusal::KeyMismatch);
143    }
144    Ok(VerifiedCertificate {
145        machine: offered,
146        digest: blake3::hash(der),
147        not_after: u64::try_from(key.not_after).map_err(|_| CertificateRefusal::Encoding)?,
148    })
149}
150
151fn exact_der(der: &[u8]) -> Result<(), CertificateRefusal> {
152    let [0x30, first, rest @ ..] = der else {
153        return Err(CertificateRefusal::Encoding);
154    };
155    let (length, header) = if *first < 128 {
156        (usize::from(*first), 2)
157    } else {
158        let digits = usize::from(first & 0x7f);
159        if digits == 0 || digits > std::mem::size_of::<usize>() {
160            return Err(CertificateRefusal::Encoding);
161        }
162        let encoded = rest.get(..digits).ok_or(CertificateRefusal::Encoding)?;
163        if encoded[0] == 0 {
164            return Err(CertificateRefusal::Encoding);
165        }
166        let length = encoded
167            .iter()
168            .try_fold(0_usize, |length, byte| {
169                length.checked_mul(256)?.checked_add(usize::from(*byte))
170            })
171            .ok_or(CertificateRefusal::Encoding)?;
172        if length < 128 {
173            return Err(CertificateRefusal::Encoding);
174        }
175        (length, 2 + digits)
176    };
177    if length.checked_add(header) != Some(der.len()) {
178        return Err(CertificateRefusal::Encoding);
179    }
180    Ok(())
181}