mirror of
https://github.com/certd/certd.git
synced 2026-04-24 12:27:25 +08:00
🔱: [acme] sync upgrade with 21 commits [trident-sync]
Bump v5.0.0
This commit is contained in:
@@ -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));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user