Skip to main content

structured_pkix/name/
selector.rs

1//! Strict RFC 4514 lexical identities, independent of input/parser lifetimes.
2
3use super::{Attribute, AttributeValue, Name, Strings, Syntax, prepared, segments};
4use crate::error::{Error, ErrorCode, Located, Result};
5use crate::oid;
6use core::cmp::Ordering;
7use core::ops::Range;
8use structured_asn1::memory::{Memory, OwnedVec};
9use structured_asn1::{Class, Oid, OwnedOid, Parser, Tag, universal};
10
11const FIELD: &str = "name.selector";
12
13/// An independently owned RFC 4514 distinguished-name assertion.
14///
15/// Parsing is strict: separator whitespace is not implicitly discarded.
16/// Values retain their decoded assertion octets or complete original BER.
17/// RDN order and unordered membership use the certificate Name's shared
18/// comparison core. Storage remains charged after the parser/account handle
19/// is dropped; each subsequent operation takes its own execution account.
20#[derive(Debug)]
21pub struct Selector {
22    parts: OwnedVec<OwnedVec<Selected>>,
23}
24
25#[derive(Debug)]
26struct Selected {
27    kind: Kind,
28    bytes: OwnedVec<u8>,
29    value: Value,
30    offset: u64,
31}
32
33#[derive(Debug)]
34enum Kind {
35    Known(Oid<'static>),
36    Numeric(OwnedOid),
37}
38
39impl Kind {
40    fn as_oid(&self) -> Oid<'_> {
41        match self {
42            Self::Known(kind) => *kind,
43            Self::Numeric(kind) => kind.as_oid(),
44        }
45    }
46}
47
48#[derive(Debug)]
49enum Value {
50    Assertion,
51    Ber {
52        tag: Tag,
53        contents: Range<usize>,
54        joined: Option<OwnedVec<u8>>,
55    },
56}
57
58impl Selector {
59    /// Parses the grammar of RFC 4514 §3 without compatibility trimming.
60    /// Unknown numeric types are preserved. An unknown descriptor, malformed
61    /// escape, incomplete BER value or syntax violation is a located error;
62    /// unavailable equality semantics remain distinct at comparison time.
63    ///
64    /// ```
65    /// use std::sync::Arc;
66    /// use structured_asn1::memory::{Memory, MemoryAccount};
67    /// use structured_pkix::name::Selector;
68    /// let memory = Memory::new(Arc::new(MemoryAccount::new(4096, 100_000)));
69    /// let left = Selector::parse("CN=A\\,B,O=Example", &memory)?;
70    /// let right = Selector::parse("cn=A\\2CB,o=example", &memory)?;
71    /// assert!(left.matches(&right, &memory)?);
72    /// # Ok::<(), structured_pkix::Error>(())
73    /// ```
74    pub fn parse(text: &str, execution: &Memory) -> Result<Self> {
75        // Lexical validation precedes allocation. This admits all fixed
76        // descriptor comparisons and both source passes before scanning.
77        let work = (text.len() as u64)
78            .checked_mul(64)
79            .and_then(|n| n.checked_add(1))
80            .ok_or_else(|| {
81                Error::resource(structured_asn1::memory::MemoryError::WorkExceeded, FIELD, 0)
82            })?;
83        execution
84            .charge_work(work)
85            .map_err(|reason| Error::resource(reason, FIELD, 0))?;
86        scan::<true>(text, |_, _, _, _| Ok(()))?;
87        let parser = Parser::ber(execution.clone());
88        let mut parts = execution.vector();
89        let mut current = execution.vector();
90        scan::<true>(text, |label, value, offset, last| {
91            current
92                .try_push(selected(label, value, offset, &parser, execution)?)
93                .map_err(|reason| Error::resource(reason, FIELD, offset))?;
94            if last {
95                parts
96                    .try_push(core::mem::replace(&mut current, execution.vector()))
97                    .map_err(|reason| Error::resource(reason, FIELD, offset))?;
98            }
99            Ok(())
100        })?;
101        execution
102            .charge_work(parts.len() as u64)
103            .map_err(|reason| Error::resource(reason, FIELD, 0))?;
104        parts.reverse(); // RFC 4514 §2.1: textual last RDN is ASN.1 first.
105        Ok(Self { parts })
106    }
107
108    /// Compares directly with a certificate Name without serializing it.
109    /// Unknown LDAP assertion syntax returns `UnsupportedNameSyntax` when
110    /// semantic preparation is needed; it is never guessed as `UTF8String`.
111    pub fn matches_name(&self, name: &Name<'_>, execution: &Memory) -> Result<bool> {
112        if self.parts.len() != name.parts().len() {
113            return Ok(false);
114        }
115        for (selected, part) in self.parts.iter().zip(name.parts()) {
116            execution
117                .charge_work(1)
118                .map_err(|reason| Error::resource(reason, FIELD, 0))?;
119            let fail = |reason| Error::resource(reason, FIELD, 0);
120            let order = prepared::compare_views(
121                selected.iter().map(|item| project(item, execution)),
122                part.attributes()
123                    .iter()
124                    .copied()
125                    .map(|attribute| prepared::Prepared::of(attribute, execution)),
126                execution,
127                fail,
128            )?;
129            if order != Ordering::Equal {
130                return Ok(false);
131            }
132        }
133        Ok(true)
134    }
135
136    /// Compares two independently owned selectors under the same Name rules.
137    pub fn matches(&self, other: &Self, execution: &Memory) -> Result<bool> {
138        if self.parts.len() != other.parts.len() {
139            return Ok(false);
140        }
141        for (left, right) in self.parts.iter().zip(&other.parts) {
142            execution
143                .charge_work(1)
144                .map_err(|reason| Error::resource(reason, FIELD, 0))?;
145            if prepared::compare_views(
146                left.iter().map(|item| project(item, execution)),
147                right.iter().map(|item| project(item, execution)),
148                execution,
149                |reason| Error::resource(reason, FIELD, 0),
150            )? != Ordering::Equal
151            {
152                return Ok(false);
153            }
154        }
155        Ok(true)
156    }
157}
158
159fn selected(
160    label: &str,
161    value: &str,
162    offset: u64,
163    parser: &Parser,
164    execution: &Memory,
165) -> Result<Selected> {
166    let kind = match descriptor(label) {
167        Some(kind) => Kind::Known(kind),
168        None => Kind::Numeric(OwnedOid::parse(label, execution).map_err(|error| {
169            Error::new(
170                ErrorCode::Encoding(error.code()),
171                FIELD,
172                offset + error.offset(),
173            )
174        })?),
175    };
176    let mut bytes = execution.vector();
177    let value = if let Some(hex) = value.strip_prefix('#') {
178        for pair in hex.as_bytes().as_chunks::<2>().0 {
179            bytes
180                .try_push(
181                    (digit(pair[0]).expect("validated hex") << 4)
182                        | digit(pair[1]).expect("validated hex"),
183                )
184                .map_err(|reason| Error::resource(reason, FIELD, offset))?;
185        }
186        ber(&bytes, kind.as_oid(), parser, execution).map_err(|error| {
187            Error::new(
188                error.code(),
189                FIELD,
190                offset + label.len() as u64 + 2 + 2 * error.offset(),
191            )
192        })?
193    } else {
194        let mut at = 0;
195        let input = value.as_bytes();
196        while at < input.len() {
197            let byte = if input[at] == b'\\' {
198                at += 1;
199                if let Some(high) = digit(input[at]) {
200                    at += 1;
201                    (high << 4) | digit(input[at]).expect("validated escape")
202                } else {
203                    input[at]
204                }
205            } else {
206                input[at]
207            };
208            bytes
209                .try_push(byte)
210                .map_err(|reason| Error::resource(reason, FIELD, offset))?;
211            at += 1;
212        }
213        Value::Assertion
214    };
215    Ok(Selected {
216        kind,
217        bytes,
218        value,
219        offset,
220    })
221}
222
223fn ber(bytes: &[u8], kind: Oid<'_>, parser: &Parser, execution: &Memory) -> Result<Value> {
224    let mut reader = parser.read(bytes, 0).field(FIELD)?;
225    let element = reader
226        .next_element()
227        .field(FIELD)?
228        .ok_or_else(|| malformed(0))?;
229    if !reader.is_empty() {
230        return Err(malformed(element.span().end()));
231    }
232    // RFC4514 §2.4's reversible #BER form preserves the original value,
233    // including string payloads Name retained for opaque wire identity.
234    // Structural BER remains checked; textual assertions are validated in project.
235    // https://www.rfc-editor.org/rfc/rfc4514.html#section-2.4
236    super::walk_der(&reader, &element, FIELD, Strings::AsWritten)?;
237    let tag = element.tag();
238    let start = element.as_bytes().len()
239        - element.contents().len()
240        - if element.is_indefinite() { 2 } else { 0 };
241    let contents = start..start + element.contents().len();
242    execution
243        .charge_work(
244            (kind.as_bytes().len() as u64)
245                .checked_mul(40)
246                .ok_or_else(|| {
247                    Error::resource(structured_asn1::memory::MemoryError::WorkExceeded, FIELD, 0)
248                })?,
249        )
250        .map_err(|reason| Error::resource(reason, FIELD, 0))?;
251    let logical_encoding = match tag.number() {
252        Some(number) => super::AttributeValue {
253            tag: Tag::primitive(number),
254            contents: &[],
255            der: &[],
256            offset: 0,
257            depth: 0,
258        }
259        .encoding(),
260        None => super::Encoding::Other,
261    };
262    let joined = if tag.class() == Class::Universal
263        && tag.is_constructed()
264        && tag.number().is_some_and(super::segmented_universal_type)
265        && tag.number() != Some(universal::BIT_STRING)
266        && super::permits(kind, logical_encoding)
267    {
268        let mut joined = execution.vector();
269        segments::walk_in(
270            &reader,
271            &element,
272            FIELD,
273            false,
274            Some(&mut joined),
275            execution,
276        )?;
277        Some(joined)
278    } else {
279        None
280    };
281    Ok(Value::Ber {
282        tag,
283        contents,
284        joined,
285    })
286}
287
288fn project<'a>(item: &'a Selected, execution: &Memory) -> Result<prepared::Prepared<'a>> {
289    let kind = item.kind.as_oid();
290    let value = match &item.value {
291        Value::Assertion => {
292            execution
293                .charge_work((kind.as_bytes().len() as u64) * 40)
294                .map_err(|reason| Error::resource(reason, FIELD, item.offset))?;
295            let tag = match super::syntax_of(kind) {
296                Some(Syntax::Directory) => universal::UTF8_STRING,
297                Some(Syntax::Printable) => universal::PRINTABLE_STRING,
298                Some(Syntax::Ia5) => universal::IA5_STRING,
299                None => {
300                    return Err(Error::new(
301                        ErrorCode::UnsupportedNameSyntax,
302                        FIELD,
303                        item.offset,
304                    ));
305                }
306            };
307            AttributeValue {
308                tag: Tag::primitive(tag),
309                contents: &item.bytes,
310                der: &[],
311                offset: item.offset,
312                depth: 0,
313            }
314        }
315        Value::Ber {
316            tag,
317            contents,
318            joined,
319        } => AttributeValue {
320            tag: if joined.is_some() {
321                Tag::primitive(tag.number().expect("universal string"))
322            } else {
323                *tag
324            },
325            contents: joined.as_deref().unwrap_or(&item.bytes[contents.clone()]),
326            der: &item.bytes,
327            offset: item.offset,
328            depth: 0,
329        },
330    };
331    let attribute = Attribute { kind, value };
332    if matches!(item.value, Value::Assertion) {
333        let work = (kind.as_bytes().len() as u64)
334            .checked_mul(64)
335            .and_then(|kinds| {
336                (value.contents.len() as u64)
337                    .checked_mul(2)
338                    .and_then(|contents| kinds.checked_add(contents))
339            })
340            .and_then(|work| work.checked_add(1))
341            .ok_or_else(|| {
342                Error::resource(
343                    structured_asn1::memory::MemoryError::WorkExceeded,
344                    FIELD,
345                    item.offset,
346                )
347            })?;
348        execution
349            .charge_work(work)
350            .map_err(|reason| Error::resource(reason, FIELD, item.offset))?;
351        if !super::well_formed(&attribute) {
352            return Err(Error::new(ErrorCode::MalformedValue, FIELD, item.offset));
353        }
354    }
355    let prepared = prepared::Prepared::of(attribute, execution)?;
356    if matches!(item.value, Value::Assertion) && !prepared.is_prepared() {
357        return Err(Error::new(
358            ErrorCode::UnsupportedNameSyntax,
359            FIELD,
360            item.offset,
361        ));
362    }
363    Ok(prepared)
364}
365
366fn malformed(offset: u64) -> Error {
367    Error::new(ErrorCode::MalformedValue, FIELD, offset)
368}
369
370fn digit(byte: u8) -> Option<u8> {
371    match byte {
372        b'0'..=b'9' => Some(byte - b'0'),
373        b'a'..=b'f' => Some(byte - b'a' + 10),
374        b'A'..=b'F' => Some(byte - b'A' + 10),
375        _ => None,
376    }
377}
378
379fn label_fits<const REGISTERED: bool>(label: &str, offset: u64) -> Result<()> {
380    let Some(first) = label.as_bytes().first() else {
381        return Err(malformed(offset));
382    };
383    if first.is_ascii_digit() {
384        let mut count = 0;
385        for component in label.split('.') {
386            if component.is_empty()
387                || !component.bytes().all(|b| b.is_ascii_digit())
388                || (component.len() > 1 && component.starts_with('0'))
389            {
390                return Err(malformed(offset));
391            }
392            count += 1;
393        }
394        if count < 2 {
395            return Err(malformed(offset));
396        }
397    } else if !first.is_ascii_alphabetic()
398        || !label
399            .bytes()
400            .all(|b| b.is_ascii_alphanumeric() || b == b'-')
401    {
402        return Err(malformed(offset));
403    } else if REGISTERED && descriptor(label).is_none() {
404        return Err(Error::new(ErrorCode::UnsupportedNameSyntax, FIELD, offset));
405    }
406    Ok(())
407}
408
409// RFC 4514 §3. Decode escaped octets only after finding delimiters; an
410// escaped comma is part of one value, not a separator to scan a second time.
411fn scan<const REGISTERED: bool>(
412    text: &str,
413    mut visit: impl FnMut(&str, &str, u64, bool) -> Result<()>,
414) -> Result<()> {
415    if text.is_empty() {
416        return Ok(());
417    }
418    let bytes = text.as_bytes();
419    let mut at = 0;
420    loop {
421        let start = at;
422        while at < bytes.len() && bytes[at] != b'=' {
423            at += 1;
424        }
425        if at == bytes.len() {
426            return Err(malformed(start as u64));
427        }
428        let label = &text[start..at];
429        label_fits::<REGISTERED>(label, start as u64)?;
430        at += 1;
431        let value = at;
432        if bytes.get(at) == Some(&b'#') {
433            at += 1;
434            let hex = at;
435            while at < bytes.len() && !matches!(bytes[at], b',' | b'+') {
436                if digit(bytes[at]).is_none() {
437                    return Err(malformed(at as u64));
438                }
439                at += 1;
440            }
441            if at == hex || !(at - hex).is_multiple_of(2) {
442                return Err(malformed(hex as u64));
443            }
444        } else {
445            let mut last_space = false;
446            while at < bytes.len() && !matches!(bytes[at], b',' | b'+') {
447                let byte = bytes[at];
448                if byte == b'\\' {
449                    at += 1;
450                    let Some(&escaped) = bytes.get(at) else {
451                        return Err(malformed(at as u64));
452                    };
453                    if digit(escaped).is_some() {
454                        at += 1;
455                        if bytes.get(at).copied().and_then(digit).is_none() {
456                            return Err(malformed(at as u64));
457                        }
458                    } else if !matches!(
459                        escaped,
460                        b' ' | b'"' | b'#' | b'+' | b',' | b';' | b'<' | b'=' | b'>' | b'\\'
461                    ) {
462                        return Err(malformed(at as u64));
463                    }
464                    last_space = false;
465                } else {
466                    if matches!(byte, 0 | b'"' | b';' | b'<' | b'>')
467                        || (at == value && matches!(byte, b' ' | b'#'))
468                    {
469                        return Err(malformed(at as u64));
470                    }
471                    last_space = byte == b' ';
472                }
473                at += 1;
474            }
475            if last_space {
476                return Err(malformed((at - 1) as u64));
477            }
478        }
479        let last = bytes.get(at) != Some(&b'+');
480        visit(label, &text[value..at], start as u64, last)?;
481        if at == bytes.len() {
482            return Ok(());
483        }
484        at += 1;
485        if at == bytes.len() {
486            return Err(malformed(at as u64));
487        }
488    }
489}
490
491/// RFC4514 lexical grammar only: unknown descriptors do not require local
492/// equality semantics when preserving an unresolved LDAP location. The caller
493/// admits the scan; no storage, schema lookup or identity assertion is made.
494pub(crate) fn syntax_fits(text: &str) -> bool {
495    scan::<false>(text, |_, _, _, _| Ok(())).is_ok()
496}
497
498/// Shared RFC4512 type/matching-rule token grammar, without schema lookup.
499pub(crate) fn attribute_type_syntax_fits(text: &str) -> bool {
500    label_fits::<false>(text, 0).is_ok()
501}
502
503const DESCRIPTORS: &[(&str, Oid<'static>)] = &[
504    ("CN", oid::COMMON_NAME),
505    ("L", oid::LOCALITY),
506    ("ST", oid::STATE_OR_PROVINCE),
507    ("O", oid::ORGANIZATION),
508    ("OU", oid::ORGANIZATIONAL_UNIT),
509    ("C", oid::COUNTRY),
510    ("STREET", oid::STREET_ADDRESS),
511    ("DC", oid::DOMAIN_COMPONENT),
512    ("UID", oid::USER_ID),
513    ("emailAddress", oid::EMAIL_ADDRESS),
514    ("serialNumber", oid::SERIAL_NUMBER),
515    ("surname", oid::SURNAME),
516    ("givenName", oid::GIVEN_NAME),
517    ("initials", oid::INITIALS),
518    ("generationQualifier", oid::GENERATION_QUALIFIER),
519    ("title", oid::TITLE),
520    ("dnQualifier", oid::DN_QUALIFIER),
521    ("pseudonym", oid::PSEUDONYM),
522    ("organizationIdentifier", oid::ORGANIZATION_IDENTIFIER),
523];
524
525/// RFC4514 §2.3 requires every registered descriptor known here, including
526/// RFC4519 names beyond §3's mandatory nine and emailAddress/pseudonym.
527/// organizationIdentifier is an input alias, absent from this IANA registry.
528/// <https://www.iana.org/assignments/ldap-parameters/ldap-parameters.xhtml>
529pub(super) const REGISTERED_DESCRIPTORS: usize = 18;
530
531pub(super) fn registered_name(kind: Oid<'_>) -> Option<&'static str> {
532    DESCRIPTORS[..REGISTERED_DESCRIPTORS]
533        .iter()
534        .find(|(_, oid)| *oid == kind)
535        .map(|(name, _)| *name)
536}
537
538fn descriptor(text: &str) -> Option<Oid<'static>> {
539    if text.eq_ignore_ascii_case("sn") {
540        return Some(oid::SURNAME);
541    }
542    DESCRIPTORS
543        .iter()
544        .find(|(name, _)| name.eq_ignore_ascii_case(text))
545        .map(|(_, oid)| *oid)
546}