certd/packages/core/acme-client/test/70-auto.spec.js

439 lines
16 KiB
JavaScript

/**
* ACME client.auto tests
*/
const { randomUUID: uuid } = require('crypto');
const { assert } = require('chai');
const cts = require('./challtestsrv');
const getCertIssuers = require('./get-cert-issuers');
const spec = require('./spec');
const acme = require('./../');
const domainName = process.env.ACME_DOMAIN_NAME || 'example.com';
const directoryUrl = process.env.ACME_DIRECTORY_URL || acme.directory.letsencrypt.staging;
const capEabEnabled = (('ACME_CAP_EAB_ENABLED' in process.env) && (process.env.ACME_CAP_EAB_ENABLED === '1'));
const capAlternateCertRoots = !(('ACME_CAP_ALTERNATE_CERT_ROOTS' in process.env) && (process.env.ACME_CAP_ALTERNATE_CERT_ROOTS === '0'));
const clientOpts = {
directoryUrl,
backoffAttempts: 5,
backoffMin: 1000,
backoffMax: 5000,
};
if (capEabEnabled && process.env.ACME_EAB_KID && process.env.ACME_EAB_HMAC_KEY) {
clientOpts.externalAccountBinding = {
kid: process.env.ACME_EAB_KID,
hmacKey: process.env.ACME_EAB_HMAC_KEY,
};
}
describe('client.auto', () => {
const testDomain = `${uuid()}.${domainName}`;
const testHttpDomain = `${uuid()}.${domainName}`;
const testHttpsDomain = `${uuid()}.${domainName}`;
const testDnsDomain = `${uuid()}.${domainName}`;
const testAlpnDomain = `${uuid()}.${domainName}`;
const testWildcardDomain = `${uuid()}.${domainName}`;
const testSanDomains = [
`${uuid()}.${domainName}`,
`${uuid()}.${domainName}`,
`${uuid()}.${domainName}`,
];
/**
* Pebble CTS required
*/
before(function () {
if (!cts.isEnabled()) {
this.skip();
}
});
/**
* Key types
*/
Object.entries({
rsa: {
createKeyFn: () => acme.crypto.createPrivateRsaKey(),
createKeyAltFns: {
s1024: () => acme.crypto.createPrivateRsaKey(1024),
s4096: () => acme.crypto.createPrivateRsaKey(4096),
},
},
ecdsa: {
createKeyFn: () => acme.crypto.createPrivateEcdsaKey(),
createKeyAltFns: {
p384: () => acme.crypto.createPrivateEcdsaKey('P-384'),
p521: () => acme.crypto.createPrivateEcdsaKey('P-521'),
},
},
}).forEach(([name, { createKeyFn, createKeyAltFns }]) => {
describe(name, () => {
let testIssuers;
let testClient;
let testCertificate;
let testSanCertificate;
let testWildcardCertificate;
/**
* Fixtures
*/
it('should resolve certificate issuers [ACME_CAP_ALTERNATE_CERT_ROOTS]', async function () {
if (!capAlternateCertRoots) {
this.skip();
}
testIssuers = await getCertIssuers();
assert.isArray(testIssuers);
assert.isTrue(testIssuers.length > 1);
testIssuers.forEach((i) => {
assert.isString(i);
assert.strictEqual(1, testIssuers.filter((c) => (c === i)).length);
});
});
/**
* Initialize client
*/
it('should initialize client', async () => {
testClient = new acme.Client({
...clientOpts,
accountKey: await createKeyFn(),
});
});
/**
* Invalid challenge response
*/
it('should throw on invalid challenge response', async () => {
const [, csr] = await acme.crypto.createCsr({
commonName: `${uuid()}.${domainName}`,
}, await createKeyFn());
await assert.isRejected(testClient.auto({
csr,
termsOfServiceAgreed: true,
challengeCreateFn: cts.challengeNoopFn,
challengeRemoveFn: cts.challengeNoopFn,
}), /^authorization not found/i);
});
it('should throw on invalid challenge response with opts.skipChallengeVerification=true', async () => {
const [, csr] = await acme.crypto.createCsr({
commonName: `${uuid()}.${domainName}`,
}, await createKeyFn());
await assert.isRejected(testClient.auto({
csr,
termsOfServiceAgreed: true,
skipChallengeVerification: true,
challengeCreateFn: cts.challengeNoopFn,
challengeRemoveFn: cts.challengeNoopFn,
}));
});
/**
* Challenge function exceptions
*/
it('should throw on challengeCreate exception', async () => {
const [, csr] = await acme.crypto.createCsr({
commonName: `${uuid()}.${domainName}`,
}, await createKeyFn());
await assert.isRejected(testClient.auto({
csr,
termsOfServiceAgreed: true,
challengeCreateFn: cts.challengeThrowFn,
challengeRemoveFn: cts.challengeNoopFn,
}), /^oops$/);
});
it('should not throw on challengeRemove exception', async () => {
const [, csr] = await acme.crypto.createCsr({
commonName: `${uuid()}.${domainName}`,
}, await createKeyFn());
const cert = await testClient.auto({
csr,
termsOfServiceAgreed: true,
challengeCreateFn: cts.challengeCreateFn,
challengeRemoveFn: cts.challengeThrowFn,
});
assert.isString(cert);
});
it('should settle all challenges before rejecting', async () => {
const results = [];
const [, csr] = await acme.crypto.createCsr({
commonName: `${uuid()}.${domainName}`,
altNames: [
`${uuid()}.${domainName}`,
`${uuid()}.${domainName}`,
`${uuid()}.${domainName}`,
`${uuid()}.${domainName}`,
],
}, await createKeyFn());
await assert.isRejected(testClient.auto({
csr,
termsOfServiceAgreed: true,
challengeCreateFn: async (...args) => {
if ([0, 1, 2].includes(results.length)) {
results.push(false);
throw new Error('oops');
}
await new Promise((resolve) => { setTimeout(resolve, 500); });
results.push(true);
return cts.challengeCreateFn(...args);
},
challengeRemoveFn: cts.challengeRemoveFn,
}));
assert.strictEqual(results.length, 5);
assert.deepStrictEqual(results, [false, false, false, true, true]);
});
/**
* Order certificates
*/
it('should order certificate', async () => {
const [, csr] = await acme.crypto.createCsr({
commonName: testDomain,
}, await createKeyFn());
const cert = await testClient.auto({
csr,
termsOfServiceAgreed: true,
challengeCreateFn: cts.challengeCreateFn,
challengeRemoveFn: cts.challengeRemoveFn,
});
assert.isString(cert);
testCertificate = cert;
});
it('should order certificate using http-01', async () => {
const [, csr] = await acme.crypto.createCsr({
commonName: testHttpDomain,
}, await createKeyFn());
const cert = await testClient.auto({
csr,
termsOfServiceAgreed: true,
challengeCreateFn: cts.assertHttpChallengeCreateFn,
challengeRemoveFn: cts.challengeRemoveFn,
challengePriority: ['http-01'],
});
assert.isString(cert);
});
it('should order certificate using https-01', async () => {
const [, csr] = await acme.crypto.createCsr({
commonName: testHttpsDomain,
}, await createKeyFn());
const cert = await testClient.auto({
csr,
termsOfServiceAgreed: true,
challengeCreateFn: cts.assertHttpsChallengeCreateFn,
challengeRemoveFn: cts.challengeRemoveFn,
challengePriority: ['http-01'],
});
assert.isString(cert);
});
it('should order certificate using dns-01', async () => {
const [, csr] = await acme.crypto.createCsr({
commonName: testDnsDomain,
}, await createKeyFn());
const cert = await testClient.auto({
csr,
termsOfServiceAgreed: true,
challengeCreateFn: cts.assertDnsChallengeCreateFn,
challengeRemoveFn: cts.challengeRemoveFn,
challengePriority: ['dns-01'],
});
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({
altNames: testSanDomains,
}, await createKeyFn());
const cert = await testClient.auto({
csr,
termsOfServiceAgreed: true,
challengeCreateFn: cts.challengeCreateFn,
challengeRemoveFn: cts.challengeRemoveFn,
});
assert.isString(cert);
testSanCertificate = cert;
});
it('should order wildcard certificate', async () => {
const [, csr] = await acme.crypto.createCsr({
altNames: [testWildcardDomain, `*.${testWildcardDomain}`],
}, await createKeyFn());
const cert = await testClient.auto({
csr,
termsOfServiceAgreed: true,
challengeCreateFn: cts.challengeCreateFn,
challengeRemoveFn: cts.challengeRemoveFn,
});
assert.isString(cert);
testWildcardCertificate = cert;
});
it('should order certificate with opts.skipChallengeVerification=true', async () => {
const [, csr] = await acme.crypto.createCsr({
commonName: `${uuid()}.${domainName}`,
}, await createKeyFn());
const cert = await testClient.auto({
csr,
termsOfServiceAgreed: true,
skipChallengeVerification: true,
challengeCreateFn: cts.challengeCreateFn,
challengeRemoveFn: cts.challengeRemoveFn,
});
assert.isString(cert);
});
it('should order alternate certificate chain [ACME_CAP_ALTERNATE_CERT_ROOTS]', async function () {
if (!capAlternateCertRoots) {
this.skip();
}
await Promise.all(testIssuers.map(async (issuer) => {
const [, csr] = await acme.crypto.createCsr({
commonName: `${uuid()}.${domainName}`,
}, await createKeyFn());
const cert = await testClient.auto({
csr,
termsOfServiceAgreed: true,
preferredChain: issuer,
challengeCreateFn: cts.challengeCreateFn,
challengeRemoveFn: cts.challengeRemoveFn,
});
const rootCert = acme.crypto.splitPemChain(cert).pop();
const info = acme.crypto.readCertificateInfo(rootCert);
assert.strictEqual(issuer, info.issuer.commonName);
}));
});
it('should get default chain with invalid preference [ACME_CAP_ALTERNATE_CERT_ROOTS]', async function () {
if (!capAlternateCertRoots) {
this.skip();
}
const [, csr] = await acme.crypto.createCsr({
commonName: `${uuid()}.${domainName}`,
}, await createKeyFn());
const cert = await testClient.auto({
csr,
termsOfServiceAgreed: true,
preferredChain: uuid(),
challengeCreateFn: cts.challengeCreateFn,
challengeRemoveFn: cts.challengeRemoveFn,
});
const rootCert = acme.crypto.splitPemChain(cert).pop();
const info = acme.crypto.readCertificateInfo(rootCert);
assert.strictEqual(testIssuers[0], info.issuer.commonName);
});
/**
* Order certificate with alternate key sizes
*/
Object.entries(createKeyAltFns).forEach(([k, altKeyFn]) => {
it(`should order certificate with key=${k}`, async () => {
const [, csr] = await acme.crypto.createCsr({
commonName: testDomain,
}, await altKeyFn());
const cert = await testClient.auto({
csr,
termsOfServiceAgreed: true,
challengeCreateFn: cts.challengeCreateFn,
challengeRemoveFn: cts.challengeRemoveFn,
});
assert.isString(cert);
});
});
/**
* Read certificates
*/
it('should read certificate info', () => {
const info = acme.crypto.readCertificateInfo(testCertificate);
spec.crypto.certificateInfo(info);
assert.isNull(info.domains.commonName);
assert.deepStrictEqual(info.domains.altNames, [testDomain]);
});
it('should read san certificate info', () => {
const info = acme.crypto.readCertificateInfo(testSanCertificate);
spec.crypto.certificateInfo(info);
assert.isNull(info.domains.commonName);
assert.deepStrictEqual(info.domains.altNames, testSanDomains);
});
it('should read wildcard certificate info', () => {
const info = acme.crypto.readCertificateInfo(testWildcardCertificate);
spec.crypto.certificateInfo(info);
assert.isNull(info.domains.commonName);
assert.deepStrictEqual(info.domains.altNames, [testWildcardDomain, `*.${testWildcardDomain}`]);
});
});
});
});