first commit
This commit is contained in:
88
docs/security-stack/crypto/crypto-rsa/src/asn1/reader.md
Normal file
88
docs/security-stack/crypto/crypto-rsa/src/asn1/reader.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# `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.
|
||||
Reference in New Issue
Block a user