From 258231e58c7b3289d12910471446d9c27df2b9c7 Mon Sep 17 00:00:00 2001 From: Slawomir Koszewski Date: Sat, 23 May 2026 10:25:36 +0200 Subject: [PATCH] feat: add TypeScripts programs to reproduce and demonstrate workaround for importCertificate policy.contentType issue --- docs/BugReports.md | 37 ++++++--------- docs/bug-reproduce.ts | 96 ++++++++++++++++++++++++++++++++++++++ docs/bug-workaround.ts | 102 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 211 insertions(+), 24 deletions(-) create mode 100644 docs/bug-reproduce.ts create mode 100644 docs/bug-workaround.ts diff --git a/docs/BugReports.md b/docs/BugReports.md index 5fbcbdf..439e06b 100644 --- a/docs/BugReports.md +++ b/docs/BugReports.md @@ -73,33 +73,22 @@ export function certificateImportParametersSerializer(item: CertificateImportPar ### Reproduction -```typescript -import { DefaultAzureCredential } from "@azure/identity"; -import { CertificateClient } from "@azure/keyvault-certificates"; -import { readFileSync } from "fs"; +A self-contained runnable script is provided in [`docs/bug-reproduce.ts`](bug-reproduce.ts): -const credential = new DefaultAzureCredential(); -const client = new CertificateClient( - "https://.vault.azure.net", - credential, -); - -// Step 1: import a PEM certificate (works — Azure auto-detects PEM) -const pemBytes = Buffer.from(readFileSync("cert.pem", "utf8")); -await client.importCertificate("MyCert", pemBytes, { - policy: { contentType: "application/x-pem-file" }, -}); - -// Step 2: import the same certificate as PFX (fails — policy.contentType is dropped, -// Azure uses existing PEM policy and rejects the binary PFX bytes) -const pfxBytes = readFileSync("cert.pfx"); -await client.importCertificate("MyCert", pfxBytes, { - password: "pfx-password", - policy: { contentType: "application/x-pkcs12" }, -}); -// ^ throws: "The specified PEM X.509 certificate content is in an unexpected format." +```sh +KEYVAULT_NAME= npx tsx docs/bug-reproduce.ts ``` +The script generates a self-signed certificate, imports it as PEM (Step 1 — succeeds), then attempts to re-import it as PFX with `policy.contentType: "application/x-pkcs12"` (Step 2 — fails with the error above, confirming the bug). + +The workaround is demonstrated in [`docs/bug-workaround.ts`](bug-workaround.ts): + +```sh +KEYVAULT_NAME= npx tsx docs/bug-workaround.ts +``` + +This calls `updateCertificatePolicy()` before the PFX import to pre-set the stored `content_type`, allowing the import to succeed. + --- ### Fix diff --git a/docs/bug-reproduce.ts b/docs/bug-reproduce.ts new file mode 100644 index 0000000..2bddd64 --- /dev/null +++ b/docs/bug-reproduce.ts @@ -0,0 +1,96 @@ +// Reproduces the bug in @azure/keyvault-certificates: +// CertificateClient.importCertificate() silently drops policy.contentType, +// so Azure validates incoming bytes against the existing stored policy. +// Importing PFX into a certificate previously stored as PEM fails with: +// "The specified PEM X.509 certificate content is in an unexpected format." +// +// Usage: KEYVAULT_NAME= npx tsx docs/bug-reproduce.ts + +import { webcrypto, randomBytes } from 'node:crypto'; +import * as forge from 'node-forge'; +import { DefaultAzureCredential } from '@azure/identity'; +import { CertificateClient } from '@azure/keyvault-certificates'; + +const ALG: RsaHashedKeyGenParams = { + name: 'RSASSA-PKCS1-v1_5', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', +}; + +async function generateSelfSigned(cn: string): Promise<{ privateKeyPem: string; certPem: string }> { + const keyPair = await webcrypto.subtle.generateKey(ALG, true, ['sign', 'verify']); + + const privateKeyDer = await webcrypto.subtle.exportKey('pkcs8', keyPair.privateKey); + const privateKeyB64 = Buffer.from(privateKeyDer).toString('base64').match(/.{1,64}/g)!.join('\n'); + const privateKeyPem = `-----BEGIN PRIVATE KEY-----\n${privateKeyB64}\n-----END PRIVATE KEY-----\n`; + + const privateKey = forge.pki.privateKeyFromPem(privateKeyPem); + const publicKey = forge.pki.rsa.setPublicKey(privateKey.n, privateKey.e); + + const cert = forge.pki.createCertificate(); + cert.publicKey = publicKey; + cert.serialNumber = '01'; + cert.validity.notBefore = new Date(); + cert.validity.notAfter = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000); + const attrs = [{ name: 'commonName', value: cn }]; + cert.setSubject(attrs); + cert.setIssuer(attrs); + cert.sign(privateKey, forge.md.sha256.create()); + + return { privateKeyPem, certPem: forge.pki.certificateToPem(cert) }; +} + +function pemToPfx(privateKeyPem: string, certPem: string, password: string): Buffer { + const key = forge.pki.privateKeyFromPem(privateKeyPem); + const cert = forge.pki.certificateFromPem(certPem); + const p12 = forge.pkcs12.toPkcs12Asn1(key, [cert], password, { algorithm: '3des' }); + return Buffer.from(forge.asn1.toDer(p12).getBytes(), 'binary'); +} + +async function main(): Promise { + const vaultName = process.env.KEYVAULT_NAME; + if (!vaultName) { + console.error('Set KEYVAULT_NAME'); + process.exit(1); + } + const vaultUrl = `https://${vaultName}.vault.azure.net`; + + const hash = randomBytes(4).toString('hex'); + const CERT_NAME = `bug-reproduce-${hash}`; + const PFX_PASSWORD = 'test-password-123'; + + const credential = new DefaultAzureCredential(); + const client = new CertificateClient(vaultUrl, credential); + + console.log(`Generating self-signed certificate (${CERT_NAME})...`); + const { privateKeyPem, certPem } = await generateSelfSigned(CERT_NAME); + const pemBytes = Buffer.from(certPem + privateKeyPem); + const pfxBytes = pemToPfx(privateKeyPem, certPem, PFX_PASSWORD); + + console.log(`\nStep 1: importing '${CERT_NAME}' as PEM...`); + try { + await client.importCertificate(CERT_NAME, pemBytes, { + policy: { contentType: 'application/x-pem-file', issuerName: 'Unknown', subject: `CN=${CERT_NAME}` }, + }); + console.log(' OK — PEM import succeeded.'); + } catch (err: unknown) { + console.error(` FAILED — ${err instanceof Error ? err.message : err}`); + process.exit(1); + } + + console.log(`\nStep 2: re-importing '${CERT_NAME}' as PFX (policy.contentType should tell Azure to expect PFX)...`); + try { + await client.importCertificate(CERT_NAME, pfxBytes, { + password: PFX_PASSWORD, + policy: { contentType: 'application/x-pkcs12', issuerName: 'Unknown', subject: `CN=${CERT_NAME}` }, + }); + console.log(' OK — PFX import succeeded (bug may be fixed in this SDK version).'); + } catch (err: unknown) { + console.error(` FAILED — ${err instanceof Error ? err.message : err}`); + console.error('\n This confirms the bug: policy.contentType was silently dropped.'); + console.error(' Azure validated the PFX bytes against the existing PEM policy and rejected them.'); + } +} + +main().catch(err => { console.error(err); process.exit(1); }); diff --git a/docs/bug-workaround.ts b/docs/bug-workaround.ts new file mode 100644 index 0000000..a64cb36 --- /dev/null +++ b/docs/bug-workaround.ts @@ -0,0 +1,102 @@ +// Demonstrates the workaround for the bug in @azure/keyvault-certificates: +// Since importCertificate() silently drops policy.contentType, we call +// updateCertificatePolicy() first to shift the stored content_type to the +// new format before importing. Azure then validates incoming bytes correctly. +// +// Usage: KEYVAULT_NAME= npx tsx docs/bug-workaround.ts + +import { webcrypto, randomBytes } from 'node:crypto'; +import * as forge from 'node-forge'; +import { DefaultAzureCredential } from '@azure/identity'; +import { CertificateClient, CertificatePolicy } from '@azure/keyvault-certificates'; + +const ALG: RsaHashedKeyGenParams = { + name: 'RSASSA-PKCS1-v1_5', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', +}; + +async function generateSelfSigned(cn: string): Promise<{ privateKeyPem: string; certPem: string }> { + const keyPair = await webcrypto.subtle.generateKey(ALG, true, ['sign', 'verify']); + + const privateKeyDer = await webcrypto.subtle.exportKey('pkcs8', keyPair.privateKey); + const privateKeyB64 = Buffer.from(privateKeyDer).toString('base64').match(/.{1,64}/g)!.join('\n'); + const privateKeyPem = `-----BEGIN PRIVATE KEY-----\n${privateKeyB64}\n-----END PRIVATE KEY-----\n`; + + const privateKey = forge.pki.privateKeyFromPem(privateKeyPem); + const publicKey = forge.pki.rsa.setPublicKey(privateKey.n, privateKey.e); + + const cert = forge.pki.createCertificate(); + cert.publicKey = publicKey; + cert.serialNumber = '01'; + cert.validity.notBefore = new Date(); + cert.validity.notAfter = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000); + const attrs = [{ name: 'commonName', value: cn }]; + cert.setSubject(attrs); + cert.setIssuer(attrs); + cert.sign(privateKey, forge.md.sha256.create()); + + return { privateKeyPem, certPem: forge.pki.certificateToPem(cert) }; +} + +function pemToPfx(privateKeyPem: string, certPem: string, password: string): Buffer { + const key = forge.pki.privateKeyFromPem(privateKeyPem); + const cert = forge.pki.certificateFromPem(certPem); + const p12 = forge.pkcs12.toPkcs12Asn1(key, [cert], password, { algorithm: '3des' }); + return Buffer.from(forge.asn1.toDer(p12).getBytes(), 'binary'); +} + +async function main(): Promise { + const vaultName = process.env.KEYVAULT_NAME; + if (!vaultName) { + console.error('Set KEYVAULT_NAME'); + process.exit(1); + } + const vaultUrl = `https://${vaultName}.vault.azure.net`; + + const hash = randomBytes(4).toString('hex'); + const CERT_NAME = `bug-workaround-${hash}`; + const PFX_PASSWORD = 'test-password-123'; + + const credential = new DefaultAzureCredential(); + const client = new CertificateClient(vaultUrl, credential); + + console.log(`Generating self-signed certificate (${CERT_NAME})...`); + const { privateKeyPem, certPem } = await generateSelfSigned(CERT_NAME); + const pemBytes = Buffer.from(certPem + privateKeyPem); + const pfxBytes = pemToPfx(privateKeyPem, certPem, PFX_PASSWORD); + + console.log(`\nStep 1: importing '${CERT_NAME}' as PEM...`); + try { + await client.importCertificate(CERT_NAME, pemBytes, { + policy: { contentType: 'application/x-pem-file', issuerName: 'Unknown', subject: `CN=${CERT_NAME}` }, + }); + console.log(' OK — PEM import succeeded.'); + } catch (err: unknown) { + console.error(` FAILED — ${err instanceof Error ? err.message : err}`); + process.exit(1); + } + + console.log(`\nStep 2: updating policy to application/x-pkcs12 before importing PFX...`); + try { + await client.updateCertificatePolicy(CERT_NAME, { contentType: 'application/x-pkcs12', issuerName: 'Unknown', subject: `CN=${CERT_NAME}` } as CertificatePolicy); + console.log(' OK — policy updated.'); + } catch (err: unknown) { + console.error(` FAILED — ${err instanceof Error ? err.message : err}`); + process.exit(1); + } + + console.log(`\nStep 3: importing '${CERT_NAME}' as PFX...`); + try { + await client.importCertificate(CERT_NAME, pfxBytes, { password: PFX_PASSWORD }); + console.log(' OK — PFX import succeeded.'); + } catch (err: unknown) { + console.error(` FAILED — ${err instanceof Error ? err.message : err}`); + process.exit(1); + } + + console.log('\nWorkaround confirmed: updateCertificatePolicy() before import allows format change.'); +} + +main().catch(err => { console.error(err); process.exit(1); });