1use std::{fmt, str::FromStr, sync::Arc};
4
5use anyhow::{Context, anyhow};
6use base64::Engine;
7use lexe_api::auth::BearerAuthenticator;
8use lexe_common::{
9 api::{
10 auth::{BearerAuthToken, Scope},
11 revocable_clients::CreateRevocableClientResponse,
12 user::UserPk,
13 },
14 env::DeployEnv,
15 root_seed::RootSeed,
16};
17#[cfg(any(test, feature = "test-utils"))]
18use lexe_crypto::rng::FastRng;
19use lexe_crypto::{ed25519, rng::Crng};
20#[cfg(any(test, feature = "test-utils"))]
21use lexe_tls::shared_seed::certs::{
22 EphemeralIssuingCaCert, RevocableClientCert, RevocableIssuingCaCert,
23};
24use lexe_tls::{
25 rustls, shared_seed,
26 types::{LxCertificateDer, LxPrivatePkcs8KeyDer},
27};
28#[cfg(any(test, feature = "test-utils"))]
29use proptest::{
30 prelude::{Arbitrary, any},
31 strategy::{BoxedStrategy, Strategy},
32};
33use serde::{Deserialize, Serialize};
34
35pub enum Credentials {
39 RootSeed(RootSeed),
41 ClientCredentials(ClientCredentials),
43}
44
45#[derive(Copy, Clone)]
47pub enum CredentialsRef<'a> {
48 RootSeed(&'a RootSeed),
50 ClientCredentials(&'a ClientCredentials),
52}
53
54#[derive(Clone, Serialize, Deserialize)]
58#[cfg_attr(any(test, feature = "test-utils"), derive(Eq, PartialEq))]
59pub struct ClientCredentials {
60 #[serde(skip_serializing_if = "Option::is_none")]
64 pub user_pk: Option<UserPk>,
69 pub lexe_auth_token: BearerAuthToken,
71 pub client_pk: ed25519::PublicKey,
73 pub rev_client_key_der: LxPrivatePkcs8KeyDer,
75 pub rev_client_cert_der: LxCertificateDer,
77 pub eph_ca_cert_der: LxCertificateDer,
79}
80
81impl Credentials {
84 pub fn as_ref(&self) -> CredentialsRef<'_> {
85 match self {
86 Credentials::RootSeed(root_seed) =>
87 CredentialsRef::RootSeed(root_seed),
88 Credentials::ClientCredentials(client_credentials) =>
89 CredentialsRef::ClientCredentials(client_credentials),
90 }
91 }
92}
93
94impl From<RootSeed> for Credentials {
95 fn from(root_seed: RootSeed) -> Self {
96 Credentials::RootSeed(root_seed)
97 }
98}
99
100impl From<ClientCredentials> for Credentials {
101 fn from(client_credentials: ClientCredentials) -> Self {
102 Credentials::ClientCredentials(client_credentials)
103 }
104}
105
106impl<'a> From<&'a RootSeed> for CredentialsRef<'a> {
107 fn from(root_seed: &'a RootSeed) -> Self {
108 CredentialsRef::RootSeed(root_seed)
109 }
110}
111
112impl<'a> From<&'a ClientCredentials> for CredentialsRef<'a> {
113 fn from(client_credentials: &'a ClientCredentials) -> Self {
114 CredentialsRef::ClientCredentials(client_credentials)
115 }
116}
117
118impl<'a> CredentialsRef<'a> {
119 pub fn user_pk(&self) -> Option<UserPk> {
123 match self {
124 Self::RootSeed(root_seed) => Some(root_seed.derive_user_pk()),
125 Self::ClientCredentials(cc) => cc.user_pk,
126 }
127 }
128
129 pub fn bearer_authenticator(&self) -> Arc<BearerAuthenticator> {
133 match self {
134 Self::RootSeed(root_seed) => {
135 let maybe_cached_token = None;
136 Arc::new(BearerAuthenticator::new_with_scope(
137 root_seed.derive_user_key_pair(),
138 maybe_cached_token,
139 Some(Scope::NodeConnect),
140 ))
141 }
142 Self::ClientCredentials(client_credentials) =>
143 Arc::new(BearerAuthenticator::new_static_token(
144 client_credentials.lexe_auth_token.clone(),
145 )),
146 }
147 }
148
149 pub fn tls_config(
151 &self,
152 rng: &mut impl Crng,
153 deploy_env: DeployEnv,
154 ) -> anyhow::Result<rustls::ClientConfig> {
155 match self {
156 Self::RootSeed(root_seed) =>
157 shared_seed::app_node_run_client_config(
158 rng, deploy_env, root_seed,
159 )
160 .context("Failed to build RootSeed TLS client config"),
161 Self::ClientCredentials(client_credentials) =>
162 shared_seed::sdk_node_run_client_config(
163 deploy_env,
164 &client_credentials.eph_ca_cert_der,
165 client_credentials.rev_client_cert_der.clone(),
166 client_credentials.rev_client_key_der.clone(),
167 )
168 .context("Failed to build revocable client TLS config"),
169 }
170 }
171}
172
173impl fmt::Debug for ClientCredentials {
176 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177 f.write_str("ClientCredentials(..)")
178 }
179}
180
181impl FromStr for ClientCredentials {
182 type Err = anyhow::Error;
183
184 fn from_str(s: &str) -> Result<Self, Self::Err> {
185 Self::try_from_base64_blob(s)
186 }
187}
188
189impl ClientCredentials {
190 pub fn from_response(
191 lexe_auth_token: BearerAuthToken,
192 resp: CreateRevocableClientResponse,
193 ) -> Self {
194 ClientCredentials {
195 user_pk: resp.user_pk,
196 lexe_auth_token,
197 client_pk: resp.pubkey,
198 rev_client_key_der: LxPrivatePkcs8KeyDer(
199 resp.rev_client_cert_key_der,
200 ),
201 rev_client_cert_der: LxCertificateDer(resp.rev_client_cert_der),
202 eph_ca_cert_der: LxCertificateDer(resp.eph_ca_cert_der),
203 }
204 }
205
206 pub fn to_base64_blob(&self) -> String {
221 let json_str =
222 serde_json::to_string(self).expect("Failed to JSON serialize");
223 base64::engine::general_purpose::STANDARD_NO_PAD
224 .encode(json_str.as_bytes())
225 }
226
227 pub fn try_from_base64_blob(s: &str) -> anyhow::Result<Self> {
236 let s = s.trim().trim_end_matches('=');
237 let bytes = base64::engine::general_purpose::STANDARD_NO_PAD
238 .decode(s)
239 .context("String is not valid base64")?;
240 let string =
241 String::from_utf8(bytes).context("String is not valid UTF-8")?;
242 let cc = serde_json::from_str::<ClientCredentials>(&string)
243 .context("Failed to deserialize")?;
244
245 let rev_client_keypair = ed25519::KeyPair::deserialize_pkcs8_der(
248 cc.rev_client_key_der.as_bytes(),
249 )
250 .map_err(|_| anyhow!("Client key is invalid or corrupted"))?;
251 if rev_client_keypair.public_key() != &cc.client_pk {
252 return Err(anyhow!("Client key does not match client public key"));
253 }
254
255 Ok(cc)
256 }
257}
258
259#[cfg(any(test, feature = "test-utils"))]
261impl Arbitrary for ClientCredentials {
262 type Parameters = ();
263 type Strategy = BoxedStrategy<ClientCredentials>;
264 fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
265 let root_seed = any::<RootSeed>();
266 let rng = any::<FastRng>();
267 let auth_token = any::<BearerAuthToken>();
268 let has_user_pk = any::<bool>();
269
270 (root_seed, rng, auth_token, has_user_pk)
271 .prop_map(|(root_seed, mut rng, auth_token, has_user_pk)| {
272 let eph_ca_cert =
274 EphemeralIssuingCaCert::from_root_seed(&root_seed);
275 let rev_ca_cert =
276 RevocableIssuingCaCert::from_root_seed(&root_seed);
277 let rev_client_cert =
278 RevocableClientCert::generate_from_rng(&mut rng);
279
280 let user_pk = has_user_pk.then(|| root_seed.derive_user_pk());
282 let lexe_auth_token = auth_token;
283 let client_pk = rev_client_cert.public_key().to_owned();
284 let rev_client_key_der = rev_client_cert.serialize_key_der();
285 let rev_client_cert_der = rev_client_cert
286 .serialize_der_ca_signed(&rev_ca_cert)
287 .unwrap();
288 let eph_ca_cert_der =
289 eph_ca_cert.serialize_der_self_signed().unwrap();
290
291 ClientCredentials {
292 user_pk,
293 lexe_auth_token,
294 client_pk,
295 rev_client_key_der,
296 rev_client_cert_der,
297 eph_ca_cert_der,
298 }
299 })
300 .boxed()
301 }
302}
303
304#[cfg(test)]
305mod test {
306 use std::fs;
307
308 use lexe_common::{
309 byte_str::ByteStr,
310 test_utils::{arbitrary, snapshot},
311 };
312 use lexe_crypto::rng::FastRng;
313 use lexe_tls::shared_seed::certs::{
314 EphemeralIssuingCaCert, RevocableIssuingCaCert,
315 };
316 use proptest::{prelude::any, prop_assert_eq, proptest};
317
318 use super::*;
319
320 #[test]
326 fn prop_client_credentials_base64_roundtrip() {
327 proptest!(|(creds1 in proptest::prelude::any::<ClientCredentials>())| {
328 {
331 let new_base64_blob = creds1.to_base64_blob();
332
333 let creds2 =
334 ClientCredentials::try_from_base64_blob(&new_base64_blob)
335 .expect("Failed to decode from new format");
336
337 prop_assert_eq!(&creds1, &creds2);
338 }
339
340 {
344 let json_str = serde_json::to_string(&creds1)
345 .expect("Failed to JSON serialize");
346 let old_base64_blob = base64::engine::general_purpose::STANDARD
347 .encode(json_str.as_bytes());
348 let creds2 =
349 ClientCredentials::try_from_base64_blob(&old_base64_blob)
350 .expect("Failed to decode from old format");
351
352 prop_assert_eq!(&creds1, &creds2);
353 }
354 });
355 }
356
357 #[test]
360 fn prop_base64_pad_to_no_pad_compat() {
361 proptest!(|(bytes1 in any::<Vec<u8>>())| {
362 let string =
363 base64::engine::general_purpose::STANDARD.encode(&bytes1);
364 let trimmed = string.trim_end_matches('=');
365 let bytes2 = base64::engine::general_purpose::STANDARD_NO_PAD
366 .decode(trimmed)
367 .expect("Failed to decode base64");
368 prop_assert_eq!(bytes1, bytes2);
369 })
370 }
371
372 #[test]
373 fn test_client_auth_encoding() {
374 let mut rng = FastRng::from_u64(202505121546);
375 let root_seed = RootSeed::from_rng(&mut rng);
376
377 let user_pk = root_seed.derive_user_pk();
378
379 let eph_ca_cert = EphemeralIssuingCaCert::from_root_seed(&root_seed);
380 let eph_ca_cert_der = eph_ca_cert.serialize_der_self_signed().unwrap();
381
382 let rev_ca_cert = RevocableIssuingCaCert::from_root_seed(&root_seed);
383
384 let rev_client_cert = RevocableClientCert::generate_from_rng(&mut rng);
385 let rev_client_cert_der = rev_client_cert
386 .serialize_der_ca_signed(&rev_ca_cert)
387 .unwrap();
388 let rev_client_key_der = rev_client_cert.serialize_key_der();
389 let client_pk = rev_client_cert.public_key();
390
391 let credentials = ClientCredentials {
392 user_pk: Some(user_pk),
393 lexe_auth_token: BearerAuthToken(ByteStr::from_static(
394 "9dTCUvC8y7qcNyUbqynz3nwIQQHbQqPVKeMhXUj1Afr-vgj9E217_2tCS1IQM7LFqfBUC8Ec9fcb-dQiCRy6ot2FN-kR60edRFJUztAa2Rxao1Q0BS1s6vE8grgfhMYIAJDLMWgAAAAASE4zaAAAAABpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaQE",
395 )),
396 client_pk: *client_pk,
397 rev_client_key_der,
398 rev_client_cert_der,
399 eph_ca_cert_der,
400 };
401
402 let credentials_str = credentials.to_base64_blob();
403
404 let expected_str = "eyJ1c2VyX3BrIjoiNmZkNzY0MTU2OTMwNTA5ZmFkNTM2MWQzYjIyYjYxZjc1YWE5MWVkNjQwMjE1YjJjNDFjMmZmODZiMmJmYzQ3MiIsImxleGVfYXV0aF90b2tlbiI6IjlkVENVdkM4eTdxY055VWJxeW56M253SVFRSGJRcVBWS2VNaFhVajFBZnItdmdqOUUyMTdfMnRDUzFJUU03TEZxZkJVQzhFYzlmY2ItZFFpQ1J5Nm90MkZOLWtSNjBlZFJGSlV6dEFhMlJ4YW8xUTBCUzFzNnZFOGdyZ2ZoTVlJQUpETE1XZ0FBQUFBU0U0emFBQUFBQUJwYVdscGFXbHBhV2xwYVdscGFXbHBhV2xwYVdscGFXbHBhV2xwYVdscGFRRSIsImNsaWVudF9wayI6IjcwODhhZjFmYzEyYWIwNGFkNmRkMTY1YmMzYTNjNWViMzA2MmI0MTFhMmY1NWExNjZiMGU0MDBiMzkwZmU0ZGIiLCJyZXZfY2xpZW50X2tleV9kZXIiOiIzMDUxMDIwMTAxMzAwNTA2MDMyYjY1NzAwNDIyMDQyMDBmNTgwZDM0NjFjNGVhMGIzNmI4MzZkNDUxYzFjMTk5ZWUzZTA2NDZhZDBkNjQyMzUzNzk3MzlkNjg2OTkyODk4MTIxMDA3MDg4YWYxZmMxMmFiMDRhZDZkZDE2NWJjM2EzYzVlYjMwNjJiNDExYTJmNTVhMTY2YjBlNDAwYjM5MGZlNGRiIiwicmV2X2NsaWVudF9jZXJ0X2RlciI6IjMwODIwMTgzMzA4MjAxMzVhMDAzMDIwMTAyMDIxNDQwYmVkYzU2ZDAzZDZiNTJmMjg0MmQ2NGRmOTBkMDJkNmRhMzZhNWIzMDA1MDYwMzJiNjU3MDMwNTYzMTBiMzAwOTA2MDM1NTA0MDYwYzAyNTU1MzMxMGIzMDA5MDYwMzU1MDQwODBjMDI0MzQxMzExMTMwMGYwNjAzNTUwNDBhMGMwODZjNjU3ODY1MmQ2MTcwNzAzMTI3MzAyNTA2MDM1NTA0MDMwYzFlNGM2NTc4NjUyMDcyNjU3NjZmNjM2MTYyNmM2NTIwNjk3MzczNzU2OTZlNjcyMDQzNDEyMDYzNjU3Mjc0MzAyMDE3MGQzNzM1MzAzMTMwMzEzMDMwMzAzMDMwMzA1YTE4MGYzNDMwMzkzNjMwMzEzMDMxMzAzMDMwMzAzMDMwNWEzMDUyMzEwYjMwMDkwNjAzNTUwNDA2MGMwMjU1NTMzMTBiMzAwOTA2MDM1NTA0MDgwYzAyNDM0MTMxMTEzMDBmMDYwMzU1MDQwYTBjMDg2YzY1Nzg2NTJkNjE3MDcwMzEyMzMwMjEwNjAzNTUwNDAzMGMxYTRjNjU3ODY1MjA3MjY1NzY2ZjYzNjE2MjZjNjUyMDYzNmM2OTY1NmU3NDIwNjM2NTcyNzQzMDJhMzAwNTA2MDMyYjY1NzAwMzIxMDA3MDg4YWYxZmMxMmFiMDRhZDZkZDE2NWJjM2EzYzVlYjMwNjJiNDExYTJmNTVhMTY2YjBlNDAwYjM5MGZlNGRiYTMxNzMwMTUzMDEzMDYwMzU1MWQxMTA0MGMzMDBhODIwODZjNjU3ODY1MmU2MTcwNzAzMDA1MDYwMzJiNjU3MDAzNDEwMDdiMTdiYzk1MzgyNjdiMzU0ZjA3MjZkODljYjFlYzMxMGIxMDJlNDIyYWI5Njk2Yjg3ZDlhZTcwMGNlZjJlODNjMTM2NmQwYWQxOTAzNWQ5ZTNlZDA0Y2Y1ZjdmMDVkZWY2OGE3MWRlMjEyYjg5ODM0NDc3OTQyYWU3NjNhMjBmIiwiZXBoX2NhX2NlcnRfZGVyIjoiMzA4MjAxYWUzMDgyMDE2MGEwMDMwMjAxMDIwMjE0MTBjZDVjOTk4OWY5NjUyMDk0OWUwZTlhYjRjZTRkYmUxNDc2NjcxMDMwMDUwNjAzMmI2NTcwMzA1MDMxMGIzMDA5MDYwMzU1MDQwNjBjMDI1NTUzMzEwYjMwMDkwNjAzNTUwNDA4MGMwMjQzNDEzMTExMzAwZjA2MDM1NTA0MGEwYzA4NmM2NTc4NjUyZDYxNzA3MDMxMjEzMDFmMDYwMzU1MDQwMzBjMTg0YzY1Nzg2NTIwNzM2ODYxNzI2NTY0MjA3MzY1NjU2NDIwNDM0MTIwNjM2NTcyNzQzMDIwMTcwZDM3MzUzMDMxMzAzMTMwMzAzMDMwMzAzMDVhMTgwZjM0MzAzOTM2MzAzMTMwMzEzMDMwMzAzMDMwMzA1YTMwNTAzMTBiMzAwOTA2MDM1NTA0MDYwYzAyNTU1MzMxMGIzMDA5MDYwMzU1MDQwODBjMDI0MzQxMzExMTMwMGYwNjAzNTUwNDBhMGMwODZjNjU3ODY1MmQ2MTcwNzAzMTIxMzAxZjA2MDM1NTA0MDMwYzE4NGM2NTc4NjUyMDczNjg2MTcyNjU2NDIwNzM2NTY1NjQyMDQzNDEyMDYzNjU3Mjc0MzAyYTMwMDUwNjAzMmI2NTcwMDMyMTAwZWZlOWNlMWFiY2FlYWJjZWY4ZWEyZjU0YTU2OTU1MGRjZWQ0YThmM2E4Y2JiMDRjZDk0NWQxYjRlMjQ1ZjY4N2EzNGEzMDQ4MzAxMzA2MDM1NTFkMTEwNDBjMzAwYTgyMDg2YzY1Nzg2NTJlNjE3MDcwMzAxZDA2MDM1NTFkMGUwNDE2MDQxNDkwY2Q1Yzk5ODlmOTY1MjA5NDllMGU5YWI0Y2U0ZGJlMTQ3NjY3MTAzMDEyMDYwMzU1MWQxMzAxMDFmZjA0MDgzMDA2MDEwMWZmMDIwMTAwMzAwNTA2MDMyYjY1NzAwMzQxMDAzNzI1NDI5ZjViY2E4MDU2MjFjMmIyZGM0NDU4MDJlZDIxY2FiMjQ2YjQ1YWQxMjFkZDJhNDMyZWZhMmY5M2VmNzI1ZWZhMTc4MmU2NDEwOGQyMjk4ZTg2OTRmNDY4NmNlZDk4Y2U5MjgwZWQ3NDlkMGFkNGI0NGE0YTFjZWUwZCJ9";
410 assert_eq!(credentials_str, expected_str);
411
412 let credentials2 =
413 ClientCredentials::try_from_base64_blob(&credentials_str)
414 .expect("Failed to decode ClientAuth");
415 assert_eq!(credentials, credentials2);
416 }
417
418 #[test]
424 #[ignore]
425 fn take_client_credentials_snapshot() {
426 let mut rng = FastRng::from_u64(202512210138);
427 const N: usize = 3;
428
429 let samples: Vec<ClientCredentials> =
430 arbitrary::gen_values(&mut rng, any::<ClientCredentials>(), N);
431
432 for sample in samples {
433 println!("{}", serde_json::to_string(&sample).unwrap());
434 }
435 }
436
437 #[test]
439 fn client_credentials_deser_compat() {
440 let snapshot =
441 fs::read_to_string("test_data/client_credentials_snapshot.txt")
442 .unwrap();
443
444 for input in snapshot::parse_sample_data(&snapshot) {
445 let value1: ClientCredentials =
446 serde_json::from_str(input).unwrap();
447 let output = serde_json::to_string(&value1).unwrap();
448 let value2: ClientCredentials =
449 serde_json::from_str(&output).unwrap();
450 assert_eq!(value1, value2);
451 }
452 }
453}