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