Skip to main content

uor_addr/asn1/
value.rs

1//! ASN.1 DER typed input (ADR-023 amended by ADR-060).
2//!
3//! DER is the canonical form by construction (ITU-T X.690 §10): a
4//! well-formed DER byte sequence is its own canonical representative, so
5//! the ψ₉ canonicalizer is the **identity** on it. The realization
6//! therefore validates the input is valid DER at the host boundary and
7//! flows the **input bytes themselves** through the pipeline as a
8//! zero-copy [`TermValue::Borrowed`] carrier — no transformation, no
9//! buffer, no width / element-count ceiling. The only retained bound is
10//! [`MAX_ASN1_DEPTH`], a native-stack-overflow guard on the recursive
11//! TLV validator.
12//!
13//! [`Asn1Value`] (the owned DER **builder**, `alloc`-gated) constructs
14//! canonical DER programmatically (`boolean`, `integer`, `sequence`,
15//! `set`, …) for reference and testing; [`Asn1Carrier`] is the borrowed
16//! model-input handle the pipeline binds.
17//!
18//! # Supported universal-tag cases
19//!
20//! `Boolean`, `Integer`, `BitString`, `OctetString`, `Null`,
21//! `ObjectIdentifier`, `Utf8String`, `PrintableString`, `IA5String`,
22//! `UTCTime`, `GeneralizedTime`, `Sequence`, `Set`.
23
24use prism::operation::TermValue;
25use prism::pipeline::{
26    ConstrainedTypeShape, ConstraintRef, IntoBindingValue, PartitionProductFields, ShapeViolation,
27    ViolationKind,
28};
29
30use crate::asn1::shapes::bounds::MAX_ASN1_DEPTH;
31
32// ─── DER tag bytes ──────────────────────────────────────────────────────
33
34pub(crate) const TAG_BOOLEAN: u8 = 0x01;
35pub(crate) const TAG_INTEGER: u8 = 0x02;
36pub(crate) const TAG_BIT_STRING: u8 = 0x03;
37pub(crate) const TAG_OCTET_STRING: u8 = 0x04;
38pub(crate) const TAG_NULL: u8 = 0x05;
39pub(crate) const TAG_OID: u8 = 0x06;
40pub(crate) const TAG_UTF8_STRING: u8 = 0x0C;
41pub(crate) const TAG_PRINTABLE_STRING: u8 = 0x13;
42pub(crate) const TAG_IA5_STRING: u8 = 0x16;
43pub(crate) const TAG_UTC_TIME: u8 = 0x17;
44pub(crate) const TAG_GENERALIZED_TIME: u8 = 0x18;
45pub(crate) const TAG_SEQUENCE: u8 = 0x30;
46pub(crate) const TAG_SET: u8 = 0x31;
47
48// ─── ShapeViolation IRIs ────────────────────────────────────────────────
49
50const INVALID_DER_VIOLATION: ShapeViolation = ShapeViolation {
51    shape_iri: "https://uor.foundation/addr/Asn1Value",
52    constraint_iri: "https://uor.foundation/addr/Asn1Value/validDer",
53    property_iri: "https://uor.foundation/addr/inputBytes",
54    expected_range: "https://uor.foundation/addr/ValidDerBytes",
55    min_count: 0,
56    max_count: 1,
57    kind: ViolationKind::ValueCheck,
58};
59
60const DEPTH_BOUND_VIOLATION: ShapeViolation = ShapeViolation {
61    shape_iri: "https://uor.foundation/addr/Asn1Value",
62    constraint_iri: "https://uor.foundation/addr/Asn1Value/depthBound",
63    property_iri: "https://uor.foundation/addr/Asn1Value/depth",
64    expected_range: "http://www.w3.org/2001/XMLSchema#nonNegativeInteger",
65    min_count: 0,
66    max_count: MAX_ASN1_DEPTH as u32,
67    kind: ViolationKind::CardinalityViolation,
68};
69
70// ─── DER validation (no_alloc) ──────────────────────────────────────────
71
72/// Validate that `raw` is a single well-formed DER value per X.690 §§ 8 /
73/// 10 / 11 (not merely valid BER — long-form lengths below the short-form
74/// threshold and indefinite lengths are rejected).
75///
76/// # Errors
77///
78/// - [`INVALID_DER_VIOLATION`] (`validDer`) — malformed or non-canonical
79///   DER, or trailing bytes after the top-level value.
80/// - [`DEPTH_BOUND_VIOLATION`] (`depthBound`) — nesting exceeds the
81///   [`MAX_ASN1_DEPTH`] native-stack-safety bound.
82pub fn validate_der(raw: &[u8]) -> Result<(), ShapeViolation> {
83    let mut pos = 0;
84    validate_tlv(raw, &mut pos, 0)?;
85    if pos != raw.len() {
86        return Err(INVALID_DER_VIOLATION);
87    }
88    Ok(())
89}
90
91fn validate_tlv(buf: &[u8], pos: &mut usize, depth: usize) -> Result<(), ShapeViolation> {
92    if depth > MAX_ASN1_DEPTH {
93        return Err(DEPTH_BOUND_VIOLATION);
94    }
95    if *pos >= buf.len() {
96        return Err(INVALID_DER_VIOLATION);
97    }
98    let tag = buf[*pos];
99    *pos += 1;
100    let content_len = decode_length(buf, pos)?;
101    if *pos + content_len > buf.len() {
102        return Err(INVALID_DER_VIOLATION);
103    }
104    let content_end = *pos + content_len;
105    match tag {
106        TAG_BOOLEAN => {
107            if content_len != 1 {
108                return Err(INVALID_DER_VIOLATION);
109            }
110            let b = buf[*pos];
111            if b != 0x00 && b != 0xFF {
112                return Err(INVALID_DER_VIOLATION);
113            }
114            *pos += 1;
115        }
116        TAG_INTEGER => {
117            if content_len == 0 {
118                return Err(INVALID_DER_VIOLATION);
119            }
120            if content_len >= 2 {
121                let b0 = buf[*pos];
122                let b1 = buf[*pos + 1];
123                if b0 == 0x00 && (b1 & 0x80) == 0 {
124                    return Err(INVALID_DER_VIOLATION);
125                }
126                if b0 == 0xFF && (b1 & 0x80) != 0 {
127                    return Err(INVALID_DER_VIOLATION);
128                }
129            }
130            *pos = content_end;
131        }
132        TAG_OCTET_STRING => {
133            *pos = content_end;
134        }
135        TAG_NULL => {
136            if content_len != 0 {
137                return Err(INVALID_DER_VIOLATION);
138            }
139        }
140        TAG_BIT_STRING => {
141            if content_len == 0 {
142                return Err(INVALID_DER_VIOLATION);
143            }
144            let unused = buf[*pos];
145            if unused > 7 {
146                return Err(INVALID_DER_VIOLATION);
147            }
148            if content_len == 1 && unused != 0 {
149                return Err(INVALID_DER_VIOLATION);
150            }
151            if content_len > 1 && unused > 0 {
152                let last = buf[content_end - 1];
153                let mask = (1u8 << unused) - 1;
154                if last & mask != 0 {
155                    return Err(INVALID_DER_VIOLATION);
156                }
157            }
158            *pos = content_end;
159        }
160        TAG_OID => {
161            if content_len == 0 {
162                return Err(INVALID_DER_VIOLATION);
163            }
164            let mut p = *pos;
165            while p < content_end {
166                let sub_start = p;
167                while p < content_end && buf[p] & 0x80 != 0 {
168                    p += 1;
169                }
170                if p >= content_end {
171                    return Err(INVALID_DER_VIOLATION);
172                }
173                p += 1;
174                if p - sub_start > 1 && buf[sub_start] == 0x80 {
175                    return Err(INVALID_DER_VIOLATION);
176                }
177            }
178            if p != content_end {
179                return Err(INVALID_DER_VIOLATION);
180            }
181            *pos = content_end;
182        }
183        TAG_UTF8_STRING => {
184            let bytes = &buf[*pos..content_end];
185            core::str::from_utf8(bytes).map_err(|_| INVALID_DER_VIOLATION)?;
186            *pos = content_end;
187        }
188        TAG_PRINTABLE_STRING => {
189            for &b in &buf[*pos..content_end] {
190                let ok = b.is_ascii_alphanumeric()
191                    || matches!(
192                        b,
193                        b' ' | b'\''
194                            | b'('
195                            | b')'
196                            | b'+'
197                            | b','
198                            | b'-'
199                            | b'.'
200                            | b'/'
201                            | b':'
202                            | b'='
203                            | b'?'
204                    );
205                if !ok {
206                    return Err(INVALID_DER_VIOLATION);
207                }
208            }
209            *pos = content_end;
210        }
211        TAG_IA5_STRING => {
212            for &b in &buf[*pos..content_end] {
213                if b > 127 {
214                    return Err(INVALID_DER_VIOLATION);
215                }
216            }
217            *pos = content_end;
218        }
219        TAG_UTC_TIME | TAG_GENERALIZED_TIME => {
220            for &b in &buf[*pos..content_end] {
221                if !b.is_ascii() {
222                    return Err(INVALID_DER_VIOLATION);
223                }
224            }
225            *pos = content_end;
226        }
227        TAG_SEQUENCE | TAG_SET => {
228            while *pos < content_end {
229                validate_tlv(buf, pos, depth + 1)?;
230            }
231            if *pos != content_end {
232                return Err(INVALID_DER_VIOLATION);
233            }
234        }
235        _ => return Err(INVALID_DER_VIOLATION),
236    }
237    Ok(())
238}
239
240fn decode_length(buf: &[u8], pos: &mut usize) -> Result<usize, ShapeViolation> {
241    if *pos >= buf.len() {
242        return Err(INVALID_DER_VIOLATION);
243    }
244    let first = buf[*pos];
245    *pos += 1;
246    if first < 0x80 {
247        Ok(first as usize)
248    } else {
249        let nbytes = (first & 0x7F) as usize;
250        if nbytes == 0 {
251            return Err(INVALID_DER_VIOLATION);
252        }
253        if nbytes > core::mem::size_of::<usize>() || *pos + nbytes > buf.len() {
254            return Err(INVALID_DER_VIOLATION);
255        }
256        let mut len: usize = 0;
257        for _ in 0..nbytes {
258            len = (len << 8) | (buf[*pos] as usize);
259            *pos += 1;
260        }
261        if len < 128 {
262            return Err(INVALID_DER_VIOLATION);
263        }
264        Ok(len)
265    }
266}
267
268// ─── Asn1Carrier — the borrowed model-input handle (no_alloc) ───────────
269
270/// Borrowed validated-DER input handle (ADR-060 borrowed carrier). DER is
271/// canonical, so the handle borrows the input bytes directly and
272/// `as_binding_value` returns them as a zero-copy `Borrowed` carrier.
273#[derive(Clone, Copy, Debug)]
274pub struct Asn1Carrier<'a>(&'a [u8]);
275
276impl<'a> Asn1Carrier<'a> {
277    /// Wrap a validated DER byte slice as a model input handle. Call
278    /// [`validate_der`] first.
279    #[must_use]
280    pub fn new(der: &'a [u8]) -> Self {
281        Self(der)
282    }
283
284    /// Borrow the canonical (DER) bytes.
285    #[must_use]
286    pub fn canonical_bytes(&self) -> &'a [u8] {
287        self.0
288    }
289}
290
291impl ConstrainedTypeShape for Asn1Carrier<'_> {
292    const IRI: &'static str = "https://uor.foundation/addr/Asn1Value";
293    const SITE_COUNT: usize = 1;
294    const CONSTRAINTS: &'static [ConstraintRef] = &[];
295    const CYCLE_SIZE: u64 = u64::MAX;
296}
297
298impl prism::uor_foundation::pipeline::__sdk_seal::Sealed for Asn1Carrier<'_> {}
299
300impl<'a> IntoBindingValue<'a> for Asn1Carrier<'a> {
301    fn as_binding_value<const INLINE_BYTES: usize>(&self) -> TermValue<'a, INLINE_BYTES> {
302        // DER is canonical (X.690 §10); ψ₉ folds the input bytes directly.
303        TermValue::borrowed(self.0)
304    }
305}
306
307impl PartitionProductFields for Asn1Carrier<'_> {
308    const FIELDS: &'static [(u32, u32)] = &[];
309    const FIELD_NAMES: &'static [&'static str] = &[];
310}
311
312// ─── Asn1Value — the owned DER builder (alloc) ──────────────────────────
313
314/// Owned DER value + builder. Constructs canonical X.690 DER
315/// programmatically for reference and testing. **`alloc`-gated** — the
316/// pipeline binds the borrowed [`Asn1Carrier`] handle, which needs no
317/// allocator. There is no width or element-count ceiling.
318#[cfg(feature = "alloc")]
319#[derive(Clone, PartialEq, Eq)]
320pub struct Asn1Value {
321    bytes: alloc::vec::Vec<u8>,
322}
323
324#[cfg(feature = "alloc")]
325impl core::fmt::Debug for Asn1Value {
326    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
327        f.debug_struct("Asn1Value")
328            .field("len", &self.bytes.len())
329            .finish_non_exhaustive()
330    }
331}
332
333#[cfg(feature = "alloc")]
334impl Asn1Value {
335    fn from_vec(bytes: alloc::vec::Vec<u8>) -> Self {
336        Self { bytes }
337    }
338
339    /// Validate a DER byte sequence and retain an owned copy.
340    ///
341    /// # Errors
342    ///
343    /// Surfaces the [`ShapeViolation`] [`validate_der`] would raise.
344    pub fn parse(raw: &[u8]) -> Result<Self, ShapeViolation> {
345        validate_der(raw)?;
346        Ok(Self::from_vec(raw.to_vec()))
347    }
348
349    /// Build a Boolean (DER tag `0x01`).
350    #[must_use]
351    pub fn boolean(value: bool) -> Self {
352        Self::from_vec(alloc::vec![TAG_BOOLEAN, 1, if value { 0xFF } else { 0x00 }])
353    }
354
355    /// Build a Null (DER tag `0x05`).
356    #[must_use]
357    pub fn null() -> Self {
358        Self::from_vec(alloc::vec![TAG_NULL, 0])
359    }
360
361    /// Build an Integer (DER tag `0x02`) from a signed 64-bit value.
362    /// DER §8.3: minimum-octets two's-complement big-endian.
363    #[must_use]
364    pub fn integer(value: i64) -> Self {
365        let be = value.to_be_bytes();
366        let mut start = 0;
367        if value >= 0 {
368            while start < 7 && be[start] == 0x00 && (be[start + 1] & 0x80) == 0 {
369                start += 1;
370            }
371        } else {
372            while start < 7 && be[start] == 0xFF && (be[start + 1] & 0x80) != 0 {
373                start += 1;
374            }
375        }
376        Self::primitive_vec(TAG_INTEGER, &be[start..])
377    }
378
379    /// Build an OctetString (DER tag `0x04`) from raw bytes.
380    #[must_use]
381    pub fn octet_string(bytes: &[u8]) -> Self {
382        Self::primitive_vec(TAG_OCTET_STRING, bytes)
383    }
384
385    fn primitive_vec(tag: u8, content: &[u8]) -> Self {
386        let mut out = alloc::vec::Vec::new();
387        out.push(tag);
388        push_length(&mut out, content.len());
389        out.extend_from_slice(content);
390        Self::from_vec(out)
391    }
392
393    /// Build a Sequence (DER tag `0x30`).
394    #[must_use]
395    pub fn sequence(children: &[Asn1Value]) -> Self {
396        Self::constructed(TAG_SEQUENCE, children, false)
397    }
398
399    /// Build a Set (DER tag `0x31`). DER (X.690 §11.6) requires Set
400    /// element ordering by ascending encoded-element byte sequence.
401    #[must_use]
402    pub fn set(children: &[Asn1Value]) -> Self {
403        Self::constructed(TAG_SET, children, true)
404    }
405
406    fn constructed(tag: u8, children: &[Asn1Value], sort: bool) -> Self {
407        let mut kids: alloc::vec::Vec<&[u8]> = children.iter().map(|c| c.tagged_bytes()).collect();
408        if sort {
409            kids.sort_unstable();
410        }
411        let total: usize = kids.iter().map(|k| k.len()).sum();
412        let mut out = alloc::vec::Vec::new();
413        out.push(tag);
414        push_length(&mut out, total);
415        for k in kids {
416            out.extend_from_slice(k);
417        }
418        Self::from_vec(out)
419    }
420
421    /// Build a BIT STRING (DER tag `0x03`). X.690 §8.6 / §11.2.
422    ///
423    /// # Errors
424    ///
425    /// [`INVALID_DER_VIOLATION`] for invalid unused-bit counts or
426    /// non-zero trailing bits.
427    pub fn bit_string(bits: &[u8], unused_bits: u8) -> Result<Self, ShapeViolation> {
428        if unused_bits > 7 {
429            return Err(INVALID_DER_VIOLATION);
430        }
431        if bits.is_empty() && unused_bits != 0 {
432            return Err(INVALID_DER_VIOLATION);
433        }
434        if !bits.is_empty() && unused_bits > 0 {
435            let last = bits[bits.len() - 1];
436            let mask = (1u8 << unused_bits) - 1;
437            if last & mask != 0 {
438                return Err(INVALID_DER_VIOLATION);
439            }
440        }
441        let mut content = alloc::vec::Vec::with_capacity(1 + bits.len());
442        content.push(unused_bits);
443        content.extend_from_slice(bits);
444        Ok(Self::primitive_vec(TAG_BIT_STRING, &content))
445    }
446
447    /// Build an OBJECT IDENTIFIER (DER tag `0x06`). X.690 §8.19.
448    ///
449    /// # Errors
450    ///
451    /// [`INVALID_DER_VIOLATION`] for fewer than two arcs or out-of-range
452    /// leading arcs.
453    pub fn object_identifier(arcs: &[u32]) -> Result<Self, ShapeViolation> {
454        if arcs.len() < 2 {
455            return Err(INVALID_DER_VIOLATION);
456        }
457        let x1 = arcs[0];
458        let x2 = arcs[1];
459        if x1 > 2 {
460            return Err(INVALID_DER_VIOLATION);
461        }
462        if x1 < 2 && x2 >= 40 {
463            return Err(INVALID_DER_VIOLATION);
464        }
465        let mut content = alloc::vec::Vec::new();
466        encode_oid_subid(40 * x1 + x2, &mut content);
467        for &arc in &arcs[2..] {
468            encode_oid_subid(arc, &mut content);
469        }
470        Ok(Self::primitive_vec(TAG_OID, &content))
471    }
472
473    /// Build a UTF8String (DER tag `0x0C`).
474    #[must_use]
475    pub fn utf8_string(s: &str) -> Self {
476        Self::primitive_vec(TAG_UTF8_STRING, s.as_bytes())
477    }
478
479    /// Build a PrintableString (DER tag `0x13`). X.680 §41.4 character set.
480    ///
481    /// # Errors
482    ///
483    /// [`INVALID_DER_VIOLATION`] for characters outside the
484    /// PrintableString set.
485    pub fn printable_string(s: &str) -> Result<Self, ShapeViolation> {
486        for c in s.chars() {
487            let ok = c.is_ascii_alphanumeric()
488                || matches!(
489                    c,
490                    ' ' | '\'' | '(' | ')' | '+' | ',' | '-' | '.' | '/' | ':' | '=' | '?'
491                );
492            if !ok {
493                return Err(INVALID_DER_VIOLATION);
494            }
495        }
496        Ok(Self::primitive_vec(TAG_PRINTABLE_STRING, s.as_bytes()))
497    }
498
499    /// Build an IA5String (DER tag `0x16`). X.680 §41.2.
500    ///
501    /// # Errors
502    ///
503    /// [`INVALID_DER_VIOLATION`] for non-ASCII input.
504    pub fn ia5_string(s: &str) -> Result<Self, ShapeViolation> {
505        if !s.is_ascii() {
506            return Err(INVALID_DER_VIOLATION);
507        }
508        Ok(Self::primitive_vec(TAG_IA5_STRING, s.as_bytes()))
509    }
510
511    /// Borrow the DER-encoded canonical bytes.
512    #[must_use]
513    pub fn tagged_bytes(&self) -> &[u8] {
514        &self.bytes
515    }
516}
517
518/// X.690 §8.19.2 — base-128 encoding of an OID sub-identifier into `out`.
519#[cfg(feature = "alloc")]
520fn encode_oid_subid(mut value: u32, out: &mut alloc::vec::Vec<u8>) {
521    if value == 0 {
522        out.push(0);
523        return;
524    }
525    let mut buf = [0u8; 5];
526    let mut i = 0;
527    while value > 0 {
528        buf[i] = (value & 0x7F) as u8;
529        value >>= 7;
530        i += 1;
531    }
532    for j in (1..i).rev() {
533        out.push(buf[j] | 0x80);
534    }
535    out.push(buf[0]);
536}
537
538/// X.690 §8.1.3 length octets appended to `out`.
539#[cfg(feature = "alloc")]
540fn push_length(out: &mut alloc::vec::Vec<u8>, len: usize) {
541    if len < 128 {
542        out.push(len as u8);
543        return;
544    }
545    let mut value = len;
546    let mut bytes = [0u8; 8];
547    let mut count = 0;
548    while value > 0 {
549        bytes[count] = (value & 0xFF) as u8;
550        value >>= 8;
551        count += 1;
552    }
553    out.push(0x80 | (count as u8));
554    for i in 0..count {
555        out.push(bytes[count - 1 - i]);
556    }
557}
558
559/// Canonical-bytes accessor — DER is the canonical form per X.690 §10, so
560/// canonicalization is the identity on validated input.
561///
562/// **Available only under the `alloc` feature.**
563///
564/// # Errors
565///
566/// Surfaces the [`ShapeViolation`] [`validate_der`] would raise.
567#[cfg(feature = "alloc")]
568pub fn canonicalize(raw: &[u8]) -> Result<alloc::vec::Vec<u8>, ShapeViolation> {
569    validate_der(raw)?;
570    Ok(raw.to_vec())
571}
572
573#[cfg(all(test, feature = "alloc"))]
574mod tests {
575    use super::*;
576
577    #[test]
578    fn boolean_der_encoding_matches_x690_8_2_2() {
579        assert_eq!(Asn1Value::boolean(true).tagged_bytes(), &[0x01, 0x01, 0xFF]);
580        assert_eq!(
581            Asn1Value::boolean(false).tagged_bytes(),
582            &[0x01, 0x01, 0x00]
583        );
584    }
585
586    #[test]
587    fn null_der_encoding_matches_x690_8_8() {
588        assert_eq!(Asn1Value::null().tagged_bytes(), &[0x05, 0x00]);
589    }
590
591    #[test]
592    fn integer_der_encoding_minimum_octets() {
593        assert_eq!(Asn1Value::integer(0).tagged_bytes(), &[0x02, 0x01, 0x00]);
594        assert_eq!(Asn1Value::integer(127).tagged_bytes(), &[0x02, 0x01, 0x7F]);
595        assert_eq!(
596            Asn1Value::integer(128).tagged_bytes(),
597            &[0x02, 0x02, 0x00, 0x80]
598        );
599        assert_eq!(Asn1Value::integer(-1).tagged_bytes(), &[0x02, 0x01, 0xFF]);
600        assert_eq!(Asn1Value::integer(-128).tagged_bytes(), &[0x02, 0x01, 0x80]);
601    }
602
603    #[test]
604    fn set_sorts_children_by_encoding() {
605        let sorted = Asn1Value::set(&[Asn1Value::integer(2), Asn1Value::integer(1)]);
606        let direct = Asn1Value::set(&[Asn1Value::integer(1), Asn1Value::integer(2)]);
607        assert_eq!(sorted.tagged_bytes(), direct.tagged_bytes());
608    }
609
610    #[test]
611    fn parse_round_trips_well_formed_der() {
612        let cases: &[Asn1Value] = &[
613            Asn1Value::boolean(true),
614            Asn1Value::null(),
615            Asn1Value::integer(42),
616            Asn1Value::octet_string(b"hello"),
617            Asn1Value::sequence(&[Asn1Value::integer(1), Asn1Value::boolean(true)]),
618        ];
619        for v in cases {
620            let parsed = Asn1Value::parse(v.tagged_bytes()).expect("valid DER");
621            assert_eq!(parsed.tagged_bytes(), v.tagged_bytes());
622        }
623    }
624
625    #[test]
626    fn rejects_non_canonical_boolean_byte() {
627        let err = validate_der(&[0x01, 0x01, 0x01]).expect_err("rejects non-canonical");
628        assert_eq!(err.constraint_iri, INVALID_DER_VIOLATION.constraint_iri);
629    }
630
631    #[test]
632    fn rejects_non_minimum_integer_encoding() {
633        let err = validate_der(&[0x02, 0x02, 0x00, 0x01]).expect_err("non-minimal");
634        assert_eq!(err.constraint_iri, INVALID_DER_VIOLATION.constraint_iri);
635    }
636
637    #[test]
638    fn rejects_long_form_length_under_128() {
639        let err = validate_der(&[0x04, 0x81, 0x05, 0, 0, 0, 0, 0]).expect_err("non-canonical");
640        assert_eq!(err.constraint_iri, INVALID_DER_VIOLATION.constraint_iri);
641    }
642
643    #[test]
644    fn rejects_indefinite_length() {
645        let err = validate_der(&[0x30, 0x80]).expect_err("BER not DER");
646        assert_eq!(err.constraint_iri, INVALID_DER_VIOLATION.constraint_iri);
647    }
648}