mirror of https://github.com/certd/certd
🔱: [acme] sync upgrade with 7 commits [trident-sync]
CHANGELOG Fix tls-alpn-01 pebble test on Node v18+ Return correct tls-alpn-01 key authorization, tests Support tls-alpn-01 internal challenge verification Add tls-alpn-01 challenge test server support Add ALPN crypto utility methodspull/29/head
parent
08c1f338d5
commit
fc9e71bed2
|
@ -1,5 +1,12 @@
|
|||
# Changelog
|
||||
|
||||
## v5.3.0
|
||||
|
||||
* `added` Support and tests for satisfying `tls-alpn-01` challenges
|
||||
* `changed` Method `getChallengeKeyAuthorization()` now returns `$token.$thumbprint` when called with a `tls-alpn-01` challenge
|
||||
* Previously returned base64url encoded SHA256 digest of `$token.$thumbprint` erroneously
|
||||
* This change is not considered breaking since the previous behavior was incorrect
|
||||
|
||||
## v5.2.0 (2024-01-22)
|
||||
|
||||
* `fixed` Allow self-signed or invalid certs when validating `http-01` challenges that redirect to HTTPS - [#65](https://github.com/publishlab/node-acme-client/issues/65)
|
||||
|
|
|
@ -18,7 +18,8 @@ instance.defaults.headers.common['User-Agent'] = `node-${pkg.name}/${pkg.version
|
|||
/* Default ACME settings */
|
||||
instance.defaults.acmeSettings = {
|
||||
httpChallengePort: 80,
|
||||
httpsChallengePort: 443
|
||||
httpsChallengePort: 443,
|
||||
tlsAlpnChallengePort: 443
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -462,22 +462,19 @@ class AcmeClient {
|
|||
const thumbprint = keysum.digest('base64url');
|
||||
const result = `${challenge.token}.${thumbprint}`;
|
||||
|
||||
/**
|
||||
* https://tools.ietf.org/html/rfc8555#section-8.3
|
||||
*/
|
||||
|
||||
/* https://tools.ietf.org/html/rfc8555#section-8.3 */
|
||||
if (challenge.type === 'http-01') {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* https://tools.ietf.org/html/rfc8555#section-8.4
|
||||
* https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01
|
||||
*/
|
||||
/* https://tools.ietf.org/html/rfc8555#section-8.4 */
|
||||
if (challenge.type === 'dns-01') {
|
||||
return createHash('sha256').update(result).digest('base64url');
|
||||
}
|
||||
|
||||
if ((challenge.type === 'dns-01') || (challenge.type === 'tls-alpn-01')) {
|
||||
const shasum = createHash('sha256').update(result);
|
||||
return shasum.digest('base64url');
|
||||
/* https://tools.ietf.org/html/rfc8737 */
|
||||
if (challenge.type === 'tls-alpn-01') {
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new Error(`Unable to produce key authorization, unknown challenge type: ${challenge.type}`);
|
||||
|
|
|
@ -9,8 +9,12 @@ const { promisify } = require('util');
|
|||
const crypto = require('crypto');
|
||||
const jsrsasign = require('jsrsasign');
|
||||
|
||||
const randomInt = promisify(crypto.randomInt);
|
||||
const generateKeyPair = promisify(crypto.generateKeyPair);
|
||||
|
||||
/* https://datatracker.ietf.org/doc/html/rfc8737#section-6.1 */
|
||||
const alpnAcmeIdentifierOID = '1.3.6.1.5.5.7.1.31';
|
||||
|
||||
|
||||
/**
|
||||
* Determine key type and info by attempting to derive public key
|
||||
|
@ -231,7 +235,7 @@ exports.getPemBodyAsB64u = (pem) => {
|
|||
throw new Error('Unable to parse PEM body from string');
|
||||
}
|
||||
|
||||
/* First object, hex and back to b64 without new lines */
|
||||
/* Select first object, decode to hex and b64u */
|
||||
return jsrsasign.hextob64u(jsrsasign.pemtohex(chain[0]));
|
||||
};
|
||||
|
||||
|
@ -303,6 +307,28 @@ exports.readCsrDomains = (csrPem) => {
|
|||
};
|
||||
|
||||
|
||||
/**
|
||||
* Parse params from a single or chain of PEM encoded certificates
|
||||
*
|
||||
* @private
|
||||
* @param {buffer|string} certPem PEM encoded certificate or chain
|
||||
* @returns {object} Certificate params
|
||||
*/
|
||||
|
||||
function getCertificateParams(certPem) {
|
||||
const chain = splitPemChain(certPem);
|
||||
|
||||
if (!chain.length) {
|
||||
throw new Error('Unable to parse PEM body from string');
|
||||
}
|
||||
|
||||
/* Parse certificate */
|
||||
const obj = new jsrsasign.X509();
|
||||
obj.readCertPEM(chain[0]);
|
||||
return obj.getParam();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Read information from a certificate
|
||||
* If multiple certificates are chained, the first will be read
|
||||
|
@ -324,16 +350,7 @@ exports.readCsrDomains = (csrPem) => {
|
|||
*/
|
||||
|
||||
exports.readCertificateInfo = (certPem) => {
|
||||
const chain = splitPemChain(certPem);
|
||||
|
||||
if (!chain.length) {
|
||||
throw new Error('Unable to parse PEM body from string');
|
||||
}
|
||||
|
||||
/* Parse certificate */
|
||||
const obj = new jsrsasign.X509();
|
||||
obj.readCertPEM(chain[0]);
|
||||
const params = obj.getParam();
|
||||
const params = getCertificateParams(certPem);
|
||||
|
||||
return {
|
||||
issuer: {
|
||||
|
@ -462,7 +479,7 @@ function formatCsrAltNames(altNames) {
|
|||
* }, certificateKey);
|
||||
*/
|
||||
|
||||
exports.createCsr = async (data, keyPem = null) => {
|
||||
async function createCsr(data, keyPem = null) {
|
||||
if (!keyPem) {
|
||||
keyPem = await createPrivateRsaKey(data.keySize);
|
||||
}
|
||||
|
@ -517,10 +534,95 @@ exports.createCsr = async (data, keyPem = null) => {
|
|||
extreq: extensionRequests
|
||||
});
|
||||
|
||||
/* Sign CSR, get PEM */
|
||||
csr.sign();
|
||||
/* Done */
|
||||
const pem = csr.getPEM();
|
||||
return [keyPem, Buffer.from(pem)];
|
||||
}
|
||||
|
||||
exports.createCsr = createCsr;
|
||||
|
||||
|
||||
/**
|
||||
* Create a self-signed ALPN certificate for TLS-ALPN-01 challenges
|
||||
*
|
||||
* https://tools.ietf.org/html/rfc8737
|
||||
*
|
||||
* @param {object} authz Identifier authorization
|
||||
* @param {string} keyAuthorization Challenge key authorization
|
||||
* @param {string} [keyPem] PEM encoded CSR private key
|
||||
* @returns {Promise<buffer[]>} [privateKey, certificate]
|
||||
*
|
||||
* @example Create a ALPN certificate
|
||||
* ```js
|
||||
* const [alpnKey, alpnCertificate] = await acme.crypto.createAlpnCertificate(authz, keyAuthorization);
|
||||
* ```
|
||||
*
|
||||
* @example Create a ALPN certificate with ECDSA private key
|
||||
* ```js
|
||||
* const alpnKey = await acme.crypto.createPrivateEcdsaKey();
|
||||
* const [, alpnCertificate] = await acme.crypto.createAlpnCertificate(authz, keyAuthorization, alpnKey);
|
||||
*/
|
||||
|
||||
exports.createAlpnCertificate = async (authz, keyAuthorization, keyPem = null) => {
|
||||
/* Create CSR first */
|
||||
const now = new Date();
|
||||
const commonName = authz.identifier.value;
|
||||
const [key, csr] = await createCsr({ commonName }, keyPem);
|
||||
|
||||
/* Parse params and grab stuff we need */
|
||||
const params = jsrsasign.KJUR.asn1.csr.CSRUtil.getParam(csr.toString());
|
||||
const { subject, sbjpubkey, extreq, sigalg } = params;
|
||||
|
||||
/* ALPN extension */
|
||||
const alpnExt = {
|
||||
critical: true,
|
||||
extname: alpnAcmeIdentifierOID,
|
||||
extn: new jsrsasign.KJUR.asn1.DEROctetString({
|
||||
hex: crypto.createHash('sha256').update(keyAuthorization).digest('hex')
|
||||
})
|
||||
};
|
||||
|
||||
/* Pseudo-random serial - max 20 bytes, 11 for epoch (year 5138), 9 random */
|
||||
const random = await randomInt(1, 999999999);
|
||||
const serial = `${Math.floor(now.getTime() / 1000)}${random}`;
|
||||
|
||||
/* Self-signed ALPN certificate */
|
||||
const certificate = new jsrsasign.KJUR.asn1.x509.Certificate({
|
||||
subject,
|
||||
sbjpubkey,
|
||||
sigalg,
|
||||
version: 3,
|
||||
serial: { hex: Buffer.from(serial).toString('hex') },
|
||||
issuer: subject,
|
||||
notbefore: jsrsasign.datetozulu(now),
|
||||
notafter: jsrsasign.datetozulu(now),
|
||||
cakey: key.toString(),
|
||||
ext: extreq.concat([alpnExt])
|
||||
});
|
||||
|
||||
/* Done */
|
||||
return [keyPem, Buffer.from(pem)];
|
||||
const pem = certificate.getPEM();
|
||||
return [key, Buffer.from(pem)];
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Validate that a ALPN certificate contains the expected key authorization
|
||||
*
|
||||
* @param {buffer|string} certPem PEM encoded certificate
|
||||
* @param {string} keyAuthorization Expected challenge key authorization
|
||||
* @returns {boolean} True when valid
|
||||
*/
|
||||
|
||||
exports.isAlpnCertificateAuthorizationValid = (certPem, keyAuthorization) => {
|
||||
const params = getCertificateParams(certPem);
|
||||
const expectedHex = crypto.createHash('sha256').update(keyAuthorization).digest('hex');
|
||||
const acmeExt = (params.ext || []).find((e) => (e && e.extname && (e.extname === alpnAcmeIdentifierOID)));
|
||||
|
||||
if (!acmeExt || !acmeExt.extn || !acmeExt.extn.octstr || !acmeExt.extn.octstr.hex) {
|
||||
throw new Error('Unable to locate ALPN extension within parsed certificate');
|
||||
}
|
||||
|
||||
/* Return true if match */
|
||||
return (acmeExt.extn.octstr.hex === expectedHex);
|
||||
};
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
* Utility methods
|
||||
*/
|
||||
|
||||
const tls = require('tls');
|
||||
const dns = require('dns').promises;
|
||||
const { readCertificateInfo, splitPemChain } = require('./crypto');
|
||||
const { log } = require('./logger');
|
||||
|
@ -245,6 +246,60 @@ async function getAuthoritativeDnsResolver(recordName) {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Attempt to retrieve TLS ALPN certificate from peer
|
||||
*
|
||||
* https://nodejs.org/api/tls.html#tlsconnectoptions-callback
|
||||
*
|
||||
* @param {string} host Host the TLS client should connect to
|
||||
* @param {number} port Port the client should connect to
|
||||
* @param {string} servername Server name for the SNI (Server Name Indication)
|
||||
* @returns {Promise<string>} PEM encoded certificate
|
||||
*/
|
||||
|
||||
async function retrieveTlsAlpnCertificate(host, port, timeout = 30000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let result;
|
||||
|
||||
/* TLS connection */
|
||||
const socket = tls.connect({
|
||||
host,
|
||||
port,
|
||||
servername: host,
|
||||
rejectUnauthorized: false,
|
||||
ALPNProtocols: ['acme-tls/1']
|
||||
});
|
||||
|
||||
socket.setTimeout(timeout);
|
||||
socket.setEncoding('utf-8');
|
||||
|
||||
/* Grab certificate once connected and close */
|
||||
socket.on('secureConnect', () => {
|
||||
result = socket.getPeerX509Certificate();
|
||||
socket.end();
|
||||
});
|
||||
|
||||
/* Errors */
|
||||
socket.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
socket.on('timeout', () => {
|
||||
socket.destroy(new Error('TLS ALPN certificate lookup request timed out'));
|
||||
});
|
||||
|
||||
/* Done, return cert as PEM if found */
|
||||
socket.on('end', () => {
|
||||
if (result) {
|
||||
return resolve(result.toString());
|
||||
}
|
||||
|
||||
return reject(new Error('TLS ALPN lookup failed to retrieve certificate'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Export utils
|
||||
*/
|
||||
|
@ -254,5 +309,6 @@ module.exports = {
|
|||
parseLinkHeader,
|
||||
findCertificateChainForIssuer,
|
||||
formatResponseError,
|
||||
getAuthoritativeDnsResolver
|
||||
getAuthoritativeDnsResolver,
|
||||
retrieveTlsAlpnCertificate
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@ const https = require('https');
|
|||
const { log } = require('./logger');
|
||||
const axios = require('./axios');
|
||||
const util = require('./util');
|
||||
const { isAlpnCertificateAuthorizationValid } = require('./crypto');
|
||||
|
||||
|
||||
/**
|
||||
|
@ -121,11 +122,40 @@ async function verifyDnsChallenge(authz, challenge, keyAuthorization, prefix = '
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Verify ACME TLS ALPN challenge
|
||||
*
|
||||
* https://tools.ietf.org/html/rfc8737
|
||||
*
|
||||
* @param {object} authz Identifier authorization
|
||||
* @param {object} challenge Authorization challenge
|
||||
* @param {string} keyAuthorization Challenge key authorization
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
|
||||
async function verifyTlsAlpnChallenge(authz, challenge, keyAuthorization) {
|
||||
const tlsAlpnPort = axios.defaults.acmeSettings.tlsAlpnChallengePort || 443;
|
||||
const host = authz.identifier.value;
|
||||
log(`Establishing TLS connection with host: ${host}:${tlsAlpnPort}`);
|
||||
|
||||
const certificate = await util.retrieveTlsAlpnCertificate(host, tlsAlpnPort);
|
||||
log('Certificate received from server successfully, matching key authorization in ALPN');
|
||||
|
||||
if (!isAlpnCertificateAuthorizationValid(certificate, keyAuthorization)) {
|
||||
throw new Error(`Authorization not found in certificate from ${authz.identifier.value}`);
|
||||
}
|
||||
|
||||
log(`Key authorization match for ${challenge.type}/${authz.identifier.value}, ACME challenge verified`);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Export API
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
'http-01': verifyHttpChallenge,
|
||||
'dns-01': verifyDnsChallenge
|
||||
'dns-01': verifyDnsChallenge,
|
||||
'tls-alpn-01': verifyTlsAlpnChallenge
|
||||
};
|
||||
|
|
|
@ -8,10 +8,13 @@ const https = require('https');
|
|||
const { assert } = require('chai');
|
||||
const cts = require('./challtestsrv');
|
||||
const axios = require('./../src/axios');
|
||||
const { retrieveTlsAlpnCertificate } = require('./../src/util');
|
||||
const { isAlpnCertificateAuthorizationValid } = require('./../src/crypto');
|
||||
|
||||
const domainName = process.env.ACME_DOMAIN_NAME || 'example.com';
|
||||
const httpPort = axios.defaults.acmeSettings.httpChallengePort || 80;
|
||||
const httpsPort = axios.defaults.acmeSettings.httpsChallengePort || 443;
|
||||
const tlsAlpnPort = axios.defaults.acmeSettings.tlsAlpnChallengePort || 443;
|
||||
|
||||
|
||||
describe('pebble', () => {
|
||||
|
@ -33,6 +36,9 @@ describe('pebble', () => {
|
|||
const testDns01ChallengeHost = `_acme-challenge.${uuid()}.${domainName}.`;
|
||||
const testDns01ChallengeValue = uuid();
|
||||
|
||||
const testTlsAlpn01ChallengeHost = `${uuid()}.${domainName}`;
|
||||
const testTlsAlpn01ChallengeValue = uuid();
|
||||
|
||||
|
||||
/**
|
||||
* Pebble CTS required
|
||||
|
@ -181,4 +187,29 @@ describe('pebble', () => {
|
|||
assert.deepStrictEqual(resp, [[testDns01ChallengeValue]]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* TLS-ALPN-01 challenge response
|
||||
*/
|
||||
|
||||
describe('tls-alpn-01', () => {
|
||||
it('should not locate challenge response', async () => {
|
||||
await assert.isRejected(retrieveTlsAlpnCertificate(testTlsAlpn01ChallengeHost, tlsAlpnPort), /(failed to retrieve)|(ssl3_read_bytes:tlsv1 alert internal error)/);
|
||||
});
|
||||
|
||||
it('should timeout challenge response', async () => {
|
||||
await assert.isRejected(retrieveTlsAlpnCertificate('example.org', tlsAlpnPort, 500), /timed out/);
|
||||
});
|
||||
|
||||
it('should add challenge response', async () => {
|
||||
const resp = await cts.addTlsAlpn01ChallengeResponse(testTlsAlpn01ChallengeHost, testTlsAlpn01ChallengeValue);
|
||||
assert.isTrue(resp);
|
||||
});
|
||||
|
||||
it('should locate challenge response', async () => {
|
||||
const resp = await retrieveTlsAlpnCertificate(testTlsAlpn01ChallengeHost, tlsAlpnPort);
|
||||
assert.isTrue(isAlpnCertificateAuthorizationValid(resp, testTlsAlpn01ChallengeValue));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -26,6 +26,10 @@ describe('verify', () => {
|
|||
const testDns01Key = uuid();
|
||||
const testDns01Cname = `${uuid()}.${domainName}`;
|
||||
|
||||
const testTlsAlpn01Authz = { identifier: { type: 'dns', value: `${uuid()}.${domainName}` } };
|
||||
const testTlsAlpn01Challenge = { type: 'dns-01', status: 'pending', token: uuid() };
|
||||
const testTlsAlpn01Key = uuid();
|
||||
|
||||
|
||||
/**
|
||||
* Pebble CTS required
|
||||
|
@ -128,4 +132,25 @@ describe('verify', () => {
|
|||
assert.isTrue(resp);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* tls-alpn-01
|
||||
*/
|
||||
|
||||
describe('tls-alpn-01', () => {
|
||||
it('should reject challenge', async () => {
|
||||
await assert.isRejected(verify['tls-alpn-01'](testTlsAlpn01Authz, testTlsAlpn01Challenge, testTlsAlpn01Key));
|
||||
});
|
||||
|
||||
it('should mock challenge response', async () => {
|
||||
const resp = await cts.addTlsAlpn01ChallengeResponse(testTlsAlpn01Authz.identifier.value, testTlsAlpn01Key);
|
||||
assert.isTrue(resp);
|
||||
});
|
||||
|
||||
it('should verify challenge', async () => {
|
||||
const resp = await verify['tls-alpn-01'](testTlsAlpn01Authz, testTlsAlpn01Challenge, testTlsAlpn01Key);
|
||||
assert.isTrue(resp);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -95,6 +95,7 @@ describe('crypto', () => {
|
|||
let testSanCsr;
|
||||
let testNonCnCsr;
|
||||
let testNonAsciiCsr;
|
||||
let testAlpnCertificate;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -215,6 +216,31 @@ describe('crypto', () => {
|
|||
assert.strictEqual(result.commonName, testCsrDomain);
|
||||
assert.deepStrictEqual(result.altNames, [testCsrDomain]);
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* ALPN
|
||||
*/
|
||||
|
||||
it(`${n}/should generate alpn certificate`, async () => {
|
||||
const authz = { identifier: { value: 'test.example.com' } };
|
||||
const [key, cert] = await crypto.createAlpnCertificate(authz, 'super-secret.12345', await createFn());
|
||||
|
||||
assert.isTrue(Buffer.isBuffer(key));
|
||||
assert.isTrue(Buffer.isBuffer(cert));
|
||||
|
||||
testAlpnCertificate = cert;
|
||||
});
|
||||
|
||||
it(`${n}/should not validate invalid alpn certificate key authorization`, () => {
|
||||
assert.isFalse(crypto.isAlpnCertificateAuthorizationValid(testAlpnCertificate, 'aaaaaaa'));
|
||||
assert.isFalse(crypto.isAlpnCertificateAuthorizationValid(testAlpnCertificate, 'bbbbbbb'));
|
||||
assert.isFalse(crypto.isAlpnCertificateAuthorizationValid(testAlpnCertificate, 'ccccccc'));
|
||||
});
|
||||
|
||||
it(`${n}/should validate valid alpn certificate key authorization`, () => {
|
||||
assert.isTrue(crypto.isAlpnCertificateAuthorizationValid(testAlpnCertificate, 'super-secret.12345'));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -250,7 +276,7 @@ describe('crypto', () => {
|
|||
* CSR with auto-generated key
|
||||
*/
|
||||
|
||||
it('should generate a csr with auto-generated key', async () => {
|
||||
it('should generate a csr with default key', async () => {
|
||||
const [key, csr] = await crypto.createCsr({
|
||||
commonName: testCsrDomain
|
||||
});
|
||||
|
@ -281,6 +307,19 @@ describe('crypto', () => {
|
|||
});
|
||||
|
||||
|
||||
/**
|
||||
* ALPN
|
||||
*/
|
||||
|
||||
it('should generate alpn certificate with default key', async () => {
|
||||
const authz = { identifier: { value: 'test.example.com' } };
|
||||
const [key, cert] = await crypto.createAlpnCertificate(authz, 'abc123');
|
||||
|
||||
assert.isTrue(Buffer.isBuffer(key));
|
||||
assert.isTrue(Buffer.isBuffer(cert));
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* PEM utils
|
||||
*/
|
||||
|
|
|
@ -33,6 +33,7 @@ if (capEabEnabled && process.env.ACME_EAB_KID && process.env.ACME_EAB_HMAC_KEY)
|
|||
|
||||
describe('client', () => {
|
||||
const testDomain = `${uuid()}.${domainName}`;
|
||||
const testDomainAlpn = `${uuid()}.${domainName}`;
|
||||
const testDomainWildcard = `*.${testDomain}`;
|
||||
const testContact = `mailto:test-${uuid()}@nope.com`;
|
||||
|
||||
|
@ -78,16 +79,22 @@ describe('client', () => {
|
|||
let testAccount;
|
||||
let testAccountUrl;
|
||||
let testOrder;
|
||||
let testOrderAlpn;
|
||||
let testOrderWildcard;
|
||||
let testAuthz;
|
||||
let testAuthzAlpn;
|
||||
let testAuthzWildcard;
|
||||
let testChallenge;
|
||||
let testChallengeAlpn;
|
||||
let testChallengeWildcard;
|
||||
let testKeyAuthorization;
|
||||
let testKeyAuthorizationAlpn;
|
||||
let testKeyAuthorizationWildcard;
|
||||
let testCsr;
|
||||
let testCsrAlpn;
|
||||
let testCsrWildcard;
|
||||
let testCertificate;
|
||||
let testCertificateAlpn;
|
||||
let testCertificateWildcard;
|
||||
|
||||
|
||||
|
@ -107,6 +114,7 @@ describe('client', () => {
|
|||
|
||||
it('should generate certificate signing request', async () => {
|
||||
[, testCsr] = await acme.crypto.createCsr({ commonName: testDomain }, await createKeyFn());
|
||||
[, testCsrAlpn] = await acme.crypto.createCsr({ commonName: testDomainAlpn }, await createKeyFn());
|
||||
[, testCsrWildcard] = await acme.crypto.createCsr({ commonName: testDomainWildcard }, await createKeyFn());
|
||||
});
|
||||
|
||||
|
@ -336,12 +344,14 @@ describe('client', () => {
|
|||
|
||||
it('should create new order', async () => {
|
||||
const data1 = { identifiers: [{ type: 'dns', value: testDomain }] };
|
||||
const data2 = { identifiers: [{ type: 'dns', value: testDomainWildcard }] };
|
||||
const data2 = { identifiers: [{ type: 'dns', value: testDomainAlpn }] };
|
||||
const data3 = { identifiers: [{ type: 'dns', value: testDomainWildcard }] };
|
||||
|
||||
testOrder = await testClient.createOrder(data1);
|
||||
testOrderWildcard = await testClient.createOrder(data2);
|
||||
testOrderAlpn = await testClient.createOrder(data2);
|
||||
testOrderWildcard = await testClient.createOrder(data3);
|
||||
|
||||
[testOrder, testOrderWildcard].forEach((item) => {
|
||||
[testOrder, testOrderAlpn, testOrderWildcard].forEach((item) => {
|
||||
spec.rfc8555.order(item);
|
||||
assert.strictEqual(item.status, 'pending');
|
||||
});
|
||||
|
@ -353,7 +363,7 @@ describe('client', () => {
|
|||
*/
|
||||
|
||||
it('should get existing order', async () => {
|
||||
await Promise.all([testOrder, testOrderWildcard].map(async (existing) => {
|
||||
await Promise.all([testOrder, testOrderAlpn, testOrderWildcard].map(async (existing) => {
|
||||
const result = await testClient.getOrder(existing);
|
||||
|
||||
spec.rfc8555.order(result);
|
||||
|
@ -368,9 +378,10 @@ describe('client', () => {
|
|||
|
||||
it('should get identifier authorization', async () => {
|
||||
const orderAuthzCollection = await testClient.getAuthorizations(testOrder);
|
||||
const alpnAuthzCollection = await testClient.getAuthorizations(testOrderAlpn);
|
||||
const wildcardAuthzCollection = await testClient.getAuthorizations(testOrderWildcard);
|
||||
|
||||
[orderAuthzCollection, wildcardAuthzCollection].forEach((collection) => {
|
||||
[orderAuthzCollection, alpnAuthzCollection, wildcardAuthzCollection].forEach((collection) => {
|
||||
assert.isArray(collection);
|
||||
assert.isNotEmpty(collection);
|
||||
|
||||
|
@ -381,9 +392,10 @@ describe('client', () => {
|
|||
});
|
||||
|
||||
testAuthz = orderAuthzCollection.pop();
|
||||
testAuthzAlpn = alpnAuthzCollection.pop();
|
||||
testAuthzWildcard = wildcardAuthzCollection.pop();
|
||||
|
||||
testAuthz.challenges.concat(testAuthzWildcard.challenges).forEach((item) => {
|
||||
testAuthz.challenges.concat(testAuthzAlpn.challenges).concat(testAuthzWildcard.challenges).forEach((item) => {
|
||||
spec.rfc8555.challenge(item);
|
||||
assert.strictEqual(item.status, 'pending');
|
||||
});
|
||||
|
@ -396,12 +408,14 @@ describe('client', () => {
|
|||
|
||||
it('should get challenge key authorization', async () => {
|
||||
testChallenge = testAuthz.challenges.find((c) => (c.type === 'http-01'));
|
||||
testChallengeAlpn = testAuthzAlpn.challenges.find((c) => (c.type === 'tls-alpn-01'));
|
||||
testChallengeWildcard = testAuthzWildcard.challenges.find((c) => (c.type === 'dns-01'));
|
||||
|
||||
testKeyAuthorization = await testClient.getChallengeKeyAuthorization(testChallenge);
|
||||
testKeyAuthorizationAlpn = await testClient.getChallengeKeyAuthorization(testChallengeAlpn);
|
||||
testKeyAuthorizationWildcard = await testClient.getChallengeKeyAuthorization(testChallengeWildcard);
|
||||
|
||||
[testKeyAuthorization, testKeyAuthorizationWildcard].forEach((k) => assert.isString(k));
|
||||
[testKeyAuthorization, testKeyAuthorizationAlpn, testKeyAuthorizationWildcard].forEach((k) => assert.isString(k));
|
||||
});
|
||||
|
||||
|
||||
|
@ -438,9 +452,11 @@ describe('client', () => {
|
|||
|
||||
it('should verify challenge', async () => {
|
||||
await cts.assertHttpChallengeCreateFn(testAuthz, testChallenge, testKeyAuthorization);
|
||||
await cts.assertTlsAlpnChallengeCreateFn(testAuthzAlpn, testChallengeAlpn, testKeyAuthorizationAlpn);
|
||||
await cts.assertDnsChallengeCreateFn(testAuthzWildcard, testChallengeWildcard, testKeyAuthorizationWildcard);
|
||||
|
||||
await testClient.verifyChallenge(testAuthz, testChallenge);
|
||||
await testClient.verifyChallenge(testAuthzAlpn, testChallengeAlpn);
|
||||
await testClient.verifyChallenge(testAuthzWildcard, testChallengeWildcard);
|
||||
});
|
||||
|
||||
|
@ -450,7 +466,7 @@ describe('client', () => {
|
|||
*/
|
||||
|
||||
it('should complete challenge', async () => {
|
||||
await Promise.all([testChallenge, testChallengeWildcard].map(async (challenge) => {
|
||||
await Promise.all([testChallenge, testChallengeAlpn, testChallengeWildcard].map(async (challenge) => {
|
||||
const result = await testClient.completeChallenge(challenge);
|
||||
|
||||
spec.rfc8555.challenge(result);
|
||||
|
@ -464,7 +480,7 @@ describe('client', () => {
|
|||
*/
|
||||
|
||||
it('should wait for valid challenge status', async () => {
|
||||
await Promise.all([testChallenge, testChallengeWildcard].map(async (c) => testClient.waitForValidStatus(c)));
|
||||
await Promise.all([testChallenge, testChallengeAlpn, testChallengeWildcard].map(async (c) => testClient.waitForValidStatus(c)));
|
||||
});
|
||||
|
||||
|
||||
|
@ -474,11 +490,13 @@ describe('client', () => {
|
|||
|
||||
it('should finalize order', async () => {
|
||||
const finalize = await testClient.finalizeOrder(testOrder, testCsr);
|
||||
const finalizeAlpn = await testClient.finalizeOrder(testOrderAlpn, testCsrAlpn);
|
||||
const finalizeWildcard = await testClient.finalizeOrder(testOrderWildcard, testCsrWildcard);
|
||||
|
||||
[finalize, finalizeWildcard].forEach((f) => spec.rfc8555.order(f));
|
||||
[finalize, finalizeAlpn, finalizeWildcard].forEach((f) => spec.rfc8555.order(f));
|
||||
|
||||
assert.strictEqual(testOrder.url, finalize.url);
|
||||
assert.strictEqual(testOrderAlpn.url, finalizeAlpn.url);
|
||||
assert.strictEqual(testOrderWildcard.url, finalizeWildcard.url);
|
||||
});
|
||||
|
||||
|
@ -488,7 +506,7 @@ describe('client', () => {
|
|||
*/
|
||||
|
||||
it('should wait for valid order status', async () => {
|
||||
await Promise.all([testOrder, testOrderWildcard].map(async (o) => testClient.waitForValidStatus(o)));
|
||||
await Promise.all([testOrder, testOrderAlpn, testOrderWildcard].map(async (o) => testClient.waitForValidStatus(o)));
|
||||
});
|
||||
|
||||
|
||||
|
@ -498,9 +516,10 @@ describe('client', () => {
|
|||
|
||||
it('should get certificate', async () => {
|
||||
testCertificate = await testClient.getCertificate(testOrder);
|
||||
testCertificateAlpn = await testClient.getCertificate(testOrderAlpn);
|
||||
testCertificateWildcard = await testClient.getCertificate(testOrderWildcard);
|
||||
|
||||
[testCertificate, testCertificateWildcard].forEach((cert) => {
|
||||
[testCertificate, testCertificateAlpn, testCertificateWildcard].forEach((cert) => {
|
||||
assert.isString(cert);
|
||||
acme.crypto.readCertificateInfo(cert);
|
||||
});
|
||||
|
@ -539,11 +558,13 @@ describe('client', () => {
|
|||
|
||||
it('should revoke certificate', async () => {
|
||||
await testClient.revokeCertificate(testCertificate);
|
||||
await testClient.revokeCertificate(testCertificateAlpn, { reason: 0 });
|
||||
await testClient.revokeCertificate(testCertificateWildcard, { reason: 4 });
|
||||
});
|
||||
|
||||
it('should not allow getting revoked certificate', async () => {
|
||||
await assert.isRejected(testClient.getCertificate(testOrder));
|
||||
await assert.isRejected(testClient.getCertificate(testOrderAlpn));
|
||||
await assert.isRejected(testClient.getCertificate(testOrderWildcard));
|
||||
});
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ describe('client.auto', () => {
|
|||
const testHttpDomain = `${uuid()}.${domainName}`;
|
||||
const testHttpsDomain = `${uuid()}.${domainName}`;
|
||||
const testDnsDomain = `${uuid()}.${domainName}`;
|
||||
const testAlpnDomain = `${uuid()}.${domainName}`;
|
||||
const testWildcardDomain = `${uuid()}.${domainName}`;
|
||||
|
||||
const testSanDomains = [
|
||||
|
@ -280,6 +281,22 @@ describe('client.auto', () => {
|
|||
assert.isString(cert);
|
||||
});
|
||||
|
||||
it('should order certificate using tls-alpn-01', async () => {
|
||||
const [, csr] = await acme.crypto.createCsr({
|
||||
commonName: testAlpnDomain
|
||||
}, await createKeyFn());
|
||||
|
||||
const cert = await testClient.auto({
|
||||
csr,
|
||||
termsOfServiceAgreed: true,
|
||||
challengeCreateFn: cts.assertTlsAlpnChallengeCreateFn,
|
||||
challengeRemoveFn: cts.challengeRemoveFn,
|
||||
challengePriority: ['tls-alpn-01']
|
||||
});
|
||||
|
||||
assert.isString(cert);
|
||||
});
|
||||
|
||||
it('should order san certificate', async () => {
|
||||
const [, csr] = await acme.crypto.createCsr({
|
||||
commonName: testSanDomains[0],
|
||||
|
|
|
@ -63,9 +63,14 @@ async function addDns01ChallengeResponse(host, value) {
|
|||
return request('set-txt', { host, value });
|
||||
}
|
||||
|
||||
async function addTlsAlpn01ChallengeResponse(host, content) {
|
||||
return request('add-tlsalpn01', { host, content });
|
||||
}
|
||||
|
||||
exports.addHttp01ChallengeResponse = addHttp01ChallengeResponse;
|
||||
exports.addHttps01ChallengeResponse = addHttps01ChallengeResponse;
|
||||
exports.addDns01ChallengeResponse = addDns01ChallengeResponse;
|
||||
exports.addTlsAlpn01ChallengeResponse = addTlsAlpn01ChallengeResponse;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -87,6 +92,11 @@ async function assertDnsChallengeCreateFn(authz, challenge, keyAuthorization) {
|
|||
return addDns01ChallengeResponse(`_acme-challenge.${authz.identifier.value}.`, keyAuthorization);
|
||||
}
|
||||
|
||||
async function assertTlsAlpnChallengeCreateFn(authz, challenge, keyAuthorization) {
|
||||
assert.strictEqual(challenge.type, 'tls-alpn-01');
|
||||
return addTlsAlpn01ChallengeResponse(authz.identifier.value, keyAuthorization);
|
||||
}
|
||||
|
||||
async function challengeCreateFn(authz, challenge, keyAuthorization) {
|
||||
if (challenge.type === 'http-01') {
|
||||
return assertHttpChallengeCreateFn(authz, challenge, keyAuthorization);
|
||||
|
@ -96,6 +106,10 @@ async function challengeCreateFn(authz, challenge, keyAuthorization) {
|
|||
return assertDnsChallengeCreateFn(authz, challenge, keyAuthorization);
|
||||
}
|
||||
|
||||
if (challenge.type === 'tls-alpn-01') {
|
||||
return assertTlsAlpnChallengeCreateFn(authz, challenge, keyAuthorization);
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported challenge type ${challenge.type}`);
|
||||
}
|
||||
|
||||
|
@ -106,4 +120,5 @@ exports.challengeThrowFn = async () => { throw new Error('oops'); };
|
|||
exports.assertHttpChallengeCreateFn = assertHttpChallengeCreateFn;
|
||||
exports.assertHttpsChallengeCreateFn = assertHttpsChallengeCreateFn;
|
||||
exports.assertDnsChallengeCreateFn = assertDnsChallengeCreateFn;
|
||||
exports.assertTlsAlpnChallengeCreateFn = assertTlsAlpnChallengeCreateFn;
|
||||
exports.challengeCreateFn = challengeCreateFn;
|
||||
|
|
|
@ -27,6 +27,10 @@ if (process.env.ACME_HTTPS_PORT) {
|
|||
axios.defaults.acmeSettings.httpsChallengePort = process.env.ACME_HTTPS_PORT;
|
||||
}
|
||||
|
||||
if (process.env.ACME_TLSALPN_PORT) {
|
||||
axios.defaults.acmeSettings.tlsAlpnChallengePort = process.env.ACME_TLSALPN_PORT;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* External account binding
|
||||
|
|
|
@ -156,6 +156,8 @@ export interface CryptoInterface {
|
|||
readCsrDomains(csrPem: CsrBuffer | CsrString): CertificateDomains;
|
||||
readCertificateInfo(certPem: CertificateBuffer | CertificateString): CertificateInfo;
|
||||
createCsr(data: CsrOptions, keyPem?: PrivateKeyBuffer | PrivateKeyString): Promise<[PrivateKeyBuffer, CsrBuffer]>;
|
||||
createAlpnCertificate(authz: Authorization, keyAuthorization: string, keyPem?: PrivateKeyBuffer | PrivateKeyString): Promise<[PrivateKeyBuffer, CertificateBuffer]>;
|
||||
isAlpnCertificateAuthorizationValid(certPem: CertificateBuffer | CertificateString, keyAuthorization: string): boolean;
|
||||
}
|
||||
|
||||
export const crypto: CryptoInterface;
|
||||
|
|
Loading…
Reference in New Issue