mirror of https://github.com/certd/certd
575 lines
20 KiB
JavaScript
575 lines
20 KiB
JavaScript
/**
|
|
* 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));
|
|
});
|
|
});
|
|
});
|
|
});
|