Files
skills/docs/security-stack/crypto/crypto-rsa/src/asn1/reader.md
2026-05-29 08:20:56 +01:00

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-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

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, 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.