Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
export {
AuthorityKeyIdentifierSchema,
BasicConstraintsSchema,
CrlDistributionPointSchema,
ExtendedKeyUsageSchema,
GeneralNameSchema,
KeyUsageSchema,
NameConstraintsSchema,
ParseCertificateOptionsSchema,
SubjectKeyIdentifierSchema,
ValidateChainOptionsSchema,
X509CertificateLikeSchema,
} from "./schemas.js";

View File

@@ -0,0 +1,56 @@
# `schemas/schemas` — Zod boundary schemas
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines Zod schemas for every public option-bundle and value-object type exposed by `@suaveplan/x509`. The most important boundary is `ValidateChainOptionsSchema`, which `validateChain` uses to refuse malformed caller input — passing `null` for `leaf`, an array of non-certificate values for `intermediates`, or a non-`AbortSignal` for `signal` is rejected before any work happens, with the original Zod issue list preserved as `cause` on the `X509Error(CERTIFICATE_MALFORMED)` it wraps with.
Schemas for the extension result types (`BasicConstraintsSchema`, `KeyUsageSchema`, `ExtendedKeyUsageSchema`, `GeneralNameSchema`, `NameConstraintsSchema`, `AuthorityKeyIdentifierSchema`, `SubjectKeyIdentifierSchema`, `CrlDistributionPointSchema`) pin the structural shape so tests can assert via `safeParse` on synthetic inputs and so callers serialising through JSON can validate before consuming.
## Features
- Strict objects (`.strict()`) on option types so unknown keys are rejected.
- Pass-through objects on value-object types where the runtime carries extra raw-bytes fields the schema doesn't enumerate.
- `instanceof Uint8Array` refinements on byte-typed fields.
- `instanceof AbortSignal` refinement on the optional cancellation signal.
- Reuse of the same `GeneralNameSchema` across every extension that references one.
## Usage: Basic Example
```ts
import { ValidateChainOptionsSchema } from "@suaveplan/x509";
const parsed = ValidateChainOptionsSchema.parse({
leaf: someCert,
intermediates: [],
trustAnchors: [root],
});
```
## Usage: Advanced Example
```ts
import { BasicConstraintsSchema } from "@suaveplan/x509";
const result = BasicConstraintsSchema.safeParse({
cA: true,
pathLenConstraint: 3,
});
if (!result.success) {
throw new Error("invariant violated");
}
```
## API Reference
- `ValidateChainOptionsSchema` — pins the `validateChain` boundary shape.
- `X509CertificateLikeSchema` — pass-through certificate shape carrying `tbs`, `signatureAlgorithm`, `signatureValue`, `tbsCertificateBytes`, `raw`.
- `BasicConstraintsSchema`, `KeyUsageSchema`, `ExtendedKeyUsageSchema`, `GeneralNameSchema`, `NameConstraintsSchema`, `AuthorityKeyIdentifierSchema`, `SubjectKeyIdentifierSchema`, `CrlDistributionPointSchema` — extension value shapes.
## Implementation Notes
- `validateChain` runs the options through Zod first; this is what produces the typed refusal of `{ leaf: null }` documented in the spec. The Zod issue is preserved as `cause` so consumers can inspect it programmatically.
- `X509CertificateLikeSchema` is intentionally loose — the schema doesn't reproduce the full TBSCertificate Zod hierarchy. The runtime validator inside `validateChain` does the real structural work; the schema is a guard against gross caller bugs.
- Schemas use `instanceof` refinements (`Uint8Array`, `AbortSignal`) so the runtime check matches the static type. A plain `{ buffer: ArrayBuffer }` would type-check but fail the `instanceof` predicate at runtime — exactly what the boundary is there to catch.

View File

