🔱: [acme] sync upgrade with 21 commits [trident-sync]

Bump v5.0.0
This commit is contained in:
xiaojunnuo
2023-01-29 15:27:11 +08:00
parent 62e3945d30
commit f64ea78c44
54 changed files with 8136 additions and 2 deletions
@@ -0,0 +1,121 @@
/**
* Pebble Challenge Test Server tests
*/
const dns = require('dns').promises;
const { assert } = require('chai');
const { v4: uuid } = require('uuid');
const cts = require('./challtestsrv');
const axios = require('./../src/axios');
const domainName = process.env.ACME_DOMAIN_NAME || 'example.com';
const httpPort = axios.defaults.acmeSettings.httpChallengePort || 80;
describe('pebble', () => {
const testAHost = `${uuid()}.${domainName}`;
const testARecords = ['1.1.1.1', '2.2.2.2'];
const testCnameHost = `${uuid()}.${domainName}`;
const testCnameRecord = `${uuid()}.${domainName}`;
const testHttp01ChallengeHost = `${uuid()}.${domainName}`;
const testHttp01ChallengeToken = uuid();
const testHttp01ChallengeContent = uuid();
const testDns01ChallengeHost = `_acme-challenge.${uuid()}.${domainName}.`;
const testDns01ChallengeValue = uuid();
/**
* Pebble CTS required
*/
before(function() {
if (!cts.isEnabled()) {
this.skip();
}
});
/**
* DNS mocking
*/
describe('dns', () => {
it('should not locate a records', async () => {
const resp = await dns.resolve4(testAHost);
assert.isArray(resp);
assert.notDeepEqual(resp, testARecords);
});
it('should add dns a records', async () => {
const resp = await cts.addDnsARecord(testAHost, testARecords);
assert.isTrue(resp);
});
it('should locate a records', async () => {
const resp = await dns.resolve4(testAHost);
assert.isArray(resp);
assert.deepStrictEqual(resp, testARecords);
});
it('should not locate cname records', async () => {
await assert.isRejected(dns.resolveCname(testCnameHost));
});
it('should set dns cname record', async () => {
const resp = await cts.setDnsCnameRecord(testCnameHost, testCnameRecord);
assert.isTrue(resp);
});
it('should locate cname record', async () => {
const resp = await dns.resolveCname(testCnameHost);
assert.isArray(resp);
assert.deepStrictEqual(resp, [testCnameRecord]);
});
});
/**
* Challenge response
*/
describe('challenges', () => {
it('should not locate http-01 challenge response', async () => {
const resp = await axios.get(`http://${testHttp01ChallengeHost}:${httpPort}/.well-known/acme-challenge/${testHttp01ChallengeToken}`);
assert.isString(resp.data);
assert.notEqual(resp.data, testHttp01ChallengeContent);
});
it('should add http-01 challenge response', async () => {
const resp = await cts.addHttp01ChallengeResponse(testHttp01ChallengeToken, testHttp01ChallengeContent);
assert.isTrue(resp);
});
it('should locate http-01 challenge response', async () => {
const resp = await axios.get(`http://${testHttp01ChallengeHost}:${httpPort}/.well-known/acme-challenge/${testHttp01ChallengeToken}`);
assert.isString(resp.data);
assert.strictEqual(resp.data, testHttp01ChallengeContent);
});
it('should not locate dns-01 challenge response', async () => {
await assert.isRejected(dns.resolveTxt(testDns01ChallengeHost));
});
it('should add dns-01 challenge response', async () => {
const resp = await cts.addDns01ChallengeResponse(testDns01ChallengeHost, testDns01ChallengeValue);
assert.isTrue(resp);
});
it('should locate dns-01 challenge response', async () => {
const resp = await dns.resolveTxt(testDns01ChallengeHost);
assert.isArray(resp);
assert.deepStrictEqual(resp, [[testDns01ChallengeValue]]);
});
});
});
@@ -0,0 +1,101 @@
/**
* HTTP client tests
*/
const { assert } = require('chai');
const { v4: uuid } = require('uuid');
const nock = require('nock');
const axios = require('./../src/axios');
const HttpClient = require('./../src/http');
const pkg = require('./../package.json');
describe('http', () => {
let testClient;
const defaultUserAgent = `node-${pkg.name}/${pkg.version}`;
const customUserAgent = 'custom-ua-123';
const primaryEndpoint = `http://${uuid()}.example.com`;
const defaultUaEndpoint = `http://${uuid()}.example.com`;
const customUaEndpoint = `http://${uuid()}.example.com`;
/**
* HTTP mocking
*/
before(() => {
axios.defaults.acmeSettings.bypassCustomDnsResolver = true;
const defaultUaOpts = { reqheaders: { 'User-Agent': defaultUserAgent } };
const customUaOpts = { reqheaders: { 'User-Agent': customUserAgent } };
nock(primaryEndpoint)
.persist().get('/').reply(200, 'ok');
nock(defaultUaEndpoint, defaultUaOpts)
.persist().get('/').reply(200, 'ok');
nock(customUaEndpoint, customUaOpts)
.persist().get('/').reply(200, 'ok');
});
after(() => {
axios.defaults.headers.common['User-Agent'] = defaultUserAgent;
axios.defaults.acmeSettings.bypassCustomDnsResolver = false;
});
/**
* Initialize
*/
it('should initialize clients', () => {
testClient = new HttpClient();
});
/**
* HTTP verbs
*/
it('should http get', async () => {
const resp = await testClient.request(primaryEndpoint, 'get');
assert.isObject(resp);
assert.strictEqual(resp.status, 200);
assert.strictEqual(resp.data, 'ok');
});
/**
* User-Agent
*/
it('should request using default user-agent', async () => {
const resp = await testClient.request(defaultUaEndpoint, 'get');
assert.isObject(resp);
assert.strictEqual(resp.status, 200);
assert.strictEqual(resp.data, 'ok');
});
it('should not request using custom user-agent', async () => {
await assert.isRejected(testClient.request(customUaEndpoint, 'get'));
});
it('should request using custom user-agent', async () => {
axios.defaults.headers.common['User-Agent'] = customUserAgent;
const resp = await testClient.request(customUaEndpoint, 'get');
assert.isObject(resp);
assert.strictEqual(resp.status, 200);
assert.strictEqual(resp.data, 'ok');
});
it('should not request using default user-agent', async () => {
axios.defaults.headers.common['User-Agent'] = customUserAgent;
await assert.isRejected(testClient.request(defaultUaEndpoint, 'get'));
});
});
@@ -0,0 +1,35 @@
/**
* Logger tests
*/
const { assert } = require('chai');
const logger = require('./../src/logger');
describe('logger', () => {
let lastLogMessage = null;
function customLoggerFn(msg) {
lastLogMessage = msg;
}
/**
* Logger
*/
it('should log without custom logger', () => {
logger.log('something');
assert.isNull(lastLogMessage);
});
it('should log with custom logger', () => {
logger.setLogger(customLoggerFn);
['abc123', 'def456', 'ghi789'].forEach((m) => {
logger.log(m);
assert.strictEqual(lastLogMessage, m);
});
});
});
@@ -0,0 +1,106 @@
/**
* Challenge verification tests
*/
const { assert } = require('chai');
const { v4: uuid } = require('uuid');
const cts = require('./challtestsrv');
const verify = require('./../src/verify');
const domainName = process.env.ACME_DOMAIN_NAME || 'example.com';
describe('verify', () => {
const challengeTypes = ['http-01', 'dns-01'];
const testHttp01Authz = { identifier: { type: 'dns', value: `${uuid()}.${domainName}` } };
const testHttp01Challenge = { type: 'http-01', status: 'pending', token: uuid() };
const testHttp01Key = uuid();
const testDns01Authz = { identifier: { type: 'dns', value: `${uuid()}.${domainName}` } };
const testDns01Challenge = { type: 'dns-01', status: 'pending', token: uuid() };
const testDns01Key = uuid();
const testDns01Cname = `${uuid()}.${domainName}`;
/**
* Pebble CTS required
*/
before(function() {
if (!cts.isEnabled()) {
this.skip();
}
});
/**
* API
*/
it('should expose verification api', async () => {
assert.containsAllKeys(verify, challengeTypes);
});
/**
* http-01
*/
describe('http-01', () => {
it('should reject challenge', async () => {
await assert.isRejected(verify['http-01'](testHttp01Authz, testHttp01Challenge, testHttp01Key));
});
it('should mock challenge response', async () => {
const resp = await cts.addHttp01ChallengeResponse(testHttp01Challenge.token, testHttp01Key);
assert.isTrue(resp);
});
it('should verify challenge', async () => {
const resp = await verify['http-01'](testHttp01Authz, testHttp01Challenge, testHttp01Key);
assert.isTrue(resp);
});
it('should mock challenge response with trailing newline', async () => {
const resp = await cts.addHttp01ChallengeResponse(testHttp01Challenge.token, `${testHttp01Key}\n`);
assert.isTrue(resp);
});
it('should verify challenge with trailing newline', async () => {
const resp = await verify['http-01'](testHttp01Authz, testHttp01Challenge, testHttp01Key);
assert.isTrue(resp);
});
});
/**
* dns-01
*/
describe('dns-01', () => {
it('should reject challenge', async () => {
await assert.isRejected(verify['dns-01'](testDns01Authz, testDns01Challenge, testDns01Key));
});
it('should mock challenge response', async () => {
const resp = await cts.addDns01ChallengeResponse(`_acme-challenge.${testDns01Authz.identifier.value}.`, testDns01Key);
assert.isTrue(resp);
});
it('should add cname to challenge response', async () => {
const resp = await cts.setDnsCnameRecord(testDns01Cname, `_acme-challenge.${testDns01Authz.identifier.value}.`);
assert.isTrue(resp);
});
it('should verify challenge', async () => {
const resp = await verify['dns-01'](testDns01Authz, testDns01Challenge, testDns01Key);
assert.isTrue(resp);
});
it('should verify challenge using cname', async () => {
const resp = await verify['dns-01'](testDns01Authz, testDns01Challenge, testDns01Key);
assert.isTrue(resp);
});
});
});
@@ -0,0 +1,276 @@
/**
* Legacy crypto tests
*/
const fs = require('fs').promises;
const path = require('path');
const { assert } = require('chai');
const spec = require('./spec');
const forge = require('./../src/crypto/forge');
const cryptoEngines = {
forge
};
describe('crypto-legacy', () => {
let testPemKey;
let testCert;
let testSanCert;
const modulusStore = [];
const exponentStore = [];
const publicKeyStore = [];
const testCsrDomain = 'example.com';
const testSanCsrDomains = ['example.com', 'test.example.com', 'abc.example.com'];
const testKeyPath = path.join(__dirname, 'fixtures', 'private.key');
const testCertPath = path.join(__dirname, 'fixtures', 'certificate.crt');
const testSanCertPath = path.join(__dirname, 'fixtures', 'san-certificate.crt');
/**
* Fixtures
*/
describe('fixtures', () => {
it('should read private key fixture', async () => {
testPemKey = await fs.readFile(testKeyPath);
assert.isTrue(Buffer.isBuffer(testPemKey));
});
it('should read certificate fixture', async () => {
testCert = await fs.readFile(testCertPath);
assert.isTrue(Buffer.isBuffer(testCert));
});
it('should read san certificate fixture', async () => {
testSanCert = await fs.readFile(testSanCertPath);
assert.isTrue(Buffer.isBuffer(testSanCert));
});
});
/**
* Engines
*/
Object.entries(cryptoEngines).forEach(([name, engine]) => {
describe(`engine/${name}`, () => {
let testCsr;
let testSanCsr;
let testNonCnCsr;
let testNonAsciiCsr;
/**
* Key generation
*/
it('should generate a private key', async () => {
const key = await engine.createPrivateKey();
assert.isTrue(Buffer.isBuffer(key));
});
it('should generate a private key with size=1024', async () => {
const key = await engine.createPrivateKey(1024);
assert.isTrue(Buffer.isBuffer(key));
});
it('should generate a public key', async () => {
const key = await engine.createPublicKey(testPemKey);
assert.isTrue(Buffer.isBuffer(key));
publicKeyStore.push(key.toString().replace(/[\r\n]/gm, ''));
});
/**
* Certificate Signing Request
*/
it('should generate a csr', async () => {
const [key, csr] = await engine.createCsr({
commonName: testCsrDomain
});
assert.isTrue(Buffer.isBuffer(key));
assert.isTrue(Buffer.isBuffer(csr));
testCsr = csr;
});
it('should generate a san csr', async () => {
const [key, csr] = await engine.createCsr({
commonName: testSanCsrDomains[0],
altNames: testSanCsrDomains.slice(1, testSanCsrDomains.length)
});
assert.isTrue(Buffer.isBuffer(key));
assert.isTrue(Buffer.isBuffer(csr));
testSanCsr = csr;
});
it('should generate a csr without common name', async () => {
const [key, csr] = await engine.createCsr({
altNames: testSanCsrDomains
});
assert.isTrue(Buffer.isBuffer(key));
assert.isTrue(Buffer.isBuffer(csr));
testNonCnCsr = csr;
});
it('should generate a non-ascii csr', async () => {
const [key, csr] = await engine.createCsr({
commonName: testCsrDomain,
organization: '大安區',
organizationUnit: '中文部門'
});
assert.isTrue(Buffer.isBuffer(key));
assert.isTrue(Buffer.isBuffer(csr));
testNonAsciiCsr = csr;
});
it('should resolve domains from csr', async () => {
const result = await engine.readCsrDomains(testCsr);
spec.crypto.csrDomains(result);
assert.strictEqual(result.commonName, testCsrDomain);
assert.deepStrictEqual(result.altNames, [testCsrDomain]);
});
it('should resolve domains from san csr', async () => {
const result = await engine.readCsrDomains(testSanCsr);
spec.crypto.csrDomains(result);
assert.strictEqual(result.commonName, testSanCsrDomains[0]);
assert.deepStrictEqual(result.altNames, testSanCsrDomains);
});
it('should resolve domains from san without common name', async () => {
const result = await engine.readCsrDomains(testNonCnCsr);
spec.crypto.csrDomains(result);
assert.isNull(result.commonName);
assert.deepStrictEqual(result.altNames, testSanCsrDomains);
});
it('should resolve domains from non-ascii csr', async () => {
const result = await engine.readCsrDomains(testNonAsciiCsr);
spec.crypto.csrDomains(result);
assert.strictEqual(result.commonName, testCsrDomain);
assert.deepStrictEqual(result.altNames, [testCsrDomain]);
});
/**
* Certificate
*/
it('should read info from certificate', async () => {
const info = await engine.readCertificateInfo(testCert);
spec.crypto.certificateInfo(info);
assert.strictEqual(info.domains.commonName, testCsrDomain);
assert.strictEqual(info.domains.altNames.length, 0);
});
it('should read info from san certificate', async () => {
const info = await engine.readCertificateInfo(testSanCert);
spec.crypto.certificateInfo(info);
assert.strictEqual(info.domains.commonName, testSanCsrDomains[0]);
assert.deepEqual(info.domains.altNames, testSanCsrDomains.slice(1, testSanCsrDomains.length));
});
/**
* PEM utils
*/
it('should get pem body', () => {
[testPemKey, testCert, testSanCert].forEach((pem) => {
const body = engine.getPemBody(pem);
assert.isString(body);
assert.notInclude(body, '\r');
assert.notInclude(body, '\n');
assert.notInclude(body, '\r\n');
});
});
it('should split pem chain', () => {
[testPemKey, testCert, testSanCert].forEach((pem) => {
const chain = engine.splitPemChain(pem);
assert.isArray(chain);
assert.isNotEmpty(chain);
chain.forEach((c) => assert.isString(c));
});
});
/**
* Modulus and exponent
*/
it('should get modulus', async () => {
const result = await Promise.all([testPemKey, testCert, testSanCert].map(async (item) => {
const mod = await engine.getModulus(item);
assert.isTrue(Buffer.isBuffer(mod));
return mod;
}));
modulusStore.push(result);
});
it('should get public exponent', async () => {
const result = await Promise.all([testPemKey, testCert, testSanCert].map(async (item) => {
const exp = await engine.getPublicExponent(item);
assert.isTrue(Buffer.isBuffer(exp));
const b64exp = exp.toString('base64');
assert.strictEqual(b64exp, 'AQAB');
return b64exp;
}));
exponentStore.push(result);
});
});
});
/**
* Verify identical results
*/
describe('verification', () => {
it('should have identical public keys', () => {
if (publicKeyStore.length > 1) {
const reference = publicKeyStore.shift();
publicKeyStore.forEach((item) => assert.strictEqual(reference, item));
}
});
it('should have identical moduli', () => {
if (modulusStore.length > 1) {
const reference = modulusStore.shift();
modulusStore.forEach((item) => assert.deepStrictEqual(reference, item));
}
});
it('should have identical public exponents', () => {
if (exponentStore.length > 1) {
const reference = exponentStore.shift();
exponentStore.forEach((item) => assert.deepStrictEqual(reference, item));
}
});
});
});
@@ -0,0 +1,317 @@
/**
* Crypto tests
*/
const fs = require('fs').promises;
const path = require('path');
const { assert } = require('chai');
const spec = require('./spec');
const { crypto } = require('./../');
const emptyBodyChain1 = `
-----BEGIN TEST-----
a
-----END TEST-----
-----BEGIN TEST-----
b
-----END TEST-----
-----BEGIN TEST-----
-----END TEST-----
-----BEGIN TEST-----
c
-----END TEST-----
`;
const emptyBodyChain2 = `
-----BEGIN TEST-----
-----END TEST-----
-----BEGIN TEST-----
-----END TEST-----
-----BEGIN TEST-----
a
-----END TEST-----
-----BEGIN TEST-----
b
-----END TEST-----
-----BEGIN TEST-----
c
-----END TEST-----
`;
describe('crypto', () => {
const testCsrDomain = 'example.com';
const testSanCsrDomains = ['example.com', 'test.example.com', 'abc.example.com'];
const testKeyPath = path.join(__dirname, 'fixtures', 'private.key');
const testCertPath = path.join(__dirname, 'fixtures', 'certificate.crt');
const testSanCertPath = path.join(__dirname, 'fixtures', 'san-certificate.crt');
/**
* Key types
*/
Object.entries({
rsa: {
createKeyFns: {
s1024: () => crypto.createPrivateRsaKey(1024),
s2048: () => crypto.createPrivateRsaKey(),
s4096: () => crypto.createPrivateRsaKey(4096)
},
jwkSpecFn: spec.jwk.rsa
},
ecdsa: {
createKeyFns: {
p256: () => crypto.createPrivateEcdsaKey(),
p384: () => crypto.createPrivateEcdsaKey('P-384'),
p521: () => crypto.createPrivateEcdsaKey('P-521')
},
jwkSpecFn: spec.jwk.ecdsa
}
}).forEach(([name, { createKeyFns, jwkSpecFn }]) => {
describe(name, () => {
const testPrivateKeys = {};
const testPublicKeys = {};
/**
* Iterate through all generator variations
*/
Object.entries(createKeyFns).forEach(([n, createFn]) => {
let testCsr;
let testSanCsr;
let testNonCnCsr;
let testNonAsciiCsr;
/**
* Keys and JWK
*/
it(`${n}/should generate private key`, async () => {
testPrivateKeys[n] = await createFn();
assert.isTrue(Buffer.isBuffer(testPrivateKeys[n]));
});
it(`${n}/should get public key`, () => {
testPublicKeys[n] = crypto.getPublicKey(testPrivateKeys[n]);
assert.isTrue(Buffer.isBuffer(testPublicKeys[n]));
});
it(`${n}/should get jwk from private key`, () => {
const jwk = crypto.getJwk(testPrivateKeys[n]);
jwkSpecFn(jwk);
});
it(`${n}/should get jwk from public key`, () => {
const jwk = crypto.getJwk(testPublicKeys[n]);
jwkSpecFn(jwk);
});
/**
* Certificate Signing Request
*/
it(`${n}/should generate a csr`, async () => {
const [key, csr] = await crypto.createCsr({
commonName: testCsrDomain
}, testPrivateKeys[n]);
assert.isTrue(Buffer.isBuffer(key));
assert.isTrue(Buffer.isBuffer(csr));
testCsr = csr;
});
it(`${n}/should generate a san csr`, async () => {
const [key, csr] = await crypto.createCsr({
commonName: testSanCsrDomains[0],
altNames: testSanCsrDomains.slice(1, testSanCsrDomains.length)
}, testPrivateKeys[n]);
assert.isTrue(Buffer.isBuffer(key));
assert.isTrue(Buffer.isBuffer(csr));
testSanCsr = csr;
});
it(`${n}/should generate a csr without common name`, async () => {
const [key, csr] = await crypto.createCsr({
altNames: testSanCsrDomains
}, testPrivateKeys[n]);
assert.isTrue(Buffer.isBuffer(key));
assert.isTrue(Buffer.isBuffer(csr));
testNonCnCsr = csr;
});
it(`${n}/should generate a non-ascii csr`, async () => {
const [key, csr] = await crypto.createCsr({
commonName: testCsrDomain,
organization: '大安區',
organizationUnit: '中文部門'
}, testPrivateKeys[n]);
assert.isTrue(Buffer.isBuffer(key));
assert.isTrue(Buffer.isBuffer(csr));
testNonAsciiCsr = csr;
});
it(`${n}/should throw with invalid key`, async () => {
await assert.isRejected(crypto.createCsr({
commonName: testCsrDomain
}, testPublicKeys[n]));
});
/**
* Domain and info resolver
*/
it(`${n}/should resolve domains from csr`, () => {
const result = crypto.readCsrDomains(testCsr);
spec.crypto.csrDomains(result);
assert.strictEqual(result.commonName, testCsrDomain);
assert.deepStrictEqual(result.altNames, [testCsrDomain]);
});
it(`${n}/should resolve domains from san csr`, () => {
const result = crypto.readCsrDomains(testSanCsr);
spec.crypto.csrDomains(result);
assert.strictEqual(result.commonName, testSanCsrDomains[0]);
assert.deepStrictEqual(result.altNames, testSanCsrDomains);
});
it(`${n}/should resolve domains from csr without common name`, () => {
const result = crypto.readCsrDomains(testNonCnCsr);
spec.crypto.csrDomains(result);
assert.isNull(result.commonName);
assert.deepStrictEqual(result.altNames, testSanCsrDomains);
});
it(`${n}/should resolve domains from non-ascii csr`, () => {
const result = crypto.readCsrDomains(testNonAsciiCsr);
spec.crypto.csrDomains(result);
assert.strictEqual(result.commonName, testCsrDomain);
assert.deepStrictEqual(result.altNames, [testCsrDomain]);
});
});
});
});
/**
* Common functionality
*/
describe('common', () => {
let testPemKey;
let testCert;
let testSanCert;
it('should read private key fixture', async () => {
testPemKey = await fs.readFile(testKeyPath);
assert.isTrue(Buffer.isBuffer(testPemKey));
});
it('should read certificate fixture', async () => {
testCert = await fs.readFile(testCertPath);
assert.isTrue(Buffer.isBuffer(testCert));
});
it('should read san certificate fixture', async () => {
testSanCert = await fs.readFile(testSanCertPath);
assert.isTrue(Buffer.isBuffer(testSanCert));
});
/**
* CSR with auto-generated key
*/
it('should generate a csr with auto-generated key', async () => {
const [key, csr] = await crypto.createCsr({
commonName: testCsrDomain
});
assert.isTrue(Buffer.isBuffer(key));
assert.isTrue(Buffer.isBuffer(csr));
});
/**
* Certificate
*/
it('should read certificate info', () => {
const info = crypto.readCertificateInfo(testCert);
spec.crypto.certificateInfo(info);
assert.strictEqual(info.domains.commonName, testCsrDomain);
assert.strictEqual(info.domains.altNames.length, 0);
});
it('should read certificate info with san', () => {
const info = crypto.readCertificateInfo(testSanCert);
spec.crypto.certificateInfo(info);
assert.strictEqual(info.domains.commonName, testSanCsrDomains[0]);
assert.deepEqual(info.domains.altNames, testSanCsrDomains.slice(1, testSanCsrDomains.length));
});
/**
* PEM utils
*/
it('should get pem body as b64u', () => {
[testPemKey, testCert, testSanCert].forEach((pem) => {
const body = crypto.getPemBodyAsB64u(pem);
assert.isString(body);
assert.notInclude(body, '\r');
assert.notInclude(body, '\n');
assert.notInclude(body, '\r\n');
});
});
it('should split pem chain', () => {
[testPemKey, testCert, testSanCert].forEach((pem) => {
const chain = crypto.splitPemChain(pem);
assert.isArray(chain);
assert.isNotEmpty(chain);
chain.forEach((c) => assert.isString(c));
});
});
it('should split pem chain with empty bodies', () => {
const c1 = crypto.splitPemChain(emptyBodyChain1);
const c2 = crypto.splitPemChain(emptyBodyChain2);
assert.strictEqual(c1.length, 3);
assert.strictEqual(c2.length, 3);
});
});
});
@@ -0,0 +1,574 @@
/**
* ACME client tests
*/
const { assert } = require('chai');
const { v4: uuid } = require('uuid');
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 capMetaTosField = !(('ACME_CAP_META_TOS_FIELD' in process.env) && (process.env.ACME_CAP_META_TOS_FIELD === '0'));
const capUpdateAccountKey = !(('ACME_CAP_UPDATE_ACCOUNT_KEY' in process.env) && (process.env.ACME_CAP_UPDATE_ACCOUNT_KEY === '0'));
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', () => {
const testDomain = `${uuid()}.${domainName}`;
const testDomainWildcard = `*.${testDomain}`;
const testContact = `mailto:test-${uuid()}@nope.com`;
/**
* 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)
},
jwkSpecFn: spec.jwk.rsa
},
ecdsa: {
createKeyFn: () => acme.crypto.createPrivateEcdsaKey(),
createKeyAltFns: {
p384: () => acme.crypto.createPrivateEcdsaKey('P-384'),
p521: () => acme.crypto.createPrivateEcdsaKey('P-521')
},
jwkSpecFn: spec.jwk.ecdsa
}
}).forEach(([name, { createKeyFn, createKeyAltFns, jwkSpecFn }]) => {
describe(name, () => {
let testIssuers;
let testAccountKey;
let testAccountSecondaryKey;
let testClient;
let testAccount;
let testAccountUrl;
let testOrder;
let testOrderWildcard;
let testAuthz;
let testAuthzWildcard;
let testChallenge;
let testChallengeWildcard;
let testKeyAuthorization;
let testKeyAuthorizationWildcard;
let testCsr;
let testCsrWildcard;
let testCertificate;
let testCertificateWildcard;
/**
* Fixtures
*/
it('should generate a private key', async () => {
testAccountKey = await createKeyFn();
assert.isTrue(Buffer.isBuffer(testAccountKey));
});
it('should create a second private key', async () => {
testAccountSecondaryKey = await createKeyFn();
assert.isTrue(Buffer.isBuffer(testAccountSecondaryKey));
});
it('should generate certificate signing request', async () => {
[, testCsr] = await acme.crypto.createCsr({ commonName: testDomain }, await createKeyFn());
[, testCsrWildcard] = await acme.crypto.createCsr({ commonName: testDomainWildcard }, await createKeyFn());
});
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 clients
*/
it('should initialize client', () => {
testClient = new acme.Client({
...clientOpts,
accountKey: testAccountKey
});
});
it('should produce a valid jwk', () => {
const jwk = testClient.http.getJwk();
jwkSpecFn(jwk);
});
/**
* Terms of Service
*/
it('should produce tos url [ACME_CAP_META_TOS_FIELD]', async function() {
if (!capMetaTosField) {
this.skip();
}
const tos = await testClient.getTermsOfServiceUrl();
assert.isString(tos);
});
it('should not produce tos url [!ACME_CAP_META_TOS_FIELD]', async function() {
if (capMetaTosField) {
this.skip();
}
const tos = await testClient.getTermsOfServiceUrl();
assert.isNull(tos);
});
/**
* Create account
*/
it('should refuse account creation without tos [ACME_CAP_META_TOS_FIELD]', async function() {
if (!capMetaTosField) {
this.skip();
}
await assert.isRejected(testClient.createAccount());
});
it('should refuse account creation without eab [ACME_CAP_EAB_ENABLED]', async function() {
if (!capEabEnabled) {
this.skip();
}
const client = new acme.Client({
...clientOpts,
accountKey: testAccountKey,
externalAccountBinding: null
});
await assert.isRejected(client.createAccount({
termsOfServiceAgreed: true
}));
});
it('should create an account', async () => {
testAccount = await testClient.createAccount({
termsOfServiceAgreed: true
});
spec.rfc8555.account(testAccount);
assert.strictEqual(testAccount.status, 'valid');
});
it('should produce an account url', () => {
testAccountUrl = testClient.getAccountUrl();
assert.isString(testAccountUrl);
});
/**
* Create account with alternate key sizes
*/
Object.entries(createKeyAltFns).forEach(([k, altKeyFn]) => {
it(`should create account with key=${k}`, async () => {
const client = new acme.Client({
...clientOpts,
accountKey: await altKeyFn()
});
const account = await client.createAccount({
termsOfServiceAgreed: true
});
spec.rfc8555.account(account);
assert.strictEqual(account.status, 'valid');
});
});
/**
* Find existing account using secondary client
*/
it('should throw when trying to find account using invalid account key', async () => {
const client = new acme.Client({
...clientOpts,
accountKey: testAccountSecondaryKey
});
await assert.isRejected(client.createAccount({
onlyReturnExisting: true
}));
});
it('should find existing account using account key', async () => {
const client = new acme.Client({
...clientOpts,
accountKey: testAccountKey
});
const account = await client.createAccount({
onlyReturnExisting: true
});
spec.rfc8555.account(account);
assert.strictEqual(account.status, 'valid');
assert.deepStrictEqual(account.key, testAccount.key);
});
/**
* Account URL
*/
it('should refuse invalid account url', async () => {
const client = new acme.Client({
...clientOpts,
accountKey: testAccountKey,
accountUrl: 'https://acme-staging-v02.api.letsencrypt.org/acme/acct/1'
});
await assert.isRejected(client.updateAccount());
});
it('should find existing account using account url', async () => {
const client = new acme.Client({
...clientOpts,
accountKey: testAccountKey,
accountUrl: testAccountUrl
});
const account = await client.createAccount({
onlyReturnExisting: true
});
spec.rfc8555.account(account);
assert.strictEqual(account.status, 'valid');
assert.deepStrictEqual(account.key, testAccount.key);
});
/**
* Update account contact info
*/
it('should update account contact info', async () => {
const data = { contact: [testContact] };
const account = await testClient.updateAccount(data);
spec.rfc8555.account(account);
assert.strictEqual(account.status, 'valid');
assert.deepStrictEqual(account.key, testAccount.key);
assert.isArray(account.contact);
assert.include(account.contact, testContact);
});
/**
* Change account private key
*/
it('should change account private key [ACME_CAP_UPDATE_ACCOUNT_KEY]', async function() {
if (!capUpdateAccountKey) {
this.skip();
}
await testClient.updateAccountKey(testAccountSecondaryKey);
const account = await testClient.createAccount({
onlyReturnExisting: true
});
spec.rfc8555.account(account);
assert.strictEqual(account.status, 'valid');
assert.notDeepEqual(account.key, testAccount.key);
});
/**
* Create new certificate order
*/
it('should create new order', async () => {
const data1 = { identifiers: [{ type: 'dns', value: testDomain }] };
const data2 = { identifiers: [{ type: 'dns', value: testDomainWildcard }] };
testOrder = await testClient.createOrder(data1);
testOrderWildcard = await testClient.createOrder(data2);
[testOrder, testOrderWildcard].forEach((item) => {
spec.rfc8555.order(item);
assert.strictEqual(item.status, 'pending');
});
});
/**
* Get status of existing certificate order
*/
it('should get existing order', async () => {
await Promise.all([testOrder, testOrderWildcard].map(async (existing) => {
const result = await testClient.getOrder(existing);
spec.rfc8555.order(result);
assert.deepStrictEqual(existing, result);
}));
});
/**
* Get identifier authorization
*/
it('should get identifier authorization', async () => {
const orderAuthzCollection = await testClient.getAuthorizations(testOrder);
const wildcardAuthzCollection = await testClient.getAuthorizations(testOrderWildcard);
[orderAuthzCollection, wildcardAuthzCollection].forEach((collection) => {
assert.isArray(collection);
assert.isNotEmpty(collection);
collection.forEach((authz) => {
spec.rfc8555.authorization(authz);
assert.strictEqual(authz.status, 'pending');
});
});
testAuthz = orderAuthzCollection.pop();
testAuthzWildcard = wildcardAuthzCollection.pop();
testAuthz.challenges.concat(testAuthzWildcard.challenges).forEach((item) => {
spec.rfc8555.challenge(item);
assert.strictEqual(item.status, 'pending');
});
});
/**
* Generate challenge key authorization
*/
it('should get challenge key authorization', async () => {
testChallenge = testAuthz.challenges.find((c) => (c.type === 'http-01'));
testChallengeWildcard = testAuthzWildcard.challenges.find((c) => (c.type === 'dns-01'));
testKeyAuthorization = await testClient.getChallengeKeyAuthorization(testChallenge);
testKeyAuthorizationWildcard = await testClient.getChallengeKeyAuthorization(testChallengeWildcard);
[testKeyAuthorization, testKeyAuthorizationWildcard].forEach((k) => assert.isString(k));
});
/**
* Deactivate identifier authorization
*/
it('should deactivate identifier authorization', async () => {
const order = await testClient.createOrder({
identifiers: [
{ type: 'dns', value: `${uuid()}.${domainName}` },
{ type: 'dns', value: `${uuid()}.${domainName}` }
]
});
const authzCollection = await testClient.getAuthorizations(order);
const results = await Promise.all(authzCollection.map(async (authz) => {
spec.rfc8555.authorization(authz);
assert.strictEqual(authz.status, 'pending');
return testClient.deactivateAuthorization(authz);
}));
results.forEach((authz) => {
spec.rfc8555.authorization(authz);
assert.strictEqual(authz.status, 'deactivated');
});
});
/**
* Verify satisfied challenge
*/
it('should verify challenge', async () => {
await cts.assertHttpChallengeCreateFn(testAuthz, testChallenge, testKeyAuthorization);
await cts.assertDnsChallengeCreateFn(testAuthzWildcard, testChallengeWildcard, testKeyAuthorizationWildcard);
await testClient.verifyChallenge(testAuthz, testChallenge);
await testClient.verifyChallenge(testAuthzWildcard, testChallengeWildcard);
});
/**
* Complete challenge
*/
it('should complete challenge', async () => {
await Promise.all([testChallenge, testChallengeWildcard].map(async (challenge) => {
const result = await testClient.completeChallenge(challenge);
spec.rfc8555.challenge(result);
assert.strictEqual(challenge.url, result.url);
}));
});
/**
* Wait for valid challenge
*/
it('should wait for valid challenge status', async () => {
await Promise.all([testChallenge, testChallengeWildcard].map(async (c) => testClient.waitForValidStatus(c)));
});
/**
* Finalize order
*/
it('should finalize order', async () => {
const finalize = await testClient.finalizeOrder(testOrder, testCsr);
const finalizeWildcard = await testClient.finalizeOrder(testOrderWildcard, testCsrWildcard);
[finalize, finalizeWildcard].forEach((f) => spec.rfc8555.order(f));
assert.strictEqual(testOrder.url, finalize.url);
assert.strictEqual(testOrderWildcard.url, finalizeWildcard.url);
});
/**
* Wait for valid order
*/
it('should wait for valid order status', async () => {
await Promise.all([testOrder, testOrderWildcard].map(async (o) => testClient.waitForValidStatus(o)));
});
/**
* Get certificate
*/
it('should get certificate', async () => {
testCertificate = await testClient.getCertificate(testOrder);
testCertificateWildcard = await testClient.getCertificate(testOrderWildcard);
[testCertificate, testCertificateWildcard].forEach((cert) => {
assert.isString(cert);
acme.crypto.readCertificateInfo(cert);
});
});
it('should get alternate certificate chain [ACME_CAP_ALTERNATE_CERT_ROOTS]', async function() {
if (!capAlternateCertRoots) {
this.skip();
}
await Promise.all(testIssuers.map(async (issuer) => {
const cert = await testClient.getCertificate(testOrder, issuer);
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 cert = await testClient.getCertificate(testOrder, uuid());
const rootCert = acme.crypto.splitPemChain(cert).pop();
const info = acme.crypto.readCertificateInfo(rootCert);
assert.strictEqual(testIssuers[0], info.issuer.commonName);
});
/**
* Revoke certificate
*/
it('should revoke certificate', async () => {
await testClient.revokeCertificate(testCertificate);
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(testOrderWildcard));
});
/**
* Deactivate account
*/
it('should deactivate account', async () => {
const data = { status: 'deactivated' };
const account = await testClient.updateAccount(data);
spec.rfc8555.account(account);
assert.strictEqual(account.status, 'deactivated');
});
/**
* Verify that no new orders can be made
*/
it('should not allow new orders from deactivated account', async () => {
const data = { identifiers: [{ type: 'dns', value: 'nope.com' }] };
await assert.isRejected(testClient.createOrder(data));
});
});
});
});
@@ -0,0 +1,384 @@
/**
* ACME client.auto tests
*/
const { assert } = require('chai');
const { v4: uuid } = require('uuid');
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 testDnsDomain = `${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);
});
/**
* 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 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 san certificate', async () => {
const [, csr] = await acme.crypto.createCsr({
commonName: testSanDomains[0],
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({
commonName: testWildcardDomain,
altNames: [`*.${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.strictEqual(info.domains.commonName, testDomain);
assert.deepStrictEqual(info.domains.altNames, [testDomain]);
});
it('should read san certificate info', () => {
const info = acme.crypto.readCertificateInfo(testSanCertificate);
spec.crypto.certificateInfo(info);
assert.strictEqual(info.domains.commonName, testSanDomains[0]);
assert.deepStrictEqual(info.domains.altNames, testSanDomains);
});
it('should read wildcard certificate info', () => {
const info = acme.crypto.readCertificateInfo(testWildcardCertificate);
spec.crypto.certificateInfo(info);
assert.strictEqual(info.domains.commonName, testWildcardDomain);
assert.deepStrictEqual(info.domains.altNames, [testWildcardDomain, `*.${testWildcardDomain}`]);
});
});
});
});
@@ -0,0 +1,93 @@
/**
* Pebble Challenge Test Server integration
*/
const { assert } = require('chai');
const axios = require('./../src/axios');
const apiBaseUrl = process.env.ACME_CHALLTESTSRV_URL || null;
/**
* Send request
*/
async function request(apiPath, data = {}) {
if (!apiBaseUrl) {
throw new Error('No Pebble Challenge Test Server URL found');
}
await axios.request({
url: `${apiBaseUrl}/${apiPath}`,
method: 'post',
data
});
return true;
}
/**
* State
*/
exports.isEnabled = () => !!apiBaseUrl;
/**
* DNS
*/
exports.addDnsARecord = async (host, addresses) => request('add-a', { host, addresses });
exports.setDnsCnameRecord = async (host, target) => request('set-cname', { host, target });
/**
* Challenge response
*/
async function addHttp01ChallengeResponse(token, content) {
return request('add-http01', { token, content });
}
async function addDns01ChallengeResponse(host, value) {
return request('set-txt', { host, value });
}
exports.addHttp01ChallengeResponse = addHttp01ChallengeResponse;
exports.addDns01ChallengeResponse = addDns01ChallengeResponse;
/**
* Challenge response mock functions
*/
async function assertHttpChallengeCreateFn(authz, challenge, keyAuthorization) {
assert.strictEqual(challenge.type, 'http-01');
return addHttp01ChallengeResponse(challenge.token, keyAuthorization);
}
async function assertDnsChallengeCreateFn(authz, challenge, keyAuthorization) {
assert.strictEqual(challenge.type, 'dns-01');
return addDns01ChallengeResponse(`_acme-challenge.${authz.identifier.value}.`, keyAuthorization);
}
async function challengeCreateFn(authz, challenge, keyAuthorization) {
if (challenge.type === 'http-01') {
return assertHttpChallengeCreateFn(authz, challenge, keyAuthorization);
}
if (challenge.type === 'dns-01') {
return assertDnsChallengeCreateFn(authz, challenge, keyAuthorization);
}
throw new Error(`Unsupported challenge type ${challenge.type}`);
}
exports.challengeRemoveFn = async () => true;
exports.challengeNoopFn = async () => true;
exports.challengeThrowFn = async () => { throw new Error('oops'); };
exports.assertHttpChallengeCreateFn = assertHttpChallengeCreateFn;
exports.assertDnsChallengeCreateFn = assertDnsChallengeCreateFn;
exports.challengeCreateFn = challengeCreateFn;
+30
View File
@@ -0,0 +1,30 @@
-----BEGIN CERTIFICATE-----
MIIFMjCCAxoCCQCVordquLnq8TANBgkqhkiG9w0BAQUFADBbMQswCQYDVQQGEwJB
VTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0
cyBQdHkgTHRkMRQwEgYDVQQDEwtleGFtcGxlLmNvbTAeFw0xNzA5MTQxNDMzMTRa
Fw0xODA5MTQxNDMzMTRaMFsxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0
YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxFDASBgNVBAMT
C2V4YW1wbGUuY29tMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwi2P
YBNGl1n78niRGDKgcsWK03TcTeVbQ1HztA57Rr1iDHAZNx3Mv4E/Sha8VKbKoshc
mUcOS3AlmbIZX+7+9c7lL2oD+vtUZF1YUR/69fWuO72wk6fKj/eofxH9Ud5KFje8
qrYZdJWKkPMdWlYgjD6qpA5wl60NiuxmUr44ADZDytqHzNThN3wrFruz74PcMfak
cSUMxkh98LuNeGtqHpEAw+wliko3oDD4PanvDvp5mRgiQVKHEGT7dm85Up+W1iJK
J65fkc/j940MaLbdISZYYCT5dtPgCGKCHgVuVrY+OXFJrD3TTm94ILsR/BkS/VSK
NigGVPXg3q8tgIS++k13CzLUO0PNRMuod1RD9j5NEc2CVic9rcH06ugZyHlOcuVv
vRsPGd52BPn+Jf1aePKPPQHxT9i5GOs80CJw0eduZCDZB32biRYNwUtjFkHbu8ii
2IGkvhnWonjd4w5wOldG+RPr+XoFCIaHp5TszQ+HnUTLIXKtBgzzCKjK4eZqrck7
xpo5B5m5V7EUxBze2LYVky+GsDsqL8CggQqJL4ZKuZVoxgPwhnDy5nMs057NCU9E
nXcauMW9UEqEHu5NXnmGJrCvQ56wjYN3lgvCHEtmIpsRjCCWaBJYiawu1J5ZAf1y
GTVNh8pEvO//zL9ImUxrSfOGUeFiN1tzSFlTfbcCAwEAATANBgkqhkiG9w0BAQUF
AAOCAgEAdZZpgWv79CgF5ny6HmMaYgsXJKJyQE9RhJ1cmzDY8KAF+nzT7q4Pgt3W
bA9bpdji7C0WqKjX7hLipqhgFnqb8qZcodEKhX788qBj4X45+4nT6QipyJlz5x6K
cCn/v9gQNKks7U+dBlqquiVfbXaa1EAKMeGtqinf+Y51nR/fBcr/P9TBnSJqH61K
DO3qrE5KGTwHQ9VXoeKyeppGt5sYf8G0vwoHhtPTOO8TuLEIlFcXtzbC3zAtmQj6
Su//fI5yjuYTkiayxMx8nCGrQhQSXdC8gYpYd0os7UY01DVu4BTCXEvf0GYXtiGJ
eG8lQT/eu7WdK83uJ93U/BMYzoq4lSVcqY4LNxlfAQXKhaAbioA5XyT7co7FQ0g+
s2CGBUKa11wPDe8M2GVLPsxT2bXDQap5DQyVIuTwjtgL0tykGxPJPAnL2zuUy6T3
/YzrWaJ9Os+6mUCVdLnXtDgZ10Ujel7mq6wo9Ns+u07grXZkXpmJYnJXBrwOsY8K
Za5vFwgJrDXhWe+Fmgt1EP5VIqRCQAxH2iYvAaELi8udbN/ZiUU3K9t79MP/M3U/
tEWAubHXsaAv03jRy43X0VjlZHmagU/4dU7RBWfyuwRarYIXLNT2FCd2z4kd3fsL
3rB5iI+RH0uoNuOa1+UApfFCv0O65TYkp5jEWSlU8PhKYD43nXA=
-----END CERTIFICATE-----
+27
View File
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAo0nBEFBeo2XnR1kx0jV00W9EszE5Ei/zuJKLXXwTeUGMhy9h
CqPFWQnTOD5PQUcja98p96LCdRZpfsfoL1RewksD6BCJN+9hZucImBASpmg2M432
wsF7fa3/FMtICrEmQh+LJ48zOottr93YcipY1fNxHWzFr8Hvv+OZCMmQvL4W5u5U
rxdi7jptbAbFvv470TN8lpVwneG7kG3cemPhW6RmfTTUQ2Qp/QP6fptpWIIy5kQe
zvgpql2xRPBjqb4VDy1kVTTCe/Lpt7bNGe2eZOzXJcjrU+d5LEOrfQoX5ZO4H5UC
9YlT6wqyPv9VZ5g2slz198LGV8hdGEVix8XPiQIDAQABAoIBAQCaoo4jVPlK5IZS
GzYDTHyEmksFJ+hUQPUeJim1LnuCqYDbxRKxcMbDu3o8GUYVG7l/vqePzKM7Hy5o
0gggSlYyybe5XW+VeS1UthZ9azs+PBKYYCj/5xt7ufuHRbvD5F/G3vh5TjPFjaUi
l4UTGOdoNlM4+nl8KL1Ti8axe7GGCztxmjJL7VnN4RWc5yzBrU6oiQED0BM/6KFx
nJHPuwzRemRRjz8Lk1ryMsCymtZx70slxVJeHPdoMc9vkseOulooBMZtXqOixoHO
UtFuKGgIkg6KA9qI+8RmqSPUeXrbrPeRZtu3N9NcsPUYVptNo1ZjLpa9Eigd0tkq
1+/TyGDBAoGBANCnK/+uXIZWt4QoF+7AUGeckOmRAbJnWf8KrScSa/TSGNObGP00
LpeM10eNDqvfY9RepM6RH5R75vDWltJd4+fyQPaHqG4AhlMk1JglZn71F91FWstx
K/qrPfnBQP7qq4yuQ0zavPkgIUWzryLk0JnQ4wPNLiXFAfQYDt+F8Vg3AoGBAMhX
S+sej87zRHbV/wj7zLa/QwnDLiU7wswv9zUf2Ot+49pfwEzzSGuLHFHoIXVrGo2y
QQl6sovJ6dFi7GFPikiwj9em/EF4JgTmWZhoYH1HmThTUeziLa2/VT4eIZn7Viwb
/goxKAvGvHkcdQIeNPPFaEi0m7vDkTAv/WG/prY/AoGAcKWwVWuXPFfY4BqdQSLG
xgl7Gv5UgjLWHaFv9iY17oj3KlcT2K+xb9Rz7Yc0IoqKZP9rzrH+8LUr616PMqfK
AVGCzRZUUn8qBf1eYX3fpi9AYQ+ugyNocP6+iPZS1s1vLJZwcy+s0nsMO4tUxGvw
SvrBdS3y+iUwds3+SaMQt2UCgYAg0BuBIPpQ3QtDo30oDYXUELN8L9mpA4a+RsTo
kJTIzXmoVLJ8aAReiORUjf6c6rPorV91nAEOYD3Jq7gnoA14JmMI4TLDzlf7yXa3
PbFAE7AGx67Na6YrpQDjMbAzNjVA+Dy9kpuKgjxwYbbQZ/4oRxbzgZFYSYnIKLQJ
hIhbpQKBgEc8fYYc3UqyNIGNupYBZhb0pF7FPMwldzv4UufMjkYzKREaCT2D3HIC
FEKiJxatIhOtCW5oa5sXK/mGR9EEzW1ltlljymu0u+slIoWvWWGfQU9DxFBnZ2x5
4/nzeq4zI+qYt8qXZTnhY/bpZI0pdQqWT9+AoFJJn8Bfdk1mLuIs
-----END RSA PRIVATE KEY-----
@@ -0,0 +1,18 @@
-----BEGIN CERTIFICATE-----
MIIC3zCCAcegAwIBAgIJAPZkD9qD+FX8MA0GCSqGSIb3DQEBBQUAMBYxFDASBgNV
BAMMC2V4YW1wbGUuY29tMB4XDTE3MDkyMTIwMzY1MFoXDTE4MDkyMTIwMzY1MFow
FjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
ggEKAoIBAQDNygRzPEyGP90Q3ms1DzUIj597u4t22TU08TQywMTt+/Sd+LNDvgwI
yhCbbwetVq+rvEayAMaQjFzqgoQOxY8GDrrqfPfQ50ED79vu5VPaqVSTN5FwK7hq
6Bl+kT2MUMIwhhGTfrn7inGhxB1hhYtAaUJDuLN2JjB6Ax9BfVv5NJLPeN1V6qdV
edtmNrUV5eWwEPfl4kCJ8Ytes6YttN2UDnet/B19po3/JEdy5YgPmeAfW1wbA+kl
oU475uPpKPV79M+6hrKNlS2hPFcGOiL/7glKgXURg7Ih+e53Qx6tgqKrgmjRM8Jq
0bLwM1+xY0O/2C9wbkpElBLU9CKS9I+PAgMBAAGjMDAuMCwGA1UdEQQlMCOCEHRl
c3QuZXhhbXBsZS5jb22CD2FiYy5leGFtcGxlLmNvbTANBgkqhkiG9w0BAQUFAAOC
AQEAGCVsiJZOe0LVYQ40a/N/PaVVs1zj1KXmVDrKEWW8fhjEMao/j/Bb4rXuKCkC
DQIZR1jsFC4IWyL4eOpUp4SPFn6kbdzhxjl+42kuzQLqTc1EiEobwEbroQSoUJpT
xj2j0YnrFn/9hVBHUgsA3tONNXL5McEtiHOQ+iXUmoPw9sRvs0DEshS1XeYvTuzY
Jua6uev1QBXxll3+pw7i2Wbt9ifeX6NBe+MOGIYxn6aMwwmgtoLbxDMThtVJJGCH
V0JrSBhEkVlfK1LukSUeSO1RpCsV+97Xx2jEsNwbiji/xKnXk44sVJhJ/yQnWkiC
wZLUm/SNOOtPT68U5RopRC0IXA==
-----END CERTIFICATE-----
@@ -0,0 +1,40 @@
/**
* Get ACME certificate issuers
*/
const acme = require('./../');
const util = require('./../src/util');
const pebbleManagementUrl = process.env.ACME_PEBBLE_MANAGEMENT_URL || null;
/**
* Pebble
*/
async function getPebbleCertIssuers() {
/* Get intermediate certificate and resolve alternates */
const root = await acme.axios.get(`${pebbleManagementUrl}/intermediates/0`);
const links = util.parseLinkHeader(root.headers.link || '');
const alternates = await Promise.all(links.map(async (link) => acme.axios.get(link)));
/* Get certificate info */
const certs = [root].concat(alternates).map((c) => c.data);
const info = certs.map((c) => acme.crypto.readCertificateInfo(c));
/* Return issuers */
return info.map((i) => i.issuer.commonName);
}
/**
* Get certificate issuers
*/
module.exports = async () => {
if (pebbleManagementUrl) {
return getPebbleCertIssuers();
}
throw new Error('Unable to resolve list of certificate issuers');
};
+87
View File
@@ -0,0 +1,87 @@
/**
* Setup testing
*/
const url = require('url');
const net = require('net');
const fs = require('fs');
const dns = require('dns').promises;
const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');
const axios = require('./../src/axios');
/**
* Add promise support to Chai
*/
chai.use(chaiAsPromised);
/**
* HTTP challenge port
*/
if (process.env.ACME_HTTP_PORT) {
axios.defaults.acmeSettings.httpChallengePort = process.env.ACME_HTTP_PORT;
}
/**
* External account binding
*/
if (('ACME_CAP_EAB_ENABLED' in process.env) && (process.env.ACME_CAP_EAB_ENABLED === '1')) {
const pebbleConfig = JSON.parse(fs.readFileSync('/etc/pebble/pebble.json').toString());
const [kid, hmacKey] = Object.entries(pebbleConfig.pebble.externalAccountMACKeys)[0];
process.env.ACME_EAB_KID = kid;
process.env.ACME_EAB_HMAC_KEY = hmacKey;
}
/**
* Custom DNS resolver
*/
if (process.env.ACME_DNS_RESOLVER) {
dns.setServers([process.env.ACME_DNS_RESOLVER]);
/**
* Axios DNS resolver
*/
axios.interceptors.request.use(async (config) => {
const urlObj = url.parse(config.url);
/* Bypass */
if (axios.defaults.acmeSettings.bypassCustomDnsResolver === true) {
return config;
}
/* Skip IP addresses and localhost */
if (net.isIP(urlObj.hostname) || (urlObj.hostname === 'localhost')) {
return config;
}
/* Lookup hostname */
const result = await dns.resolve4(urlObj.hostname);
if (!result.length) {
throw new Error(`Unable to lookup address: ${urlObj.hostname}`);
}
/* Place hostname in header */
config.headers = config.headers || {};
config.headers.Host = urlObj.hostname;
/* Inject address into URL */
delete urlObj.host;
urlObj.hostname = result[0];
config.url = url.format(urlObj);
/* Done */
return config;
});
}
+178
View File
@@ -0,0 +1,178 @@
/**
* Assertions
*/
const { assert } = require('chai');
const spec = {};
module.exports = spec;
/**
* ACME
*/
spec.rfc8555 = {};
spec.rfc8555.account = (obj) => {
assert.isObject(obj);
assert.isString(obj.status);
assert.include(['valid', 'deactivated', 'revoked'], obj.status);
assert.isString(obj.orders);
if ('contact' in obj) {
assert.isArray(obj.contact);
obj.contact.forEach((c) => assert.isString(c));
}
if ('termsOfServiceAgreed' in obj) {
assert.isBoolean(obj.termsOfServiceAgreed);
}
if ('externalAccountBinding' in obj) {
assert.isObject(obj.externalAccountBinding);
}
};
spec.rfc8555.order = (obj) => {
assert.isObject(obj);
assert.isString(obj.status);
assert.include(['pending', 'ready', 'processing', 'valid', 'invalid'], obj.status);
assert.isArray(obj.identifiers);
obj.identifiers.forEach((i) => spec.rfc8555.identifier(i));
assert.isArray(obj.authorizations);
obj.authorizations.forEach((a) => assert.isString(a));
assert.isString(obj.finalize);
if ('expires' in obj) {
assert.isString(obj.expires);
}
if ('notBefore' in obj) {
assert.isString(obj.notBefore);
}
if ('notAfter' in obj) {
assert.isString(obj.notAfter);
}
if ('error' in obj) {
assert.isObject(obj.error);
}
if ('certificate' in obj) {
assert.isString(obj.certificate);
}
/* Augmentations */
assert.isString(obj.url);
};
spec.rfc8555.authorization = (obj) => {
assert.isObject(obj);
spec.rfc8555.identifier(obj.identifier);
assert.isString(obj.status);
assert.include(['pending', 'valid', 'invalid', 'deactivated', 'expires', 'revoked'], obj.status);
assert.isArray(obj.challenges);
obj.challenges.forEach((c) => spec.rfc8555.challenge(c));
if ('expires' in obj) {
assert.isString(obj.expires);
}
if ('wildcard' in obj) {
assert.isBoolean(obj.wildcard);
}
/* Augmentations */
assert.isString(obj.url);
};
spec.rfc8555.identifier = (obj) => {
assert.isObject(obj);
assert.isString(obj.type);
assert.isString(obj.value);
};
spec.rfc8555.challenge = (obj) => {
assert.isObject(obj);
assert.isString(obj.type);
assert.isString(obj.url);
assert.isString(obj.status);
assert.include(['pending', 'processing', 'valid', 'invalid'], obj.status);
if ('validated' in obj) {
assert.isString(obj.validated);
}
if ('error' in obj) {
assert.isObject(obj.error);
}
};
/**
* Crypto
*/
spec.crypto = {};
spec.crypto.csrDomains = (obj) => {
assert.isObject(obj);
assert.isDefined(obj.commonName);
assert.isArray(obj.altNames);
obj.altNames.forEach((a) => assert.isString(a));
};
spec.crypto.certificateInfo = (obj) => {
assert.isObject(obj);
assert.isObject(obj.issuer);
assert.isDefined(obj.issuer.commonName);
assert.isObject(obj.domains);
assert.isDefined(obj.domains.commonName);
assert.isArray(obj.domains.altNames);
obj.domains.altNames.forEach((a) => assert.isString(a));
assert.strictEqual(Object.prototype.toString.call(obj.notBefore), '[object Date]');
assert.strictEqual(Object.prototype.toString.call(obj.notAfter), '[object Date]');
};
/**
* JWK
*/
spec.jwk = {};
spec.jwk.rsa = (obj) => {
assert.isObject(obj);
assert.isString(obj.e);
assert.isString(obj.kty);
assert.isString(obj.n);
assert.strictEqual(obj.e, 'AQAB');
assert.strictEqual(obj.kty, 'RSA');
};
spec.jwk.ecdsa = (obj) => {
assert.isObject(obj);
assert.isString(obj.crv);
assert.isString(obj.kty);
assert.isString(obj.x);
assert.isString(obj.y);
assert.strictEqual(obj.kty, 'EC');
};