feat: add TypeScripts programs to reproduce and demonstrate workaround for importCertificate policy.contentType issue

This commit is contained in:
2026-05-23 10:25:36 +02:00
parent d373c8a72d
commit 258231e58c
3 changed files with 211 additions and 24 deletions
+13 -24
View File
@@ -73,33 +73,22 @@ export function certificateImportParametersSerializer(item: CertificateImportPar
### Reproduction ### Reproduction
```typescript A self-contained runnable script is provided in [`docs/bug-reproduce.ts`](bug-reproduce.ts):
import { DefaultAzureCredential } from "@azure/identity";
import { CertificateClient } from "@azure/keyvault-certificates";
import { readFileSync } from "fs";
const credential = new DefaultAzureCredential(); ```sh
const client = new CertificateClient( KEYVAULT_NAME=<vault> npx tsx docs/bug-reproduce.ts
"https://<YOUR_VAULT>.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."
``` ```
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=<vault> 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 ### Fix
+96
View File
@@ -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=<vault> 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<void> {
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); });
+102
View File
@@ -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=<vault> 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<void> {
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); });