acme-http-01-azure-key-vault-middleware
Version:
Use Let's Encrypt and other RFC 8555 providers natively and securely on Azure
240 lines (175 loc) • 7.37 kB
JavaScript
const { CertificateClient } = require('@azure/keyvault-certificates');
const { SecretClient } = require('@azure/keyvault-secrets');
const debug = require('debug')('acme:cli');
const forge = require('node-forge');
const ACMEClient = require('./ACMEClient');
const createAzureKeyVaultURL = require('../util/createAzureKeyVaultURL');
const createCertificationRequest = require('../util/createCertificationRequest');
const createChallengeSecretName = require('../util/createChallengeSecretName');
const fromBase64URL = require('../util/fromBase64URL');
const CHECK_ORDER_LOOP = 5;
const CHECK_ORDER_INTERVAL = 1000;
const LETS_ENCRYPT_STAGING_DIRECTORY_URL = 'https://acme-staging-v02.api.letsencrypt.org/directory';
async function order({
acmeAccountContacts,
acmeAccountKeyName,
acmeAccountTermsOfServiceAgreed,
acmeDirectoryURL = LETS_ENCRYPT_STAGING_DIRECTORY_URL,
azureCredential,
azureKeyVaultName,
domains,
sslCertificateKeyVaultName
}) {
if (!acmeAccountContacts) {
throw new Error('"acmeAccountContacts" must be specified');
} else if (!acmeAccountKeyName) {
throw new Error('"acmeAccountKeyName" must be specified');
} else if (!acmeAccountTermsOfServiceAgreed) {
throw new Error('"acmeAccountTermsOfServiceAgreed" must be specified');
} else if (!azureCredential) {
throw new Error('"azureCredential" must be specified');
} else if (!azureKeyVaultName) {
throw new Error('"azureKeyVaultName" must be specified');
} else if (!domains) {
throw new Error('"domains" must be specified');
} else if (!sslCertificateKeyVaultName) {
throw new Error('"sslCertificateKeyVaultName" must be specified');
}
debug(`creating a new ACME account or signing into existing one`);
const acmeClient = new ACMEClient({
acmeAccountKeyName,
acmeDirectoryURL,
azureCredential,
azureKeyVaultName
});
console.log('Creating or signing into ACME provider.');
try {
await acmeClient.newAccount({
contact: acmeAccountContacts,
termsOfServiceAgreed: acmeAccountTermsOfServiceAgreed
});
} catch (err) {
console.error(`Failed to create a new account or sign into existing one.`);
throw err;
}
let orderResult;
console.log('Creating a new certificate order.');
try {
orderResult = await acmeClient.newOrder({
identifiers: domains.map(domain => ({ type: 'dns', value: domain.trim() }))
});
} catch (err) {
console.error(`Failed to create a new order`);
throw err;
}
const { _location: orderURL, authorizations, expires, finalize: finalizeURL, status: orderStatus } = orderResult;
debug(`new order opened`, orderResult);
console.log(`Order created at ${orderURL}.`);
let orderReady = orderStatus === 'ready';
if (orderStatus !== 'ready') {
console.log(`Preparing HTTP-01 challenge responses.`);
const secretClient = new SecretClient(createAzureKeyVaultURL(azureKeyVaultName), azureCredential);
// Since anti-replay nonce will block when reusing a nonce, we cannot use Promise.all to parallel the call.
for (let authorizationURL of authorizations) {
let authorizeResult;
try {
authorizeResult = await acmeClient.authorize(authorizationURL);
} catch (err) {
console.error(`Failed to get authorization information from ${authorizationURL}.`);
throw err;
}
const { challenges, identifier } = authorizeResult;
const http01Challenge = challenges.find(({ type }) => type === 'http-01');
if (!http01Challenge) {
throw new Error('No HTTP-01 challenge found, ACME server must support HTTP-01 challenges');
}
debug(`got HTTP-01 challenge for ${identifier.type}:${identifier.value}`, {
authorizationURL,
http01Challenge,
identifier
});
const { token } = http01Challenge;
const response = await acmeClient.prepareHTTP01ChallengeResponse(token);
try {
await secretClient.setSecret(createChallengeSecretName(token), '', {
expiresOn: new Date(expires),
tags: {
response
}
});
} catch (err) {
console.error(`Failed to upload HTTP-01 challenge response to Azure Key Vault as a secret.`);
throw err;
}
debug(`signaling readiness for HTTP-01 challenge`);
try {
await acmeClient.post(http01Challenge.url);
} catch (err) {
console.error(`Failed to send HTTP-01 challenge acceptance.`);
throw err;
}
}
}
if (!orderReady) {
console.log('Waiting for order to become ready.');
for (let i = 0; i < CHECK_ORDER_LOOP; i++) {
await new Promise(resolve => setTimeout(resolve, CHECK_ORDER_INTERVAL));
debug('checking order status');
let orderResult;
try {
orderResult = await acmeClient.getOrder(orderURL);
} catch (err) {
console.error(`Failed to get order status for order ${orderURL}`);
throw err;
}
debug('order status', orderResult);
if (orderResult.status === 'ready') {
orderReady = true;
break;
}
}
}
if (!orderReady) {
console.log('Order is not ready after retries. Please check if web server is responding HTTP-01 correctly.');
throw new Error('order is not ready');
}
console.log(`Order is ready for pickup (finalize) at ${finalizeURL}.`);
debug('creating certificate signing request');
const keyPair = forge.pki.rsa.generateKeyPair(2048);
const certificationRequest = await createCertificationRequest(keyPair, domains);
debug(`finalizing order`, { finalizeURL });
let finalizeResult;
try {
finalizeResult = await acmeClient.finalize(finalizeURL, certificationRequest);
} catch (err) {
console.error(`Failed to finalize order at ${finalizeURL}`);
throw err;
}
const { certificate: certificateURL, status: finalizeStatus } = finalizeResult;
if (finalizeStatus !== 'valid') {
console.error(`Received invalid finalize response with status "${finalizeStatus}".`);
throw new Error(`invalid finalize response`);
}
console.log(`Downloading certificate from ${certificateURL}.`);
const certificateInPEM = await acmeClient.downloadCertificate(certificateURL);
const certificate = forge.pki.certificateFromPem(certificateInPEM);
console.log(
`Certificate downloaded, serial number is ${
certificate.serialNumber
} and will expires at ${certificate.validity.notAfter.toISOString()}.`
);
debug(`certificate downloaded, archiving to PKCS #12 with private key`, certificate);
const pkcs12InASN1 = forge.pkcs12.toPkcs12Asn1(keyPair.privateKey, certificate, '');
const pkcs12InDER = forge.asn1.toDer(pkcs12InASN1).getBytes();
const pkcs12 = fromBase64URL(forge.util.encode64(pkcs12InDER));
const certificateClient = new CertificateClient(createAzureKeyVaultURL(azureKeyVaultName), azureCredential);
console.log(`Uploading certificate to Azure Key Vault as "${sslCertificateKeyVaultName}".`);
try {
await certificateClient.importCertificate(sslCertificateKeyVaultName, pkcs12);
} catch (err) {
console.error(`Failed to upload certificate to Azure Key Vault.`);
throw err;
}
console.log(`Certificate uploaded to Azure Key Vault as "${sslCertificateKeyVaultName}".`);
}
module.exports = { order };