From 1eff869a698a2cae39537eafb4395745dda72c8f Mon Sep 17 00:00:00 2001 From: PekingSpades <180665176+PekingSpades@users.noreply.github.com> Date: Tue, 15 Sep 2026 20:40:34 +0800 Subject: [PATCH] fix: preserve external ca issuer encoding Keep the CA's encoded subject when issuing leaf certificates so clients can link the leaf to trusted CAs with UTF8String names. Cover both ordinary and certificate-transparency issuance. --- src/util/certificates.ts | 4 ++- test/certificate-issuer.spec.ts | 57 +++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 test/certificate-issuer.spec.ts diff --git a/src/util/certificates.ts b/src/util/certificates.ts index 37d05ff32..44c81370b 100644 --- a/src/util/certificates.ts +++ b/src/util/certificates.ts @@ -451,7 +451,9 @@ class CA { } } const subjectDistinguishedName = new x509.Name(subjectJsonNameParams).toString(); - const issuerDistinguishedName = this.caCert.subject; + // Preserve the encoded name: text conversion can change ASN.1 string types, + // preventing clients that compare issuer/subject DER from building the chain. + const issuerDistinguishedName = this.caCert.subjectName; const notBefore = new Date(); notBefore.setDate(notBefore.getDate() - 1); // Valid from 24 hours ago diff --git a/test/certificate-issuer.spec.ts b/test/certificate-issuer.spec.ts new file mode 100644 index 000000000..cc81f27dd --- /dev/null +++ b/test/certificate-issuer.spec.ts @@ -0,0 +1,57 @@ +import { Buffer } from 'buffer'; +import * as x509 from '@peculiar/x509'; +import { AsnConvert } from '@peculiar/asn1-schema'; +import { Certificate } from '@peculiar/asn1-x509'; + +import { getCA } from '../src/util/certificates'; +import { expect, nodeOnly } from './test-utils'; + +nodeOnly(() => { + describe("External CA issuer encoding", () => { + for (const certificateTransparency of [false, true]) { + it(`preserves the encoded CA name with CT ${certificateTransparency ? 'enabled' : 'disabled'}`, async () => { + const keys = await crypto.subtle.generateKey( + { name: 'ECDSA', namedCurve: 'P-256' }, + true, + ['sign', 'verify'] + ); + + // ASCII text can legitimately use UTF8String, as OpenSSL-generated + // names do. Converting this name to text loses that distinction. + const name = new x509.Name([ + { C: [{ printableString: 'GB' }] }, + { O: [{ utf8String: 'Example Organization' }] }, + { CN: [{ utf8String: 'Example Test CA' }] } + ]); + const root = await x509.X509CertificateGenerator.createSelfSigned({ + name, + serialNumber: '01', + notBefore: new Date(Date.now() - 60_000), + notAfter: new Date(Date.now() + 86_400_000), + signingAlgorithm: { name: 'ECDSA', hash: 'SHA-256' }, + keys, + extensions: [ + new x509.BasicConstraintsExtension(true, undefined, true), + new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign, true) + ] + }); + const key = x509.PemConverter.encode( + await crypto.subtle.exportKey('pkcs8', keys.privateKey), + 'PRIVATE KEY' + ); + + const ca = await getCA({ key, cert: root.toString('pem'), certificateTransparency }); + const leaf = new x509.X509Certificate((await ca.generateCertificate('localhost')).cert); + const rootAsn = AsnConvert.parse(root.rawData, Certificate); + const leafAsn = AsnConvert.parse(leaf.rawData, Certificate); + + expect(leaf.issuer).to.equal(root.subject); + expect(Buffer.from(AsnConvert.serialize(leafAsn.tbsCertificate.issuer))) + .to.deep.equal(Buffer.from(AsnConvert.serialize(rootAsn.tbsCertificate.subject))); + expect(await leaf.verify({ publicKey: root })).to.equal(true); + expect(leaf.getExtension('1.3.6.1.4.1.11129.2.4.2') !== null) + .to.equal(certificateTransparency); + }); + } + }); +});