58 lines
3.1 KiB
Markdown
58 lines
3.1 KiB
Markdown
# `asn1/decoder` — TLV decoder for the X.509 ASN.1 subset
|
|
|
|
## Purpose
|
|
|
|
Implements `decodeAsn1(bytes, mode)` covering every ASN.1 production used by RFC 5280 Appendix A: primitive scalars (`INTEGER`, `BIT STRING`, `OCTET STRING`, `OBJECT IDENTIFIER`, `NULL`, `BOOLEAN`), string types, time types, and constructed types (`SEQUENCE`, `SET`, `[n] EXPLICIT/IMPLICIT`). The output is a discriminated-union `Asn1Node` tree the encoder can round-trip byte-identically. Strict DER mode (default) rejects indefinite-length encodings; permissive BER mode accepts them.
|
|
|
|
## Features
|
|
|
|
- Long-form tag-number decoding (X.690 paragraph 8.1.2.4) with overflow guard and minimality check.
|
|
- Definite-length decoding (X.690 paragraph 8.1.3.3-5) with maximum 4-octet length and minimality check in DER mode.
|
|
- Indefinite-length decoding in BER mode (X.690 paragraph 8.1.3.6) terminated by `00 00` end-of-contents marker.
|
|
- Trailing-bytes rejection on the outermost item (both modes).
|
|
- Sub-array views — primitive `content` references a copied `Uint8Array` slice so the caller can store / hash it without re-entering the decoder.
|
|
|
|
## Usage: Basic Example
|
|
|
|
```ts
|
|
import { decodeAsn1 } from "@suaveplan/x509/asn1";
|
|
|
|
const node = decodeAsn1(new Uint8Array([0x30, 0x03, 0x02, 0x01, 0x05]));
|
|
// node.tag.tagNumber === 16 (SEQUENCE)
|
|
// node.children[0].tag.tagNumber === 2 (INTEGER) carrying byte 0x05
|
|
```
|
|
|
|
## Usage: Advanced Example
|
|
|
|
```ts
|
|
import { decodeAsn1, decodeAsn1Stream } from "@suaveplan/x509/asn1";
|
|
|
|
// Permissive BER mode accepts indefinite-length encoding (legacy systems).
|
|
const ber = decodeAsn1(berBytes, "ber");
|
|
|
|
// Decode N consecutive items extracted from an outer SEQUENCE body.
|
|
const seq = decodeAsn1(outerBytes);
|
|
if ("children" in seq) {
|
|
for (const child of seq.children) {
|
|
// ... structural pattern-match ...
|
|
}
|
|
}
|
|
|
|
// Or decode a raw body stream:
|
|
const items = decodeAsn1Stream(rawBody);
|
|
```
|
|
|
|
## API Reference
|
|
|
|
- `decodeAsn1(bytes: Uint8Array, mode?: "der" | "ber"): Asn1Node` — decode one TLV; throws `X509Error(ASN1_MALFORMED)` on any structural failure or trailing bytes.
|
|
- `decodeAsn1Stream(bytes: Uint8Array, mode?): readonly Asn1Node[]` — decode N consecutive TLVs across an entire buffer; the buffer must be fully consumed.
|
|
- `UNIVERSAL_TAGS` — the universal-class tag numbers referenced by callers.
|
|
|
|
## Implementation Notes
|
|
|
|
- The decoder makes no allocations beyond the `Asn1Node` tree and per-node `Uint8Array` copies of primitive content. Constructed bodies are walked through without copying.
|
|
- `tagNumber` overflow is bounded at 24-bit; long-form tag numbers larger than that throw `ASN1_MALFORMED`. X.509 never uses tag numbers anywhere near that magnitude.
|
|
- Length encoding accepts the 4-octet maximum (`>>>` to ~4 GiB), which covers every realistic certificate input on the wire (largest known WebPKI certs are under 64 KiB).
|
|
- BER mode is accepted but the `validateChain` boundary throws on BER inputs by passing `"der"` explicitly; BER support exists so callers parsing legacy data feeds (e.g. older PKCS#7 / CMS) can opt in.
|
|
- `decodeAsn1` rejects an indefinite-length form on a primitive tag — primitive types in DER and BER both use definite-length encoding only.
|