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}`]);
 | |
|             });
 | |
|         });
 | |
|     });
 | |
| });
 |