diff --git a/package-lock.json b/package-lock.json index 222566a..152eaa1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "dependencies": { "@peculiar/asn1-schema": "^2.10.0", "@peculiar/asn1-x509": "^2.10.0", - "@peculiar/x509": "^1.14.0" + "@peculiar/x509": "^2.1.0", + "reflect-metadata": "^0.2.2" }, "devDependencies": { "@axe-core/playwright": "^4.13.0", @@ -72,6 +73,22 @@ "url": "https://github.com/sponsors/oxc-project" } }, + "node_modules/@peculiar/asn1-asym-key": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-asym-key/-/asn1-asym-key-2.10.0.tgz", + "integrity": "sha512-1R8xUTeqcUTSMLFTTCzJyFuKiq3y25EQKs9DezUetJ8qMqbJyxo3XkzykiwwT/Xzlt5/vRzS/sW4M1r+pq1FTg==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-pkcs8": "^2.10.0", + "@peculiar/asn1-schema": "^2.10.0", + "@peculiar/asn1-x509": "^2.10.0", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/@peculiar/asn1-cms": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.10.0.tgz", @@ -228,6 +245,22 @@ "node": ">=14" } }, + "node_modules/@peculiar/asn1-x509-post-quantum": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-post-quantum/-/asn1-x509-post-quantum-2.10.0.tgz", + "integrity": "sha512-cXKcZ30wKbUkRK5RK4Z3W1imewXvHAB9apEAuIqJ0OOkOQCpJrwLOuN38QlsEXqOI+3gtUv8Kf3UZohUmCbf3w==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-asym-key": "^2.10.0", + "@peculiar/asn1-schema": "^2.10.0", + "@peculiar/asn1-x509": "^2.10.0", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/@peculiar/utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", @@ -238,20 +271,20 @@ } }, "node_modules/@peculiar/x509": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz", - "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-2.1.0.tgz", + "integrity": "sha512-IYbg1R03CSQGWwl24kGyqrdVtixNSbRaDvBg1r5wyYjTP+VwPQkka1BzTgU5+vxiuwqg04OxdvdJ1xYYFIdUSA==", "license": "MIT", "dependencies": { - "@peculiar/asn1-cms": "^2.6.0", - "@peculiar/asn1-csr": "^2.6.0", - "@peculiar/asn1-ecc": "^2.6.0", - "@peculiar/asn1-pkcs9": "^2.6.0", - "@peculiar/asn1-rsa": "^2.6.0", - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.0", + "@peculiar/asn1-cms": "^2.9.4", + "@peculiar/asn1-csr": "^2.9.4", + "@peculiar/asn1-ecc": "^2.9.4", + "@peculiar/asn1-pkcs9": "^2.9.4", + "@peculiar/asn1-rsa": "^2.9.4", + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "@peculiar/asn1-x509-post-quantum": "^2.9.4", "pvtsutils": "^1.3.6", - "reflect-metadata": "^0.2.2", "tslib": "^2.8.1", "tsyringe": "^4.10.0" }, diff --git a/package.json b/package.json index e4655c7..2fdfc3b 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "dependencies": { "@peculiar/asn1-schema": "^2.10.0", "@peculiar/asn1-x509": "^2.10.0", - "@peculiar/x509": "^1.14.0" + "@peculiar/x509": "^2.1.0", + "reflect-metadata": "^0.2.2" } } diff --git a/scripts/mint-toy-pki.mjs b/scripts/mint-toy-pki.mjs index fb6bb9a..6c46061 100644 --- a/scripts/mint-toy-pki.mjs +++ b/scripts/mint-toy-pki.mjs @@ -34,6 +34,8 @@ * deliberately, and update that file when you do. */ +// The standalone entry point must initialize X.509 2 independently of the UI. +import 'reflect-metadata' import { webcrypto } from 'node:crypto' import { writeFileSync } from 'node:fs' import { fileURLToPath } from 'node:url' diff --git a/src/pki/bootstrap.test.ts b/src/pki/bootstrap.test.ts new file mode 100644 index 0000000..951646d --- /dev/null +++ b/src/pki/bootstrap.test.ts @@ -0,0 +1,63 @@ +import { spawnSync } from 'node:child_process' +import { X509Certificate } from 'node:crypto' +import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, symlinkSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { describe, expect, it } from 'vitest' + +const root = fileURLToPath(new URL('../../', import.meta.url)) +const childEnvironment = { ...process.env, NODE_OPTIONS: '' } + +describe('X.509 entry points in a fresh process', () => { + it('initializes the application parser before loading the SDK, without a test setup polyfill', () => { + const parser = new URL('./parse.ts', import.meta.url).href + const certificate = fileURLToPath(new URL('../../certs/real-github-leaf.pem', import.meta.url)) + const result = spawnSync(process.execPath, ['--experimental-strip-types', '--input-type=module', '--eval', ` + import assert from 'node:assert/strict' + import { readFileSync } from 'node:fs' + assert.equal(typeof Reflect.getMetadata, 'undefined') + const { parsePem, commonName, fingerprint } = await import(${JSON.stringify(parser)}) + const cert = parsePem(readFileSync(${JSON.stringify(certificate)}, 'utf8')) + assert.equal(commonName(cert), 'github.com') + assert.equal(await fingerprint(cert), '46b601ee08b418cf8a3a1ebfe670ba5ce43bb05a917fa8b2dd087a30471cfc63') + console.log('cold parser and certificate KAT passed') + `], { cwd: root, env: childEnvironment, encoding: 'utf8', timeout: 30_000 }) + expect(result.error).toBeUndefined() + expect(result.status, result.stderr).toBe(0) + expect(result.stdout).toContain('cold parser and certificate KAT passed') + }) + + it('initializes the standalone minter and verifies all signatures in isolated scratch', () => { + // The real script rotates the toy keys. Copy it into owned scratch so this + // regression never replaces the vendored certificates or their KATs. + const scratch = mkdtempSync(join(tmpdir(), 'padlock-minter-bootstrap-')) + try { + mkdirSync(join(scratch, 'scripts')) + mkdirSync(join(scratch, 'certs')) + copyFileSync(join(root, 'scripts/mint-toy-pki.mjs'), join(scratch, 'scripts/mint-toy-pki.mjs')) + symlinkSync(join(root, 'node_modules'), join(scratch, 'node_modules')) + const result = spawnSync(process.execPath, ['scripts/mint-toy-pki.mjs'], { + cwd: scratch, env: childEnvironment, encoding: 'utf8', timeout: 30_000, + }) + expect(result.error).toBeUndefined() + expect(result.status, result.stderr).toBe(0) + expect(result.stdout.match(/\bverified\b/g)).toHaveLength(4) + expect(readdirSync(join(scratch, 'certs')).sort()).toEqual([ + 'toy-attacker-leaf.pem', 'toy-dv-ca.pem', 'toy-root.pem', 'toy-selfsigned-leaf.pem', + ]) + const cert = (name: string) => new X509Certificate(readFileSync(join(scratch, 'certs', name))) + const rootCert = cert('toy-root.pem') + const dv = cert('toy-dv-ca.pem') + const attacker = cert('toy-attacker-leaf.pem') + const selfSigned = cert('toy-selfsigned-leaf.pem') + expect(dv.verify(rootCert.publicKey)).toBe(true) + expect(attacker.verify(dv.publicKey)).toBe(true) + expect(rootCert.verify(rootCert.publicKey)).toBe(true) + expect(selfSigned.verify(selfSigned.publicKey)).toBe(true) + expect(attacker.subjectAltName).toBe('DNS:login.yourbank-security.example') + } finally { + rmSync(scratch, { recursive: true, force: true }) + } + }) +}) diff --git a/src/pki/parse.ts b/src/pki/parse.ts index 57b7e5a..736d579 100644 --- a/src/pki/parse.ts +++ b/src/pki/parse.ts @@ -1,3 +1,5 @@ +// X.509 2 requires Reflect metadata before its dependency-injection setup runs. +import 'reflect-metadata' import * as x509 from '@peculiar/x509' import type { Validity } from './types'