UNPKG

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
/*! 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