first commit
This commit is contained in:
50
docs/security-stack/security/x509/src/certificate/name.md
Normal file
50
docs/security-stack/security/x509/src/certificate/name.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# `certificate/name` — Name codec + canonical comparison
|
||||
|
||||
## Purpose
|
||||
|
||||
Decodes RFC 5280 paragraph 4.1.2.4 `Name` (`RDNSequence`) structures into a typed shape carrying both the raw DER bytes (for byte-identical issuer/subject comparison when one CA always re-encodes identically) and a canonical byte-string suitable for RFC 5280 paragraph 7.1 case-folding / whitespace-normalising comparison. `namesEqual(a, b)` is the predicate `validate-chain` uses for the name-chaining check between an issuer's `subject` and a child's `issuer`.
|
||||
|
||||
## Features
|
||||
|
||||
- Strict ASN.1 shape check — outer SEQUENCE, each child SET, each grand-child SEQUENCE of (OID, value).
|
||||
- Per-attribute value decoding via `decodeAsn1String`, supporting every typed-string variant RFC 5280 allows.
|
||||
- Canonical byte-string built with NFKD normalisation, lowercase fold, whitespace collapse, and SET-order sort (RFC 5280 paragraph 7.1 reference implementation).
|
||||
- `commonNameOf(name)` convenience for extracting the first CN attribute (used by `hasNameMatch`).
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { decodeName, namesEqual, commonNameOf } from "@suaveplan/x509/certificate";
|
||||
import { decodeAsn1 } from "@suaveplan/x509/asn1";
|
||||
|
||||
const issuer = decodeName(decodeAsn1(issuerBytes));
|
||||
const subject = decodeName(decodeAsn1(subjectBytes));
|
||||
if (namesEqual(issuer, subject)) {
|
||||
console.log("self-issued");
|
||||
}
|
||||
console.log(commonNameOf(subject)); // "example.com"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { parseCertificate } from "@suaveplan/x509/certificate";
|
||||
import { namesEqual } from "@suaveplan/x509/certificate";
|
||||
|
||||
const child = parseCertificate(childBytes);
|
||||
const parent = parseCertificate(parentBytes);
|
||||
const chained = namesEqual(child.tbs.issuer, parent.tbs.subject);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `decodeName(node: Asn1Node): Name` — throws `X509Error(CERTIFICATE_MALFORMED)` on shape violations.
|
||||
- `namesEqual(a: Name, b: Name): boolean` — case-folded, whitespace-collapsed, sort-stable comparison.
|
||||
- `commonNameOf(name: Name): string | undefined` — returns the first CN attribute value found in any RDN.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The canonical form is intentionally lossy — it discards attribute-encoding choice (PrintableString vs UTF8String) and SET ordering inside each RDN. This matches RFC 5280 paragraph 7.1 LDAP-style equality.
|
||||
- Raw DER bytes are retained alongside the canonical form so callers needing byte-identical comparison (rare; matches only when both parties use the same encoder) can use `name.raw` directly.
|
||||
- CN extraction uses OID `2.5.4.3`; the first CN in declaration order wins. This matches the heuristic browsers use when the SAN list is empty (legacy fallback).
|
||||
- Canonicalisation prepends each RDN with a `0x01` separator and inserts `0x00` between OID and value — these are non-base64 control bytes so the byte-comparison is unambiguous against any encoded attribute payload.
|
||||
54
docs/security-stack/security/x509/src/certificate/parse.md
Normal file
54
docs/security-stack/security/x509/src/certificate/parse.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# `certificate/parse` — RFC 5280 paragraph 4.1 Certificate parser
|
||||
|
||||
## Purpose
|
||||
|
||||
Implements `parseCertificate(bytes)` decoding a DER- or PEM-encoded `Certificate` into a typed `X509Certificate` value object. Every TBSCertificate field per RFC 5280 paragraph 4.1.2 is decoded into a strongly-typed shape: `version` (mapped from the raw 0/1/2 to `"v1"/"v2"/"v3"`), `serialNumber` (length-bounded to 20 bytes per paragraph 4.1.2.2), `signature` algorithm identifier, `issuer` and `subject` `Name`s, `validity` window in epoch-ms, `subjectPublicKeyInfo`, optional unique IDs, and a `RawExtension[]` (typed extension decoders consume these via the registry). The result also carries `tbsCertificateBytes` (canonical DER of the inner TBSCertificate, used for re-hashing during signature verify) and `signatureValue` (the raw outer signature bytes).
|
||||
|
||||
## Features
|
||||
|
||||
- Accepts both raw DER and bare PEM (detected by `-----BEGIN` header probe in the first 64 bytes).
|
||||
- Hard reject for serials longer than 20 bytes (`X509.CERTIFICATE_MALFORMED`).
|
||||
- Hard reject for unsupported versions (`X509.UNSUPPORTED_VERSION` when the raw INTEGER value is outside {0, 1, 2}).
|
||||
- v1/v2 certificates with extensions are rejected per RFC 5280 paragraph 4.1.2.9.
|
||||
- Captures canonical TBS DER bytes by re-encoding the parsed structure (round-trip guaranteed by the ASN.1 codec).
|
||||
- Records `x509.parse_duration_ms` and `x509.parse_total{outcome}` for every call; errors increment `x509.error_total{code}`.
|
||||
- Extension OIDs increment `x509.extension_total{oid, critical}` with bounded cardinality (recognised set plus `"unknown"`).
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { parseCertificate } from "@suaveplan/x509/certificate";
|
||||
|
||||
const cert = parseCertificate(derBytes);
|
||||
console.log(cert.tbs.version); // "v3"
|
||||
console.log(cert.tbs.subject.canonical); // Uint8Array
|
||||
console.log(cert.tbs.subjectPublicKeyInfo.algorithm.oid);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { parseCertificate } from "@suaveplan/x509/certificate";
|
||||
|
||||
// PEM input with leading whitespace and Windows line endings — both tolerated.
|
||||
const pem = `-----BEGIN CERTIFICATE-----\r\n` +
|
||||
`MII...\r\n` +
|
||||
`-----END CERTIFICATE-----\r\n`;
|
||||
const cert = parseCertificate(new TextEncoder().encode(pem));
|
||||
|
||||
// Re-hash the TBSCertificate for signature verify:
|
||||
import { sha256 } from "@suaveplan/crypto-hash";
|
||||
const digest = sha256(cert.tbsCertificateBytes);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `parseCertificate(bytes: Uint8Array): X509Certificate` — synchronous decode. Throws `X509Error` with one of `CERTIFICATE_MALFORMED`, `UNSUPPORTED_VERSION`, `ASN1_MALFORMED`, or `PEM_MALFORMED`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The parser re-encodes the TBSCertificate via `encodeAsn1` to obtain the canonical DER bytes used for signature verification. Re-encoding (rather than slicing the input) means a non-canonically-encoded input still verifies correctly when the issuing CA used a canonical encoding — but a forged input that exploits encoding ambiguity is detected because the re-encoded form differs from the input the issuer signed. This matches BoringSSL's recommended approach.
|
||||
- Serial-number length bound is RFC 5280 paragraph 4.1.2.2 — 20 octets including the optional leading zero. The bound is checked on the raw INTEGER content, not the canonical bigint value.
|
||||
- v1/v2 with extensions is a malformed certificate per RFC 5280 paragraph 4.1.2.9 — extensions are a v3 feature.
|
||||
- Optional `issuerUniqueID` (context tag 1) and `subjectUniqueID` (context tag 2) are accepted on v2/v3 and ignored by `validateChain` (deprecated per RFC 5280 paragraph 4.1.2.8).
|
||||
- PEM detection probes the first 64 bytes for `"-----BEGIN "`. This avoids decoding the entire payload through UTF-8 when the input is binary DER.
|
||||
Reference in New Issue
Block a user