polyproto/types/
pdn.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5use std::hash::Hash;
6
7use spki::ObjectIdentifier;
8use x509_cert::attr::AttributeTypeAndValue;
9use x509_cert::name::{RdnSequence, RelativeDistinguishedName};
10
11use crate::errors::{ConstraintError, InvalidInput};
12use crate::types::SessionId;
13use crate::types::local_name::LocalName;
14use crate::types::{DomainName, FederationId};
15use crate::{
16    Constrained, OID_RDN_COMMON_NAME, OID_RDN_DOMAIN_COMPONENT, OID_RDN_UID,
17    OID_RDN_UNIQUE_IDENTIFIER,
18};
19
20/// Higher-level abstraction of X.509 [distinguished names](https://ldap.com/ldap-dns-and-rdns/),
21/// providing easier access to inner values compared to using [x509_cert::name::Name] in a raw manner.
22#[derive(Debug, Clone, PartialEq, Eq, Hash)]
23pub enum PolyprotoDistinguishedName {
24    /// A `pDN` with all necessary fields for an actor.
25    ActorDn(ActorDN),
26    /// A `pDN` with all necessary fields for a home server.
27    HomeServerDn(HomeServerDN),
28}
29
30impl PolyprotoDistinguishedName {
31    /// Regardless of whether the `PolyprotoDistinguishedName` is a [ActorDN] or a [HomeServerDN],
32    /// get a reference to the [DomainName] of the issuing authority for this `PolyprotoDistinguishedName`.
33    /// This is–in either case–the `domain_name` attribute of the specific `DN`.
34    ///
35    /// This method is just a shorthand for
36    ///
37    /// ```rs
38    /// match self {
39    ///    PolyprotoDistinguishedName::ActorDn(actor_dn) => actor_dn.domain_name,
40    ///    PolyprotoDistinguishedName::HomeServerDn(home_server_dn) => home_server_dn.domain_name,
41    /// }
42    /// ```
43    pub fn issuer_name(&self) -> &DomainName {
44        match self {
45            PolyprotoDistinguishedName::ActorDn(actor_dn) => &actor_dn.domain_name,
46            PolyprotoDistinguishedName::HomeServerDn(home_server_dn) => &home_server_dn.domain_name,
47        }
48    }
49}
50
51impl From<ActorDN> for PolyprotoDistinguishedName {
52    fn from(value: ActorDN) -> Self {
53        Self::ActorDn(value)
54    }
55}
56
57impl From<HomeServerDN> for PolyprotoDistinguishedName {
58    fn from(value: HomeServerDN) -> Self {
59        Self::HomeServerDn(value)
60    }
61}
62
63impl TryFrom<RdnSequence> for PolyprotoDistinguishedName {
64    type Error = crate::errors::InvalidInput;
65
66    fn try_from(value: RdnSequence) -> Result<Self, Self::Error> {
67        // Let's see if we can find a FederationId component, then we do the conversion based on
68        // whether we were successful.
69        if value
70            .0
71            .iter()
72            .any(|rdn| rdn.0.iter().any(|attr| attr.oid == OID_RDN_UID))
73        {
74            Ok(Self::ActorDn(ActorDN::try_from(value)?))
75        } else {
76            Ok(Self::HomeServerDn(HomeServerDN::try_from(value)?))
77        }
78    }
79}
80
81#[derive(Debug, Clone, PartialEq, Eq)]
82/// A [PolyprotoDistinguishedName] with all necessary fields for an actor certificate.
83///
84/// This struct is a higher-level abstraction of X.509 [distinguished names](https://ldap.com/ldap-dns-and-rdns/),
85/// providing easier access to inner values compared to using [x509_cert::name::Name] in a raw manner.
86pub struct ActorDN {
87    /// The [FederationId] field claim of this Distinguished Name.
88    pub federation_id: FederationId,
89    /// The [LocalName] field claim of this Distinguished Name.
90    pub local_name: LocalName,
91    /// The [DomainName] field claim of this Distinguished Name, representing the instance this actor claims to be belonging to.
92    pub domain_name: DomainName,
93    /// The [SessionId] field claim of this Distinguished Name.
94    pub session_id: SessionId,
95    /// Additional claims in this Distinguished Name, if any.
96    pub additional_fields: RelativeDistinguishedName,
97}
98
99impl ActorDN {
100    /// Creates a new validated [ActorDN] with all necessary fields for an actor certificate.
101    ///
102    /// This constructor method creates an [ActorDN] instance and validates it against the
103    /// [Actor] target to ensure it meets all polyproto constraints for actor certificates.
104    ///
105    /// # Parameters
106    ///
107    /// - `federation_id`: The [FederationId] field claim for this Distinguished Name
108    /// - `local_name`: The [LocalName] field claim for this Distinguished Name  
109    /// - `domain_name`: The [DomainName] field claim representing the instance this actor belongs to
110    /// - `session_id`: The [SessionId] field claim for this Distinguished Name
111    /// - `additional_fields`: Optional additional claims in this Distinguished Name
112    ///
113    /// # Returns
114    ///
115    /// - `Ok(ActorDN)` if the constructed DN passes validation
116    /// - `Err(ConstraintError)` if the DN violates any polyproto constraints for actor certificates
117    ///
118    /// # Errors
119    ///
120    /// This method returns a [ConstraintError] if the constructed [ActorDN] fails validation
121    /// against the [Actor] target. Common validation failures include malformed field values
122    /// or constraint violations specific to actor certificates.
123    pub fn new_validated(
124        federation_id: FederationId,
125        local_name: LocalName,
126        domain_name: DomainName,
127        session_id: SessionId,
128        additional_fields: Option<RelativeDistinguishedName>,
129    ) -> Result<Self, ConstraintError> {
130        let actor_dn = ActorDN {
131            federation_id,
132            local_name,
133            domain_name,
134            session_id,
135            additional_fields: additional_fields.unwrap_or_default(),
136        };
137        actor_dn.validate(Some(crate::certs::Target::Actor))?;
138        Ok(actor_dn)
139    }
140}
141
142impl Hash for ActorDN {
143    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
144        self.federation_id.hash(state);
145        self.domain_name.hash(state);
146        self.session_id.hash(state);
147        self.additional_fields.0.iter().for_each(|item| {
148            item.oid.hash(state);
149            item.value.value().hash(state);
150        });
151    }
152}
153
154#[derive(Debug, Clone, PartialEq, Eq)]
155/// A [PolyprotoDistinguishedName] with all necessary fields for a home server certificate.
156///
157/// This struct is a higher-level abstraction of X.509 [distinguished names](https://ldap.com/ldap-dns-and-rdns/),
158/// providing easier access to inner values compared to using [x509_cert::name::Name] in a raw manner.
159pub struct HomeServerDN {
160    /// The [DomainName] field claim of this Distinguished Name, representing the domain this instance claims to represent.
161    pub domain_name: DomainName,
162    /// Additional claims in this Distinguished Name, if any.
163    pub additional_fields: RelativeDistinguishedName,
164}
165
166impl HomeServerDN {
167    /// Creates a new validated [HomeServerDN] with all necessary fields for a home server certificate.
168    ///
169    /// This constructor method creates a [HomeServerDN] instance and validates it against the
170    /// [HomeServer] target to ensure it meets all polyproto constraints for home server certificates.
171    ///
172    /// # Parameters
173    ///
174    /// - `domain_name`: The [DomainName] field claim representing the domain this instance claims to represent
175    /// - `additional_fields`: Optional additional claims in this Distinguished Name
176    ///
177    /// # Returns
178    ///
179    /// - `Ok(HomeServerDN)` if the constructed DN passes validation
180    /// - `Err(ConstraintError)` if the DN violates any polyproto constraints for home server certificates
181    ///
182    /// # Errors
183    ///
184    /// This method returns a [ConstraintError] if the constructed [HomeServerDN] fails validation
185    /// against the [HomeServer] target. Common validation failures include malformed field values
186    /// or constraint violations specific to home server certificates.
187    pub fn new_validated(
188        domain_name: DomainName,
189        additional_fields: Option<RelativeDistinguishedName>,
190    ) -> Result<Self, ConstraintError> {
191        let home_server_dn = HomeServerDN {
192            domain_name,
193            additional_fields: additional_fields.unwrap_or_default(),
194        };
195        home_server_dn.validate(Some(crate::certs::Target::HomeServer))?;
196        Ok(home_server_dn)
197    }
198}
199
200impl Hash for HomeServerDN {
201    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
202        self.domain_name.hash(state);
203        self.additional_fields.0.iter().for_each(|item| {
204            item.oid.hash(state);
205            item.value.value().hash(state);
206        });
207    }
208}
209
210impl TryFrom<RdnSequence> for ActorDN {
211    type Error = crate::errors::InvalidInput;
212
213    fn try_from(x509_distinguished_name: RdnSequence) -> Result<Self, Self::Error> {
214        check_amount_of_oid_values(
215            range::Range::new(1, 125)?,
216            &x509_distinguished_name,
217            &OID_RDN_DOMAIN_COMPONENT,
218        )?;
219        check_amount_of_oid_values(
220            range::Range::new(1, 1)?,
221            &x509_distinguished_name,
222            &OID_RDN_COMMON_NAME,
223        )?;
224        check_amount_of_oid_values(
225            range::Range::new(1, 1)?,
226            &x509_distinguished_name,
227            &OID_RDN_UID,
228        )?;
229        check_amount_of_oid_values(
230            range::Range::new(1, 1)?,
231            &x509_distinguished_name,
232            &OID_RDN_UNIQUE_IDENTIFIER,
233        )?;
234        let mut maybe_federation_id: Option<AttributeTypeAndValue> = None;
235        let mut maybe_local_name: Option<AttributeTypeAndValue> = None;
236        let mut maybe_domain_names: Vec<AttributeTypeAndValue> = Vec::new();
237        let mut maybe_session_id: Option<AttributeTypeAndValue> = None;
238        let mut maybe_additional_fields: Vec<AttributeTypeAndValue> = Vec::new();
239        for relative_distinguished_name in x509_distinguished_name.0.into_iter() {
240            for attribute_value_and_item in relative_distinguished_name.0.iter() {
241                match attribute_value_and_item.oid {
242                    OID_RDN_COMMON_NAME => {
243                        make_some_or_error(attribute_value_and_item, &mut maybe_local_name)?
244                    }
245                    OID_RDN_UID => {
246                        make_some_or_error(attribute_value_and_item, &mut maybe_federation_id)?
247                    }
248                    OID_RDN_UNIQUE_IDENTIFIER => {
249                        make_some_or_error(attribute_value_and_item, &mut maybe_session_id)?
250                    }
251                    OID_RDN_DOMAIN_COMPONENT => {
252                        maybe_domain_names.push(attribute_value_and_item.clone())
253                    }
254                    _other => maybe_additional_fields.push(attribute_value_and_item.clone()),
255                }
256            }
257        }
258        let federation_id = FederationId::try_from(match maybe_federation_id {
259            Some(fid) => fid,
260            None => {
261                return Err(crate::errors::InvalidInput::Malformed(String::from(
262                    "Expected Federation ID in ActorDN, found none",
263                )));
264            }
265        })?;
266        let local_name = LocalName::try_from(match maybe_local_name {
267            Some(ln) => ln,
268            None => {
269                return Err(crate::errors::InvalidInput::Malformed(String::from(
270                    "Expected Local Name in ActorDN, found none",
271                )));
272            }
273        })?;
274        let domain_name = federation_id.domain_name.clone();
275        let session_id = SessionId::try_from(match maybe_session_id {
276            Some(s_id) => s_id,
277            None => {
278                return Err(crate::errors::InvalidInput::Malformed(String::from(
279                    "Expected Local Name in ActorDN, found none",
280                )));
281            }
282        })?;
283        if domain_name != federation_id.domain_name {
284            return Err(InvalidInput::Malformed(format!(
285                "The domain name specified by the DC components does not the equal the domain name described in the federation ID: {domain_name} != {}",
286                federation_id.domain_name,
287            )));
288        }
289        if local_name != federation_id.local_name {
290            return Err(InvalidInput::Malformed(format!(
291                "The local name specified by the OID commonName ({OID_RDN_COMMON_NAME}) does not the equal the local name described in the federation ID (OID UID {OID_RDN_UID}): {local_name} != {}",
292                federation_id.local_name,
293            )));
294        }
295        Ok(ActorDN {
296            federation_id,
297            domain_name,
298            session_id,
299            local_name,
300            additional_fields: RelativeDistinguishedName::try_from(maybe_additional_fields).map_err(|e| crate::errors::InvalidInput::Malformed(format!("Could not parse ActorDN additional_fields: Name attribute contained additional information which was not a valid RelativeDistinguishedName: {e}")))?,
301        })
302    }
303}
304
305impl TryFrom<ActorDN> for RdnSequence {
306    type Error = InvalidInput;
307    fn try_from(value: ActorDN) -> Result<Self, Self::Error> {
308        let mut all_rdns = Vec::new();
309        
310        // Only add additional_fields if it's not empty to avoid extra empty RDNs in DER encoding
311        if !value.additional_fields.0.is_empty() {
312            all_rdns.push(value.additional_fields);
313        }
314        
315        all_rdns.append(&mut RdnSequence::try_from(value.domain_name)?.0);
316        all_rdns.push(RelativeDistinguishedName::try_from(value.federation_id)?);
317        all_rdns.push(RelativeDistinguishedName::try_from(value.local_name)?);
318        all_rdns.push(RelativeDistinguishedName::try_from(value.session_id)?);
319        Ok(RdnSequence(all_rdns))
320    }
321}
322
323impl TryFrom<RdnSequence> for HomeServerDN {
324    type Error = crate::errors::InvalidInput;
325
326    fn try_from(x509_distinguished_name: RdnSequence) -> Result<Self, Self::Error> {
327        check_amount_of_oid_values(
328            range::Range::new(1, 125)?,
329            &x509_distinguished_name,
330            &OID_RDN_DOMAIN_COMPONENT,
331        )?;
332        check_amount_of_oid_values(
333            range::Range::new(0, 0)?,
334            &x509_distinguished_name,
335            &OID_RDN_COMMON_NAME,
336        )?;
337        check_amount_of_oid_values(
338            range::Range::new(0, 0)?,
339            &x509_distinguished_name,
340            &OID_RDN_UID,
341        )?;
342        check_amount_of_oid_values(
343            range::Range::new(0, 0)?,
344            &x509_distinguished_name,
345            &OID_RDN_UNIQUE_IDENTIFIER,
346        )?;
347        let mut maybe_domain_names: Vec<AttributeTypeAndValue> = Vec::new();
348        let mut maybe_additional_fields: Vec<AttributeTypeAndValue> = Vec::new();
349        for relative_distinguished_name in x509_distinguished_name.0.into_iter() {
350            for attribute_value_and_item in relative_distinguished_name.0.iter() {
351                match attribute_value_and_item.oid {
352                    OID_RDN_DOMAIN_COMPONENT => {
353                        maybe_domain_names.push(attribute_value_and_item.clone())
354                    }
355                    _other => maybe_additional_fields.push(attribute_value_and_item.clone()),
356                }
357            }
358        }
359
360        let domain_name = DomainName::try_from(maybe_domain_names.as_slice())?;
361
362        Ok(HomeServerDN {
363            domain_name,
364            additional_fields: RelativeDistinguishedName::try_from(maybe_additional_fields).map_err(|e| crate::errors::InvalidInput::Malformed(format!("Could not parse ActorDN additional_fields: Name attribute contained additional information which was not a valid RelativeDistinguishedName: {e}")))?,
365        })
366    }
367}
368
369impl TryFrom<HomeServerDN> for RdnSequence {
370    type Error = InvalidInput;
371
372    fn try_from(value: HomeServerDN) -> Result<Self, Self::Error> {
373        let mut all_rdns = Vec::new();
374        
375        // Only add additional_fields if it's not empty to avoid extra empty RDNs in DER encoding
376        if !value.additional_fields.0.is_empty() {
377            all_rdns.push(value.additional_fields);
378        }
379        
380        all_rdns.append(&mut RdnSequence::try_from(value.domain_name)?.0);
381        Ok(RdnSequence(all_rdns))
382    }
383}
384
385impl TryFrom<PolyprotoDistinguishedName> for RdnSequence {
386    type Error = InvalidInput;
387
388    fn try_from(value: PolyprotoDistinguishedName) -> Result<Self, Self::Error> {
389        match value {
390            PolyprotoDistinguishedName::ActorDn(actor_dn) => Self::try_from(actor_dn),
391            PolyprotoDistinguishedName::HomeServerDn(home_server_dn) => {
392                Self::try_from(home_server_dn)
393            }
394        }
395    }
396}
397
398/// Helper function. Takes an exclusive reference `Option<AttributeTypeAndValue>`, inspects if it
399/// holds a value, and
400///
401/// - Errors appropriately, if it already holds a value
402/// - Else, updates the `None` value with the passed `attribute_value_and_item`, then returns `Ok(())`
403fn make_some_or_error(
404    attribute_value_and_item: &AttributeTypeAndValue,
405    value_to_update: &mut Option<AttributeTypeAndValue>,
406) -> Result<(), crate::errors::InvalidInput> {
407    if value_to_update.is_none() {
408        *value_to_update = Some(attribute_value_and_item.clone());
409        Ok(())
410    } else {
411        Err(crate::errors::InvalidInput::Malformed(
412            "Found multiple entries for same OID, where only one OID is allowed".to_owned(),
413        ))
414    }
415}
416
417pub(crate) mod range {
418    use crate::errors::InvalidInput;
419
420    /// Represents a range of positive (incl. zero (0)) numbers, where `max` is at least as large
421    /// as `min`.
422    pub(crate) struct Range {
423        min: u32,
424        max: u32,
425    }
426
427    impl Range {
428        pub(crate) fn new(min: u32, max: u32) -> Result<Self, InvalidInput> {
429            if max < min {
430                Err(InvalidInput::Malformed(
431                    "min must be equal to or smaller than max!".to_owned(),
432                ))
433            } else {
434                Ok(Self { min, max })
435            }
436        }
437
438        /// Returns `min` from `Self`.
439        pub(crate) fn min(&self) -> u32 {
440            self.min
441        }
442
443        /// Returns `max` from `Self`.
444        pub(crate) fn max(&self) -> u32 {
445            self.max
446        }
447    }
448}
449
450/// Validates that the count of attributes with a specific OID falls within the expected range.
451///
452/// This function counts all occurrences of [AttributeTypeAndValue] entries in the provided
453/// [RdnSequence] that match the given [ObjectIdentifier], and ensures the count is within
454/// the specified minimum and maximum bounds.
455///
456/// # Parameters
457///
458/// - `min_max`: A [Range] specifying the minimum and maximum allowed count of OID occurrences
459/// - `value_container`: The [RdnSequence] to search within for matching OID values
460/// - `oid`: The [ObjectIdentifier] to search for and count
461///
462/// # Returns
463///
464/// - `Ok(())` if the count of matching OID values is within the specified range
465/// - `Err(InvalidInput::Length)` if the count is below the minimum or above the maximum.
466pub(crate) fn check_amount_of_oid_values(
467    min_max: range::Range,
468    value_container: &RdnSequence,
469    oid: &ObjectIdentifier,
470) -> Result<(), InvalidInput> {
471    let mut count = 0u32;
472
473    for rdn in value_container.0.iter() {
474        for attribute_type_and_value in rdn.0.iter() {
475            if attribute_type_and_value.oid == *oid {
476                count = count.saturating_add(1);
477                if count > min_max.max() {
478                    return Err(InvalidInput::Length {
479                        min_length: min_max.min() as usize,
480                        max_length: min_max.max() as usize,
481                        actual_length: count.to_string(),
482                    });
483                }
484            }
485        }
486    }
487
488    if count < min_max.min() {
489        return Err(InvalidInput::Length {
490            min_length: min_max.min() as usize,
491            max_length: min_max.max() as usize,
492            actual_length: count.to_string(),
493        });
494    }
495
496    Ok(())
497}
498
499#[cfg(test)]
500#[allow(clippy::expect_used)]
501mod tests {
502    use super::*;
503    use der::Any;
504    use x509_cert::ext::pkix::name::DirectoryString;
505
506    /// Helper function to create an AttributeTypeAndValue for testing
507    fn create_test_attribute(oid: ObjectIdentifier, value: &str) -> AttributeTypeAndValue {
508        let directory_string = DirectoryString::Utf8String(value.to_string());
509        AttributeTypeAndValue {
510            oid,
511            value: Any::encode_from(&directory_string).expect("Valid encoding"),
512        }
513    }
514
515    /// Helper function to create a simple RdnSequence with specified attributes
516    fn create_test_rdn_sequence(attributes: Vec<AttributeTypeAndValue>) -> RdnSequence {
517        let rdn = RelativeDistinguishedName::try_from(attributes)
518            .expect("Valid RelativeDistinguishedName");
519        RdnSequence(vec![rdn])
520    }
521
522    /// Helper function to create an RdnSequence with multiple RDNs
523    fn create_multi_rdn_sequence(rdn_attributes: Vec<Vec<AttributeTypeAndValue>>) -> RdnSequence {
524        let rdns: Vec<RelativeDistinguishedName> = rdn_attributes
525            .into_iter()
526            .map(|attrs| RelativeDistinguishedName::try_from(attrs).expect("Valid RDN"))
527            .collect();
528        RdnSequence(rdns)
529    }
530
531    #[test]
532    fn test_valid_count_scenarios() {
533        // Test single occurrence within range [1,3]
534        let range = range::Range::new(1, 3).expect("Valid range");
535        let attributes = vec![
536            create_test_attribute(OID_RDN_COMMON_NAME, "test"),
537            create_test_attribute(OID_RDN_DOMAIN_COMPONENT, "example.com"),
538        ];
539        let rdn_sequence = create_test_rdn_sequence(attributes);
540        let result = check_amount_of_oid_values(range, &rdn_sequence, &OID_RDN_COMMON_NAME);
541        assert!(
542            result.is_ok(),
543            "Should succeed with 1 occurrence within range [1,3]"
544        );
545
546        // Test multiple occurrences within range [2,4]
547        let range = range::Range::new(2, 4).expect("Valid range");
548        let attributes = vec![
549            create_test_attribute(OID_RDN_COMMON_NAME, "test1"),
550            create_test_attribute(OID_RDN_COMMON_NAME, "test2"),
551            create_test_attribute(OID_RDN_DOMAIN_COMPONENT, "example.com"),
552        ];
553        let rdn_sequence = create_test_rdn_sequence(attributes);
554        let result = check_amount_of_oid_values(range, &rdn_sequence, &OID_RDN_COMMON_NAME);
555        assert!(
556            result.is_ok(),
557            "Should succeed with 2 occurrences within range [2,4]"
558        );
559    }
560
561    #[test]
562    fn test_boundary_conditions() {
563        // Test exactly at minimum boundary (2)
564        let range = range::Range::new(2, 5).expect("Valid range");
565        let attributes = vec![
566            create_test_attribute(OID_RDN_UID, "user1"),
567            create_test_attribute(OID_RDN_UID, "user2"),
568            create_test_attribute(OID_RDN_DOMAIN_COMPONENT, "example.com"),
569        ];
570        let rdn_sequence = create_test_rdn_sequence(attributes);
571        let result = check_amount_of_oid_values(range, &rdn_sequence, &OID_RDN_UID);
572        assert!(
573            result.is_ok(),
574            "Should succeed with exactly minimum count (2)"
575        );
576
577        // Test exactly at maximum boundary (3)
578        let range = range::Range::new(1, 3).expect("Valid range");
579        let attributes = vec![
580            create_test_attribute(OID_RDN_DOMAIN_COMPONENT, "part1"),
581            create_test_attribute(OID_RDN_DOMAIN_COMPONENT, "part2"),
582            create_test_attribute(OID_RDN_DOMAIN_COMPONENT, "part3"),
583            create_test_attribute(OID_RDN_COMMON_NAME, "test"),
584        ];
585        let rdn_sequence = create_test_rdn_sequence(attributes);
586        let result = check_amount_of_oid_values(range, &rdn_sequence, &OID_RDN_DOMAIN_COMPONENT);
587        assert!(
588            result.is_ok(),
589            "Should succeed with exactly maximum count (3)"
590        );
591    }
592
593    #[test]
594    fn test_error_conditions() {
595        // Test count below minimum (1 occurrence when minimum is 2)
596        let range = range::Range::new(2, 5).expect("Valid range");
597        let attributes = vec![
598            create_test_attribute(OID_RDN_COMMON_NAME, "test"),
599            create_test_attribute(OID_RDN_DOMAIN_COMPONENT, "example.com"),
600        ];
601        let rdn_sequence = create_test_rdn_sequence(attributes);
602        let result = check_amount_of_oid_values(range, &rdn_sequence, &OID_RDN_COMMON_NAME);
603        assert!(
604            result.is_err(),
605            "Should fail with 1 occurrence below minimum (2)"
606        );
607        if let Err(InvalidInput::Length {
608            min_length,
609            max_length,
610            ..
611        }) = result
612        {
613            assert_eq!(min_length, 2);
614            assert_eq!(max_length, 5);
615        } else {
616            panic!("Expected InvalidInput::Length error");
617        }
618
619        // Test count above maximum (3 occurrences when maximum is 2)
620        let range = range::Range::new(1, 2).expect("Valid range");
621        let attributes = vec![
622            create_test_attribute(OID_RDN_UNIQUE_IDENTIFIER, "id1"),
623            create_test_attribute(OID_RDN_UNIQUE_IDENTIFIER, "id2"),
624            create_test_attribute(OID_RDN_UNIQUE_IDENTIFIER, "id3"),
625            create_test_attribute(OID_RDN_COMMON_NAME, "test"),
626        ];
627        let rdn_sequence = create_test_rdn_sequence(attributes);
628        let result = check_amount_of_oid_values(range, &rdn_sequence, &OID_RDN_UNIQUE_IDENTIFIER);
629        assert!(
630            result.is_err(),
631            "Should fail with 3 occurrences above maximum (2)"
632        );
633        if let Err(InvalidInput::Length {
634            min_length,
635            max_length,
636            ..
637        }) = result
638        {
639            assert_eq!(min_length, 1);
640            assert_eq!(max_length, 2);
641        } else {
642            panic!("Expected InvalidInput::Length error");
643        }
644    }
645
646    #[test]
647    fn test_edge_cases() {
648        // Test zero occurrences below minimum (0 when minimum is 1)
649        let range = range::Range::new(1, 3).expect("Valid range");
650        let attributes = vec![
651            create_test_attribute(OID_RDN_COMMON_NAME, "test"),
652            create_test_attribute(OID_RDN_DOMAIN_COMPONENT, "example.com"),
653        ];
654        let rdn_sequence = create_test_rdn_sequence(attributes);
655        let result = check_amount_of_oid_values(range, &rdn_sequence, &OID_RDN_UID);
656        assert!(
657            result.is_err(),
658            "Should fail with 0 occurrences below minimum (1)"
659        );
660
661        // Test zero occurrences within range [0,2]
662        let range = range::Range::new(0, 2).expect("Valid range");
663        let result = check_amount_of_oid_values(range, &rdn_sequence, &OID_RDN_UID);
664        assert!(
665            result.is_ok(),
666            "Should succeed with 0 occurrences within range [0,2]"
667        );
668
669        // Test empty RdnSequence with minimum 0 (should succeed)
670        let range = range::Range::new(0, 1).expect("Valid range");
671        let empty_sequence = RdnSequence(vec![]);
672        let result = check_amount_of_oid_values(range, &empty_sequence, &OID_RDN_COMMON_NAME);
673        assert!(
674            result.is_ok(),
675            "Should succeed with empty RdnSequence when minimum is 0"
676        );
677
678        // Test empty RdnSequence with minimum > 0 (should fail)
679        let range = range::Range::new(1, 3).expect("Valid range");
680        let result = check_amount_of_oid_values(range, &empty_sequence, &OID_RDN_COMMON_NAME);
681        assert!(
682            result.is_err(),
683            "Should fail with empty RdnSequence when minimum > 0"
684        );
685
686        // Test multiple RDNs with target OID (2 occurrences across RDNs)
687        let range = range::Range::new(2, 4).expect("Valid range");
688        let rdn_attributes = vec![
689            vec![
690                create_test_attribute(OID_RDN_COMMON_NAME, "test1"),
691                create_test_attribute(OID_RDN_DOMAIN_COMPONENT, "part1"),
692            ],
693            vec![
694                create_test_attribute(OID_RDN_COMMON_NAME, "test2"),
695                create_test_attribute(OID_RDN_UID, "user"),
696            ],
697        ];
698        let multi_rdn_sequence = create_multi_rdn_sequence(rdn_attributes);
699        let result = check_amount_of_oid_values(range, &multi_rdn_sequence, &OID_RDN_COMMON_NAME);
700        assert!(
701            result.is_ok(),
702            "Should succeed with 2 occurrences across multiple RDNs"
703        );
704    }
705
706    #[test]
707    fn test_exact_count_constraints() {
708        let range = range::Range::new(1, 1).expect("Valid range");
709
710        // Test exactly 1 occurrence in range [1,1] (should succeed)
711        let attributes = vec![create_test_attribute(OID_RDN_COMMON_NAME, "single")];
712        let rdn_sequence = create_test_rdn_sequence(attributes);
713        let result = check_amount_of_oid_values(range, &rdn_sequence, &OID_RDN_COMMON_NAME);
714        assert!(
715            result.is_ok(),
716            "Should succeed with exactly 1 occurrence in range [1,1]"
717        );
718
719        // Test 2 occurrences in range [1,1] (should fail)
720        let range = range::Range::new(1, 1).expect("Valid range");
721        let attributes = vec![
722            create_test_attribute(OID_RDN_COMMON_NAME, "first"),
723            create_test_attribute(OID_RDN_COMMON_NAME, "second"),
724        ];
725        let rdn_sequence = create_test_rdn_sequence(attributes);
726        let result = check_amount_of_oid_values(range, &rdn_sequence, &OID_RDN_COMMON_NAME);
727        assert!(
728            result.is_err(),
729            "Should fail with 2 occurrences in range [1,1]"
730        );
731    }
732
733    mod constrained_tests {
734        use super::*;
735        use crate::{Constrained, certs::Target};
736        use der::asn1::SetOfVec;
737
738        /// Helper to create a valid FederationId
739        fn create_valid_federation_id() -> FederationId {
740            FederationId::new("alice@example.com").expect("Valid federation ID")
741        }
742
743        /// Helper to create a valid LocalName matching the federation ID
744        fn create_valid_local_name() -> LocalName {
745            LocalName::new("alice").expect("Valid local name")
746        }
747
748        /// Helper to create a valid DomainName matching the federation ID
749        fn create_valid_domain_name() -> DomainName {
750            DomainName::new("example.com").expect("Valid domain name")
751        }
752
753        /// Helper to create a valid SessionId
754        fn create_valid_session_id() -> SessionId {
755            SessionId::new_validated("validSessionId123").expect("Valid session ID")
756        }
757
758        /// Helper to create empty additional fields
759        fn create_empty_additional_fields() -> RelativeDistinguishedName {
760            RelativeDistinguishedName(SetOfVec::new())
761        }
762
763        /// Helper to create additional fields with forbidden OIDs
764        fn create_forbidden_oid_additional_fields(
765            oid: ObjectIdentifier,
766        ) -> RelativeDistinguishedName {
767            let attr = create_test_attribute(oid, "forbidden");
768            RelativeDistinguishedName::try_from(vec![attr]).expect("Valid RDN")
769        }
770
771        /// Helper to create a valid ActorDN
772        fn create_valid_actor_dn() -> ActorDN {
773            ActorDN {
774                federation_id: create_valid_federation_id(),
775                local_name: create_valid_local_name(),
776                domain_name: create_valid_domain_name(),
777                session_id: create_valid_session_id(),
778                additional_fields: create_empty_additional_fields(),
779            }
780        }
781
782        /// Helper to create a valid HomeServerDN
783        fn create_valid_home_server_dn() -> HomeServerDN {
784            HomeServerDN {
785                domain_name: create_valid_domain_name(),
786                additional_fields: create_empty_additional_fields(),
787            }
788        }
789
790        #[test]
791        fn polyproto_dn_actor_with_actor_target_validates() {
792            let actor_dn = create_valid_actor_dn();
793            let pdn = PolyprotoDistinguishedName::ActorDn(actor_dn);
794
795            let result = pdn.validate(Some(Target::Actor));
796            assert!(result.is_ok(), "ActorDn should validate with Actor target");
797        }
798
799        #[test]
800        fn polyproto_dn_actor_with_none_target_validates() {
801            let actor_dn = create_valid_actor_dn();
802            let pdn = PolyprotoDistinguishedName::ActorDn(actor_dn);
803
804            let result = pdn.validate(None);
805            assert!(result.is_ok(), "ActorDn should validate with None target");
806        }
807
808        #[test]
809        fn polyproto_dn_home_server_with_home_server_target_validates() {
810            let home_server_dn = create_valid_home_server_dn();
811            let pdn = PolyprotoDistinguishedName::HomeServerDn(home_server_dn);
812
813            let result = pdn.validate(Some(Target::HomeServer));
814            assert!(
815                result.is_ok(),
816                "HomeServerDn should validate with HomeServer target"
817            );
818        }
819
820        #[test]
821        fn polyproto_dn_home_server_with_none_target_validates() {
822            let home_server_dn = create_valid_home_server_dn();
823            let pdn = PolyprotoDistinguishedName::HomeServerDn(home_server_dn);
824
825            let result = pdn.validate(None);
826            assert!(
827                result.is_ok(),
828                "HomeServerDn should validate with None target"
829            );
830        }
831
832        #[test]
833        fn polyproto_dn_actor_with_home_server_target_fails() {
834            let actor_dn = create_valid_actor_dn();
835            let pdn = PolyprotoDistinguishedName::ActorDn(actor_dn);
836
837            let result = pdn.validate(Some(Target::HomeServer));
838            assert!(
839                result.is_err(),
840                "ActorDn should fail with HomeServer target"
841            );
842
843            if let Err(error) = result {
844                assert!(
845                    error.to_string().contains("malformed"),
846                    "Error should mention validation failure"
847                );
848            }
849        }
850
851        #[test]
852        fn polyproto_dn_home_server_with_actor_target_fails() {
853            let home_server_dn = create_valid_home_server_dn();
854            let pdn = PolyprotoDistinguishedName::HomeServerDn(home_server_dn);
855
856            let result = pdn.validate(Some(Target::Actor));
857            assert!(
858                result.is_err(),
859                "HomeServerDn should fail with Actor target"
860            );
861
862            if let Err(error) = result {
863                assert!(
864                    error.to_string().contains("malformed"),
865                    "Error should mention validation failure"
866                );
867            }
868        }
869
870        #[test]
871        fn actor_dn_valid_components_validates() {
872            let actor_dn = create_valid_actor_dn();
873
874            assert!(
875                actor_dn.validate(Some(Target::Actor)).is_ok(),
876                "Valid ActorDN should validate with Actor target"
877            );
878            assert!(
879                actor_dn.validate(None).is_ok(),
880                "Valid ActorDN should validate with None target"
881            );
882        }
883
884        #[test]
885        fn actor_dn_mismatched_domain_name_fails() {
886            let federation_id = create_valid_federation_id();
887            let local_name = create_valid_local_name();
888            let mismatched_domain = DomainName::new("different.com").expect("Valid domain name");
889            let session_id = create_valid_session_id();
890
891            let actor_dn = ActorDN {
892                federation_id,
893                local_name,
894                domain_name: mismatched_domain,
895                session_id,
896                additional_fields: create_empty_additional_fields(),
897            };
898
899            let result = actor_dn.validate(None);
900            assert!(
901                result.is_err(),
902                "ActorDN with mismatched domain name should fail"
903            );
904
905            if let Err(error) = result {
906                let error_str = error.to_string();
907                assert!(
908                    error_str.contains("malformed"),
909                    "Error should mention validation failure: {error_str}"
910                );
911            }
912        }
913
914        #[test]
915        fn actor_dn_mismatched_local_name_fails() {
916            let federation_id = create_valid_federation_id();
917            let mismatched_local_name = LocalName::new("bob").expect("Valid local name");
918            let domain_name = create_valid_domain_name();
919            let session_id = create_valid_session_id();
920
921            let actor_dn = ActorDN {
922                federation_id,
923                local_name: mismatched_local_name,
924                domain_name,
925                session_id,
926                additional_fields: create_empty_additional_fields(),
927            };
928
929            let result = actor_dn.validate(None);
930            assert!(
931                result.is_err(),
932                "ActorDN with mismatched local name should fail"
933            );
934
935            if let Err(error) = result {
936                let error_str = error.to_string();
937                assert!(
938                    error_str.contains("malformed"),
939                    "Error should mention validation failure: {error_str}"
940                );
941            }
942        }
943
944        #[test]
945        fn actor_dn_forbidden_domain_component_in_additional_fields_fails() {
946            let mut actor_dn = create_valid_actor_dn();
947            actor_dn.additional_fields =
948                create_forbidden_oid_additional_fields(OID_RDN_DOMAIN_COMPONENT);
949
950            let result = actor_dn.validate(Some(Target::Actor));
951            assert!(
952                result.is_err(),
953                "ActorDN with DOMAIN_COMPONENT in additional_fields should fail"
954            );
955        }
956
957        #[test]
958        fn actor_dn_forbidden_common_name_in_additional_fields_fails() {
959            let mut actor_dn = create_valid_actor_dn();
960            actor_dn.additional_fields =
961                create_forbidden_oid_additional_fields(OID_RDN_COMMON_NAME);
962
963            let result = actor_dn.validate(Some(Target::Actor));
964            assert!(
965                result.is_err(),
966                "ActorDN with COMMON_NAME in additional_fields should fail"
967            );
968        }
969
970        #[test]
971        fn actor_dn_forbidden_uid_in_additional_fields_fails() {
972            let mut actor_dn = create_valid_actor_dn();
973            actor_dn.additional_fields = create_forbidden_oid_additional_fields(OID_RDN_UID);
974
975            let result = actor_dn.validate(Some(Target::Actor));
976            assert!(
977                result.is_err(),
978                "ActorDN with UID in additional_fields should fail"
979            );
980        }
981
982        #[test]
983        fn actor_dn_forbidden_unique_identifier_in_additional_fields_fails() {
984            let mut actor_dn = create_valid_actor_dn();
985            actor_dn.additional_fields =
986                create_forbidden_oid_additional_fields(OID_RDN_UNIQUE_IDENTIFIER);
987
988            let result = actor_dn.validate(Some(Target::Actor));
989            assert!(
990                result.is_err(),
991                "ActorDN with UNIQUE_IDENTIFIER in additional_fields should fail"
992            );
993        }
994
995        #[test]
996        fn actor_dn_multiple_forbidden_oids_in_additional_fields_fails() {
997            let forbidden_attrs = vec![
998                create_test_attribute(OID_RDN_COMMON_NAME, "forbidden1"),
999                create_test_attribute(OID_RDN_UID, "forbidden2"),
1000            ];
1001            let forbidden_fields =
1002                RelativeDistinguishedName::try_from(forbidden_attrs).expect("Valid RDN");
1003
1004            let mut actor_dn = create_valid_actor_dn();
1005            actor_dn.additional_fields = forbidden_fields;
1006
1007            let result = actor_dn.validate(Some(Target::Actor));
1008            assert!(
1009                result.is_err(),
1010                "ActorDN with multiple forbidden OIDs should fail"
1011            );
1012        }
1013
1014        #[test]
1015        fn actor_dn_allowed_custom_oids_in_additional_fields_validates() {
1016            let custom_oid = ObjectIdentifier::new_unwrap("1.2.3.4.5");
1017            let custom_attr = create_test_attribute(custom_oid, "custom_value");
1018            let custom_fields =
1019                RelativeDistinguishedName::try_from(vec![custom_attr]).expect("Valid RDN");
1020
1021            let mut actor_dn = create_valid_actor_dn();
1022            actor_dn.additional_fields = custom_fields;
1023
1024            let result = actor_dn.validate(Some(Target::Actor));
1025            assert!(
1026                result.is_ok(),
1027                "ActorDN with custom OIDs in additional_fields should validate"
1028            );
1029        }
1030
1031        #[test]
1032        fn home_server_dn_valid_components_validates() {
1033            let home_server_dn = create_valid_home_server_dn();
1034
1035            assert!(
1036                home_server_dn.validate(Some(Target::HomeServer)).is_ok(),
1037                "Valid HomeServerDN should validate with HomeServer target"
1038            );
1039            assert!(
1040                home_server_dn.validate(None).is_ok(),
1041                "Valid HomeServerDN should validate with None target"
1042            );
1043        }
1044
1045        #[test]
1046        fn home_server_dn_forbidden_common_name_in_additional_fields_fails() {
1047            let mut home_server_dn = create_valid_home_server_dn();
1048            home_server_dn.additional_fields =
1049                create_forbidden_oid_additional_fields(OID_RDN_COMMON_NAME);
1050
1051            let result = home_server_dn.validate(Some(Target::HomeServer));
1052            assert!(
1053                result.is_err(),
1054                "HomeServerDN with COMMON_NAME in additional_fields should fail"
1055            );
1056        }
1057
1058        #[test]
1059        fn home_server_dn_forbidden_uid_in_additional_fields_fails() {
1060            let mut home_server_dn = create_valid_home_server_dn();
1061            home_server_dn.additional_fields = create_forbidden_oid_additional_fields(OID_RDN_UID);
1062
1063            let result = home_server_dn.validate(Some(Target::HomeServer));
1064            assert!(
1065                result.is_err(),
1066                "HomeServerDN with UID in additional_fields should fail"
1067            );
1068        }
1069
1070        #[test]
1071        fn home_server_dn_forbidden_unique_identifier_in_additional_fields_fails() {
1072            let mut home_server_dn = create_valid_home_server_dn();
1073            home_server_dn.additional_fields =
1074                create_forbidden_oid_additional_fields(OID_RDN_UNIQUE_IDENTIFIER);
1075
1076            let result = home_server_dn.validate(Some(Target::HomeServer));
1077            assert!(
1078                result.is_err(),
1079                "HomeServerDN with UNIQUE_IDENTIFIER in additional_fields should fail"
1080            );
1081        }
1082
1083        #[test]
1084        fn home_server_dn_forbidden_domain_component_in_additional_fields_fails() {
1085            let mut home_server_dn = create_valid_home_server_dn();
1086            home_server_dn.additional_fields =
1087                create_forbidden_oid_additional_fields(OID_RDN_DOMAIN_COMPONENT);
1088
1089            let result = home_server_dn.validate(Some(Target::HomeServer));
1090            assert!(
1091                result.is_err(),
1092                "HomeServerDN with DOMAIN_COMPONENT in additional_fields should fail"
1093            );
1094        }
1095
1096        #[test]
1097        fn home_server_dn_multiple_forbidden_oids_in_additional_fields_fails() {
1098            let forbidden_attrs = vec![
1099                create_test_attribute(OID_RDN_COMMON_NAME, "forbidden1"),
1100                create_test_attribute(OID_RDN_DOMAIN_COMPONENT, "forbidden2"),
1101                create_test_attribute(OID_RDN_UID, "forbidden3"),
1102            ];
1103            let forbidden_fields =
1104                RelativeDistinguishedName::try_from(forbidden_attrs).expect("Valid RDN");
1105
1106            let mut home_server_dn = create_valid_home_server_dn();
1107            home_server_dn.additional_fields = forbidden_fields;
1108
1109            let result = home_server_dn.validate(Some(Target::HomeServer));
1110            assert!(
1111                result.is_err(),
1112                "HomeServerDN with multiple forbidden OIDs should fail"
1113            );
1114        }
1115
1116        #[test]
1117        fn home_server_dn_allowed_custom_oids_in_additional_fields_validates() {
1118            let custom_oids = vec![
1119                ObjectIdentifier::new_unwrap("1.2.3.4.5"),
1120                ObjectIdentifier::new_unwrap("2.5.4.10"), // organizationName
1121                ObjectIdentifier::new_unwrap("2.5.4.11"), // organizationalUnitName
1122            ];
1123
1124            let custom_attrs: Vec<_> = custom_oids
1125                .into_iter()
1126                .enumerate()
1127                .map(|(i, oid)| create_test_attribute(oid, &format!("custom_value_{i}")))
1128                .collect();
1129
1130            let custom_fields =
1131                RelativeDistinguishedName::try_from(custom_attrs).expect("Valid RDN");
1132
1133            let mut home_server_dn = create_valid_home_server_dn();
1134            home_server_dn.additional_fields = custom_fields;
1135
1136            let result = home_server_dn.validate(Some(Target::HomeServer));
1137            assert!(
1138                result.is_ok(),
1139                "HomeServerDN with custom OIDs in additional_fields should validate"
1140            );
1141        }
1142
1143        #[test]
1144        fn actor_dn_empty_additional_fields_validates() {
1145            let actor_dn = create_valid_actor_dn();
1146            // additional_fields is already empty by default
1147
1148            let result = actor_dn.validate(Some(Target::Actor));
1149            assert!(
1150                result.is_ok(),
1151                "ActorDN with empty additional_fields should validate"
1152            );
1153        }
1154
1155        #[test]
1156        fn home_server_dn_empty_additional_fields_validates() {
1157            let home_server_dn = create_valid_home_server_dn();
1158            // additional_fields is already empty by default
1159
1160            let result = home_server_dn.validate(Some(Target::HomeServer));
1161            assert!(
1162                result.is_ok(),
1163                "HomeServerDN with empty additional_fields should validate"
1164            );
1165        }
1166
1167        #[test]
1168        fn actor_dn_new_validated_success() {
1169            let federation_id = create_valid_federation_id();
1170            let local_name = create_valid_local_name();
1171            let domain_name = create_valid_domain_name();
1172            let session_id = create_valid_session_id();
1173
1174            let result =
1175                ActorDN::new_validated(federation_id, local_name, domain_name, session_id, None);
1176
1177            assert!(
1178                result.is_ok(),
1179                "ActorDN::new_validated should succeed with valid components"
1180            );
1181        }
1182
1183        #[test]
1184        fn actor_dn_new_validated_with_additional_fields_success() {
1185            let federation_id = create_valid_federation_id();
1186            let local_name = create_valid_local_name();
1187            let domain_name = create_valid_domain_name();
1188            let session_id = create_valid_session_id();
1189            let custom_oid = ObjectIdentifier::new_unwrap("1.2.3.4.5");
1190            let custom_attr = create_test_attribute(custom_oid, "custom");
1191            let additional_fields =
1192                RelativeDistinguishedName::try_from(vec![custom_attr]).expect("Valid RDN");
1193
1194            let result = ActorDN::new_validated(
1195                federation_id,
1196                local_name,
1197                domain_name,
1198                session_id,
1199                Some(additional_fields),
1200            );
1201
1202            assert!(
1203                result.is_ok(),
1204                "ActorDN::new_validated should succeed with valid additional fields"
1205            );
1206        }
1207
1208        #[test]
1209        fn actor_dn_new_validated_mismatched_components_fails() {
1210            let federation_id = create_valid_federation_id();
1211            let mismatched_local_name = LocalName::new("bob").expect("Valid local name");
1212            let domain_name = create_valid_domain_name();
1213            let session_id = create_valid_session_id();
1214
1215            let result = ActorDN::new_validated(
1216                federation_id,
1217                mismatched_local_name,
1218                domain_name,
1219                session_id,
1220                None,
1221            );
1222
1223            assert!(
1224                result.is_err(),
1225                "ActorDN::new_validated should fail with mismatched components"
1226            );
1227        }
1228
1229        #[test]
1230        fn home_server_dn_new_validated_success() {
1231            let domain_name = create_valid_domain_name();
1232
1233            let result = HomeServerDN::new_validated(domain_name, None);
1234            assert!(
1235                result.is_ok(),
1236                "HomeServerDN::new_validated should succeed with valid components"
1237            );
1238        }
1239
1240        #[test]
1241        fn home_server_dn_new_validated_with_additional_fields_success() {
1242            let domain_name = create_valid_domain_name();
1243            let custom_oid = ObjectIdentifier::new_unwrap("2.5.4.10"); // organizationName
1244            let custom_attr = create_test_attribute(custom_oid, "Example Org");
1245            let additional_fields =
1246                RelativeDistinguishedName::try_from(vec![custom_attr]).expect("Valid RDN");
1247
1248            let result = HomeServerDN::new_validated(domain_name, Some(additional_fields));
1249            assert!(
1250                result.is_ok(),
1251                "HomeServerDN::new_validated should succeed with valid additional fields"
1252            );
1253        }
1254
1255        #[test]
1256        fn home_server_dn_new_validated_forbidden_additional_fields_fails() {
1257            let domain_name = create_valid_domain_name();
1258            let forbidden_attr = create_test_attribute(OID_RDN_COMMON_NAME, "forbidden");
1259            let forbidden_fields =
1260                RelativeDistinguishedName::try_from(vec![forbidden_attr]).expect("Valid RDN");
1261
1262            let result = HomeServerDN::new_validated(domain_name, Some(forbidden_fields));
1263            assert!(
1264                result.is_err(),
1265                "HomeServerDN::new_validated should fail with forbidden additional fields"
1266            );
1267        }
1268    }
1269}