Skip to main content

lodestone_client/
config.rs

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}