3.9 KiB
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-
0x00only when MSB requires it; negative encodings rejected (RSA INTEGERs are non-negative). BIT STRINGreader copies the body so callers cannot mutate the input buffer.- Sub-readers returned by
readSequence/readContextSpecificare scoped to the child body for ergonomic recursive descent.
Usage: Basic Example
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
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, orundefinedat EOF.readInteger(): bigint— consumes a DERINTEGER; throwsINVALID_KEYon bad encoding.readSequence(): DerReader— body sub-reader for the nextSEQUENCE.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 DERNULL; throws on non-empty body.readContextSpecific(tagNumber: 0..30): DerReader— body sub-reader for[n].assertEnd(): void— throwsINVALID_KEYif 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.