feat: add TypeScripts programs to reproduce and demonstrate workaround for importCertificate policy.contentType issue
This commit is contained in:
+13
-24
@@ -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
|
||||||
|
|||||||
@@ -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); });
|
||||||
@@ -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); });
|
||||||
Reference in New Issue
Block a user