# `asn1/reader` ## Purpose Strict-DER reader scoped to the RSA key shapes defined in RFC 8017 Appendix A (PKCS#1), RFC 5280 §4.1 + Appendix A (`SubjectPublicKeyInfo`), and RFC 5208 / RFC 5958 (PKCS#8 `PrivateKeyInfo` / `OneAsymmetricKey`). The reader rejects every BER construction that DER forbids — indefinite length, non-minimal length, non-minimal integer encoding, non-zero unused-bits in `BIT STRING`, trailing data — by raising `CryptoRsaError(CRYPTO_RSA.INVALID_KEY)`. The implementation is intentionally minimal. It implements exactly the subset needed to decode RSA keys; it is not a general-purpose ASN.1 library. ## Features - Stateful cursor with `position`, `remaining`, `hasMore`, `peekTag`, `assertEnd`. - `readInteger`, `readSequence`, `readObjectIdentifier`, `readBitString`, `readOctetString`, `readNull`, `readContextSpecific(tagNumber)`. - Strict DER length parsing — non-minimal long-form lengths are rejected. - Strict DER integer parsing — leading-`0x00` only when MSB requires it; negative encodings rejected (RSA INTEGERs are non-negative). - `BIT STRING` reader copies the body so callers cannot mutate the input buffer. - Sub-readers returned by `readSequence` / `readContextSpecific` are scoped to the child body for ergonomic recursive descent. ## Usage: Basic Example ```ts import { DerReader } from "@suaveplan/crypto-rsa/asn1"; const reader = new DerReader(pkcs1PublicKeyDer); const seq = reader.readSequence(); const modulus = seq.readInteger(); const exponent = seq.readInteger(); seq.assertEnd(); reader.assertEnd(); ``` ## Usage: Advanced Example ```ts import { DerReader, TAG_INTEGER } from "@suaveplan/crypto-rsa/asn1"; const reader = new DerReader(pkcs8Bytes); const outer = reader.readSequence(); // version is OPTIONAL DEFAULT 0 in some PKCS#8 dialects const version = outer.peekTag() === TAG_INTEGER ? outer.readInteger() : 0n; const algSeq = outer.readSequence(); const oid = algSeq.readObjectIdentifier(); algSeq.readNull(); const privateKeyOctet = outer.readOctetString(); const inner = new DerReader(privateKeyOctet).readSequence(); // … recurse into RSAPrivateKey body. ``` ## API Reference - `class DerReader { constructor(bytes: Uint8Array, offset?: number) }` - `position: number` — current cursor offset (read-only). - `remaining: number` — bytes remaining in the buffer. - `hasMore(): boolean` — true if the cursor has not reached the end. - `peekTag(): number | undefined` — tag byte at the cursor, or `undefined` at EOF. - `readInteger(): bigint` — consumes a DER `INTEGER`; throws `INVALID_KEY` on bad encoding. - `readSequence(): DerReader` — body sub-reader for the next `SEQUENCE`. - `readObjectIdentifier(): string` — dotted-decimal OID (e.g. `"1.2.840.113549.1.1.1"`). - `readBitString(): Uint8Array` — payload bytes; throws when unused-bits ≠ 0. - `readOctetString(): Uint8Array` — payload bytes (defensive copy). - `readNull(): void` — consumes a DER `NULL`; throws on non-empty body. - `readContextSpecific(tagNumber: 0..30): DerReader` — body sub-reader for `[n]`. - `assertEnd(): void` — throws `INVALID_KEY` if any unread bytes remain. All failure modes throw `CryptoRsaError` with `code === CRYPTO_RSA.INVALID_KEY` and a structured `context` payload (e.g. `{ expected, actual }`, `{ unusedBits }`). ## Implementation Notes The reader rejects BER-only constructs even when they are syntactically unambiguous because RSA wire formats are defined in DER and any tooling that emits BER is non-conformant. This matters for security: silently accepting non-canonical encodings is a known fingerprinting and signature-malleability vector. Sub-readers returned by `readSequence` are constructed over `Uint8Array.subarray` (a view), not a copy, so the parser remains zero-allocation on the hot path. The single defensive copy is in `readBitString` because callers receive the payload directly and the SPKI bit-string carries the inner PKCS#1 body which is then re-parsed.