Files
azure-acme-provisioner/docs/bug-workaround.ts

103 lines
4.2 KiB
TypeScript

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