Skip to main content

clawft_kernel/workload_runtime/
seed_tls.rs

1//! TLS trust for the Cognitum Seed transport.
2//!
3//! A Seed serves HTTPS with a self-signed certificate, so public-CA
4//! verification cannot succeed. Rather than disabling verification (which
5//! would hand the per-Seed bearer token to anyone answering the handshake),
6//! the operator pins either the SHA-256 of the Seed's leaf public key
7//! ([`SeedTls::PinnedSpki`], recommended) or of the whole leaf certificate
8//! ([`SeedTls::PinnedSha256`]), recorded once over a trusted path (USB link
9//! or first pairing). Any other certificate fails the handshake before a
10//! request, and so before the `Authorization` header, is sent.
11//!
12//! Firmware 0.22.20 and later cap leaf certificates at 825 days and renew
13//! them on the 6-hourly update loop, so a certificate pin can break with
14//! no operator action. The SPKI pin survives a renewal that keeps the
15//! device key; a renewal that changes the key is refused and needs the
16//! operator to re-pin over a trusted path (a new pin, never a silent
17//! re-trust).
18
19use std::sync::Arc;
20
21use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
22use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
23use rustls::{DigitallySignedStruct, SignatureScheme};
24use sha2::{Digest, Sha256};
25
26use super::types::RuntimeError;
27
28/// How an `https://` Seed is authenticated.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum SeedTls {
31    /// Public-CA verification (a Seed behind a CA-issued certificate).
32    WebPki,
33    /// Accept exactly the leaf certificate with this SHA-256 (DER).
34    PinnedSha256([u8; 32]),
35    /// Accept any leaf certificate whose SubjectPublicKeyInfo (DER) has
36    /// this SHA-256. Survives certificate renewal under the same key.
37    PinnedSpki([u8; 32]),
38}
39
40impl SeedTls {
41    /// Parse `sha256:<64 hex>` (colons between byte pairs allowed, as
42    /// `openssl x509 -fingerprint -sha256` prints them).
43    pub fn pinned(fingerprint: &str) -> Result<Self, RuntimeError> {
44        let bad = || {
45            RuntimeError::InvalidConfig(
46                "seed certificate pin must be sha256:<64 hex digits>".into(),
47            )
48        };
49        let hex: String = fingerprint
50            .strip_prefix("sha256:")
51            .ok_or_else(bad)?
52            .chars()
53            .filter(|c| *c != ':')
54            .collect();
55        if hex.len() != 64 || !hex.bytes().all(|b| b.is_ascii_hexdigit()) {
56            return Err(bad());
57        }
58        let mut out = [0u8; 32];
59        for (i, byte) in out.iter_mut().enumerate() {
60            *byte = u8::from_str_radix(&hex[2 * i..2 * i + 2], 16).map_err(|_| bad())?;
61        }
62        Ok(Self::PinnedSha256(out))
63    }
64
65    /// Parse `spki-sha256:<64 hex>` (colons between byte pairs allowed).
66    pub fn pinned_spki(fingerprint: &str) -> Result<Self, RuntimeError> {
67        let rest = fingerprint.strip_prefix("spki-sha256:").ok_or_else(|| {
68            RuntimeError::InvalidConfig("seed key pin must be spki-sha256:<64 hex digits>".into())
69        })?;
70        match Self::pinned(&format!("sha256:{rest}"))? {
71            Self::PinnedSha256(h) => Ok(Self::PinnedSpki(h)),
72            other => Ok(other),
73        }
74    }
75
76    /// `spki-sha256:<hex>` of a DER certificate's public key (for
77    /// recording a pin). `None` if the certificate does not parse.
78    pub fn spki_fingerprint(der: &[u8]) -> Option<String> {
79        let d = Sha256::digest(spki_der(der)?);
80        let hex: String = d.iter().map(|b| format!("{b:02x}")).collect();
81        Some(format!("spki-sha256:{hex}"))
82    }
83
84    /// `sha256:<hex>` of a DER certificate (for recording a pin).
85    pub fn fingerprint(der: &[u8]) -> String {
86        let d = Sha256::digest(der);
87        let hex: String = d.iter().map(|b| format!("{b:02x}")).collect();
88        format!("sha256:{hex}")
89    }
90
91    /// The rustls client config for a pinned Seed (`None` for WebPki,
92    /// which uses reqwest's default roots).
93    pub(crate) fn client_config(&self) -> Option<rustls::ClientConfig> {
94        let pin = match self {
95            Self::PinnedSha256(p) => Pin::Cert(*p),
96            Self::PinnedSpki(p) => Pin::Spki(*p),
97            Self::WebPki => return None,
98        };
99        let provider = Arc::new(rustls::crypto::ring::default_provider());
100        let verifier = Arc::new(PinnedCert {
101            pin,
102            provider: provider.clone(),
103        });
104        let mut cfg = rustls::ClientConfig::builder_with_provider(provider)
105            .with_safe_default_protocol_versions()
106            .ok()?
107            .dangerous()
108            .with_custom_certificate_verifier(verifier)
109            .with_no_client_auth();
110        cfg.alpn_protocols = vec![b"http/1.1".to_vec()];
111        Some(cfg)
112    }
113}
114
115#[derive(Debug, Clone, Copy)]
116enum Pin {
117    Cert([u8; 32]),
118    Spki([u8; 32]),
119}
120
121/// One DER element: returns `(content, rest)` for a `tag` at the start of
122/// `b`. Lengths beyond 4 bytes are rejected.
123fn der_take(b: &[u8], tag: u8) -> Option<(&[u8], &[u8])> {
124    let (&t, b) = b.split_first()?;
125    if t != tag {
126        return None;
127    }
128    let (&l0, b) = b.split_first()?;
129    let (len, b) = if l0 < 0x80 {
130        (l0 as usize, b)
131    } else {
132        let n = (l0 & 0x7f) as usize;
133        if n == 0 || n > 4 || b.len() < n {
134            return None;
135        }
136        let len = b[..n].iter().fold(0usize, |a, x| (a << 8) | *x as usize);
137        (len, &b[n..])
138    };
139    (b.len() >= len).then(|| b.split_at(len))
140}
141
142/// The DER `SubjectPublicKeyInfo` element (header included) of an X.509
143/// certificate.
144fn spki_der(cert: &[u8]) -> Option<&[u8]> {
145    let (cert, _) = der_take(cert, 0x30)?;
146    let (tbs, _) = der_take(cert, 0x30)?;
147    let mut rest = tbs;
148    if rest.first() == Some(&0xa0) {
149        rest = der_take(rest, 0xa0)?.1; // version
150    }
151    // serial, signature, issuer, validity, subject
152    let (_, r) = der_take(rest, 0x02)?;
153    rest = r;
154    for _ in 0..4 {
155        rest = der_take(rest, 0x30)?.1;
156    }
157    let (inner, _) = der_take(rest, 0x30)?;
158    // Header length = element length minus content length.
159    let head = rest.len() - der_take(rest, 0x30)?.1.len() - inner.len();
160    Some(&rest[..head + inner.len()])
161}
162
163/// Accepts only the pinned leaf; handshake signatures are still verified,
164/// so the peer must hold the pinned certificate's private key.
165#[derive(Debug)]
166struct PinnedCert {
167    pin: Pin,
168    provider: Arc<rustls::crypto::CryptoProvider>,
169}
170
171impl ServerCertVerifier for PinnedCert {
172    fn verify_server_cert(
173        &self,
174        end_entity: &CertificateDer<'_>,
175        _intermediates: &[CertificateDer<'_>],
176        _server_name: &ServerName<'_>,
177        _ocsp_response: &[u8],
178        _now: UnixTime,
179    ) -> Result<ServerCertVerified, rustls::Error> {
180        let ok = match self.pin {
181            Pin::Cert(p) => <[u8; 32]>::from(Sha256::digest(end_entity.as_ref())) == p,
182            Pin::Spki(p) => spki_der(end_entity.as_ref())
183                .is_some_and(|k| <[u8; 32]>::from(Sha256::digest(k)) == p),
184        };
185        if ok {
186            Ok(ServerCertVerified::assertion())
187        } else {
188            Err(rustls::Error::General(
189                "seed certificate does not match the pinned fingerprint".into(),
190            ))
191        }
192    }
193
194    fn verify_tls12_signature(
195        &self,
196        message: &[u8],
197        cert: &CertificateDer<'_>,
198        dss: &DigitallySignedStruct,
199    ) -> Result<HandshakeSignatureValid, rustls::Error> {
200        rustls::crypto::verify_tls12_signature(
201            message,
202            cert,
203            dss,
204            &self.provider.signature_verification_algorithms,
205        )
206    }
207
208    fn verify_tls13_signature(
209        &self,
210        message: &[u8],
211        cert: &CertificateDer<'_>,
212        dss: &DigitallySignedStruct,
213    ) -> Result<HandshakeSignatureValid, rustls::Error> {
214        rustls::crypto::verify_tls13_signature(
215            message,
216            cert,
217            dss,
218            &self.provider.signature_verification_algorithms,
219        )
220    }
221
222    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
223        self.provider
224            .signature_verification_algorithms
225            .supported_schemes()
226    }
227}