# Capability Spec: x509 **Package**: `@suaveplan/x509` **Status**: CANONICAL — archived 2026-05-11. > Every requirement below carries a `**Verified by:**` line at authoring time. (See `openspec/AGENTS.md` Section 13.) ## Requirements ### Requirement: ASN.1 BER/DER decoder for the X.509 subset The package SHALL export `decodeAsn1(bytes: Uint8Array, mode?: "der" | "ber"): Asn1Node` covering every ASN.1 production used by RFC 5280 Appendix A: primitive types (`INTEGER`, `BIT STRING`, `OCTET STRING`, `OBJECT IDENTIFIER`, `NULL`, `BOOLEAN`), string types (`UTF8String`, `PrintableString`, `IA5String`, `TeletexString`, `BMPString`, `UniversalString`), time types (`UTCTime`, `GeneralizedTime`), and constructed types (`SEQUENCE`, `SET`, `[n] EXPLICIT`, `[n] IMPLICIT`, context-specific tags). DER mode (default) SHALL reject indefinite-length encodings; BER mode SHALL accept them. Trailing bytes after the outermost item SHALL be rejected in both modes. #### Scenario: decodes a definite-length SEQUENCE - **WHEN** `decodeAsn1(Uint8Array([0x30, 0x03, 0x02, 0x01, 0x05]))` is called - **THEN** the result is an `Asn1Node` with `tag.tagNumber === 16` (SEQUENCE) carrying a single `INTEGER` child whose value is `5` #### Scenario: rejects indefinite-length encoding under DER - **WHEN** `decodeAsn1(Uint8Array([0x30, 0x80, 0x02, 0x01, 0x05, 0x00, 0x00]), "der")` is called - **THEN** the call throws `X509Error` with code `"X509.ASN1_MALFORMED"` #### Scenario: accepts indefinite-length encoding under BER - **WHEN** `decodeAsn1(Uint8Array([0x30, 0x80, 0x02, 0x01, 0x05, 0x00, 0x00]), "ber")` is called - **THEN** the result is a `SEQUENCE` node carrying the single `INTEGER 5` child #### Scenario: rejects trailing bytes after outermost item - **WHEN** `decodeAsn1(Uint8Array([0x02, 0x01, 0x05, 0xff]))` is called - **THEN** the call throws `X509Error` with code `"X509.ASN1_MALFORMED"` **Verified by:** `packages/crypto/x509/src/asn1/decoder.test.ts::"decodes a definite-length SEQUENCE with one INTEGER child"` AND `packages/crypto/x509/src/asn1/decoder.test.ts::"rejects indefinite-length encoding under DER"` AND `packages/crypto/x509/src/asn1/decoder.test.ts::"accepts indefinite-length encoding under BER"` AND `packages/crypto/x509/src/asn1/decoder.test.ts::"rejects trailing bytes after the outermost item"` --- ### Requirement: ASN.1 DER encoder with round-trip guarantee The package SHALL export `encodeAsn1(node: Asn1Node): Uint8Array` that produces canonical DER bytes per X.690 §10 for any node the decoder accepted. For every input `bytes` that `decodeAsn1(bytes, "der")` accepts, `encodeAsn1(decodeAsn1(bytes, "der"))` SHALL return a `Uint8Array` byte-identical to `bytes`. #### Scenario: round-trips a primitive INTEGER - **GIVEN** `bytes = Uint8Array([0x02, 0x01, 0x05])` - **WHEN** `encodeAsn1(decodeAsn1(bytes, "der"))` is called - **THEN** the result equals `bytes` byte-for-byte #### Scenario: round-trips a nested SEQUENCE OF SET - **GIVEN** a 4-deep RFC 5280 §4.1.2.4 `Name` structure - **WHEN** decode then encode is applied - **THEN** the output equals the input byte-for-byte **Verified by:** `packages/crypto/x509/src/asn1/encoder.test.ts::"round-trips a primitive INTEGER"` AND `packages/crypto/x509/src/asn1/encoder.test.ts::"round-trips a nested SEQUENCE OF SET Name structure"` --- ### Requirement: PEM framing parser per RFC 7468 The package SHALL export `decodePem(text: string): PemBlock[]` and `encodePem(label: string, bytes: Uint8Array): string` per RFC 7468 §5. The decoder SHALL accept `-----BEGIN