micro-key-producer
Version:
Produces secure passwords & keys for WebCrypto, SSH, PGP, SLIP10, OTP and many others
1,298 lines (1,291 loc) • 123 kB
text/typescript
/*! micro-key-producer - MIT License (c) 2024 Paul Miller (paulmillr.com) */
/**
* x509 certificates. Conforms to parts of RFC 3820, RFC 5280, RFC 5652, RFC 5754, RFC 5912, RFC 7633.
* @module
*/
import { ed25519 } from '@noble/curves/ed25519.js';
import { ed448 } from '@noble/curves/ed448.js';
import { brainpoolP256r1, brainpoolP384r1, brainpoolP512r1 } from '@noble/curves/misc.js';
import { p256, p384, p521 } from '@noble/curves/nist.js';
import { asciiToBytes, equalBytes } from '@noble/curves/utils.js';
import { sha224, sha256, sha384, sha512 } from '@noble/hashes/sha2.js';
import { bytesToHex, concatBytes, hexToBytes } from '@noble/hashes/utils.js';
import { base64, hex } from '@scure/base';
import * as P from 'micro-packed';
import type {
ECParams as DERECParams,
PKCS8Key as DERPKCS8Key,
SPKIKey as DERSPKIKey,
} from './convert.ts';
import { CurveOID, DERUtils, curveOID } from './convert.ts';
type KnownCurve = keyof typeof CurveOID;
/** Supported signing or key-agreement curve name. */
export type CertCurve = KnownCurve | `OID:${string}`;
/** Parsed PEM block with decoded DER bytes. */
export type PemBlock = {
/** PEM block tag between `BEGIN` and `END`. */
tag: string;
/** Base64 payload exactly as it appeared in the PEM block. */
b64: string;
/** Decoded DER bytes for the PEM payload. */
der: Uint8Array;
};
/** Parsed PKCS#8 attribute entry. */
export type Pkcs8Attr = {
/** Attribute OID. */
oid: string;
/** Raw ASN.1 values carried by the attribute. */
values: Uint8Array[];
};
type RSAPrivateKey = P.UnwrapCoder<typeof DERUtils.RSAPrivateKey>;
/** Decoded X.509 certificate. */
export type Cert = P.UnwrapCoder<typeof CERTUtils.Certificate>;
type KeyBase = {
pem: string;
der: Uint8Array;
attributes?: Pkcs8Attr[];
};
/** Parsed private-key PEM/DER bundle. */
export type PrivateKey = KeyBase & { key: DERPKCS8Key; rsa?: RSAPrivateKey };
/** Leaf certificate, private key, and optional chain used for signing. */
export type SigningPem = {
/** Leaf certificate used as the signer. */
leaf: Cert;
/** Private key matching the leaf certificate. */
key: PrivateKey;
/** Optional issuer chain sent alongside the leaf. */
chain: Cert[];
};
/** CMS verification options. */
export type CmsVerifyOpts = {
/** Validation time in UNIX milliseconds. */
time?: number;
/** Allow BER normalization before decoding. */
allowBER?: boolean;
/**
* Whether to verify CMS and certificate signatures for supported algorithms.
* When `false`, structure, path, and attribute validation still runs.
*/
checkSignatures?: boolean;
/** Intended verification purpose such as S/MIME or code signing. */
purpose?: 'any' | 'smime' | 'codeSigning';
/** Optional trust anchors or intermediates used for path building. */
chain?: (string | Uint8Array | Cert)[];
};
/** Result of CMS verification. */
export type CmsVerify = {
/** Signature algorithm OID from the CMS SignerInfo. */
signatureOid: string;
/** Parsed signer certificate. */
signer: Cert;
/** Whether signed attributes were present and validated. */
signedAttrs: boolean;
/** Parsed certificate path from signer toward issuer or root candidates. */
chain: Cert[];
};
/** Detached CMS payload and signature pair. */
export type CmsDetached = {
/** Original detached content bytes. */
content: Uint8Array;
/** Detached CMS SignedData blob. */
signature: Uint8Array;
/** Certificates bundled with the signature. */
certs: Cert[];
};
/** CMS signing options. */
export type CmsSignOpts = BEROpts & {
// Optional signing-time timestamp in UNIX milliseconds.
// RFC 5652 section 11.3: signing-time is encoded as Time in signedAttrs.
createdTs?: number;
extraEntropy?: boolean | Uint8Array;
// Optional signedAttrs S/MIME Capabilities values (attribute OID 1.2.840.113549.1.9.15).
// Default behavior omits this attribute (OpenSSL `-nosmimecap` style); pass values here to include it.
// Values can be capability names from SMIME_CAPS or raw capability OIDs.
smimeCapabilities?: string[];
// Optional override for signedAttrs messageDigest value (attribute OID 1.2.840.113549.1.9.4).
messageDigest?: Uint8Array;
// Optional override for SignerInfo.digestAlgorithm OID.
digestAlgorithm?: string;
// Optional digest AlgorithmIdentifier params encoding mode.
// RFC 5754 allows SHA-2 params as absent or NULL; use 'null' for legacy byte parity.
digestAlgorithmParams?: 'absent' | 'null';
// Optional override for SignerInfo.signatureAlgorithm OID.
signatureAlgorithm?: string;
};
/** Decoded certificate extension data. */
export type CertExt = {
/** Extension OID. */
oid: string;
/** Whether the extension is marked critical. */
critical: boolean;
/** Subject Key Identifier extension. */
ski?: Uint8Array;
/** Basic Constraints extension. */
basic?: { ca?: boolean; pathLen?: bigint };
/** Key Usage extension bit string. */
keyUsage?: { unused: number; bytes: Uint8Array };
/** Extended Key Usage extension. */
eku?: { list: string[] };
/** Subject Alternative Name extension. */
san?: { list: CertGeneralName[] };
/** Authority Key Identifier extension. */
aki?: {
keyIdentifier?: Uint8Array;
authorityCertIssuer?: { list: CertGeneralName[] };
authorityCertSerialNumber?: bigint;
};
/** Authority Information Access extension. */
aia?: { list: { method: string; location: CertGeneralName }[] };
/** Proxy Certificate Information extension. */
proxyCertInfo?: { pathLen?: bigint; policy: { language: string; policy?: string } };
/** TLS Feature extension. */
tlsFeature?: { list: bigint[] };
/** Signed Certificate Timestamps extension. */
sct?: {
version: number;
logID: Uint8Array;
timestamp: bigint;
extensions: string;
hash: number;
signatureAlgorithm: number;
signature: Uint8Array;
}[];
/** CRL Distribution Points extension. */
crlDistributionPoints?: {
list: {
distributionPoint?: CertDistributionPointName;
reasons?: { unused: number; bytes: Uint8Array };
cRLIssuer?: { list: CertGeneralName[] };
}[];
};
/** Certificate Policies extension. */
policies?: {
list: {
policy: string;
qualifiers?: { list: CertPolicyQualifier[] };
}[];
};
/** Name Constraints extension. */
nameConstraints?: {
permitted?: { list: CertGeneralSubtree[] };
excluded?: { list: CertGeneralSubtree[] };
};
/** Subject Directory Attributes extension. */
subjectDirectoryAttributes?: {
list: { type: string; typeName?: string; values: CertAny[] }[];
};
/** Private Key Usage Period extension. */
privateKeyUsagePeriod?: { notBefore?: string; notAfter?: string };
/** Issuer Alternative Name extension. */
issuerAltName?: { list: CertGeneralName[] };
/** Issuing Distribution Point extension. */
issuingDistributionPoint?: {
distributionPoint?: CertDistributionPointName;
onlyContainsUserCerts?: boolean;
onlyContainsCACerts?: boolean;
onlySomeReasons?: { unused: number; bytes: Uint8Array };
indirectCRL?: boolean;
onlyContainsAttributeCerts?: boolean;
};
/** Certificate Issuer extension. */
certificateIssuer?: { list: CertGeneralName[] };
/** Policy Mappings extension. */
policyMappings?: { list: { issuerDomainPolicy: string; subjectDomainPolicy: string }[] };
/** Freshest CRL extension. */
freshestCRL?: {
list: {
distributionPoint?: CertDistributionPointName;
reasons?: { unused: number; bytes: Uint8Array };
cRLIssuer?: { list: CertGeneralName[] };
}[];
};
/** Policy Constraints extension. */
policyConstraints?: { requireExplicitPolicy?: bigint; inhibitPolicyMapping?: bigint };
/** Inhibit Any Policy extension. */
inhibitAnyPolicy?: bigint;
/** QC Statements extension. */
qcStatements?: {
list: { statementId: string; statementName?: string; statementInfo?: CertAny }[];
};
/** Subject Information Access extension. */
subjectInfoAccess?: { list: { method: string; location: CertGeneralName }[] };
/** Microsoft certificate type extension. */
msCertType?: CertAny;
};
/** Parsed GeneralName value. */
export type CertGeneralName =
| { TAG: 'otherName'; data: { type: string; value: TLVNode } }
| { TAG: 'rfc822Name'; data: string }
| { TAG: 'dNSName'; data: string }
| { TAG: 'x400Address'; data: Uint8Array }
| { TAG: 'directoryName'; data: NameCodec }
| { TAG: 'ediPartyName'; data: Uint8Array }
| { TAG: 'uniformResourceIdentifier'; data: string }
| { TAG: 'iPAddress'; data: string }
| { TAG: 'registeredID'; data: string };
/** Parsed CRL distribution-point name. */
export type CertDistributionPointName =
| { TAG: 'fullName'; data: { list: CertGeneralName[] } }
| { TAG: 'nameRelativeToCRLIssuer'; data: Array<{ oid: string; value: NameValue }> };
/** Parsed reason flags from CRL-related extensions. */
export type CertReasonFlags = {
/** End-entity private key was compromised. */
keyCompromise: boolean;
/** CA private key was compromised. */
cACompromise: boolean;
/** Subject affiliation changed. */
affiliationChanged: boolean;
/** Certificate was superseded. */
superseded: boolean;
/** Subject ceased operation. */
cessationOfOperation: boolean;
/** Certificate was placed on hold. */
certificateHold: boolean;
/** Privileges were withdrawn. */
privilegeWithdrawn: boolean;
/** Attribute authority key was compromised. */
aACompromise: boolean;
};
/** Parsed GeneralSubtree value. */
export type CertGeneralSubtree = {
/** Base GeneralName covered by the subtree. */
base: CertGeneralName;
/** Minimum subtree depth, when explicitly present. */
minimum?: bigint;
/** Maximum subtree depth, when explicitly present. */
maximum?: bigint;
};
/** Parsed certificate-policy qualifier. */
export type CertPolicyQualifier =
| { TAG: 'cps'; data: string }
| {
TAG: 'userNotice';
data: {
noticeRef?: { organization: CertText; numbers: number[] };
explicitText?: CertText;
};
}
| { TAG: 'unknown'; data: { oid: string; value: TLVNode } };
/** Generic ASN.1 tree node used for unsupported extension payloads. */
export type TLVNode = {
/** Raw ASN.1 tag number. */
tag: number;
/** Nested child nodes for constructed values. */
children?: TLVNode[];
/** Hex-encoded payload for primitive values. */
valueHex?: string;
};
/** Decoded text value from certificate fields. */
export type CertText = {
/** Underlying ASN.1 string tag used by the source field. */
tag: 'utf8' | 'ia5' | 'visible' | 'bmp';
/** Decoded text content. */
text: string;
};
/** Best-effort decoded arbitrary ASN.1 value. */
export type CertAny =
| { TAG: 'text'; data: NameValue }
| { TAG: 'oid'; data: { oid: string; name?: string } }
| { TAG: 'int'; data: bigint }
| { TAG: 'bool'; data: boolean }
| { TAG: 'time'; data: { TAG: 'utc' | 'generalized'; data: string } }
| { TAG: 'octet'; data: Uint8Array }
| { TAG: 'raw'; data: TLVNode };
const pemRE = /-----BEGIN ([^-]+)-----([\s\S]*?)-----END \1-----/g;
const hashOid = (h: { oid?: Uint8Array }) => {
if (!h.oid) throw new Error('hash.oid is missing');
return DERUtils.ASN1.OID.decode(h.oid);
};
const OID_NAME_RE = /^[0-9]+(?:\.[0-9]+)+$/;
const oidName = (m: Record<string, string>, oid: string): string => m[oid] || `OID:${oid}`;
const oidValue = (m: Record<string, string>, v: string, what: string): string => {
if (m[v]) return m[v];
if (v.startsWith('OID:')) return v.slice(4);
if (OID_NAME_RE.test(v)) return v;
throw new Error(`unknown ${what} ${v}`);
};
/**
* Extracts all PEM blocks from a text blob.
* @param text - Text containing one or more PEM blocks.
* @returns Parsed PEM blocks with decoded DER bytes.
* @example
* Extract all PEM blocks from a text blob.
* ```ts
* import { pemBlocks } from 'micro-key-producer/x509.js';
* pemBlocks(`-----BEGIN DATA-----
* AA==
* -----END DATA-----`);
* ```
*/
export const pemBlocks = (text: string): PemBlock[] => {
const out: PemBlock[] = [];
for (const m of text.matchAll(pemRE)) {
const tag = m[1].trim();
const b64 = m[2].trim();
if (!tag || !b64) continue;
out.push({ tag, b64, der: base64.decode(b64.replace(/\s+/g, '')) });
}
return out;
};
const onePem = (text: string, tag?: string) => {
const all = pemBlocks(text);
if (!all.length) throw new Error('no PEM blocks found');
if (!tag) return all[0];
const hit = all.find((i) => i.tag === tag);
if (!hit) throw new Error(`no PEM block with tag=${tag}`);
return hit;
};
const bytesNum = (bytes: Uint8Array): bigint => BigInt(`0x${bytesToHex(bytes) || '0'}`);
const explicitCurve = (
data: unknown
):
| {
fieldId: { info: { TAG: 'primeField'; data: bigint } };
curve: { a: Uint8Array; b: Uint8Array };
base: Uint8Array;
order: bigint;
cofactor?: bigint;
}
| undefined => {
if (!data || typeof data !== 'object') return;
const d = data as Record<string, unknown>;
const fieldId = d.fieldId as Record<string, unknown> | undefined;
const info = fieldId?.info as Record<string, unknown> | undefined;
const curve = d.curve as Record<string, unknown> | undefined;
if (info?.TAG !== 'primeField' || typeof info.data !== 'bigint') return;
if (!(curve?.a instanceof Uint8Array) || !(curve?.b instanceof Uint8Array)) return;
if (!(d.base instanceof Uint8Array) || typeof d.order !== 'bigint') return;
if (d.cofactor !== undefined && typeof d.cofactor !== 'bigint') return;
return {
fieldId: { info: { TAG: 'primeField', data: info.data } },
curve: { a: curve.a, b: curve.b },
base: d.base,
order: d.order,
cofactor: d.cofactor as bigint | undefined,
};
};
const explicitCurveName = (data: unknown): Curve | undefined => {
const d = explicitCurve(data);
if (!d) return;
// OpenSSL can serialize a standard EC key with explicit domain parameters while the
// matching cert/SPKI keeps the named-curve OID, so normalize equivalent parameters here.
for (const curve in CurveOID) {
const name = curve as Curve;
const known = CMS_ALG[name].ec.Point.CURVE();
if (d.fieldId.info.data !== known.p) continue;
if (bytesNum(d.curve.a) !== known.a || bytesNum(d.curve.b) !== known.b) continue;
if (d.order !== known.n) continue;
if (d.cofactor !== undefined && d.cofactor !== known.h) continue;
const base = CMS_ALG[name].ec.Point.BASE;
if (!equalBytes(d.base, base.toBytes(false)) && !equalBytes(d.base, base.toBytes(true)))
continue;
return name;
}
return;
};
const ecParamCurve = (d: DERECParams): CertCurve => {
if (d.TAG === 'namedCurve') return curveOID(d.data) as CertCurve;
if (d.TAG === 'implicitCurve') return 'OID:implicitCurve';
return explicitCurveName(d.data) || 'OID:specifiedCurve';
};
const spkiCurve = (k: DERSPKIKey): CertCurve => {
if (k.algorithm.info.TAG !== 'EC')
throw new Error(`expected EC SPKI key, got ${k.algorithm.info.TAG}`);
return ecParamCurve(k.algorithm.info.data);
};
// treeshake: these shared X.509 helpers survive through property reads unless the declaration itself is pure.
const SpkiKey = /* @__PURE__ */ (() => DERUtils.SPKI as P.CoderType<DERSPKIKey>)();
/** Supported certificate/key curves. */
export type Curve =
| 'P-256'
| 'P-384'
| 'P-521'
| 'brainpoolP256r1'
| 'brainpoolP384r1'
| 'brainpoolP512r1';
type EdKind = 'Ed25519' | 'Ed448';
type HashAlg = ((m: Uint8Array) => Uint8Array) & { oid?: Uint8Array };
type EcAlg = {
ec: {
sign: (m: Uint8Array, sk: Uint8Array, o?: any) => Uint8Array;
verify: (sig: Uint8Array, m: Uint8Array, pk: Uint8Array, o?: any) => boolean;
getPublicKey: (sk: Uint8Array, compressed?: boolean) => Uint8Array;
lengths: { signature?: number };
Point: {
CURVE: () => { p: bigint; n: bigint; h: bigint; a: bigint; b: bigint };
BASE: { toBytes: (compressed?: boolean) => Uint8Array };
};
};
sigOid: string;
hash: HashAlg;
};
type EdAlg = {
ed: {
sign: (m: Uint8Array, sk: Uint8Array) => Uint8Array;
verify: (sig: Uint8Array, m: Uint8Array, pk: Uint8Array) => boolean;
getPublicKey: (sk: Uint8Array) => Uint8Array;
};
sigOid: string;
hash: HashAlg;
};
type CmsAlg = EcAlg | EdAlg;
const CMS_ALG = {
'P-256': { ec: p256, sigOid: '1.2.840.10045.4.3.2', hash: sha256 },
'P-384': { ec: p384, sigOid: '1.2.840.10045.4.3.3', hash: sha384 },
'P-521': { ec: p521, sigOid: '1.2.840.10045.4.3.4', hash: sha512 },
brainpoolP256r1: {
ec: brainpoolP256r1,
sigOid: '1.2.840.10045.4.3.2',
hash: sha256,
},
brainpoolP384r1: {
ec: brainpoolP384r1,
sigOid: '1.2.840.10045.4.3.3',
hash: sha384,
},
brainpoolP512r1: {
ec: brainpoolP512r1,
sigOid: '1.2.840.10045.4.3.4',
hash: sha512,
},
Ed25519: {
ed: ed25519,
sigOid: '1.3.101.112',
hash: sha512,
},
Ed448: {
ed: ed448,
sigOid: '1.3.101.113',
hash: sha512,
},
} as const satisfies Record<Curve, EcAlg> & Record<EdKind, EdAlg>;
type AlgKey = Curve | EdKind;
// RFC 5754 section 2: this absent-or-NULL parameters rule applies to SHA-2
// AlgorithmIdentifiers specifically, so this set is intentionally SHA2-only
// (not a generic all-hashes OID table).
const SHA2_OID = {
'2.16.840.1.101.3.4.2.4': true,
'2.16.840.1.101.3.4.2.1': true,
'2.16.840.1.101.3.4.2.2': true,
'2.16.840.1.101.3.4.2.3': true,
} as const;
const ASN1_NULL = /* @__PURE__ */ Uint8Array.from([0x05, 0x00]);
const digestAlgParamsOk = (a: AlgorithmIdentifierCodec): boolean => {
const oid = algOID(a.algorithm);
if (!(oid in SHA2_OID)) return true;
const p = a.params ? TLVNodeCodec.encode(a.params) : undefined;
return !p || equalBytes(p, ASN1_NULL);
};
const digestAlgEqual = (a: AlgorithmIdentifierCodec, b: AlgorithmIdentifierCodec): boolean => {
const aOid = algOID(a.algorithm);
const bOid = algOID(b.algorithm);
if (aOid !== bOid) return false;
if (!digestAlgParamsOk(a) || !digestAlgParamsOk(b)) return false;
const aParams = a.params ? TLVNodeCodec.encode(a.params) : undefined;
const bParams = b.params ? TLVNodeCodec.encode(b.params) : undefined;
if (aOid in SHA2_OID) return true;
if (!aParams || !bParams) return !aParams && !bParams;
return equalBytes(aParams, bParams);
};
const ecCurve = (curve: Curve) => CMS_ALG[curve].ec;
const isSignCurve = (curve: CertCurve): curve is Curve =>
curve in CMS_ALG && 'ec' in CMS_ALG[curve as AlgKey];
const CMS_ALG_BY_SIG_OID = /* @__PURE__ */ (() =>
Object.fromEntries(Object.values(CMS_ALG).map((v) => [v.sigOid, v])) as Record<
CmsAlg['sigOid'],
CmsAlg
>)();
const CMS_HASH_BY_OID = /* @__PURE__ */ (() =>
Object.fromEntries([sha256, sha384, sha512].map((h) => [hashOid(h), h])) as Record<
string,
typeof sha256
>)();
const HASH_NAME_TO_OID = /* @__PURE__ */ Object.fromEntries(
/* @__PURE__ */ Object.entries({ sha224, sha256, sha384, sha512 }).map(([name, h]) => [
name,
hashOid(h),
])
) as Record<string, string>;
const ALG_NAME_TO_OID = /* @__PURE__ */ (() =>
({
ecPublicKey: '1.2.840.10045.2.1',
'ecdsa-with-SHA256': CMS_ALG['P-256'].sigOid,
'ecdsa-with-SHA384': CMS_ALG['P-384'].sigOid,
'ecdsa-with-SHA512': CMS_ALG['P-521'].sigOid,
Ed25519: CMS_ALG.Ed25519.sigOid,
Ed448: CMS_ALG.Ed448.sigOid,
...HASH_NAME_TO_OID,
}) as const)();
const ALG_OID_TO_NAME = /* @__PURE__ */ Object.fromEntries(
/* @__PURE__ */ Object.entries(ALG_NAME_TO_OID).map(([k, v]) => [v, k])
) as Record<string, string>;
const algOID = (v: string): string =>
oidValue(ALG_NAME_TO_OID as Record<string, string>, v, 'algorithm');
const pkcs8Attrs = (k: DERPKCS8Key): Pkcs8Attr[] | undefined =>
k.attributes?.map((raw) => PKCS8Attr.decode(raw));
const pkcs8FromPem = (pem: string, der: Uint8Array): PrivateKey => {
const key = DERUtils.PKCS8.decode(der);
const t = key.algorithm.info.TAG;
if (t === 'rsaEncryption') {
if (key.privateKey.TAG !== 'raw')
throw new Error('RSA PKCS#8: expected raw private key payload');
return {
pem,
der,
attributes: pkcs8Attrs(key),
key,
rsa: DERUtils.RSAPrivateKey.decode(key.privateKey.data),
};
}
return { pem, der, attributes: pkcs8Attrs(key), key };
};
const pkcs8SignKey = (
k: DERPKCS8Key
):
| { kind: 'EC'; curve: CertCurve; secretKey: Uint8Array; publicKey?: Uint8Array }
| { kind: EdKind; secretKey: Uint8Array; publicKey: Uint8Array } => {
const tag = k.algorithm.info.TAG;
if (tag === 'EC') {
const curve = ecParamCurve(k.algorithm.info.data);
if (k.privateKey.TAG !== 'struct')
throw new Error('EC PKCS#8: expected structured ECPrivateKey payload');
const s = k.privateKey.data;
if (s.parameters && ecParamCurve(s.parameters) !== curve)
throw new Error('EC PKCS#8: algorithm and key parameters mismatch');
return { kind: 'EC', curve, secretKey: s.privateKey, publicKey: k.publicKey || s.publicKey };
}
if (tag === 'Ed25519' || tag === 'Ed448') {
if (k.privateKey.TAG !== 'raw')
throw new Error(`${tag} PKCS#8: expected raw private key payload`);
return {
kind: tag,
secretKey: k.privateKey.data,
publicKey: k.publicKey || CMS_ALG[tag].ed.getPublicKey(k.privateKey.data),
};
}
throw new Error(`expected EC/Ed PKCS#8 key, got ${tag}`);
};
const certItem = (der: Uint8Array, opts: BEROpts = {}): Cert =>
X509C.Certificate.decode(berView(der, opts).der);
const certSpkiKey = (spki: TBSCertificateCodec['spki']): DERSPKIKey =>
SpkiKey.decode(X509SPKI.encode(spki));
const matchCertKey = (cert: Cert, key: PrivateKey): boolean => {
const k = certSpkiKey(cert.tbs.spki);
const tag = k.algorithm.info.TAG;
if (tag === 'EC') {
if (key.key.algorithm.info.TAG !== 'EC') return false;
const curve = spkiCurve(k);
if (!isSignCurve(curve)) return false;
const kk = pkcs8SignKey(key.key);
if (kk.kind !== 'EC' || curve !== kk.curve) return false;
const cmp = ecCurve(curve).getPublicKey(kk.secretKey, false);
const cmpC = ecCurve(curve).getPublicKey(kk.secretKey, true);
return equalBytes(k.publicKey, cmp) || equalBytes(k.publicKey, cmpC);
}
if (tag === 'Ed25519' || tag === 'Ed448') {
if (key.key.algorithm.info.TAG !== tag) return false;
const kk = pkcs8SignKey(key.key);
if (kk.kind !== tag) return false;
return (
equalBytes(k.publicKey, kk.publicKey) ||
equalBytes(k.publicKey, CMS_ALG[tag].ed.getPublicKey(kk.secretKey))
);
}
throw new Error('matchCertKey supports EC/Ed keys only');
};
type BERDoc = ReturnType<typeof DERUtils.BER.decode>;
type BEROpts = { allowBER?: boolean };
const berView = (src: Uint8Array, opts: BEROpts = {}): BERDoc =>
DERUtils.BER.decode(src, { allowBER: !!opts.allowBER });
const ASN1 = /* @__PURE__ */ (() => DERUtils.ASN1)();
const DERLen = P.wrap({
encodeStream(w, len: number) {
if (!Number.isSafeInteger(len) || len < 0)
throw new Error(`expected non-negative length, got ${len}`);
if (len < 0x80) return w.byte(len);
const a: number[] = [];
for (let n = len; n > 0; n >>= 8) a.unshift(n & 0xff);
w.byte(0x80 | a.length);
w.bytes(Uint8Array.from(a));
},
decodeStream(r): number {
const a = r.byte();
if (a < 0x80) return a;
const n = a & 0x7f;
if (!n) throw new Error('DER indefinite length is not supported');
const lb = r.bytes(n);
let len = 0;
for (const b of lb) len = (len << 8) | b;
if (len < 0x80) throw new Error('DER non-minimal length encoding');
return len;
},
}) satisfies P.CoderType<number>;
const TLV = P.struct({ tag: P.U8, value: P.bytes(DERLen) });
const TLVNodeCodec = P.wrap({
encodeStream(w, n: TLVNode) {
const value = n.children
? concatBytes(...n.children.map((i) => TLVNodeCodec.encode(i)))
: hexToBytes(n.valueHex || '');
w.bytes(TLV.encode({ tag: n.tag, value }));
},
decodeStream(r): TLVNode {
const t = TLV.decodeStream(r);
if (t.tag & 0x20) {
const items: TLVNode[] = [];
let at = 0;
while (at < t.value.length) {
const src = t.value.slice(at);
if (src.length < 2) throw new Error('constructed TLV child truncated');
const lb = src[1];
if (lb < 0x80) {
const total = 2 + lb;
items.push(TLVNodeCodec.decode(src.slice(0, total)));
at += total;
continue;
}
const n = lb & 0x7f;
if (!n) throw new Error('DER indefinite length is not supported');
if (src.length < 2 + n) throw new Error('constructed TLV child length truncated');
let len = 0;
for (let i = 0; i < n; i++) len = (len << 8) | src[2 + i];
if (len < 0x80) throw new Error('DER non-minimal length encoding');
const total = 2 + n + len;
items.push(TLVNodeCodec.decode(src.slice(0, total)));
at += total;
}
if (at !== t.value.length) throw new Error('constructed TLV child decode mismatch');
return { tag: t.tag, children: items };
}
return { tag: t.tag, valueHex: bytesToHex(t.value) };
},
});
// Encoded ASN.1 ANY passthrough: consume exactly one TLV from stream and keep its canonical bytes.
// This cannot be `P.bytes(null)` (greedy, would eat the rest of parent structure) and cannot be
// plain schema decode because many ANY values stay unresolved until OID-specific dispatch later.
const RawTLV = /* @__PURE__ */ P.wrap({
encodeStream(w, v: Uint8Array) {
const t = TLV.decode(v);
w.bytes(TLV.encode(t));
},
decodeStream(r): Uint8Array {
return TLV.encode(TLV.decodeStream(r));
},
});
const ASCII = /* @__PURE__ */ P.apply(/* @__PURE__ */ P.bytes(null), {
encode: (bytes: Uint8Array): string => {
let out = '';
for (let i = 0; i < bytes.length; i++) {
const c = bytes[i];
if (c > 0x7f)
throw new Error(`bytes contain non-ASCII value 0x${c.toString(16)} at position ${i}`);
out += String.fromCharCode(c);
}
return out;
},
decode: asciiToBytes,
}) satisfies P.CoderType<string>;
type ASN1Tagged<T> = P.CoderType<T> & {
tagByte: number;
tagBytes: number[];
constructed: number;
inner: P.CoderType<T>;
};
const tagged = <T>(tag: number, inner: P.CoderType<T>): ASN1Tagged<T> => {
const coder = P.wrap({
encodeStream(w, v: T) {
w.bytes(TLV.encode({ tag, value: inner.encode(v) }));
},
decodeStream(r): T {
const t = TLV.decodeStream(r);
if (t.tag !== tag)
throw new Error(`expected tag 0x${tag.toString(16)}, got 0x${t.tag.toString(16)}`);
return inner.decode(t.value);
},
});
return { tagByte: tag, tagBytes: [tag], constructed: 0, inner, ...coder };
};
const UTCTime: ASN1Tagged<string> = /* @__PURE__ */ tagged(0x17, ASCII);
const GeneralizedTime: ASN1Tagged<string> = /* @__PURE__ */ tagged(0x18, ASCII);
const Time: P.CoderType<{ TAG: 'utc'; data: string } | { TAG: 'generalized'; data: string }> =
/* @__PURE__ */ ASN1.choice({ utc: UTCTime, generalized: GeneralizedTime });
// RFC 5280 section 4.1.2.5.1 and 4.1.2.5.2: cert validity uses Zulu time and fixed second precision.
const TimeRE = /^(\d{2}|\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})Z$/;
const X509Time: { decode: (der: Uint8Array) => number; encode: (ts: number) => Uint8Array } = {
decode: (der: Uint8Array): number => {
const t = Time.decode(der);
const m = TimeRE.exec(t.data);
if (!m) throw new Error(`expected X509 time YY|YYYYMMDDHHMMSSZ, got ${t.data}`);
const yRaw = m[1];
if (t.TAG === 'utc' && yRaw.length !== 2)
throw new Error(`expected UTCTime year=2 digits, got ${t.data}`);
if (t.TAG === 'generalized' && yRaw.length !== 4)
throw new Error(`expected GeneralizedTime year=4 digits, got ${t.data}`);
const yNum = Number(yRaw);
const y = t.TAG === 'utc' ? (yNum >= 50 ? 1900 + yNum : 2000 + yNum) : yNum;
const mo = Number(m[2]);
const d = Number(m[3]);
const h = Number(m[4]);
const mi = Number(m[5]);
const s = Number(m[6]);
if (mo < 1 || mo > 12) throw new Error(`expected month 01..12, got ${m[2]}`);
if (d < 1 || d > 31) throw new Error(`expected day 01..31, got ${m[3]}`);
if (h > 23) throw new Error(`expected hour 00..23, got ${m[4]}`);
if (mi > 59) throw new Error(`expected minute 00..59, got ${m[5]}`);
if (s > 59) throw new Error(`expected second 00..59, got ${m[6]}`);
const ms = Date.UTC(y, mo - 1, d, h, mi, s);
const dt = new Date(ms);
// RFC 5280 section 4.1.2.5: certificate time fields are exact UTC calendar components and must not roll over.
if (
dt.getUTCFullYear() !== y ||
dt.getUTCMonth() + 1 !== mo ||
dt.getUTCDate() !== d ||
dt.getUTCHours() !== h ||
dt.getUTCMinutes() !== mi ||
dt.getUTCSeconds() !== s
)
throw new Error(`invalid calendar date in X509 time: ${t.data}`);
return Math.floor(ms / 1000);
},
encode: (ts: number): Uint8Array => {
if (!Number.isFinite(ts)) throw new Error(`expected finite timestamp, got ${ts}`);
const d = new Date(Math.floor(ts) * 1000);
const pad2 = (n: number): string => `${n}`.padStart(2, '0');
const pad4 = (n: number): string => `${n}`.padStart(4, '0');
const y = d.getUTCFullYear();
const text =
y >= 1950 && y <= 2049
? `${pad2(y % 100)}${pad2(d.getUTCMonth() + 1)}${pad2(d.getUTCDate())}${pad2(d.getUTCHours())}${pad2(d.getUTCMinutes())}${pad2(d.getUTCSeconds())}Z`
: `${pad4(y)}${pad2(d.getUTCMonth() + 1)}${pad2(d.getUTCDate())}${pad2(d.getUTCHours())}${pad2(d.getUTCMinutes())}${pad2(d.getUTCSeconds())}Z`;
return (y >= 1950 && y <= 2049 ? UTCTime : GeneralizedTime).encode(text);
},
} as const;
const timeEpoch = (time: P.UnwrapCoder<typeof Time>): number => X509Time.decode(Time.encode(time));
const PKCS8Attr = /* @__PURE__ */ (() =>
ASN1.sequence({ oid: ASN1.OID, values: ASN1.set(RawTLV) }))();
type NameValue =
| { TAG: 'utf8'; data: string }
| { TAG: 'printable'; data: string }
| { TAG: 'teletex'; data: string }
| { TAG: 'ia5'; data: string }
| { TAG: 'bmp'; data: string }
| { TAG: 'visible'; data: string }
| { TAG: 'numeric'; data: string };
type NameCodec = { rdns: Array<Array<{ oid: string; value: NameValue }>> };
type ValidityCodec = {
notBefore: P.UnwrapCoder<typeof Time>;
notAfter: P.UnwrapCoder<typeof Time>;
};
type ExtCodec = { oid: string; rest: Uint8Array };
type AlgorithmIdentifierCodec = { algorithm: string; params: TLVNode | undefined };
type TBSCertificateCodec = {
version: bigint | undefined;
serial: bigint;
signature: AlgorithmIdentifierCodec;
issuer: NameCodec;
validity: ValidityCodec;
subject: NameCodec;
spki: { algorithm: AlgorithmIdentifierCodec; publicKey: Uint8Array };
issuerUniqueID: Uint8Array | undefined;
subjectUniqueID: Uint8Array | undefined;
extensions: { list: ExtCodec[] } | undefined;
};
type CertificateCodec = {
tbs: TBSCertificateCodec;
sigAlg: AlgorithmIdentifierCodec;
sig: Uint8Array;
};
// RFC 5280 section 4.1.1.2 and RFC 5652 sections 10.1.1/10.1.2: AlgorithmIdentifier parameters are OPTIONAL.
// `params` keeps parsed ASN.1 ANY TLV when present; `undefined` means absent.
const HasTail = /* @__PURE__ */ P.wrap({
encodeStream() {},
decodeStream(r): boolean {
return !!r.leftBytes;
},
}) satisfies P.CoderType<boolean>;
const AlgorithmIdentifier = /* @__PURE__ */ (() =>
P.apply(
/* @__PURE__ */ ASN1.sequence({
algorithm: ASN1.OID,
params: /* @__PURE__ */ P.optional(HasTail, TLVNodeCodec),
}),
{
encode: (x: {
algorithm: string;
params: TLVNode | undefined;
}): AlgorithmIdentifierCodec => ({
algorithm: oidName(ALG_OID_TO_NAME, x.algorithm),
params: x.params,
}),
decode: (
x: AlgorithmIdentifierCodec
): { algorithm: string; params: TLVNode | undefined } => ({
algorithm: algOID(x.algorithm),
params: x.params,
}),
}
))() satisfies P.CoderType<AlgorithmIdentifierCodec>;
const IA5 = /* @__PURE__ */ tagged(0x16, ASCII);
const UTF8_DECODER = /* @__PURE__ */ new TextDecoder('utf-8', { fatal: true });
const UTF8_ENCODER = /* @__PURE__ */ new TextEncoder();
const UTF8String = /* @__PURE__ */ tagged(
0x0c,
/* @__PURE__ */ P.apply(/* @__PURE__ */ P.bytes(null), {
// X.509 UTF8String must be valid UTF-8; reject malformed byte sequences.
encode: (b: Uint8Array): string => UTF8_DECODER.decode(b),
decode: (s: string): Uint8Array => UTF8_ENCODER.encode(s),
}) satisfies P.CoderType<string>
);
const PrintableString: ASN1Tagged<string> = /* @__PURE__ */ tagged(
0x13,
/* @__PURE__ */ P.validate(ASCII, (s: string) => {
if (!/^[A-Za-z0-9 '()+,./:=?-]*$/.test(s))
throw new Error(`invalid PrintableString: ${JSON.stringify(s)}`);
return s;
})
);
// TeletexString (T61String) is treated as a byte-preserving 0x00..0xff mapping for interoperability.
// Strict T.61 character-set semantics are intentionally not enforced here.
const TeletexString: ASN1Tagged<string> = /* @__PURE__ */ tagged(
0x14,
/* @__PURE__ */ P.apply(/* @__PURE__ */ P.bytes(null), {
encode: (b: Uint8Array): string => {
let out = '';
for (let i = 0; i < b.length; i++) out += String.fromCharCode(b[i]);
return out;
},
decode: (s: string): Uint8Array => {
const out = new Uint8Array(s.length);
for (let i = 0; i < s.length; i++) {
const c = s.charCodeAt(i);
if (c > 0xff)
throw new Error(`expected latin1 character, got U+${c.toString(16).toUpperCase()}`);
out[i] = c;
}
return out;
},
}) satisfies P.CoderType<string>
);
const VisibleString = /* @__PURE__ */ tagged(0x1a, ASCII);
const NumericString: ASN1Tagged<string> = /* @__PURE__ */ tagged(
0x12,
/* @__PURE__ */ P.validate(ASCII, (s: string) => {
if (!/^[0-9 ]*$/.test(s)) throw new Error(`invalid NumericString: ${JSON.stringify(s)}`);
return s;
})
);
const BMPString = /* @__PURE__ */ tagged(
0x1e,
/* @__PURE__ */ P.apply(/* @__PURE__ */ P.bytes(null), {
encode: (b: Uint8Array): string => {
if (b.length % 2) throw new Error('BMPString length must be even');
let out = '';
for (let i = 0; i < b.length; i += 2) out += String.fromCharCode((b[i] << 8) | b[i + 1]);
return out;
},
decode: (s: string): Uint8Array => {
const out = new Uint8Array(s.length * 2);
for (let i = 0; i < s.length; i++) {
const c = s.charCodeAt(i);
out[i * 2] = c >>> 8;
out[i * 2 + 1] = c & 0xff;
}
return out;
},
}) satisfies P.CoderType<string>
);
const NameString = /* @__PURE__ */ ASN1.choice({
utf8: UTF8String,
printable: PrintableString,
teletex: TeletexString,
ia5: IA5,
bmp: BMPString,
visible: VisibleString,
numeric: NumericString,
});
const ATTR_NAME_OID: Record<string, string> = {
'2.5.4.3': 'commonName',
'2.5.4.7': 'localityName',
'2.5.4.32': 'owner',
'2.5.4.42': 'givenName',
'2.5.4.106': 'otherName',
};
const QC_STATEMENT_OID: Record<string, string> = {
'0.4.0.1862.1.1': 'etsiQcCompliance',
};
const CERT_ANY_TAG = {
bool: 0x01,
int: 0x02,
oid: 0x06,
octet: 0x04,
utc: 0x17,
generalized: 0x18,
} as const;
const CertAnyCodec = /* @__PURE__ */ P.apply(TLVNodeCodec, {
encode: (n: TLVNode): CertAny => {
const der = TLVNodeCodec.encode(n);
if (n.tag === CERT_ANY_TAG.bool) return { TAG: 'bool', data: ASN1Bool.decode(der) };
if (n.tag === CERT_ANY_TAG.int) return { TAG: 'int', data: ASN1.Integer.decode(der) };
if (n.tag === CERT_ANY_TAG.oid) {
const oid = ASN1.OID.decode(der);
return { TAG: 'oid', data: { oid, name: ATTR_NAME_OID[oid] } };
}
if (n.tag === CERT_ANY_TAG.octet) return { TAG: 'octet', data: ASN1.OctetString.decode(der) };
if (n.tag === CERT_ANY_TAG.utc || n.tag === CERT_ANY_TAG.generalized)
return { TAG: 'time', data: Time.decode(der) };
if (
n.tag === UTF8String.tagByte ||
n.tag === PrintableString.tagByte ||
n.tag === TeletexString.tagByte ||
n.tag === IA5.tagByte ||
n.tag === BMPString.tagByte ||
n.tag === VisibleString.tagByte ||
n.tag === NumericString.tagByte
)
return { TAG: 'text', data: NameString.decode(der) };
return { TAG: 'raw', data: n };
},
decode: (x: CertAny): TLVNode => {
if (x.TAG === 'raw') return x.data;
if (x.TAG === 'text') return TLVNodeCodec.decode(NameString.encode(x.data));
if (x.TAG === 'oid') return TLVNodeCodec.decode(ASN1.OID.encode(x.data.oid));
if (x.TAG === 'int') return TLVNodeCodec.decode(ASN1.Integer.encode(x.data));
if (x.TAG === 'bool') return TLVNodeCodec.decode(ASN1Bool.encode(x.data));
if (x.TAG === 'time') return TLVNodeCodec.decode(Time.encode(x.data));
return TLVNodeCodec.decode(ASN1.OctetString.encode(x.data));
},
}) satisfies P.CoderType<CertAny>;
const NameAttr = /* @__PURE__ */ (() => ASN1.sequence({ oid: ASN1.OID, value: NameString }))();
const X509Name = /* @__PURE__ */ ASN1.sequence({
rdns: /* @__PURE__ */ P.array(null, /* @__PURE__ */ ASN1.set(NameAttr)),
});
const X509Validity = /* @__PURE__ */ ASN1.sequence({ notBefore: Time, notAfter: Time });
// RFC 5912 (PKIX1Explicit-2009): Extension.
const X509Ext = /* @__PURE__ */ (() =>
ASN1.sequence({ oid: ASN1.OID, rest: /* @__PURE__ */ P.bytes(null) }))();
// RFC 5912 (PKIX1Explicit-2009): SubjectPublicKeyInfo.
const X509SPKI = /* @__PURE__ */ (() =>
ASN1.sequence({
algorithm: AlgorithmIdentifier,
publicKey: ASN1.BitString,
}))();
// RFC 5912 (PKIX1Explicit-2009): TBSCertificate.
const X509TBSCertificate = /* @__PURE__ */ (() =>
ASN1.sequence({
version: /* @__PURE__ */ ASN1.optional(/* @__PURE__ */ ASN1.explicit(0, ASN1.Integer)),
serial: ASN1.Integer,
signature: AlgorithmIdentifier,
issuer: X509Name,
validity: X509Validity,
subject: X509Name,
spki: X509SPKI,
issuerUniqueID: /* @__PURE__ */ ASN1.optional(/* @__PURE__ */ ASN1.implicit(1, ASN1.BitString)),
subjectUniqueID: /* @__PURE__ */ ASN1.optional(
/* @__PURE__ */ ASN1.implicit(2, ASN1.BitString)
),
extensions: /* @__PURE__ */ ASN1.optional(
/* @__PURE__ */ ASN1.explicit(
3,
/* @__PURE__ */ ASN1.sequence({ list: /* @__PURE__ */ P.array(null, X509Ext) })
)
),
}))();
// RFC 5912 (PKIX1Explicit-2009): Certificate.
const X509Certificate = /* @__PURE__ */ (() =>
ASN1.sequence({
tbs: X509TBSCertificate,
sigAlg: AlgorithmIdentifier,
sig: ASN1.BitString,
}))();
const X509C: {
Name: P.CoderType<NameCodec>;
TBSCertificate: P.CoderType<TBSCertificateCodec>;
Certificate: P.CoderType<CertificateCodec>;
} = /* @__PURE__ */ (() => ({
Name: X509Name,
TBSCertificate: X509TBSCertificate,
Certificate: X509Certificate,
}))();
type AttributeCodec = { oid: string; values: Uint8Array[] };
type SignerIdentifierCodec =
| { TAG: 'issuerSerial'; data: { issuer: NameCodec; serial: bigint } }
| { TAG: 'subjectKeyIdentifier'; data: Uint8Array };
type SignerInfoCodec = {
version: bigint;
sid: SignerIdentifierCodec;
digestAlg: AlgorithmIdentifierCodec;
signedAttrs: AttributeCodec[] | undefined;
signatureAlg: AlgorithmIdentifierCodec;
signature: Uint8Array;
unsignedAttrs: AttributeCodec[] | undefined;
};
type SignedDataCodec = {
version: bigint;
digestAlgorithms: AlgorithmIdentifierCodec[];
encapContentInfo: { eContentType: string; eContent: Uint8Array | undefined };
certificates: CMSCertificateChoiceCodec[] | undefined;
crls: CMSRevocationInfoChoiceCodec[] | undefined;
signerInfos: SignerInfoCodec[];
};
type ContentInfoCodec = { contentType: string; content: Uint8Array };
type CMSCertificateChoiceCodec =
| { TAG: 'certificate'; data: P.UnwrapCoder<typeof X509C.Certificate> }
| { TAG: 'extendedCertificate'; data: Uint8Array }
| { TAG: 'v1AttrCert'; data: Uint8Array }
| { TAG: 'v2AttrCert'; data: Uint8Array }
| { TAG: 'other'; data: Uint8Array };
type CMSRevocationInfoChoiceCodec =
| {
TAG: 'crl';
data: {
tbsCertList: Uint8Array;
signatureAlgorithm: AlgorithmIdentifierCodec;
signatureValue: Uint8Array;
};
}
| { TAG: 'other'; data: { format: string; info: Uint8Array } };
// RFC 5652 section 10.2.2: CertificateChoices.
const CMSCertificateChoices: P.CoderType<CMSCertificateChoiceCodec> = /* @__PURE__ */ (() =>
ASN1.choice({
certificate: X509C.Certificate,
extendedCertificate: tagged(0xa0, P.bytes(null)),
// RFC 5652 section 12.2: ACv1 module; parsed as opaque branch and not consumed by signer-cert selection.
v1AttrCert: tagged(0xa1, P.bytes(null)),
v2AttrCert: tagged(0xa2, P.bytes(null)),
other: tagged(0xa3, P.bytes(null)),
}))();
// RFC 5652 section 10.2.1: RevocationInfoChoice and OtherRevocationInfoFormat.
const CMSCertificateList = /* @__PURE__ */ (() =>
ASN1.sequence({
tbsCertList: RawTLV,
signatureAlgorithm: AlgorithmIdentifier,
signatureValue: ASN1.BitString,
}))();
const CMSOtherRevocationInfoFormat = /* @__PURE__ */ (() =>
ASN1.sequence({ format: ASN1.OID, info: RawTLV }))();
const CMSRevocationInfoChoice: P.CoderType<CMSRevocationInfoChoiceCodec> = /* @__PURE__ */ (() =>
ASN1.choice({
crl: CMSCertificateList,
other: ASN1.implicit(1, CMSOtherRevocationInfoFormat),
}))();
// RFC 5652 sections 10.1.1 and 10.1.2: DigestAlgorithmIdentifier/SignatureAlgorithmIdentifier ::= AlgorithmIdentifier.
// RFC 5652 section 5.3: Attribute ::= SEQUENCE { attrType OBJECT IDENTIFIER, attrValues SET OF AttributeValue }.
const CMSAttribute = /* @__PURE__ */ (() =>
P.validate(ASN1.sequence({ oid: ASN1.OID, values: ASN1.set(RawTLV) }), (a) => {
// RFC 5652 section 11.1: content-type attrValues is SET SIZE (1) OF AttributeValue.
// RFC 5652 section 11.2: message-digest attrValues is SET SIZE (1) OF AttributeValue.
// RFC 5652 section 11.3: signing-time attrValues is SET SIZE (1) OF AttributeValue.
const name = CMS_SIGNED_ATTR_NAME[a.oid as keyof typeof CMS_SIGNED_ATTR_NAME] || '';
if (name && a.values.length !== 1)
throw new Error(`${name} attribute must have exactly one value, got ${a.values.length}`);
return a;
}))() satisfies P.CoderType<AttributeCodec>;
// RFC 5652 section 10.2.4 (used by section 5.3 SignerIdentifier): IssuerAndSerialNumber.
const CMSIssuerAndSerial = /* @__PURE__ */ (() =>
ASN1.sequence({
issuer: X509C.Name,
serial: ASN1.Integer,
}))();
// RFC 5652 section 5.3: SignerIdentifier (IssuerAndSerialNumber / SubjectKeyIdentifier).
const CMSSignerIdentifier = /* @__PURE__ */ (() =>
ASN1.choice({
issuerSerial: CMSIssuerAndSerial,
subjectKeyIdentifier: ASN1.implicit(0, ASN1.OctetString),
}))();
// RFC 5652 section 5.3: SignerInfo.
const CMSSignerInfo = /* @__PURE__ */ (() =>
ASN1.sequence({
version: ASN1.Integer,
sid: CMSSignerIdentifier,
digestAlg: AlgorithmIdentifier,
signedAttrs: ASN1.optional(ASN1.implicit(0, ASN1.set(CMSAttribute))),
signatureAlg: AlgorithmIdentifier,
signature: ASN1.OctetString,
unsignedAttrs: ASN1.optional(ASN1.implicit(1, ASN1.set(CMSAttribute))),
}))();
// RFC 5652 section 5.2: EncapsulatedContentInfo.
const CMSEncapContentInfo = /* @__PURE__ */ (() =>
ASN1.sequence({
eContentType: ASN1.OID,
eContent: ASN1.optional(ASN1.explicit(0, ASN1.OctetString)),
}))();
// RFC 5652 section 5.1: SignedData.
const CMSSignedData: P.CoderType<SignedDataCodec> = /* @__PURE__ */ (() =>
ASN1.sequence({
version: ASN1.Integer,
digestAlgorithms: ASN1.set(AlgorithmIdentifier),
encapContentInfo: CMSEncapContentInfo,
// RFC 5652 section 10.2.3: CertificateSet ::= SET OF CertificateChoices.
certificates: ASN1.optional(ASN1.implicit(0, ASN1.set(CMSCertificateChoices))),
crls: ASN1.optional(ASN1.implicit(1, ASN1.set(CMSRevocationInfoChoice))),
signerInfos: ASN1.set(CMSSignerInfo),
}))();
const CMS_CONTENT_TYPE_NAME_TO_OID = {
data: '1.2.840.113549.1.7.1',
signedData: '1.2.840.113549.1.7.2',
envelopedData: '1.2.840.113549.1.7.3',
} as const;
const CMS_CONTENT_TYPE_OID_TO_NAME = /* @__PURE__ */ (() =>
Object.fromEntries(
Object.entries(CMS_CONTENT_TYPE_NAME_TO_OID).map(([k, v]) => [v, k])
) as Record<string, string>)();
const cmsContentTypeOID = (v: string): string =>
oidValue(CMS_CONTENT_TYPE_NAME_TO_OID as Record<string, string>, v, 'CMS contentType');
// RFC 5652 section 3: ContentInfo.
const CMSContentInfo = /* @__PURE__ */ (() =>
P.apply(
ASN1.sequence({
contentType: ASN1.OID,
content: ASN1.explicit(0, RawTLV),
}),
{
encode: (x: { contentType: string; content: Uint8Array }): ContentInfoCodec => ({
contentType: oidName(CMS_CONTENT_TYPE_OID_TO_NAME, x.contentType),
content: x.content,
}),
decode: (x: ContentInfoCodec): { contentType: string; content: Uint8Array } => ({
contentType: cmsContentTypeOID(x.contentType),
content: x.content,
}),
}
))();
const CMSX: {
AlgorithmIdentifier: P.CoderType<AlgorithmIdentifierCodec>;
Attribute: P.CoderType<AttributeCodec>;
SignerInfo: P.CoderType<SignerInfoCodec>;
SignedData: P.CoderType<SignedDataCodec>;
ContentInfo: P.CoderType<ContentInfoCodec>;
} = /* @__PURE__ */ (() => ({
AlgorithmIdentifier: AlgorithmIdentifier,
Attribute: CMSAttribute,
SignerInfo: CMSSignerInfo,
SignedData: CMSSignedData,
ContentInfo: CMSContentInfo,
}))();
// micro-packed coders for full X.509 cert decode/encode, same exposure style as DERUtils in convert.ts
/**
* Low-level X.509 coders used by the higher-level APIs.
* @example
* Use the low-level coders when you need to encode or decode individual X.509 structures.
* ```ts
* import { CERTUtils } from 'micro-key-producer/x509.js';
* CERTUtils.Name.encode({
* rdns: [[{ oid: '2.5.4.3', value: { TAG: 'utf8', data: 'example.com' } }]],
* });
* ```
*/
export const CERTUtils: {
Name: typeof X509C.Name;
TBSCertificate: typeof X509C.TBSCertificate;
Certificate: typeof X509C.Certificate;
} = /* @__PURE__ */ (() => ({
Name: X509C.Name,
TBSCertificate: X509C.TBSCertificate,
Certificate: X509C.Certificate,
}))();
const ASN1BoolInner = /* @__PURE__ */ P.wrap({
encodeStream(w, v: boolean) {
w.byte(v ? 0xff : 0x00);
},
decodeStream(r): boolean {
const b = r.byte();
if (!r.isEnd()) throw new Error('BOOLEAN length must be 1');
return b !== 0;
},
});
const ASN1Bool = /* @__PURE__ */ (() => ({
tagByte: 0x01,
tagBytes: [0x01],
constructed: 0,
inner: ASN1BoolInner,
...P.wrap({
encodeStream(w, v: boolean) {
w.bytes(Uint8Array.from([0x01, 0x01, v ? 0xff : 0x00]));
},
decodeStream(r): boolean {
const t = RawTLV.decodeStream(r);
if (t.length !== 3 || t[0] !== 0x01 || t[1] !== 0x01)
throw new Error('DER BOOLEAN must be 01 01 xx');
return t[2] !== 0;
},
}),
}))();
const ASN1BitStringInner = /* @__PURE__ */ P.struct({
unused: P.U8,
bytes: /* @__PURE__ */ P.bytes(null),
});
const ASN1BitStringRaw = /* @__PURE__ */ (() => ({
tagByte: 0x03,
tagBytes: [0x03],
constructed: 0,
inner: ASN1BitStringInner,
...P.wrap({
encodeStream(w, v: { unused: number; bytes: Uint8Array }) {
w.bytes(TLV.encode({ tag: 0x03, value: ASN1BitStringInner.encode(v) }));
},
decodeStream(r): { unused: number; bytes: Uint8Array } {
const t = TLV.decodeStream(r);
if (t.tag !== 0x03) throw new Error('expected BIT STRING');
const d = ASN1BitStringInner.decode(t.value);
if (d.unused > 7) throw new Error(`BIT STRING invalid unused bits: ${d.unused}`);
return d;
},
}),
}))();
// Generic IP coders (not ASN.1-specific): bytes <-> textual address.
const IPv4: P.CoderType<string> = /* @__PURE__ */ P.apply(/* @__PURE__ */ P.bytes(4), {
encode: (b: Uint8Array): string => `${b[0]}.${b[1]}.${b[2]}.${b[3]}`,
decode: (s: string): Uint8Array => {
const p = s.split('.');
if (p.length !== 4) throw new Error(`invalid IPv4 address ${s}`);
const out = new Uint8Array(4);
for (let i = 0; i < 4; i++) {
if (!/^[0-9]+$/.test(p[i])) throw new Error(`invalid IPv4 address ${s}`);
const n = Number(p[i]);
if (!Number.isInteger(n) || n < 0 || n > 255) throw new Error(`invalid IPv4 address ${s}`);
out[i] = n;
}
return out;
},
}) satisfies P.CoderType<string>;
// Generic IP coders (not ASN.1-specific): bytes <-> textual address.
const IPv6: P.CoderType<string> = /* @__PURE__ */ P.apply(/* @__PURE__ */ P.bytes(16), {
encode: (b: Uint8Array): string => {
const w = new Array<number>(8);
for (let i = 0; i < 8; i++) w[i] = (b[i * 2] << 8) | b[i * 2 + 1];
let bestAt = -1;
let bestLen = 0;
for (let i = 0; i < 8; ) {
if (w[i] !== 0) {
i++;
continue;
}
let j = i;
while (j < 8 && w[j] === 0) j++;
const len = j - i;
if (len > bestLen && len > 1) {
bestLen = len;
bestAt = i;
}
i = j;
}
const hexw = w.map((x) => x.toString(16));
if (bestAt < 0) return hexw.join(':');
const left = hexw.slice(0, bestAt).join(':');
const right = hexw.slice(bestAt + bestLen).join(':');
if (!left && !right) return '::';
if (!left) return `::${right}`;
if (!right) return `${left}::`;
return `${left}::${right}`;
},
decode: (s: string): Uint8Array => {
if (s.includes(':::')) throw new Error(`invalid IPv6 address ${s}`);
if ((s.match(/::/g) || []).length > 1) throw new Error(`invalid IPv6 address ${s}`);
const [l, r] = s.split('::');
const lp = l ? l.split(':').filter((i) => i.length) : [];
const rp = r !== undefined && r ? r.split(':').filter((i) => i.length) : [];
if (
!lp.every((i) => /^[0-9a-fA-F]{1,4}$/.test(i)) ||
!rp.every((i) => /^[0-9a-fA-F]{1,4}$/.test(i))
)
throw new Error(`invalid IPv6 address ${s}`);
const total = lp.length + rp.l