103 lines
4.2 KiB
TypeScript
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); });
|