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
21pub const CA_PATH: &str = "/lzcapp/run/certs/box.crt";
23pub const APP_CERT_PATH: &str = "/lzcapp/run/certs/app.crt";
25pub const APP_KEY_PATH: &str = "/lzcapp/run/certs/app.key";
27
28#[derive(Clone, Debug, Eq, PartialEq)]
30pub struct CredentialPaths {
31 pub box_certificate: PathBuf,
33 pub application_certificate: PathBuf,
35 pub application_private_key: PathBuf,
37}
38
39impl CredentialPaths {
40 #[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 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 #[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}