@@ -0,0 +1,151 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { buildEd25519Chain } from "../__fixtures__/build-certs.js";
import { X509Error } from "../errors/error-codes.js";
import { decodeBasicConstraints } from "../extensions/basic-constraints.js";
import { validateChain } from "../validate/validate-chain.js";
import {
AuthorityKeyIdentifierSchema,
BasicConstraintsSchema,
CrlDistributionPointSchema,
ExtendedKeyUsageSchema,
GeneralNameSchema,
KeyUsageSchema,
NameConstraintsSchema,
ParseCertificateOptionsSchema,
SubjectKeyIdentifierSchema,
ValidateChainOptionsSchema,
X509CertificateLikeSchema,
} from "./schemas.js";
describe("Zod boundary schemas", () => {
it("malformed ValidateChainOptions rejected at boundary with EXTENSION_MALFORMED", {
timeout: 5000,
}, async () => {
await expect(
validateChain({
leaf: null,
intermediates: [],
trustAnchors: [],
} as unknown as Parameters<typeof validateChain>[0])
).rejects.toThrow();
});
it("malformed BasicConstraints bytes rejected at boundary with EXTENSION_MALFORMED", {
timeout: 5000,
}, () => {
try {
decodeBasicConstraints(new Uint8Array([0x02, 0x01, 0x00]));
throw new Error("expected throw");
} catch (e) {
expect(e).toBeInstanceOf(X509Error);
expect((e as X509Error).code).toBe("X509.EXTENSION_MALFORMED");
}
});
it("X509CertificateLikeSchema validates a parsed cert", {
timeout: 5000,
}, () => {
const { leafCert } = buildEd25519Chain();
const ok = X509CertificateLikeSchema.safeParse(leafCert);
expect(ok.success).toBe(true);
});
it("X509CertificateLikeSchema rejects a non-object", {
timeout: 5000,
}, () => {
expect(X509CertificateLikeSchema.safeParse(null).success).toBe(false);
});
it("BasicConstraintsSchema validates a struct", { timeout: 5000 }, () => {
expect(
BasicConstraintsSchema.safeParse({ cA: true, pathLenConstraint: 0 })
.success
).toBe(true);
});
it("KeyUsageSchema validates the 9-flag shape", { timeout: 5000 }, () => {
expect(
KeyUsageSchema.safeParse({
digitalSignature: true,
nonRepudiation: false,
keyEncipherment: false,
dataEncipherment: false,
keyAgreement: false,
keyCertSign: false,
cRLSign: false,
encipherOnly: false,
decipherOnly: false,
}).success
).toBe(true);
});
it("ExtendedKeyUsageSchema rejects bad purposes", { timeout: 5000 }, () => {
expect(
ExtendedKeyUsageSchema.safeParse({ purposes: ["serverAuth"] })
.success
).toBe(true);
expect(ExtendedKeyUsageSchema.safeParse({ purposes: 5 }).success).toBe(
false
);
});
it("GeneralNameSchema validates kind/value/raw shape", {
timeout: 5000,
}, () => {
expect(
GeneralNameSchema.safeParse({
kind: "dNSName",
value: "ex.com",
raw: new Uint8Array(),
}).success
).toBe(true);
});
it("NameConstraintsSchema accepts undefined fields", {
timeout: 5000,
}, () => {
expect(NameConstraintsSchema.safeParse({}).success).toBe(true);
});
it("AuthorityKeyIdentifierSchema accepts empty AKI", {
timeout: 5000,
}, () => {
expect(AuthorityKeyIdentifierSchema.safeParse({}).success).toBe(true);
});
it("SubjectKeyIdentifierSchema requires keyIdentifier", {
timeout: 5000,
}, () => {
expect(SubjectKeyIdentifierSchema.safeParse({}).success).toBe(false);
expect(
SubjectKeyIdentifierSchema.safeParse({
keyIdentifier: new Uint8Array([1, 2, 3]),
}).success
).toBe(true);
});
it("CrlDistributionPointSchema accepts empty shape", {
timeout: 5000,
}, () => {
expect(CrlDistributionPointSchema.safeParse({}).success).toBe(true);
});
it("ParseCertificateOptionsSchema accepts empty shape", {
timeout: 5000,
}, () => {
expect(ParseCertificateOptionsSchema.safeParse({}).success).toBe(true);
});
it("ValidateChainOptionsSchema accepts a real shape", {
timeout: 5000,
}, () => {
const { leafCert, rootCert } = buildEd25519Chain();
expect(
ValidateChainOptionsSchema.safeParse({
leaf: leafCert,
intermediates: [],
trustAnchors: [rootCert],
}).success
).toBe(true);
});
});

