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(text, |_, _, _, _| Ok(()))?;
87        let parser = Parser::ber(execution.clone());
88        let mut parts = execution.vector();
89        let mut current = execution.vector();
90        scan(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        }
258        .encoding(),
259        None => super::Encoding::Other,
260    };
261    let joined = if tag.class() == Class::Universal
262        && tag.is_constructed()
263        && tag.number().is_some_and(super::segmented_universal_type)
264        && tag.number() != Some(universal::BIT_STRING)
265        && super::permits(kind, logical_encoding)
266    {
267        let mut joined = execution.vector();
268        segments::walk(&reader, &element, FIELD, false, Some(&mut joined))?;
269        Some(joined)
270    } else {
271        None
272    };
273    Ok(Value::Ber {
274        tag,
275        contents,
276        joined,
277    })
278}
279
280fn project<'a>(item: &'a Selected, execution: &Memory) -> Result<prepared::Prepared<'a>> {
281    let kind = item.kind.as_oid();
282    let value = match &item.value {
283        Value::Assertion => {
284            execution
285                .charge_work((kind.as_bytes().len() as u64) * 40)
286                .map_err(|reason| Error::resource(reason, FIELD, item.offset))?;
287            let tag = match super::syntax_of(kind) {
288                Some(Syntax::Directory) => universal::UTF8_STRING,
289                Some(Syntax::Printable) => universal::PRINTABLE_STRING,
290                Some(Syntax::Ia5) => universal::IA5_STRING,
291                None => {
292                    return Err(Error::new(
293                        ErrorCode::UnsupportedNameSyntax,
294                        FIELD,
295                        item.offset,
296                    ));
297                }
298            };
299            AttributeValue {
300                tag: Tag::primitive(tag),
301                contents: &item.bytes,
302                der: &[],
303                offset: item.offset,
304            }
305        }
306        Value::Ber {
307            tag,
308            contents,
309            joined,
310        } => AttributeValue {
311            tag: if joined.is_some() {
312                Tag::primitive(tag.number().expect("universal string"))
313            } else {
314                *tag
315            },
316            contents: joined.as_deref().unwrap_or(&item.bytes[contents.clone()]),
317            der: &item.bytes,
318            offset: item.offset,
319        },
320    };
321    let attribute = Attribute { kind, value };
322    if matches!(item.value, Value::Assertion) {
323        let work = (kind.as_bytes().len() as u64)
324            .checked_mul(64)
325            .and_then(|kinds| {
326                (value.contents.len() as u64)
327                    .checked_mul(2)
328                    .and_then(|contents| kinds.checked_add(contents))
329            })
330            .and_then(|work| work.checked_add(1))
331            .ok_or_else(|| {
332                Error::resource(
333                    structured_asn1::memory::MemoryError::WorkExceeded,
334                    FIELD,
335                    item.offset,
336                )
337            })?;
338        execution
339            .charge_work(work)
340            .map_err(|reason| Error::resource(reason, FIELD, item.offset))?;
341        if !super::well_formed(&attribute) {
342            return Err(Error::new(ErrorCode::MalformedValue, FIELD, item.offset));
343        }
344    }
345    let prepared = prepared::Prepared::of(attribute, execution)?;
346    if matches!(item.value, Value::Assertion) && !prepared.is_prepared() {
347        return Err(Error::new(
348            ErrorCode::UnsupportedNameSyntax,
349            FIELD,
350            item.offset,
351        ));
352    }
353    Ok(prepared)
354}
355
356fn malformed(offset: u64) -> Error {
357    Error::new(ErrorCode::MalformedValue, FIELD, offset)
358}
359
360fn digit(byte: u8) -> Option<u8> {
361    match byte {
362        b'0'..=b'9' => Some(byte - b'0'),
363        b'a'..=b'f' => Some(byte - b'a' + 10),
364        b'A'..=b'F' => Some(byte - b'A' + 10),
365        _ => None,
366    }
367}
368
369fn label_fits(label: &str, offset: u64) -> Result<()> {
370    let Some(first) = label.as_bytes().first() else {
371        return Err(malformed(offset));
372    };
373    if first.is_ascii_digit() {
374        let mut count = 0;
375        for component in label.split('.') {
376            if component.is_empty()
377                || !component.bytes().all(|b| b.is_ascii_digit())
378                || (component.len() > 1 && component.starts_with('0'))
379            {
380                return Err(malformed(offset));
381            }
382            count += 1;
383        }
384        if count < 2 {
385            return Err(malformed(offset));
386        }
387    } else if !first.is_ascii_alphabetic()
388        || !label
389            .bytes()
390            .all(|b| b.is_ascii_alphanumeric() || b == b'-')
391    {
392        return Err(malformed(offset));
393    } else if descriptor(label).is_none() {
394        return Err(Error::new(ErrorCode::UnsupportedNameSyntax, FIELD, offset));
395    }
396    Ok(())
397}
398
399// RFC 4514 §3. Decode escaped octets only after finding delimiters; an
400// escaped comma is part of one value, not a separator to scan a second time.
401fn scan(text: &str, mut visit: impl FnMut(&str, &str, u64, bool) -> Result<()>) -> Result<()> {
402    if text.is_empty() {
403        return Ok(());
404    }
405    let bytes = text.as_bytes();
406    let mut at = 0;
407    loop {
408        let start = at;
409        while at < bytes.len() && bytes[at] != b'=' {
410            at += 1;
411        }
412        if at == bytes.len() {
413            return Err(malformed(start as u64));
414        }
415        let label = &text[start..at];
416        label_fits(label, start as u64)?;
417        at += 1;
418        let value = at;
419        if bytes.get(at) == Some(&b'#') {
420            at += 1;
421            let hex = at;
422            while at < bytes.len() && !matches!(bytes[at], b',' | b'+') {
423                if digit(bytes[at]).is_none() {
424                    return Err(malformed(at as u64));
425                }
426                at += 1;
427            }
428            if at == hex || !(at - hex).is_multiple_of(2) {
429                return Err(malformed(hex as u64));
430            }
431        } else {
432            let mut last_space = false;
433            while at < bytes.len() && !matches!(bytes[at], b',' | b'+') {
434                let byte = bytes[at];
435                if byte == b'\\' {
436                    at += 1;
437                    let Some(&escaped) = bytes.get(at) else {
438                        return Err(malformed(at as u64));
439                    };
440                    if digit(escaped).is_some() {
441                        at += 1;
442                        if bytes.get(at).copied().and_then(digit).is_none() {
443                            return Err(malformed(at as u64));
444                        }
445                    } else if !matches!(
446                        escaped,
447                        b' ' | b'"' | b'#' | b'+' | b',' | b';' | b'<' | b'=' | b'>' | b'\\'
448                    ) {
449                        return Err(malformed(at as u64));
450                    }
451                    last_space = false;
452                } else {
453                    if matches!(byte, 0 | b'"' | b';' | b'<' | b'>')
454                        || (at == value && matches!(byte, b' ' | b'#'))
455                    {
456                        return Err(malformed(at as u64));
457                    }
458                    last_space = byte == b' ';
459                }
460                at += 1;
461            }
462            if last_space {
463                return Err(malformed((at - 1) as u64));
464            }
465        }
466        let last = bytes.get(at) != Some(&b'+');
467        visit(label, &text[value..at], start as u64, last)?;
468        if at == bytes.len() {
469            return Ok(());
470        }
471        at += 1;
472        if at == bytes.len() {
473            return Err(malformed(at as u64));
474        }
475    }
476}
477
478const DESCRIPTORS: &[(&str, Oid<'static>)] = &[
479    ("CN", oid::COMMON_NAME),
480    ("L", oid::LOCALITY),
481    ("ST", oid::STATE_OR_PROVINCE),
482    ("O", oid::ORGANIZATION),
483    ("OU", oid::ORGANIZATIONAL_UNIT),
484    ("C", oid::COUNTRY),
485    ("STREET", oid::STREET_ADDRESS),
486    ("DC", oid::DOMAIN_COMPONENT),
487    ("UID", oid::USER_ID),
488    ("emailAddress", oid::EMAIL_ADDRESS),
489    ("serialNumber", oid::SERIAL_NUMBER),
490    ("surname", oid::SURNAME),
491    ("givenName", oid::GIVEN_NAME),
492    ("initials", oid::INITIALS),
493    ("generationQualifier", oid::GENERATION_QUALIFIER),
494    ("title", oid::TITLE),
495    ("dnQualifier", oid::DN_QUALIFIER),
496    ("pseudonym", oid::PSEUDONYM),
497    ("organizationIdentifier", oid::ORGANIZATION_IDENTIFIER),
498];
499
500/// RFC4514 §2.3 requires every registered descriptor known here, including
501/// RFC4519 names beyond §3's mandatory nine and emailAddress/pseudonym.
502/// organizationIdentifier is an input alias, absent from this IANA registry.
503/// <https://www.iana.org/assignments/ldap-parameters/ldap-parameters.xhtml>
504pub(super) const REGISTERED_DESCRIPTORS: usize = 18;
505
506pub(super) fn registered_name(kind: Oid<'_>) -> Option<&'static str> {
507    DESCRIPTORS[..REGISTERED_DESCRIPTORS]
508        .iter()
509        .find(|(_, oid)| *oid == kind)
510        .map(|(name, _)| *name)
511}
512
513fn descriptor(text: &str) -> Option<Oid<'static>> {
514    if text.eq_ignore_ascii_case("sn") {
515        return Some(oid::SURNAME);
516    }
517    DESCRIPTORS
518        .iter()
519        .find(|(name, _)| name.eq_ignore_ascii_case(text))
520        .map(|(_, oid)| *oid)
521}