1use std::collections::BTreeSet;
2use std::error::Error;
3use std::fmt::{self, Display, Formatter};
4use std::sync::Arc;
5use std::time::{Duration, SystemTime, UNIX_EPOCH};
6
7use aws_lc_rs::digest;
8use http::Uri;
9use lodestone_wire::{
10 ConnectionProfile as WireConnectionProfile, LODESTONE_API_CONNECTION_PROFILE_FORMAT_VERSION,
11 LODESTONE_API_SCHEMA_ID, LODESTONE_API_SPEC_SHA256,
12};
13use rustls::pki_types::ServerName;
14
15use crate::CredentialProvider;
16
17const MAX_PROFILE_BYTES: usize = 64 * 1024;
18const MAX_ENDPOINTS: usize = 8;
19const MAX_ENDPOINT_BYTES: usize = 512;
20const MAX_REFERENCE_BYTES: usize = 512;
21const MAX_TRUST_CERTIFICATES: usize = 16;
22const MAX_TRUST_CERTIFICATE_BYTES: usize = 128 * 1024;
23const TRUST_DIGEST_DOMAIN: &[u8] = b"lodestone/client/tls-trust/v1";
24
25#[derive(Clone, Debug, PartialEq)]
26pub struct ValidatedConnectionProfile(WireConnectionProfile);
27
28impl ValidatedConnectionProfile {
29 pub fn new(profile: WireConnectionProfile) -> Result<Self, ClientConfigError> {
30 validate_profile(&profile)?;
31 Ok(Self(profile))
32 }
33
34 pub fn decode_current(bytes: &[u8]) -> Result<Self, ClientConfigError> {
35 if bytes.is_empty() || bytes.len() > MAX_PROFILE_BYTES {
36 return Err(ClientConfigError::ProfileSize);
37 }
38 let profile: WireConnectionProfile =
39 serde_json::from_slice(bytes).map_err(|_| ClientConfigError::ProfileCodec)?;
40 let value = Self::new(profile)?;
41 let canonical = serde_json::to_vec(value.as_wire()).map_err(|_| ClientConfigError::ProfileCodec)?;
42 if canonical != bytes {
43 return Err(ClientConfigError::NonCanonicalProfile);
44 }
45 Ok(value)
46 }
47
48 pub fn encode_current(&self) -> Result<Vec<u8>, ClientConfigError> {
49 validate_profile(&self.0)?;
50 let bytes = serde_json::to_vec(&self.0).map_err(|_| ClientConfigError::ProfileCodec)?;
51 if bytes.len() > MAX_PROFILE_BYTES {
52 return Err(ClientConfigError::ProfileSize);
53 }
54 Ok(bytes)
55 }
56
57 #[must_use]
58 pub const fn as_wire(&self) -> &WireConnectionProfile {
59 &self.0
60 }
61
62 #[must_use]
63 pub fn profile_generation(&self) -> u64 {
64 parse_u64(&self.0.profile_generation.value).unwrap_or(0)
65 }
66
67 pub fn compare_and_swap(
68 current: Option<&Self>,
69 expected_generation: u64,
70 replacement: &Self,
71 ) -> Result<(), ClientConfigError> {
72 validate_profile(replacement.as_wire())?;
73 match current {
74 None if expected_generation == 0 && replacement.profile_generation() == 1 => Ok(()),
75 Some(current) => {
76 validate_profile(current.as_wire())?;
77 let next = current
78 .profile_generation()
79 .checked_add(1)
80 .ok_or(ClientConfigError::GenerationExhausted)?;
81 let prior = current.as_wire();
82 let next_profile = replacement.as_wire();
83 if expected_generation != current.profile_generation()
84 || replacement.profile_generation() != next
85 || prior.expected_cluster_id != next_profile.expected_cluster_id
86 || prior.expected_collection_id != next_profile.expected_collection_id
87 || prior.expected_principal_id != next_profile.expected_principal_id
88 {
89 return Err(ClientConfigError::ProfileCasConflict);
90 }
91 Ok(())
92 }
93 None => Err(ClientConfigError::ProfileCasConflict),
94 }
95 }
96}
97
98#[derive(Clone, Debug, Eq, PartialEq)]
99pub struct TlsTrust {
100 certificates_der: Vec<Vec<u8>>,
101 digest: [u8; 32],
102}
103
104impl TlsTrust {
105 pub fn new(certificates_der: Vec<Vec<u8>>) -> Result<Self, ClientConfigError> {
106 if certificates_der.is_empty()
107 || certificates_der.len() > MAX_TRUST_CERTIFICATES
108 || certificates_der
109 .iter()
110 .any(|certificate| certificate.is_empty() || certificate.len() > MAX_TRUST_CERTIFICATE_BYTES)
111 {
112 return Err(ClientConfigError::InvalidTrustMaterial);
113 }
114 let mut hasher = digest::Context::new(&digest::SHA256);
115 hasher.update(TRUST_DIGEST_DOMAIN);
116 for certificate in &certificates_der {
117 let length = u64::try_from(certificate.len()).map_err(|_| ClientConfigError::InvalidTrustMaterial)?;
118 hasher.update(&length.to_be_bytes());
119 hasher.update(certificate);
120 }
121 let observed = hasher.finish();
122 let mut trust_digest = [0; 32];
123 trust_digest.copy_from_slice(observed.as_ref());
124 Ok(Self {
125 certificates_der,
126 digest: trust_digest,
127 })
128 }
129
130 #[must_use]
131 pub const fn digest(&self) -> [u8; 32] {
132 self.digest
133 }
134
135 #[must_use]
136 pub fn certificates_der(&self) -> &[Vec<u8>] {
137 &self.certificates_der
138 }
139}
140
141#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
142pub struct PoolConfig {
143 pub maximum_connections: usize,
144 pub maximum_idle_connections: usize,
145 pub connect_timeout: Duration,
146 pub maximum_read_retries: u8,
147 pub maximum_subscription_reconnects: u8,
148}
149
150impl Default for PoolConfig {
151 fn default() -> Self {
152 Self {
153 maximum_connections: 4,
154 maximum_idle_connections: 4,
155 connect_timeout: Duration::from_secs(5),
156 maximum_read_retries: 2,
157 maximum_subscription_reconnects: 4,
158 }
159 }
160}
161
162impl PoolConfig {
163 fn validate(self) -> Result<Self, ClientConfigError> {
164 if self.maximum_connections == 0
165 || self.maximum_connections > 32
166 || self.maximum_idle_connections > self.maximum_connections
167 || self.connect_timeout.is_zero()
168 || self.connect_timeout > Duration::from_secs(60)
169 || self.maximum_read_retries > 3
170 || self.maximum_subscription_reconnects > 8
171 {
172 return Err(ClientConfigError::InvalidPoolConfig);
173 }
174 Ok(self)
175 }
176}
177
178#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
179pub enum CellOperationClass {
180 Ordinary,
181 CollectionProvisioner,
182 CapacityObserver,
183 RecoveryOperator,
184}
185
186#[derive(Clone, Debug, Eq, Hash, PartialEq)]
187pub struct CellTransportIdentity {
188 native_endpoints: Vec<String>,
189 selected_stream_endpoint: String,
190 tls_server_name: String,
191 tls_trust_sha256: [u8; 32],
192 expected_cluster_id: String,
193 schema_id: String,
194 schema_digest: String,
195 operation_class: CellOperationClass,
196 pool: PoolConfig,
197}
198
199impl CellTransportIdentity {
200 #[must_use]
201 pub fn native_endpoints(&self) -> &[String] {
202 &self.native_endpoints
203 }
204
205 #[must_use]
206 pub fn selected_stream_endpoint(&self) -> &str {
207 &self.selected_stream_endpoint
208 }
209
210 #[must_use]
211 pub fn tls_server_name(&self) -> &str {
212 &self.tls_server_name
213 }
214
215 #[must_use]
216 pub const fn tls_trust_sha256(&self) -> [u8; 32] {
217 self.tls_trust_sha256
218 }
219
220 #[must_use]
221 pub fn expected_cluster_id(&self) -> &str {
222 &self.expected_cluster_id
223 }
224
225 #[must_use]
226 pub fn schema_id(&self) -> &str {
227 &self.schema_id
228 }
229
230 #[must_use]
231 pub fn schema_digest(&self) -> &str {
232 &self.schema_digest
233 }
234
235 #[must_use]
236 pub const fn operation_class(&self) -> CellOperationClass {
237 self.operation_class
238 }
239
240 #[must_use]
241 pub const fn pool(&self) -> PoolConfig {
242 self.pool
243 }
244}
245
246pub struct CellTransportConfig {
247 pub(crate) identity: CellTransportIdentity,
248 pub(crate) trust: TlsTrust,
249 pub(crate) pool: PoolConfig,
250 pub(crate) native_endpoints: Vec<ValidatedEndpoint>,
251 pub(crate) stream_endpoint: ValidatedEndpoint,
252}
253
254impl CellTransportConfig {
255 pub fn new(
256 profile: &ValidatedConnectionProfile,
257 trust: TlsTrust,
258 operation_class: CellOperationClass,
259 pool: PoolConfig,
260 ) -> Result<Self, ClientConfigError> {
261 validate_profile(profile.as_wire())?;
262 let expected_trust = decode_digest(&profile.as_wire().tls_trust_sha256.value)?;
263 if expected_trust != trust.digest() {
264 return Err(ClientConfigError::TrustDigestMismatch);
265 }
266 let pool = pool.validate()?;
267 let native_endpoints = profile
268 .as_wire()
269 .native_endpoints
270 .iter()
271 .map(|endpoint| ValidatedEndpoint::parse(endpoint, EndpointScheme::NativeTls))
272 .collect::<Result<Vec<_>, _>>()?;
273 let stream_endpoint =
274 ValidatedEndpoint::parse(&profile.as_wire().selected_stream_endpoint, EndpointScheme::Https)?;
275 let identity = CellTransportIdentity {
276 native_endpoints: canonical_endpoint_set(&profile.as_wire().native_endpoints),
277 selected_stream_endpoint: profile.as_wire().selected_stream_endpoint.clone(),
278 tls_server_name: profile.as_wire().tls_server_name.clone(),
279 tls_trust_sha256: trust.digest(),
280 expected_cluster_id: profile.as_wire().expected_cluster_id.value.clone(),
281 schema_id: profile.as_wire().schema_id.clone(),
282 schema_digest: profile.as_wire().schema_digest.value.clone(),
283 operation_class,
284 pool,
285 };
286 Ok(Self {
287 identity,
288 trust,
289 pool,
290 native_endpoints,
291 stream_endpoint,
292 })
293 }
294
295 #[must_use]
296 pub const fn identity(&self) -> &CellTransportIdentity {
297 &self.identity
298 }
299}
300
301pub struct CollectionSessionConfig {
302 pub(crate) profile: ValidatedConnectionProfile,
303 pub(crate) credentials: Arc<dyn CredentialProvider>,
304}
305
306impl CollectionSessionConfig {
307 pub fn new(
308 profile: ValidatedConnectionProfile,
309 credentials: Arc<dyn CredentialProvider>,
310 ) -> Result<Self, ClientConfigError> {
311 validate_profile(profile.as_wire())?;
312 if profile
313 .as_wire()
314 .credential_expires_at
315 .as_ref()
316 .is_some_and(|expiry| parse_u64(&expiry.value).is_none_or(|expiry| expiry <= unix_millis()))
317 {
318 return Err(ClientConfigError::CredentialExpired);
319 }
320 Ok(Self { profile, credentials })
321 }
322
323 #[must_use]
324 pub const fn profile(&self) -> &ValidatedConnectionProfile {
325 &self.profile
326 }
327
328 pub(crate) fn validate_transport(&self, transport: &CellTransportConfig) -> Result<(), ClientConfigError> {
329 let profile = self.profile.as_wire();
330 let identity = transport.identity();
331 if identity.operation_class != CellOperationClass::Ordinary
332 || identity.native_endpoints != canonical_endpoint_set(&profile.native_endpoints)
333 || identity.selected_stream_endpoint != profile.selected_stream_endpoint
334 || identity.tls_server_name != profile.tls_server_name
335 || identity.tls_trust_sha256 != decode_digest(&profile.tls_trust_sha256.value)?
336 || identity.expected_cluster_id != profile.expected_cluster_id.value
337 || identity.schema_id != profile.schema_id
338 || identity.schema_digest != profile.schema_digest.value
339 {
340 return Err(ClientConfigError::CellTransportMismatch);
341 }
342 Ok(())
343 }
344}
345
346fn canonical_endpoint_set(endpoints: &[String]) -> Vec<String> {
347 endpoints.iter().cloned().collect::<BTreeSet<_>>().into_iter().collect()
348}
349
350#[derive(Clone, Copy, Debug, Eq, PartialEq)]
351enum EndpointScheme {
352 NativeTls,
353 Https,
354}
355
356impl EndpointScheme {
357 const fn value(self) -> &'static str {
358 match self {
359 Self::NativeTls => "lodestone+tls",
360 Self::Https => "https",
361 }
362 }
363}
364
365#[derive(Clone, Debug, Eq, PartialEq)]
366pub struct ValidatedEndpoint {
367 original: String,
368 host: String,
369 port: u16,
370 scheme: EndpointScheme,
371}
372
373impl ValidatedEndpoint {
374 fn parse(value: &str, scheme: EndpointScheme) -> Result<Self, ClientConfigError> {
375 if value.is_empty()
376 || value.len() > MAX_ENDPOINT_BYTES
377 || value
378 .bytes()
379 .any(|byte| byte.is_ascii_control() || byte.is_ascii_whitespace())
380 || value.contains('@')
381 || value.contains('?')
382 || value.contains('#')
383 {
384 return Err(ClientConfigError::InvalidEndpoint);
385 }
386 let uri: Uri = value.parse().map_err(|_| ClientConfigError::InvalidEndpoint)?;
387 if uri.scheme_str() != Some(scheme.value()) || uri.path() != "" && uri.path() != "/" {
388 return Err(ClientConfigError::InvalidEndpoint);
389 }
390 let host = uri.host().ok_or(ClientConfigError::InvalidEndpoint)?.to_owned();
391 let port = uri.port_u16().ok_or(ClientConfigError::InvalidEndpoint)?;
392 if host.is_empty() || host.len() > 253 || port == 0 {
393 return Err(ClientConfigError::InvalidEndpoint);
394 }
395 Ok(Self {
396 original: value.to_owned(),
397 host,
398 port,
399 scheme,
400 })
401 }
402
403 #[must_use]
404 pub fn as_str(&self) -> &str {
405 &self.original
406 }
407
408 pub(crate) fn host(&self) -> &str {
409 &self.host
410 }
411
412 pub(crate) const fn port(&self) -> u16 {
413 self.port
414 }
415}
416
417fn validate_profile(profile: &WireConnectionProfile) -> Result<(), ClientConfigError> {
418 if profile.format_version != u32::from(LODESTONE_API_CONNECTION_PROFILE_FORMAT_VERSION) {
419 return Err(ClientConfigError::UnsupportedProfileVersion);
420 }
421 if parse_u64(&profile.profile_generation.value).is_none_or(|value| value == 0)
422 || profile.native_endpoints.is_empty()
423 || profile.native_endpoints.len() > MAX_ENDPOINTS
424 || profile.schema_id != LODESTONE_API_SCHEMA_ID
425 || profile.schema_digest.value != LODESTONE_API_SPEC_SHA256
426 || profile.expected_cluster_id.validate().is_err()
427 || profile.expected_collection_id.validate().is_err()
428 || profile.expected_principal_id.validate().is_err()
429 || profile.credential_id.validate().is_err()
430 || parse_u64(&profile.credential_generation.value).is_none_or(|value| value == 0)
431 || profile
432 .credential_expires_at
433 .as_ref()
434 .is_some_and(|value| parse_u64(&value.value).is_none_or(|value| value == 0))
435 || decode_digest(&profile.tls_trust_sha256.value).is_err()
436 || !valid_reference(&profile.secret_reference)
437 {
438 return Err(ClientConfigError::InvalidProfileBinding);
439 }
440 let mut endpoints = BTreeSet::new();
441 for endpoint in &profile.native_endpoints {
442 ValidatedEndpoint::parse(endpoint, EndpointScheme::NativeTls)?;
443 if !endpoints.insert(endpoint) {
444 return Err(ClientConfigError::InvalidEndpoint);
445 }
446 }
447 ValidatedEndpoint::parse(&profile.selected_stream_endpoint, EndpointScheme::Https)?;
448 let tls_server_name = ServerName::try_from(profile.tls_server_name.clone());
449 if profile.tls_server_name.is_empty()
450 || profile.tls_server_name.len() > 253
451 || profile
452 .tls_server_name
453 .bytes()
454 .any(|byte| byte.is_ascii_control() || byte.is_ascii_whitespace())
455 || !matches!(tls_server_name, Ok(ServerName::DnsName(_)))
456 {
457 return Err(ClientConfigError::InvalidServerName);
458 }
459 Ok(())
460}
461
462fn valid_reference(value: &str) -> bool {
463 !value.is_empty()
464 && value.len() <= MAX_REFERENCE_BYTES
465 && value.contains("://")
466 && !value.contains('@')
467 && !value.contains('=')
468 && value
469 .bytes()
470 .all(|byte| byte.is_ascii_graphic() && !matches!(byte, b'?' | b'#'))
471}
472
473pub(crate) fn parse_u64(value: &str) -> Option<u64> {
474 value.parse::<u64>().ok().filter(|parsed| parsed.to_string() == value)
475}
476
477pub(crate) fn parse_identity(value: &str) -> Option<u128> {
478 if value.len() != 32 {
479 return None;
480 }
481 u128::from_str_radix(value, 16).ok().filter(|value| *value != 0)
482}
483
484fn decode_digest(value: &str) -> Result<[u8; 32], ClientConfigError> {
485 if value.len() != 64 {
486 return Err(ClientConfigError::InvalidProfileBinding);
487 }
488 let mut output = [0; 32];
489 for (index, pair) in value.as_bytes().chunks_exact(2).enumerate() {
490 let text = std::str::from_utf8(pair).map_err(|_| ClientConfigError::InvalidProfileBinding)?;
491 output[index] = u8::from_str_radix(text, 16).map_err(|_| ClientConfigError::InvalidProfileBinding)?;
492 }
493 Ok(output)
494}
495
496fn unix_millis() -> u64 {
497 SystemTime::now()
498 .duration_since(UNIX_EPOCH)
499 .ok()
500 .and_then(|duration| u64::try_from(duration.as_millis()).ok())
501 .unwrap_or(u64::MAX)
502}
503
504#[derive(Clone, Copy, Debug, Eq, PartialEq)]
505pub enum ClientConfigError {
506 UnsupportedProfileVersion,
507 InvalidProfileBinding,
508 InvalidEndpoint,
509 InvalidServerName,
510 InvalidTrustMaterial,
511 TrustDigestMismatch,
512 InvalidPoolConfig,
513 CredentialExpired,
514 ProfileSize,
515 ProfileCodec,
516 NonCanonicalProfile,
517 ProfileCasConflict,
518 GenerationExhausted,
519 CellTransportMismatch,
520}
521
522impl Display for ClientConfigError {
523 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
524 formatter.write_str(match self {
525 Self::UnsupportedProfileVersion => "Core connection profile version is unsupported",
526 Self::InvalidProfileBinding => "Core connection profile binding is invalid",
527 Self::InvalidEndpoint => "Core connection endpoint is invalid",
528 Self::InvalidServerName => "Core TLS server name is invalid",
529 Self::InvalidTrustMaterial => "Core TLS trust material is invalid",
530 Self::TrustDigestMismatch => "Core TLS trust material does not match the connection profile",
531 Self::InvalidPoolConfig => "Core client pool configuration is invalid",
532 Self::CredentialExpired => "Core connection profile credential is expired",
533 Self::ProfileSize => "Core connection profile size is outside the bound",
534 Self::ProfileCodec => "Core connection profile could not be decoded",
535 Self::NonCanonicalProfile => "Core connection profile encoding is noncanonical",
536 Self::ProfileCasConflict => "Core connection profile generation CAS conflicted",
537 Self::GenerationExhausted => "Core connection profile generation is exhausted",
538 Self::CellTransportMismatch => "Core Collection session does not match the selected cell transport",
539 })
540 }
541}
542
543impl Error for ClientConfigError {}
544
545#[cfg(test)]
546mod tests {
547 use std::error::Error;
548 use std::sync::Arc;
549
550 use lodestone_wire::{
551 Digest, Identity, LODESTONE_API_CONNECTION_PROFILE_FORMAT_VERSION, LODESTONE_API_SCHEMA_ID,
552 LODESTONE_API_SPEC_SHA256, U64,
553 };
554
555 use super::*;
556 use crate::FileCredentialProvider;
557
558 fn profile(generation: u64, trust: [u8; 32]) -> Result<ValidatedConnectionProfile, ClientConfigError> {
559 ValidatedConnectionProfile::new(WireConnectionProfile {
560 format_version: u32::from(LODESTONE_API_CONNECTION_PROFILE_FORMAT_VERSION),
561 profile_generation: U64 {
562 value: generation.to_string(),
563 },
564 native_endpoints: vec!["lodestone+tls://core.example:8443".to_owned()],
565 selected_stream_endpoint: "https://core.example:9443".to_owned(),
566 tls_server_name: "core.example".to_owned(),
567 tls_trust_sha256: Digest {
568 value: lower_hex(&trust),
569 },
570 expected_cluster_id: identity(1),
571 expected_collection_id: identity(2),
572 expected_principal_id: identity(4),
573 schema_id: LODESTONE_API_SCHEMA_ID.to_owned(),
574 schema_digest: Digest {
575 value: LODESTONE_API_SPEC_SHA256.to_owned(),
576 },
577 credential_id: identity(3),
578 credential_generation: U64 { value: "1".to_owned() },
579 credential_expires_at: None,
580 secret_reference: "memory://core/admin".to_owned(),
581 })
582 }
583
584 fn identity(value: u128) -> Identity {
585 Identity {
586 value: format!("{value:032x}"),
587 }
588 }
589
590 fn lower_hex(bytes: &[u8]) -> String {
591 bytes.iter().map(|byte| format!("{byte:02x}")).collect()
592 }
593
594 #[test]
595 fn profile_codec_is_canonical_and_generation_cas_is_strict() -> Result<(), Box<dyn Error>> {
596 let trust = TlsTrust::new(vec![vec![1, 2, 3]])?;
597 let first = profile(1, trust.digest())?;
598 let encoded = first.encode_current()?;
599 assert_eq!(ValidatedConnectionProfile::decode_current(&encoded)?, first);
600 let second = profile(2, trust.digest())?;
601 ValidatedConnectionProfile::compare_and_swap(Some(&first), 1, &second)?;
602 assert_eq!(
603 ValidatedConnectionProfile::compare_and_swap(Some(&first), 0, &second),
604 Err(ClientConfigError::ProfileCasConflict)
605 );
606 let mut other_principal_wire = second.as_wire().clone();
607 other_principal_wire.expected_principal_id = identity(99);
608 let other_principal = ValidatedConnectionProfile::new(other_principal_wire)?;
609 assert_eq!(
610 ValidatedConnectionProfile::compare_and_swap(Some(&first), 1, &other_principal),
611 Err(ClientConfigError::ProfileCasConflict)
612 );
613 let mut noncanonical = encoded;
614 noncanonical.push(b' ');
615 assert_eq!(
616 ValidatedConnectionProfile::decode_current(&noncanonical),
617 Err(ClientConfigError::NonCanonicalProfile)
618 );
619 Ok(())
620 }
621
622 #[test]
623 fn endpoints_have_no_downgrade_or_ambient_authority_forms() -> Result<(), Box<dyn Error>> {
624 let trust = TlsTrust::new(vec![vec![1]])?;
625 let mut value = profile(1, trust.digest())?.as_wire().clone();
626 value.native_endpoints[0] = "tcp://core.example:8443".to_owned();
627 assert_eq!(
628 ValidatedConnectionProfile::new(value),
629 Err(ClientConfigError::InvalidEndpoint)
630 );
631 Ok(())
632 }
633
634 #[test]
635 fn explicit_tls_server_name_is_validated_independently_of_endpoint_hosts() -> Result<(), Box<dyn Error>> {
636 let trust = TlsTrust::new(vec![vec![1]])?;
637 let mut value = profile(1, trust.digest())?.as_wire().clone();
638 value.native_endpoints[0] = "lodestone+tls://127.0.0.1:8443".to_owned();
639 value.selected_stream_endpoint = "https://127.0.0.1:9443".to_owned();
640 assert!(ValidatedConnectionProfile::new(value.clone()).is_ok());
641 value.tls_server_name = "bad name".to_owned();
642 assert_eq!(
643 ValidatedConnectionProfile::new(value),
644 Err(ClientConfigError::InvalidServerName)
645 );
646 let mut value = profile(1, trust.digest())?.as_wire().clone();
647 value.tls_server_name = "127.0.0.1".to_owned();
648 assert_eq!(
649 ValidatedConnectionProfile::new(value),
650 Err(ClientConfigError::InvalidServerName)
651 );
652 Ok(())
653 }
654
655 #[test]
656 fn cell_identity_excludes_collection_and_credential_but_sessions_stay_exact() -> Result<(), Box<dyn Error>> {
657 let trust = TlsTrust::new(vec![vec![1, 2, 3]])?;
658 let first = profile(1, trust.digest())?;
659 let mut second_wire = first.as_wire().clone();
660 second_wire.profile_generation = U64 { value: "7".to_owned() };
661 second_wire.expected_collection_id = identity(20);
662 second_wire.expected_principal_id = identity(40);
663 second_wire.credential_id = identity(30);
664 second_wire.credential_generation = U64 { value: "4".to_owned() };
665 second_wire.secret_reference = "memory://core/second".to_owned();
666 let second = ValidatedConnectionProfile::new(second_wire)?;
667
668 let first_transport = CellTransportConfig::new(
669 &first,
670 trust.clone(),
671 CellOperationClass::Ordinary,
672 PoolConfig::default(),
673 )?;
674 let second_transport = CellTransportConfig::new(
675 &second,
676 trust.clone(),
677 CellOperationClass::Ordinary,
678 PoolConfig::default(),
679 )?;
680 assert_eq!(first_transport.identity(), second_transport.identity());
681
682 let second_session = CollectionSessionConfig::new(second, Arc::new(FileCredentialProvider))?;
683 assert!(second_session.validate_transport(&first_transport).is_ok());
684
685 let control_transport = CellTransportConfig::new(
686 &first,
687 trust,
688 CellOperationClass::CollectionProvisioner,
689 PoolConfig::default(),
690 )?;
691 let first_session = CollectionSessionConfig::new(first, Arc::new(FileCredentialProvider))?;
692 assert_eq!(
693 first_session.validate_transport(&control_transport),
694 Err(ClientConfigError::CellTransportMismatch)
695 );
696 Ok(())
697 }
698
699 #[test]
700 fn cluster_or_trust_omission_cannot_alias_a_cell_transport() -> Result<(), Box<dyn Error>> {
701 let trust = TlsTrust::new(vec![vec![1, 2, 3]])?;
702 let first = profile(1, trust.digest())?;
703 let first_transport = CellTransportConfig::new(
704 &first,
705 trust.clone(),
706 CellOperationClass::Ordinary,
707 PoolConfig::default(),
708 )?;
709
710 let mut wrong_cluster_wire = first.as_wire().clone();
711 wrong_cluster_wire.expected_cluster_id = identity(99);
712 let wrong_cluster = ValidatedConnectionProfile::new(wrong_cluster_wire)?;
713 let wrong_cluster_transport = CellTransportConfig::new(
714 &wrong_cluster,
715 trust,
716 CellOperationClass::Ordinary,
717 PoolConfig::default(),
718 )?;
719 assert_ne!(first_transport.identity(), wrong_cluster_transport.identity());
720
721 let other_trust = TlsTrust::new(vec![vec![9, 8, 7]])?;
722 let mut wrong_trust_wire = first.as_wire().clone();
723 wrong_trust_wire.tls_trust_sha256 = Digest {
724 value: lower_hex(&other_trust.digest()),
725 };
726 let wrong_trust = ValidatedConnectionProfile::new(wrong_trust_wire)?;
727 let wrong_trust_transport = CellTransportConfig::new(
728 &wrong_trust,
729 other_trust,
730 CellOperationClass::Ordinary,
731 PoolConfig::default(),
732 )?;
733 assert_ne!(first_transport.identity(), wrong_trust_transport.identity());
734 Ok(())
735 }
736
737 #[test]
738 fn endpoint_order_does_not_split_one_cell_transport_identity() -> Result<(), Box<dyn Error>> {
739 let trust = TlsTrust::new(vec![vec![1, 2, 3]])?;
740 let mut first_wire = profile(1, trust.digest())?.as_wire().clone();
741 first_wire
742 .native_endpoints
743 .push("lodestone+tls://core.example:8444".to_owned());
744 let first = ValidatedConnectionProfile::new(first_wire.clone())?;
745 first_wire.native_endpoints.reverse();
746 first_wire.expected_collection_id = identity(20);
747 first_wire.expected_principal_id = identity(40);
748 first_wire.credential_id = identity(30);
749 first_wire.secret_reference = "memory://core/second".to_owned();
750 let second = ValidatedConnectionProfile::new(first_wire)?;
751
752 let first_transport = CellTransportConfig::new(
753 &first,
754 trust.clone(),
755 CellOperationClass::Ordinary,
756 PoolConfig::default(),
757 )?;
758 let second_transport =
759 CellTransportConfig::new(&second, trust, CellOperationClass::Ordinary, PoolConfig::default())?;
760 assert_eq!(first_transport.identity(), second_transport.identity());
761 Ok(())
762 }
763}