ASN.1 and DER
asn1-der is Cruft's DER decoder, the code that reads an X.509 certificate's raw bytes before any signature or name check runs. It rejects non-canonical encodings, parses iteratively so it cannot stack-overflow, and offers a depth-bounded tree walk consumers call to cap nesting.
When Cruft opens an HTTPS connection, the other side hands over an X.509 certificate. Before any signature is checked and before any name is matched, those bytes have to be parsed: a tag, a length, a value, nested thousands of levels deep if the sender feels like it. On npm that job belongs to der or asn1. In Cruft it belongs to a dedicated crate, asn1-der.
That parser is the very first code that touches bytes an attacker fully controls. If it can be made to panic, over-read, or disagree with the parser at the other end of the connection, nothing above it (the signature check, the hostname check) matters, because the attacker chose what it parses.
Alpha:asn1-der(Cruft 0.0.10), replacesder/asn1. DER reader and minimal writer for the X.509 / TLS / PKCS path. Externally unaudited, limited interop testing, not production-ready. Do not rely on it in production or against adversarial input without your own review.
DER is a strict subset that must be rejected outward
DER (Distinguished Encoding Rules, ITU-T X.690) is the canonical subset of BER, and its whole point is that there is exactly one legal byte-encoding for any given value. Two different byte strings must never decode to the same structure.
So the parser's job is not just "read the tree". It is "reject every encoding that is not the canonical one", because the moment two parsers accept slightly different sets of byte strings, an attacker can craft a certificate one accepts and the other rejects. That is the parser-differential attack class, and it is why der on npm is strict where a lazy BER parser would be lenient.
So the real question for Cruft's crate is not "does it read a SEQUENCE". It is: what does it refuse?
What it refuses today
The reader is DerReader. These rejections are backed by a default hostile corpus and run under the crate's tests:
- Non-minimal long-form lengths.
parse_lengthrejects a long-form length with a leading zero byte (0x82 0x00 0xFF), the classic encodingder/OpenSSL reject and a naive parser accepts. - Indefinite and oversized lengths. BER indefinite length (
0x80) and any long form wider than 4 bytes are rejected asInvalidLength. - Invalid or high-tag-number tags.
validate_tagrejects the high-tag-number form (0x1F) and unknown universal tags before any slicing happens. - Constructed encodings of primitive types. A constructed OCTET STRING or BIT STRING (forbidden by DER) returns
NotPrimitive, and a primitive SEQUENCE / SET returnsNotConstructed. - Non-canonical BIT STRING.
as_bit_stringrejects an unused-bit count above 7, and rejects a final byte whose unused low bits are not zero. - Non-minimal INTEGER.
as_integer_bytesrejects leading0x00/0xFFpadding that is not needed for the sign. - OID arc overflow.
as_oidrejects a sub-identifier that would overflowu64rather than wrapping. - Trailing data.
parse_singlerejects any bytes left over after the top value, so a message cannot smuggle a second value past the first.
The reader is zero-copy (typed accessors return slices into the original buffer) and, critically, iterative: read_tlv slices the content and into_reader hands back a fresh reader over that slice. The parser itself cannot stack-overflow on nested input, because it does not recurse to descend.
There is also a minimal writer (the enc_* free functions) that emits definite-length primitives (SEQUENCE, INTEGER, OCTET / BIT STRING, OID, NULL, context tags). It exists to serve crypto.subtle.exportKey('spki' / 'pkcs8'), not to be a general DER encoder. Round-trip through the reader is covered by the crate's tests.
Bounding recursion depth in the consumers
The reader not recursing is real and good. But real consumers (x509, TLS) do recurse: they call into_reader and descend into each nested SEQUENCE / context tag in their own parse loop. A hostile certificate with thousands of nested context tags (A0 03 A0 03 ...) drives the consumer's stack down with no bound. The parser is safe; the consumer built on top of it may not be.
Cruft's answer is validate_der_tree(buf, max_depth): a bounded constructed-tree walk that a consumer can call before recursive descent, so it caps nesting depth and returns MaxDepthExceeded instead of overflowing. The validator is exercised by the nested-depth cases in the hostile corpus. The guard is called on the untrusted path: x509 / TLS certificate parsing runs through a fixed 64-level DER-tree cap at parse_certificate entry (plus an x509-local bounded sub-DER parser for SPKI parameters, extension OCTET payloads, and ECDSA signatures). The depth-DoS is contained because a consumer invokes the bound, not merely because the crate offers one.
What it does not do
- No BER, no indefinite length, no streaming. By design. For lenient BER, this is the wrong parser, and that strictness is a feature here.
- No high-tag-number tags, no long form beyond 4 length-bytes. Fine for any real certificate, but it is a hard limit, not a configurable one.
as_stringhandles only UTF8String / PrintableString / IA5String. TeletexString / T.61, BMPString, and UniversalString are recognized as tags but not decoded to text; the consuming format must handle them.- The writer is minimal. It emits exactly the primitives the key-export path needs. It is not a general-purpose DER encoder and makes no completeness claim.
- No external audit, no interop conformance suite. There is no test against a corpus of real-world certificates from many issuers, and no formal spec-vector suite.
Test coverage
- The canonical-DER rejections listed above, memory-safe slicing on the malformed inputs in the hostile corpus, and the bounded-tree validator's own depth cap run under the crate's tests.
- A differential lane feeds saved accept/reject seeds to both Cruft and OpenSSL
asn1parsewhen it is available. It records that OpenSSL is BER-tolerant for several malformed encodings where Cruft deliberately stays strict. This is a seed-based differential, not a broad-corpus one. - Memory safety across all malformed inputs is not yet covered exhaustively. There is a fuzz target driving
parse_single,read_tlv, andvalidate_der_tree, but it is a scaffold whose crashes and new differentials are meant to be promoted into the default seed tests, not a standing continuous campaign. The coverage is hostile-corpus-strength, stronger than pure inspection, but not fuzz-campaign-strength.
How it compares to der / asn1
der and asn1 on crates.io are years old, fuzzed continuously, run against large real-world certificate corpora, and used by production TLS stacks. Their canonical-encoding enforcement has been stress-tested by adversaries in the wild. Cruft's crate implements the same strictness intent and, on the hostile corpus it ships, matches the important rejections. What it does not have is the mileage: the breadth of malformed inputs a mature crate has already survived. The baseline crate's maturity is the yardstick. Cruft's is a faithful young reimplementation, not a drop-in with the same battle history.
Limitations
This crate is the lowest untrusted-byte boundary in the entire crypto stack, and it is young. The canonical-DER rejections and the hostile corpus are real and running today, which moves the memory-safety picture off pure inspection. But the differential is seed-based, not broad-corpus; the fuzzer is a scaffold, not a standing campaign; and there is no external audit and no real-world interop suite. The mature der / asn1 crates have survived adversaries this one has not yet met. The depth-DoS is contained only because a consumer (x509 / TLS) calls the bounded validator: the crate hands you the guard, and safety depends on it being used. Everything above rests on this parser, so read it before you trust it.