View File

@@ -0,0 +1,130 @@
import z from "zod";
/**
* Schema for an `X509Certificate`-shaped value object. Used at the boundary
* of `validateChain` so callers that pass through `JSON.parse` get a typed
* refusal rather than a confusing crash deep inside the validator.
*/
export const X509CertificateLikeSchema = z
.object({
tbs: z.object({}).passthrough(),
signatureAlgorithm: z.object({}).passthrough(),
signatureValue: z.instanceof(Uint8Array),
tbsCertificateBytes: z.instanceof(Uint8Array),
raw: z.instanceof(Uint8Array),
})
.passthrough();
export const ValidateChainOptionsSchema = z
.object({
leaf: X509CertificateLikeSchema,
intermediates: z.array(X509CertificateLikeSchema),
trustAnchors: z.array(X509CertificateLikeSchema),
clock: z.function().optional(),
signal: z.instanceof(AbortSignal).optional(),
maxChainDepth: z.number().int().positive().optional(),
ocspResponder: z.object({ check: z.function() }).optional(),
hostName: z.string().optional(),
ekuRequired: z.string().optional(),
initialExplicitPolicy: z.boolean().optional(),
initialPolicySet: z.array(z.string()).optional(),
})
// The Zod schema is intentionally loose — the validator does the real
// structural validation. The schema's job is to catch obvious caller
// bugs (null leaf, wrong array element types).
.strict() as unknown as z.ZodType<
// The runtime cast keeps the callsite happy; ValidateChainOptions has
// more precise generic information than Zod can express.
import("../validate/validate-chain.types.js").ValidateChainOptions
>;
export const ParseCertificateOptionsSchema = z
.object({
// No options at this time. Reserved for future extension knobs.
})
.strict();
/**
* Lightweight schemas for the extension result types. These are not used at
* a runtime boundary today (extension decoders construct values directly),
* but they pin the shape so tests can assert via `safeParse` on synthetic
* inputs and so callers passing through serialisation can validate.
*/
export const BasicConstraintsSchema = z.object({
cA: z.boolean(),
pathLenConstraint: z.number().int().nonnegative().optional(),
});
export const KeyUsageSchema = z.object({
digitalSignature: z.boolean(),
nonRepudiation: z.boolean(),
keyEncipherment: z.boolean(),
dataEncipherment: z.boolean(),
keyAgreement: z.boolean(),
keyCertSign: z.boolean(),
cRLSign: z.boolean(),
encipherOnly: z.boolean(),
decipherOnly: z.boolean(),
});
export const ExtendedKeyUsageSchema = z.object({
purposes: z.array(z.string()),
});
export const GeneralNameSchema = z.object({
kind: z.enum([
"rfc822Name",
"dNSName",
"uniformResourceIdentifier",
"iPAddress",
"directoryName",
"otherName",
"x400Address",
"ediPartyName",
"registeredID",
]),
value: z.string(),
raw: z.instanceof(Uint8Array),
});
export const NameConstraintsSchema = z.object({
permittedSubtrees: z
.array(
z.object({
base: GeneralNameSchema,
minimum: z.number().int().optional(),
maximum: z.number().int().optional(),
})
)
.optional(),
excludedSubtrees: z
.array(
z.object({
base: GeneralNameSchema,
minimum: z.number().int().optional(),
maximum: z.number().int().optional(),
})
)
.optional(),
});
export const AuthorityKeyIdentifierSchema = z.object({
keyIdentifier: z.instanceof(Uint8Array).optional(),
authorityCertIssuer: z.array(GeneralNameSchema).optional(),
authorityCertSerialNumber: z.instanceof(Uint8Array).optional(),
});
export const SubjectKeyIdentifierSchema = z.object({
keyIdentifier: z.instanceof(Uint8Array),
});
export const CrlDistributionPointSchema = z.object({
distributionPoint: z
.object({
fullName: z.array(GeneralNameSchema).optional(),
nameRelativeToCRLIssuer: z.instanceof(Uint8Array).optional(),
})
.optional(),
reasons: z.instanceof(Uint8Array).optional(),
cRLIssuer: z.array(GeneralNameSchema).optional(),
});