Skip to main content

lzc_sdk/
credentials.rs

1use std::fmt;
2use std::io::Cursor;
3use std::path::{Path, PathBuf};
4use std::sync::Arc;
5
6use ed25519_dalek::pkcs8::DecodePrivateKey as _;
7use ed25519_dalek::{Signer as _, SigningKey as Ed25519SigningKey};
8use rsa::RsaPrivateKey;
9use rsa::pkcs1::DecodeRsaPrivateKey as _;
10use rsa::pkcs1v15::Pkcs1v15Sign;
11use rustls::crypto::ring;
12use rustls::pki_types::PrivateKeyDer;
13use rustls::sign::CertifiedKey;
14use secrecy::{ExposeSecret as _, SecretSlice};
15use tonic::transport::{ClientTlsConfig, Identity};
16use x509_parser::oid_registry::Oid;
17use x509_parser::parse_x509_certificate;
18
19use crate::Error;
20
21/// Runtime box certificate path used by `LazyCat` applications.
22pub const CA_PATH: &str = "/lzcapp/run/certs/box.crt";
23/// Runtime application certificate path used by `LazyCat` applications.
24pub const APP_CERT_PATH: &str = "/lzcapp/run/certs/app.crt";
25/// Runtime application private-key path used by `LazyCat` applications.
26pub const APP_KEY_PATH: &str = "/lzcapp/run/certs/app.key";
27
28/// Files that make up a `LazyCat` application client identity.
29#[derive(Clone, Debug, Eq, PartialEq)]
30pub struct CredentialPaths {
31    /// Box certificate sent to the device authentication API.
32    pub box_certificate: PathBuf,
33    /// Application certificate presented for mTLS.
34    pub application_certificate: PathBuf,
35    /// Application private key presented for mTLS and request signing.
36    pub application_private_key: PathBuf,
37}
38
39impl CredentialPaths {
40    /// Return the fixed credential paths mounted into `LazyCat` applications.
41    #[must_use]
42    pub fn runtime() -> Self {
43        Self {
44            box_certificate: CA_PATH.into(),
45            application_certificate: APP_CERT_PATH.into(),
46            application_private_key: APP_KEY_PATH.into(),
47        }
48    }
49}
50
51impl Default for CredentialPaths {
52    fn default() -> Self {
53        Self::runtime()
54    }
55}
56
57#[derive(Clone)]
58pub struct ClientCredentials {
59    material: Arc<CredentialMaterial>,
60}
61
62struct CredentialMaterial {
63    paths: CredentialPaths,
64    box_certificate_pem: Vec<u8>,
65    application_certificate_pem: Vec<u8>,
66    application_private_key_pem: SecretSlice<u8>,
67    application_id: String,
68    signing_key: ApplicationSigningKey,
69}
70
71enum ApplicationSigningKey {
72    Ed25519(Ed25519SigningKey),
73    Rsa(RsaPrivateKey),
74    Unsupported,
75}
76
77pub(crate) struct AuthRequestMaterial<'a> {
78    pub(crate) box_certificate: &'a [u8],
79    pub(crate) application_certificate: &'a [u8],
80    pub(crate) signature: Vec<u8>,
81}
82
83impl ClientCredentials {
84    /// Load and validate certificate and private-key material from `paths`.
85    ///
86    /// # Errors
87    ///
88    /// Returns a typed error if a file cannot be read, a certificate cannot be
89    /// decoded, or the application key is unsupported or does not match its
90    /// certificate.
91    pub async fn load(paths: CredentialPaths) -> Result<Self, Error> {
92        let box_certificate_pem = read_credential(&paths.box_certificate).await?;
93        let application_certificate_pem = read_credential(&paths.application_certificate).await?;
94        let application_private_key_pem = read_credential(&paths.application_private_key).await?;
95
96        validate_certificates(&paths.box_certificate, &box_certificate_pem)?;
97        let application_certificates =
98            validate_certificates(&paths.application_certificate, &application_certificate_pem)?;
99        let application_id = application_serial_number(&application_certificates);
100        let signing_key = validate_application_key(
101            &paths.application_private_key,
102            &application_private_key_pem,
103            application_certificates,
104        )?;
105
106        Ok(Self {
107            material: Arc::new(CredentialMaterial {
108                paths,
109                box_certificate_pem,
110                application_certificate_pem,
111                application_private_key_pem: application_private_key_pem.into(),
112                application_id,
113                signing_key,
114            }),
115        })
116    }
117
118    /// Return the paths from which this identity was loaded.
119    #[must_use]
120    pub fn paths(&self) -> &CredentialPaths {
121        &self.material.paths
122    }
123
124    pub(crate) fn tls_config(&self, domain_name: &str) -> ClientTlsConfig {
125        ClientTlsConfig::new()
126            .domain_name(domain_name)
127            .identity(Identity::from_pem(
128                &self.material.application_certificate_pem,
129                self.material.application_private_key_pem.expose_secret(),
130            ))
131            .assume_http2(true)
132    }
133
134    pub(crate) fn auth_request_material(&self) -> Result<AuthRequestMaterial<'_>, Error> {
135        let signature = match &self.material.signing_key {
136            ApplicationSigningKey::Ed25519(key) => key
137                .sign(self.material.application_id.as_bytes())
138                .to_bytes()
139                .to_vec(),
140            ApplicationSigningKey::Rsa(key) => key
141                .sign(
142                    Pkcs1v15Sign::new_unprefixed(),
143                    self.material.application_id.as_bytes(),
144                )
145                .map_err(|_| Error::AuthSigning)?,
146            ApplicationSigningKey::Unsupported => {
147                return Err(Error::UnsupportedAuthSigningKey);
148            }
149        };
150        Ok(AuthRequestMaterial {
151            box_certificate: &self.material.box_certificate_pem,
152            application_certificate: &self.material.application_certificate_pem,
153            signature,
154        })
155    }
156}
157
158impl fmt::Debug for ClientCredentials {
159    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
160        formatter
161            .debug_struct("ClientCredentials")
162            .field("paths", &self.material.paths)
163            .field(
164                "box_certificate",
165                &RedactedBytes(&self.material.box_certificate_pem),
166            )
167            .field(
168                "application_certificate",
169                &RedactedBytes(&self.material.application_certificate_pem),
170            )
171            .field(
172                "application_private_key",
173                &self.material.application_private_key_pem,
174            )
175            .finish()
176    }
177}
178
179struct RedactedBytes<'a>(&'a [u8]);
180
181impl fmt::Debug for RedactedBytes<'_> {
182    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
183        let _length = self.0.len();
184        formatter.write_str("<redacted>")
185    }
186}
187
188async fn read_credential(path: &Path) -> Result<Vec<u8>, Error> {
189    tokio::fs::read(path)
190        .await
191        .map_err(|source| Error::CredentialRead {
192            path: path.to_owned(),
193            source,
194        })
195}
196
197fn validate_certificates(
198    path: &Path,
199    pem: &[u8],
200) -> Result<Vec<rustls::pki_types::CertificateDer<'static>>, Error> {
201    let certificates = rustls_pemfile::certs(&mut Cursor::new(pem))
202        .collect::<Result<Vec<_>, _>>()
203        .map_err(|_| Error::InvalidCertificate {
204            path: path.to_owned(),
205            reason: "malformed PEM certificate",
206        })?;
207    if certificates.is_empty() {
208        return Err(Error::InvalidCertificate {
209            path: path.to_owned(),
210            reason: "no certificate found",
211        });
212    }
213    if certificates
214        .iter()
215        .any(|certificate| parse_x509_certificate(certificate.as_ref()).is_err())
216    {
217        return Err(Error::InvalidCertificate {
218            path: path.to_owned(),
219            reason: "invalid X.509 certificate",
220        });
221    }
222    Ok(certificates)
223}
224
225fn validate_application_key(
226    path: &Path,
227    pem: &[u8],
228    certificate_chain: Vec<rustls::pki_types::CertificateDer<'static>>,
229) -> Result<ApplicationSigningKey, Error> {
230    let key = rustls_pemfile::private_key(&mut Cursor::new(pem))
231        .map_err(|_| Error::InvalidPrivateKey {
232            path: path.to_owned(),
233            reason: "malformed PEM private key",
234        })?
235        .ok_or_else(|| Error::InvalidPrivateKey {
236            path: path.to_owned(),
237            reason: "no private key found",
238        })?;
239    let signing_key = application_signing_key(&key);
240    CertifiedKey::from_der(certificate_chain, key, &ring::default_provider()).map_err(|_| {
241        Error::InvalidPrivateKey {
242            path: path.to_owned(),
243            reason: "unsupported key or certificate mismatch",
244        }
245    })?;
246    Ok(signing_key)
247}
248
249fn application_serial_number(
250    certificates: &[rustls::pki_types::CertificateDer<'static>],
251) -> String {
252    let Some(certificate) = certificates.first() else {
253        return String::new();
254    };
255    let Ok((_, certificate)) = parse_x509_certificate(certificate.as_ref()) else {
256        return String::new();
257    };
258    let oid = Oid::from(&[2, 5, 4, 5]).expect("serialNumber OID is valid");
259    certificate
260        .subject()
261        .iter_by_oid(&oid)
262        .next()
263        .and_then(|value| value.as_str().ok())
264        .unwrap_or_default()
265        .to_owned()
266}
267
268fn application_signing_key(key: &PrivateKeyDer<'_>) -> ApplicationSigningKey {
269    match key {
270        PrivateKeyDer::Pkcs8(key) => {
271            if let Ok(key) = Ed25519SigningKey::from_pkcs8_der(key.secret_pkcs8_der()) {
272                ApplicationSigningKey::Ed25519(key)
273            } else if let Ok(key) = RsaPrivateKey::from_pkcs8_der(key.secret_pkcs8_der()) {
274                ApplicationSigningKey::Rsa(key)
275            } else {
276                ApplicationSigningKey::Unsupported
277            }
278        }
279        PrivateKeyDer::Pkcs1(key) => RsaPrivateKey::from_pkcs1_der(key.secret_pkcs1_der()).map_or(
280            ApplicationSigningKey::Unsupported,
281            ApplicationSigningKey::Rsa,
282        ),
283        _ => ApplicationSigningKey::Unsupported,
284    }
285}