1use std::{fmt, str::FromStr, sync::Arc};
4
5use anyhow::Context;
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};
17use lexe_crypto::{ed25519, rng::Crng};
18use lexe_tls::{
19 rustls, shared_seed,
20 types::{LxCertificateDer, LxPrivatePkcs8KeyDer},
21};
22#[cfg(any(test, feature = "test-utils"))]
23use proptest::{prelude::any, strategy::Strategy};
24#[cfg(any(test, feature = "test-utils"))]
25use proptest_derive::Arbitrary;
26use serde::{Deserialize, Serialize};
27
28pub enum Credentials {
32 RootSeed(RootSeed),
34 ClientCredentials(ClientCredentials),
36}
37
38#[derive(Copy, Clone)]
40pub enum CredentialsRef<'a> {
41 RootSeed(&'a RootSeed),
43 ClientCredentials(&'a ClientCredentials),
45}
46
47#[derive(Clone, Serialize, Deserialize)]
51#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary, Eq, PartialEq))]
52pub struct ClientCredentials {
53 #[serde(skip_serializing_if = "Option::is_none")]
57 #[cfg_attr(
58 any(test, feature = "test-utils"),
59 proptest(strategy = "any::<UserPk>().prop_map(Some)")
60 )]
61 pub user_pk: Option<UserPk>,
62 pub lexe_auth_token: BearerAuthToken,
64 pub client_pk: ed25519::PublicKey,
66 pub rev_client_key_der: LxPrivatePkcs8KeyDer,
68 pub rev_client_cert_der: LxCertificateDer,
70 pub eph_ca_cert_der: LxCertificateDer,
72}
73
74impl Credentials {
77 pub fn as_ref(&self) -> CredentialsRef<'_> {
78 match self {
79 Credentials::RootSeed(root_seed) =>
80 CredentialsRef::RootSeed(root_seed),
81 Credentials::ClientCredentials(client_credentials) =>
82 CredentialsRef::ClientCredentials(client_credentials),
83 }
84 }
85}
86
87impl From<RootSeed> for Credentials {
88 fn from(root_seed: RootSeed) -> Self {
89 Credentials::RootSeed(root_seed)
90 }
91}
92
93impl From<ClientCredentials> for Credentials {
94 fn from(client_credentials: ClientCredentials) -> Self {
95 Credentials::ClientCredentials(client_credentials)
96 }
97}
98
99impl<'a> From<&'a RootSeed> for CredentialsRef<'a> {
100 fn from(root_seed: &'a RootSeed) -> Self {
101 CredentialsRef::RootSeed(root_seed)
102 }
103}
104
105impl<'a> From<&'a ClientCredentials> for CredentialsRef<'a> {
106 fn from(client_credentials: &'a ClientCredentials) -> Self {
107 CredentialsRef::ClientCredentials(client_credentials)
108 }
109}
110
111impl<'a> CredentialsRef<'a> {
112 pub fn user_pk(&self) -> Option<UserPk> {
116 match self {
117 Self::RootSeed(root_seed) => Some(root_seed.derive_user_pk()),
118 Self::ClientCredentials(cc) => cc.user_pk,
119 }
120 }
121
122 pub fn bearer_authenticator(&self) -> Arc<BearerAuthenticator> {
126 match self {
127 Self::RootSeed(root_seed) => {
128 let maybe_cached_token = None;
129 Arc::new(BearerAuthenticator::new_with_scope(
130 root_seed.derive_user_key_pair(),
131 maybe_cached_token,
132 Some(Scope::NodeConnect),
133 ))
134 }
135 Self::ClientCredentials(client_credentials) =>
136 Arc::new(BearerAuthenticator::new_static_token(
137 client_credentials.lexe_auth_token.clone(),
138 )),
139 }
140 }
141
142 pub fn tls_config(
144 &self,
145 rng: &mut impl Crng,
146 deploy_env: DeployEnv,
147 ) -> anyhow::Result<rustls::ClientConfig> {
148 match self {
149 Self::RootSeed(root_seed) =>
150 shared_seed::app_node_run_client_config(
151 rng, deploy_env, root_seed,
152 )
153 .context("Failed to build RootSeed TLS client config"),
154 Self::ClientCredentials(client_credentials) =>
155 shared_seed::sdk_node_run_client_config(
156 deploy_env,
157 &client_credentials.eph_ca_cert_der,
158 client_credentials.rev_client_cert_der.clone(),
159 client_credentials.rev_client_key_der.clone(),
160 )
161 .context("Failed to build revocable client TLS config"),
162 }
163 }
164}
165
166impl fmt::Debug for ClientCredentials {
169 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170 f.write_str("ClientCredentials(..)")
171 }
172}
173
174impl FromStr for ClientCredentials {
175 type Err = anyhow::Error;
176
177 fn from_str(s: &str) -> Result<Self, Self::Err> {
178 Self::try_from_base64_blob(s)
179 }
180}
181
182impl ClientCredentials {
183 pub fn from_response(
184 lexe_auth_token: BearerAuthToken,
185 resp: CreateRevocableClientResponse,
186 ) -> Self {
187 ClientCredentials {
188 user_pk: resp.user_pk,
189 lexe_auth_token,
190 client_pk: resp.pubkey,
191 rev_client_key_der: LxPrivatePkcs8KeyDer(
192 resp.rev_client_cert_key_der,
193 ),
194 rev_client_cert_der: LxCertificateDer(resp.rev_client_cert_der),
195 eph_ca_cert_der: LxCertificateDer(resp.eph_ca_cert_der),
196 }
197 }
198
199 pub fn to_base64_blob(&self) -> String {
214 let json_str =
215 serde_json::to_string(self).expect("Failed to JSON serialize");
216 base64::engine::general_purpose::STANDARD_NO_PAD
217 .encode(json_str.as_bytes())
218 }
219
220 pub fn try_from_base64_blob(s: &str) -> anyhow::Result<Self> {
229 let s = s.trim().trim_end_matches('=');
230 let bytes = base64::engine::general_purpose::STANDARD_NO_PAD
231 .decode(s)
232 .context("String is not valid base64")?;
233 let string =
234 String::from_utf8(bytes).context("String is not valid UTF-8")?;
235 serde_json::from_str(&string).context("Failed to deserialize")
236 }
237}
238
239#[cfg(test)]
240mod test {
241 use std::fs;
242
243 use lexe_common::{
244 byte_str::ByteStr,
245 test_utils::{arbitrary, snapshot},
246 };
247 use lexe_crypto::rng::FastRng;
248 use lexe_tls::shared_seed::certs::{
249 EphemeralIssuingCaCert, RevocableClientCert, RevocableIssuingCaCert,
250 };
251 use proptest::{prelude::any, prop_assert_eq, proptest};
252
253 use super::*;
254
255 #[test]
261 fn prop_client_credentials_base64_roundtrip() {
262 proptest!(|(creds1 in proptest::prelude::any::<ClientCredentials>())| {
263 {
266 let new_base64_blob = creds1.to_base64_blob();
267
268 let creds2 =
269 ClientCredentials::try_from_base64_blob(&new_base64_blob)
270 .expect("Failed to decode from new format");
271
272 prop_assert_eq!(&creds1, &creds2);
273 }
274
275 {
279 let json_str = serde_json::to_string(&creds1)
280 .expect("Failed to JSON serialize");
281 let old_base64_blob = base64::engine::general_purpose::STANDARD
282 .encode(json_str.as_bytes());
283 let creds2 =
284 ClientCredentials::try_from_base64_blob(&old_base64_blob)
285 .expect("Failed to decode from old format");
286
287 prop_assert_eq!(&creds1, &creds2);
288 }
289 });
290 }
291
292 #[test]
295 fn prop_base64_pad_to_no_pad_compat() {
296 proptest!(|(bytes1 in any::<Vec<u8>>())| {
297 let string =
298 base64::engine::general_purpose::STANDARD.encode(&bytes1);
299 let trimmed = string.trim_end_matches('=');
300 let bytes2 = base64::engine::general_purpose::STANDARD_NO_PAD
301 .decode(trimmed)
302 .expect("Failed to decode base64");
303 prop_assert_eq!(bytes1, bytes2);
304 })
305 }
306
307 #[test]
308 fn test_client_auth_encoding() {
309 let mut rng = FastRng::from_u64(202505121546);
310 let root_seed = RootSeed::from_rng(&mut rng);
311
312 let user_pk = root_seed.derive_user_pk();
313
314 let eph_ca_cert = EphemeralIssuingCaCert::from_root_seed(&root_seed);
315 let eph_ca_cert_der = eph_ca_cert.serialize_der_self_signed().unwrap();
316
317 let rev_ca_cert = RevocableIssuingCaCert::from_root_seed(&root_seed);
318
319 let rev_client_cert = RevocableClientCert::generate_from_rng(&mut rng);
320 let rev_client_cert_der = rev_client_cert
321 .serialize_der_ca_signed(&rev_ca_cert)
322 .unwrap();
323 let rev_client_key_der = rev_client_cert.serialize_key_der();
324 let client_pk = rev_client_cert.public_key();
325
326 let credentials = ClientCredentials {
327 user_pk: Some(user_pk),
328 lexe_auth_token: BearerAuthToken(ByteStr::from_static(
329 "9dTCUvC8y7qcNyUbqynz3nwIQQHbQqPVKeMhXUj1Afr-vgj9E217_2tCS1IQM7LFqfBUC8Ec9fcb-dQiCRy6ot2FN-kR60edRFJUztAa2Rxao1Q0BS1s6vE8grgfhMYIAJDLMWgAAAAASE4zaAAAAABpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaQE",
330 )),
331 client_pk: *client_pk,
332 rev_client_key_der,
333 rev_client_cert_der,
334 eph_ca_cert_der,
335 };
336
337 let credentials_str = credentials.to_base64_blob();
338
339 let expected_str = "eyJ1c2VyX3BrIjoiNmZkNzY0MTU2OTMwNTA5ZmFkNTM2MWQzYjIyYjYxZjc1YWE5MWVkNjQwMjE1YjJjNDFjMmZmODZiMmJmYzQ3MiIsImxleGVfYXV0aF90b2tlbiI6IjlkVENVdkM4eTdxY055VWJxeW56M253SVFRSGJRcVBWS2VNaFhVajFBZnItdmdqOUUyMTdfMnRDUzFJUU03TEZxZkJVQzhFYzlmY2ItZFFpQ1J5Nm90MkZOLWtSNjBlZFJGSlV6dEFhMlJ4YW8xUTBCUzFzNnZFOGdyZ2ZoTVlJQUpETE1XZ0FBQUFBU0U0emFBQUFBQUJwYVdscGFXbHBhV2xwYVdscGFXbHBhV2xwYVdscGFXbHBhV2xwYVdscGFRRSIsImNsaWVudF9wayI6IjcwODhhZjFmYzEyYWIwNGFkNmRkMTY1YmMzYTNjNWViMzA2MmI0MTFhMmY1NWExNjZiMGU0MDBiMzkwZmU0ZGIiLCJyZXZfY2xpZW50X2tleV9kZXIiOiIzMDUxMDIwMTAxMzAwNTA2MDMyYjY1NzAwNDIyMDQyMDBmNTgwZDM0NjFjNGVhMGIzNmI4MzZkNDUxYzFjMTk5ZWUzZTA2NDZhZDBkNjQyMzUzNzk3MzlkNjg2OTkyODk4MTIxMDA3MDg4YWYxZmMxMmFiMDRhZDZkZDE2NWJjM2EzYzVlYjMwNjJiNDExYTJmNTVhMTY2YjBlNDAwYjM5MGZlNGRiIiwicmV2X2NsaWVudF9jZXJ0X2RlciI6IjMwODIwMTgzMzA4MjAxMzVhMDAzMDIwMTAyMDIxNDQwYmVkYzU2ZDAzZDZiNTJmMjg0MmQ2NGRmOTBkMDJkNmRhMzZhNWIzMDA1MDYwMzJiNjU3MDMwNTYzMTBiMzAwOTA2MDM1NTA0MDYwYzAyNTU1MzMxMGIzMDA5MDYwMzU1MDQwODBjMDI0MzQxMzExMTMwMGYwNjAzNTUwNDBhMGMwODZjNjU3ODY1MmQ2MTcwNzAzMTI3MzAyNTA2MDM1NTA0MDMwYzFlNGM2NTc4NjUyMDcyNjU3NjZmNjM2MTYyNmM2NTIwNjk3MzczNzU2OTZlNjcyMDQzNDEyMDYzNjU3Mjc0MzAyMDE3MGQzNzM1MzAzMTMwMzEzMDMwMzAzMDMwMzA1YTE4MGYzNDMwMzkzNjMwMzEzMDMxMzAzMDMwMzAzMDMwNWEzMDUyMzEwYjMwMDkwNjAzNTUwNDA2MGMwMjU1NTMzMTBiMzAwOTA2MDM1NTA0MDgwYzAyNDM0MTMxMTEzMDBmMDYwMzU1MDQwYTBjMDg2YzY1Nzg2NTJkNjE3MDcwMzEyMzMwMjEwNjAzNTUwNDAzMGMxYTRjNjU3ODY1MjA3MjY1NzY2ZjYzNjE2MjZjNjUyMDYzNmM2OTY1NmU3NDIwNjM2NTcyNzQzMDJhMzAwNTA2MDMyYjY1NzAwMzIxMDA3MDg4YWYxZmMxMmFiMDRhZDZkZDE2NWJjM2EzYzVlYjMwNjJiNDExYTJmNTVhMTY2YjBlNDAwYjM5MGZlNGRiYTMxNzMwMTUzMDEzMDYwMzU1MWQxMTA0MGMzMDBhODIwODZjNjU3ODY1MmU2MTcwNzAzMDA1MDYwMzJiNjU3MDAzNDEwMDdiMTdiYzk1MzgyNjdiMzU0ZjA3MjZkODljYjFlYzMxMGIxMDJlNDIyYWI5Njk2Yjg3ZDlhZTcwMGNlZjJlODNjMTM2NmQwYWQxOTAzNWQ5ZTNlZDA0Y2Y1ZjdmMDVkZWY2OGE3MWRlMjEyYjg5ODM0NDc3OTQyYWU3NjNhMjBmIiwiZXBoX2NhX2NlcnRfZGVyIjoiMzA4MjAxYWUzMDgyMDE2MGEwMDMwMjAxMDIwMjE0MTBjZDVjOTk4OWY5NjUyMDk0OWUwZTlhYjRjZTRkYmUxNDc2NjcxMDMwMDUwNjAzMmI2NTcwMzA1MDMxMGIzMDA5MDYwMzU1MDQwNjBjMDI1NTUzMzEwYjMwMDkwNjAzNTUwNDA4MGMwMjQzNDEzMTExMzAwZjA2MDM1NTA0MGEwYzA4NmM2NTc4NjUyZDYxNzA3MDMxMjEzMDFmMDYwMzU1MDQwMzBjMTg0YzY1Nzg2NTIwNzM2ODYxNzI2NTY0MjA3MzY1NjU2NDIwNDM0MTIwNjM2NTcyNzQzMDIwMTcwZDM3MzUzMDMxMzAzMTMwMzAzMDMwMzAzMDVhMTgwZjM0MzAzOTM2MzAzMTMwMzEzMDMwMzAzMDMwMzA1YTMwNTAzMTBiMzAwOTA2MDM1NTA0MDYwYzAyNTU1MzMxMGIzMDA5MDYwMzU1MDQwODBjMDI0MzQxMzExMTMwMGYwNjAzNTUwNDBhMGMwODZjNjU3ODY1MmQ2MTcwNzAzMTIxMzAxZjA2MDM1NTA0MDMwYzE4NGM2NTc4NjUyMDczNjg2MTcyNjU2NDIwNzM2NTY1NjQyMDQzNDEyMDYzNjU3Mjc0MzAyYTMwMDUwNjAzMmI2NTcwMDMyMTAwZWZlOWNlMWFiY2FlYWJjZWY4ZWEyZjU0YTU2OTU1MGRjZWQ0YThmM2E4Y2JiMDRjZDk0NWQxYjRlMjQ1ZjY4N2EzNGEzMDQ4MzAxMzA2MDM1NTFkMTEwNDBjMzAwYTgyMDg2YzY1Nzg2NTJlNjE3MDcwMzAxZDA2MDM1NTFkMGUwNDE2MDQxNDkwY2Q1Yzk5ODlmOTY1MjA5NDllMGU5YWI0Y2U0ZGJlMTQ3NjY3MTAzMDEyMDYwMzU1MWQxMzAxMDFmZjA0MDgzMDA2MDEwMWZmMDIwMTAwMzAwNTA2MDMyYjY1NzAwMzQxMDAzNzI1NDI5ZjViY2E4MDU2MjFjMmIyZGM0NDU4MDJlZDIxY2FiMjQ2YjQ1YWQxMjFkZDJhNDMyZWZhMmY5M2VmNzI1ZWZhMTc4MmU2NDEwOGQyMjk4ZTg2OTRmNDY4NmNlZDk4Y2U5MjgwZWQ3NDlkMGFkNGI0NGE0YTFjZWUwZCJ9";
345 assert_eq!(credentials_str, expected_str);
346
347 let credentials2 =
348 ClientCredentials::try_from_base64_blob(&credentials_str)
349 .expect("Failed to decode ClientAuth");
350 assert_eq!(credentials, credentials2);
351 }
352
353 #[test]
359 #[ignore]
360 fn take_client_credentials_snapshot() {
361 let mut rng = FastRng::from_u64(202512210138);
362 const N: usize = 3;
363
364 let samples: Vec<ClientCredentials> =
365 arbitrary::gen_values(&mut rng, any::<ClientCredentials>(), N);
366
367 for sample in samples {
368 println!("{}", serde_json::to_string(&sample).unwrap());
369 }
370 }
371
372 #[test]
374 fn client_credentials_deser_compat() {
375 let snapshot =
376 fs::read_to_string("test_data/client_credentials_snapshot.txt")
377 .unwrap();
378
379 for input in snapshot::parse_sample_data(&snapshot) {
380 let value1: ClientCredentials =
381 serde_json::from_str(input).unwrap();
382 let output = serde_json::to_string(&value1).unwrap();
383 let value2: ClientCredentials =
384 serde_json::from_str(&output).unwrap();
385 assert_eq!(value1, value2);
386 }
387 }
388}