diff --git a/src/lib/keyvault.ts b/src/lib/keyvault.ts index c83f3a3..8a138ae 100644 --- a/src/lib/keyvault.ts +++ b/src/lib/keyvault.ts @@ -1,6 +1,7 @@ import { TokenCredential } from '@azure/identity'; import { CertificateClient, + CertificatePolicy, KeyVaultCertificateWithPolicy, } from '@azure/keyvault-certificates'; import { SecretClient } from '@azure/keyvault-secrets'; @@ -48,24 +49,17 @@ export class KeyVaultStore { async importCertificate(name: string, cert: string | Buffer, format: 'pem' | 'pfx' = 'pem', password?: string): Promise { const certBuffer = typeof cert === 'string' ? Buffer.from(cert) : cert; - // The high-level CertificateClient spreads `policy` into import params but the - // generated serializer reads `certificatePolicy` — a key mismatch that silently - // drops content_type from the REST body. Call the internal client directly so - // secret_props.content_type reaches Azure (required for PFX; without it Azure - // defaults to PEM parsing and rejects binary PFX data). - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const internalClient = (this.certClient as any).client; - await internalClient.importCertificate(name, { - base64EncodedCertificate: format === 'pem' - ? certBuffer.toString('ascii') - : certBuffer.toString('base64'), - password, - certificatePolicy: { - secretProperties: { - contentType: format === 'pfx' ? 'application/x-pkcs12' : 'application/x-pem-file', - }, - }, - }, {}); + const contentType = format === 'pfx' ? 'application/x-pkcs12' : 'application/x-pem-file'; + try { + // When a certificate already exists, Azure validates the incoming bytes against + // its stored policy's content_type. Updating the policy first tells Azure to + // expect the new format; without this, converting PEM→PFX (or vice-versa) + // fails because Azure tries to parse binary PFX data as PEM. + await this.certClient.updateCertificatePolicy(name, { contentType } as CertificatePolicy); + } catch { + // Certificate doesn't exist yet — no policy to update, proceed to import. + } + await this.certClient.importCertificate(name, certBuffer, { password }); } } diff --git a/src/lib/pfx.ts b/src/lib/pfx.ts index 59a0ba2..c92a75e 100644 --- a/src/lib/pfx.ts +++ b/src/lib/pfx.ts @@ -32,9 +32,15 @@ export function pfxToPem(pfxBuffer: Buffer, password = ''): PemBundle { if (!keyBag?.key) throw new Error('No private key found in PFX'); if (allCertBags.length === 0) throw new Error('No certificates found in PFX'); + // Export as PKCS#8 — Azure Key Vault rejects PKCS#1 (BEGIN RSA PRIVATE KEY) in PEM imports + const pkcs8Asn1 = forge.pki.wrapRsaPrivateKey(forge.pki.privateKeyToAsn1(keyBag.key)); + const pkcs8Der = forge.asn1.toDer(pkcs8Asn1).getBytes(); + const pkcs8B64 = forge.util.encode64(pkcs8Der).match(/.{1,64}/g)!.join('\n'); + const privateKeyPem = `-----BEGIN PRIVATE KEY-----\n${pkcs8B64}\n-----END PRIVATE KEY-----\n`; + const [first, ...rest] = allCertBags.map(bag => forge.pki.certificateToPem(bag.cert!)); return { - privateKeyPem: forge.pki.privateKeyToPem(keyBag.key), + privateKeyPem, certPem: first, chainPem: rest.join(''), };