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.
|
||||
87
docs/security-stack/crypto/crypto-rsa/src/asn1/writer.md
Normal file
87
docs/security-stack/crypto/crypto-rsa/src/asn1/writer.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# `asn1/writer`
|
||||
|
||||
## Purpose
|
||||
|
||||
Strict-DER encoder mirroring `asn1/reader`. Encodes the same minimal subset of
|
||||
ASN.1 needed to serialise RSA keys in PKCS#1, SPKI, and PKCS#8 wire formats with
|
||||
byte-identical canonical form, so `parse(serialise(key))` round-trips bit-for-bit.
|
||||
|
||||
The writer never emits BER constructions even when the underlying X.690 grammar
|
||||
permits them; minimal definite-length encoding, minimal integer encoding, and
|
||||
zero-unused-bit `BIT STRING` are the only forms produced.
|
||||
|
||||
## Features
|
||||
|
||||
- `encodeLength` — minimal definite-length form per X.690 §8.1.3.
|
||||
- `encodeInteger(bigint)` — non-negative `bigint` with leading-`0x00` insertion
|
||||
when the high bit is set (so positive interpretation is unambiguous).
|
||||
- `encodeSequence(...children)` — variadic concatenation of pre-encoded child TLVs.
|
||||
- `encodeObjectIdentifier(oid)` — dotted-decimal OID with base-128 sub-identifier
|
||||
encoding and validation of the first two arcs.
|
||||
- `encodeBitString` — primitive `BIT STRING` carrying a whole-byte payload.
|
||||
- `encodeOctetString`, `encodeNull`, `encodeContextSpecific(tagNumber, body)`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
encodeInteger,
|
||||
encodeSequence,
|
||||
} from "@suaveplan/crypto-rsa/asn1";
|
||||
|
||||
const pkcs1Public = encodeSequence(
|
||||
encodeInteger(modulus),
|
||||
encodeInteger(exponent)
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
encodeBitString,
|
||||
encodeNull,
|
||||
encodeObjectIdentifier,
|
||||
encodeSequence,
|
||||
} from "@suaveplan/crypto-rsa/asn1";
|
||||
|
||||
const RSA_ENCRYPTION_OID = "1.2.840.113549.1.1.1";
|
||||
|
||||
const algorithmIdentifier = encodeSequence(
|
||||
encodeObjectIdentifier(RSA_ENCRYPTION_OID),
|
||||
encodeNull()
|
||||
);
|
||||
|
||||
const subjectPublicKeyInfo = encodeSequence(
|
||||
algorithmIdentifier,
|
||||
encodeBitString(pkcs1PublicKeyDer)
|
||||
);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `encodeLength(length: number): Uint8Array` — DER length octet(s).
|
||||
- `encodeInteger(value: bigint): Uint8Array` — full INTEGER TLV.
|
||||
- `encodeSequence(...children: Uint8Array[]): Uint8Array` — full SEQUENCE TLV.
|
||||
- `encodeObjectIdentifier(oid: string): Uint8Array` — full OID TLV.
|
||||
- `encodeBitString(payload: Uint8Array): Uint8Array` — full BIT STRING TLV.
|
||||
- `encodeOctetString(payload: Uint8Array): Uint8Array` — full OCTET STRING TLV.
|
||||
- `encodeNull(): Uint8Array` — `0x05 0x00`.
|
||||
- `encodeContextSpecific(tagNumber: 0..30, body: Uint8Array): Uint8Array` — `[n]` wrapping.
|
||||
|
||||
All inputs are validated. Negative integers, malformed OID strings, lengths that
|
||||
overflow, and out-of-range context tag numbers throw
|
||||
`CryptoRsaError(CRYPTO_RSA.INVALID_KEY)`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The writer is fully synchronous and allocates one `Uint8Array` per TLV node plus
|
||||
one final concatenation through `concatBytes` from `@suaveplan/crypto-utils`.
|
||||
For the small key-shape graph (≤ 8 INTEGERs for an RSA private key) this is well
|
||||
below any allocation budget.
|
||||
|
||||
OID encoding implements the standard base-128 sub-identifier rule: the high bit of
|
||||
every byte except the last is set, so each sub-identifier reconstructs as a
|
||||
big-endian variable-length integer. The first two arcs are folded into a single
|
||||
byte (`arc1 * 40 + arc2`) per X.690 §8.19; arc1 is constrained to `{0, 1, 2}`,
|
||||
and arc2 to `< 40` when arc1 is 0 or 1. Both constraints are enforced.
|
||||
Reference in New Issue
Block a user