// 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); });