From 49b96592a0cc23b061e2491504fc7d130d847b4f Mon Sep 17 00:00:00 2001 From: xiaojunnuo Date: Tue, 22 Dec 2020 22:47:07 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/certd/src/acme.js | 36 +- packages/certd/src/acme/api.cjs | 227 ------ packages/certd/src/acme/auto.cjs | 145 ---- packages/certd/src/acme/axios.cjs | 37 - packages/certd/src/acme/client.cjs | 690 ------------------ packages/certd/src/acme/crypto/forge.cjs | 445 ----------- packages/certd/src/acme/http.cjs | 228 ------ packages/certd/src/acme/index.cjs | 28 - packages/certd/src/acme/util.cjs | 158 ---- packages/certd/src/acme/verify.cjs | 90 --- .../certd/src/dns-provider/dns-provider.js | 2 +- .../certd/src/dns-provider/impl/aliyun.js | 4 +- packages/certd/src/index.js | 15 +- packages/certd/test/index.test.js | 4 +- packages/certd/test/options.js | 50 +- packages/executor/src/index.js | 9 + packages/executor/test/options.js | 2 + 17 files changed, 45 insertions(+), 2125 deletions(-) delete mode 100644 packages/certd/src/acme/api.cjs delete mode 100644 packages/certd/src/acme/auto.cjs delete mode 100644 packages/certd/src/acme/axios.cjs delete mode 100644 packages/certd/src/acme/client.cjs delete mode 100644 packages/certd/src/acme/crypto/forge.cjs delete mode 100644 packages/certd/src/acme/http.cjs delete mode 100644 packages/certd/src/acme/index.cjs delete mode 100644 packages/certd/src/acme/util.cjs delete mode 100644 packages/certd/src/acme/verify.cjs diff --git a/packages/certd/src/acme.js b/packages/certd/src/acme.js index f55d1943..d544a1e5 100644 --- a/packages/certd/src/acme.js +++ b/packages/certd/src/acme.js @@ -30,7 +30,7 @@ export class AcmeService { accountKey: key, backoffAttempts: 10, backoffMin: 5000, - backoffMax: 30000 + backoffMax: 10000 }) return client } @@ -67,15 +67,7 @@ export class AcmeService { /* Replace this */ log.info(`Would create TXT record "${dnsRecord}" with value "${recordValue}"`) - try { - await dnsProvider.createRecord(dnsRecord, 'TXT', recordValue) - } catch (e) { - if (e.code === 'DomainRecordDuplicate') { - await dnsProvider.removeRecord(dnsRecord, 'TXT') - await sleep(1000) - await dnsProvider.createRecord(dnsRecord, 'TXT', recordValue) - } - } + return await dnsProvider.createRecord(dnsRecord, 'TXT', recordValue) } } @@ -112,7 +104,7 @@ export class AcmeService { } } - async order ({ email, domains, dnsProvider, csrInfo }) { + async order ({ email, domains, dnsProvider, dnsProviderCreator, csrInfo }) { const client = await this.getAcmeClient(email) /* Create CSR */ const { commonName, altNames } = this.buildCommonNameByDomains(domains) @@ -122,26 +114,30 @@ export class AcmeService { ...csrInfo, altNames }) - + if (dnsProvider == null && dnsProviderCreator) { + dnsProvider = await dnsProviderCreator() + } + if (dnsProvider == null) { + throw new Error('dnsProvider 不能为空') + } /* Certificate */ const crt = await client.auto({ csr, email: email, termsOfServiceAgreed: true, challengePriority: ['dns-01'], - challengeCreateFn: (authz, challenge, keyAuthorization) => { - return this.challengeCreateFn(authz, challenge, keyAuthorization, dnsProvider) + challengeCreateFn: async (authz, challenge, keyAuthorization) => { + return await this.challengeCreateFn(authz, challenge, keyAuthorization, dnsProvider) }, - challengeRemoveFn: (authz, challenge, keyAuthorization) => { - return this.challengeRemoveFn(authz, challenge, keyAuthorization, dnsProvider) + challengeRemoveFn: async (authz, challenge, keyAuthorization) => { + return await this.challengeRemoveFn(authz, challenge, keyAuthorization, dnsProvider) } }) /* Done */ - log.info(`CSR:\n${csr.toString()}`) - log.info(`Private key:\n${key.toString()}`) - log.info(`Certificate:\n${crt.toString()}`) - + log.debug(`CSR:\n${csr.toString()}`) + log.debug(`Certificate:\n${crt.toString()}`) + log.info('证书申请成功') return { key, crt, csr } } diff --git a/packages/certd/src/acme/api.cjs b/packages/certd/src/acme/api.cjs deleted file mode 100644 index f80ab48d..00000000 --- a/packages/certd/src/acme/api.cjs +++ /dev/null @@ -1,227 +0,0 @@ -/** - * ACME API client - */ - -const util = require('./util.cjs') - -/** - * AcmeApi - * - * @class - * @param {HttpClient} httpClient - */ - -class AcmeApi { - constructor (httpClient, accountUrl = null) { - this.http = httpClient - this.accountUrl = accountUrl - } - - /** - * Get account URL - * - * @private - * @returns {string} Account URL - */ - - getAccountUrl () { - if (!this.accountUrl) { - throw new Error('No account URL found, register account first') - } - - return this.accountUrl - } - - /** - * ACME API request - * - * @private - * @param {string} url Request URL - * @param {object} [payload] Request payload, default: `null` - * @param {array} [validStatusCodes] Array of valid HTTP response status codes, default: `[]` - * @param {boolean} [jwsKid] Use KID in JWS header, default: `true` - * @returns {Promise} HTTP response - */ - - async apiRequest (url, payload = null, validStatusCodes = [], jwsKid = true) { - const kid = jwsKid ? this.getAccountUrl() : null - const resp = await this.http.signedRequest(url, payload, kid) - - if (validStatusCodes.length && (validStatusCodes.indexOf(resp.status) === -1)) { - throw new Error(util.formatResponseError(resp)) - } - - return resp - } - - /** - * ACME API request by resource name helper - * - * @private - * @param {string} resource Request resource name - * @param {object} [payload] Request payload, default: `null` - * @param {array} [validStatusCodes] Array of valid HTTP response status codes, default: `[]` - * @param {boolean} [jwsKid] Use KID in JWS header, default: `true` - * @returns {Promise} HTTP response - */ - - async apiResourceRequest (resource, payload = null, validStatusCodes = [], jwsKid = true) { - const resourceUrl = await this.http.getResourceUrl(resource) - return this.apiRequest(resourceUrl, payload, validStatusCodes, jwsKid) - } - - /** - * Get Terms of Service URL if available - * - * https://tools.ietf.org/html/rfc8555#section-7.1.1 - * - * @returns {Promise} ToS URL - */ - - async getTermsOfServiceUrl () { - return this.http.getMetaField('termsOfService') - } - - /** - * Update account - * - * https://tools.ietf.org/html/rfc8555#section-7.3.2 - * - * @param {object} data Request payload - * @returns {Promise} HTTP response - */ - - updateAccount (data) { - return this.apiRequest(this.getAccountUrl(), data, [200, 202]) - } - - /** - * Create new account - * - * https://tools.ietf.org/html/rfc8555#section-7.3 - * - * @param {object} data Request payload - * @returns {Promise} HTTP response - */ - - async createAccount (data) { - const resp = await this.apiResourceRequest('newAccount', data, [200, 201], false) - - /* Set account URL */ - if (resp.headers.location) { - this.accountUrl = resp.headers.location - } - - return resp - } - - /** - * Update account key - * - * https://tools.ietf.org/html/rfc8555#section-7.3.5 - * - * @param {object} data Request payload - * @returns {Promise} HTTP response - */ - - updateAccountKey (data) { - return this.apiResourceRequest('keyChange', data, [200]) - } - - /** - * Create new order - * - * https://tools.ietf.org/html/rfc8555#section-7.4 - * - * @param {object} data Request payload - * @returns {Promise} HTTP response - */ - - createOrder (data) { - return this.apiResourceRequest('newOrder', data, [201]) - } - - /** - * Get order - * - * https://tools.ietf.org/html/rfc8555#section-7.4 - * - * @param {string} url Order URL - * @returns {Promise} HTTP response - */ - - getOrder (url) { - return this.apiRequest(url, null, [200]) - } - - /** - * Finalize order - * - * https://tools.ietf.org/html/rfc8555#section-7.4 - * - * @param {string} url Finalization URL - * @param {object} data Request payload - * @returns {Promise} HTTP response - */ - - finalizeOrder (url, data) { - return this.apiRequest(url, data, [200]) - } - - /** - * Get identifier authorization - * - * https://tools.ietf.org/html/rfc8555#section-7.5 - * - * @param {string} url Authorization URL - * @returns {Promise} HTTP response - */ - - getAuthorization (url) { - return this.apiRequest(url, null, [200]) - } - - /** - * Update identifier authorization - * - * https://tools.ietf.org/html/rfc8555#section-7.5.2 - * - * @param {string} url Authorization URL - * @param {object} data Request payload - * @returns {Promise} HTTP response - */ - - updateAuthorization (url, data) { - return this.apiRequest(url, data, [200]) - } - - /** - * Complete challenge - * - * https://tools.ietf.org/html/rfc8555#section-7.5.1 - * - * @param {string} url Challenge URL - * @param {object} data Request payload - * @returns {Promise} HTTP response - */ - - completeChallenge (url, data) { - return this.apiRequest(url, data, [200]) - } - - /** - * Revoke certificate - * - * https://tools.ietf.org/html/rfc8555#section-7.6 - * - * @param {object} data Request payload - * @returns {Promise} HTTP response - */ - - revokeCert (data) { - return this.apiResourceRequest('revokeCert', data, [200]) - } -} - -/* Export API */ -module.exports = AcmeApi diff --git a/packages/certd/src/acme/auto.cjs b/packages/certd/src/acme/auto.cjs deleted file mode 100644 index 62ee540d..00000000 --- a/packages/certd/src/acme/auto.cjs +++ /dev/null @@ -1,145 +0,0 @@ -/** - * ACME auto helper - */ -const Promise = require('bluebird') -const debug = require('debug')('acme-client') -const forge = require('./crypto/forge.cjs') - -const defaultOpts = { - csr: null, - email: null, - preferredChain: null, - termsOfServiceAgreed: false, - skipChallengeVerification: false, - challengePriority: ['http-01', 'dns-01'], - challengeCreateFn: async () => { throw new Error('Missing challengeCreateFn()') }, - challengeRemoveFn: async () => { throw new Error('Missing challengeRemoveFn()') } -} - -/** - * ACME client auto mode - * - * @param {AcmeClient} client ACME client - * @param {object} userOpts Options - * @returns {Promise} Certificate - */ - -module.exports = async function (client, userOpts) { - const opts = Object.assign({}, defaultOpts, userOpts) - const accountPayload = { termsOfServiceAgreed: opts.termsOfServiceAgreed } - - if (!Buffer.isBuffer(opts.csr)) { - opts.csr = Buffer.from(opts.csr) - } - - if (opts.email) { - accountPayload.contact = [`mailto:${opts.email}`] - } - - /** - * Register account - */ - - debug('[auto] Checking account') - - try { - client.getAccountUrl() - debug('[auto] Account URL already exists, skipping account registration') - } catch (e) { - debug('[auto] Registering account') - await client.createAccount(accountPayload) - } - - /** - * Parse domains from CSR - */ - - debug('[auto] Parsing domains from Certificate Signing Request') - const csrDomains = await forge.readCsrDomains(opts.csr) - const domains = [csrDomains.commonName].concat(csrDomains.altNames) - - debug(`[auto] Resolved ${domains.length} domains from parsing the Certificate Signing Request`) - - /** - * Place order - */ - - debug('[auto] Placing new certificate order with ACME provider') - const orderPayload = { identifiers: domains.map((d) => ({ type: 'dns', value: d })) } - const order = await client.createOrder(orderPayload) - const authorizations = await client.getAuthorizations(order) - - debug(`[auto] Placed certificate order successfully, received ${authorizations.length} identity authorizations`) - - /** - * Resolve and satisfy challenges - */ - - debug('[auto] Resolving and satisfying authorization challenges') - - async function challengeHandle (authz) { - const d = authz.identifier.value - - /* Select challenge based on priority */ - const challenge = authz.challenges.sort((a, b) => { - const aidx = opts.challengePriority.indexOf(a.type) - const bidx = opts.challengePriority.indexOf(b.type) - - if (aidx === -1) return 1 - if (bidx === -1) return -1 - return aidx - bidx - }).slice(0, 1)[0] - - if (!challenge) { - throw new Error(`Unable to select challenge for ${d}, no challenge found`) - } - - debug(`[auto] [${d}] Found ${authz.challenges.length} challenges, selected type: ${challenge.type}`) - - /* Trigger challengeCreateFn() */ - debug(`[auto] [${d}] Trigger challengeCreateFn()`) - const keyAuthorization = await client.getChallengeKeyAuthorization(challenge) - - try { - await opts.challengeCreateFn(authz, challenge, keyAuthorization) - - /* Challenge verification */ - if (opts.skipChallengeVerification === true) { - debug(`[auto] [${d}] Skipping challenge verification since skipChallengeVerification=true`) - } else { - debug(`[auto] [${d}] Running challenge verification`) - await client.verifyChallenge(authz, challenge) - } - - /* Complete challenge and wait for valid status */ - debug(`[auto] [${d}] Completing challenge with ACME provider and waiting for valid status`) - await client.completeChallenge(challenge) - await client.waitForValidStatus(challenge) - } finally { - /* Trigger challengeRemoveFn(), suppress errors */ - debug(`[auto] [${d}] Trigger challengeRemoveFn()`) - - try { - await opts.challengeRemoveFn(authz, challenge, keyAuthorization) - } catch (e) { - debug(`[auto] [${d}] challengeRemoveFn threw error: ${e.message}`) - } - } - } - - debug('[auto] Waiting for challenge valid status') - // await Promise.all(challengePromises) - for (const authz of authorizations) { - debug('------------------------------------------------') - await challengeHandle(authz) - await Promise.delay(1000) - debug('------------------------------------------------') - } - /** - * Finalize order and download certificate - */ - - debug('[auto] Finalizing order and downloading certificate') - await client.finalizeOrder(order, opts.csr) - return client.getCertificate(order, opts.preferredChain) -} diff --git a/packages/certd/src/acme/axios.cjs b/packages/certd/src/acme/axios.cjs deleted file mode 100644 index 1218733b..00000000 --- a/packages/certd/src/acme/axios.cjs +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Axios instance - */ - -const axios = require('axios') -const adapter = require('axios/lib/adapters/http') -const pkg = require('../../package.json') - -/** - * Instance - */ - -const instance = axios.create() - -/* Default User-Agent */ -instance.defaults.headers.common['User-Agent'] = `node-${pkg.name}/${pkg.version}` - -/* Default ACME settings */ -instance.defaults.acmeSettings = { - httpChallengePort: 80, - bypassCustomDnsResolver: false -} - -/** - * Explicitly set Node as default HTTP adapter - * - * https://github.com/axios/axios/issues/1180 - * https://stackoverflow.com/questions/42677387 - */ - -instance.defaults.adapter = adapter - -/** - * Export instance - */ - -module.exports = instance diff --git a/packages/certd/src/acme/client.cjs b/packages/certd/src/acme/client.cjs deleted file mode 100644 index 7f1f45d8..00000000 --- a/packages/certd/src/acme/client.cjs +++ /dev/null @@ -1,690 +0,0 @@ -/** - * ACME client - * - * @namespace Client - */ -const Promise = require('bluebird') -const crypto = require('crypto') -const debug = require('debug')('acme-client') -const HttpClient = require('./http.cjs') -const AcmeApi = require('./api.cjs') -const verify = require('./verify.cjs') -const util = require('./util.cjs') -const auto = require('./auto.cjs') -const forge = require('./crypto/forge.cjs') - -/** - * Default options - */ - -const defaultOpts = { - directoryUrl: undefined, - accountKey: undefined, - accountUrl: null, - backoffAttempts: 10, - backoffMin: 5000, - backoffMax: 30000 -} - -/** - * AcmeClient - * - * @class - * @param {object} opts - * @param {string} opts.directoryUrl ACME directory URL - * @param {buffer|string} opts.accountKey PEM encoded account private key - * @param {string} [opts.accountUrl] Account URL, default: `null` - * @param {number} [opts.backoffAttempts] Maximum number of backoff attempts, default: `5` - * @param {number} [opts.backoffMin] Minimum backoff attempt delay in milliseconds, default: `5000` - * @param {number} [opts.backoffMax] Maximum backoff attempt delay in milliseconds, default: `30000` - * - * @example Create ACME client instance - * ```js - * const client = new acme.Client({ - * directoryUrl: acme.directory.letsencrypt.staging, - * accountKey: 'Private key goes here' - * }); - * ``` - * - * @example Create ACME client instance - * ```js - * const client = new acme.Client({ - * directoryUrl: acme.directory.letsencrypt.staging, - * accountKey: 'Private key goes here', - * accountUrl: 'Optional account URL goes here', - * backoffAttempts: 5, - * backoffMin: 5000, - * backoffMax: 30000 - * }); - * ``` - */ - -class AcmeClient { - constructor (opts) { - if (!Buffer.isBuffer(opts.accountKey)) { - opts.accountKey = Buffer.from(opts.accountKey) - } - - this.opts = Object.assign({}, defaultOpts, opts) - - this.backoffOpts = { - attempts: this.opts.backoffAttempts, - min: this.opts.backoffMin, - max: this.opts.backoffMax - } - - this.http = new HttpClient(this.opts.directoryUrl, this.opts.accountKey) - this.api = new AcmeApi(this.http, this.opts.accountUrl) - } - - /** - * Get Terms of Service URL if available - * - * @returns {Promise} ToS URL - * - * @example Get Terms of Service URL - * ```js - * const termsOfService = client.getTermsOfServiceUrl(); - * - * if (!termsOfService) { - * // CA did not provide Terms of Service - * } - * ``` - */ - - getTermsOfServiceUrl () { - return this.api.getTermsOfServiceUrl() - } - - /** - * Get current account URL - * - * @returns {string} Account URL - * @throws {Error} No account URL found - * - * @example Get current account URL - * ```js - * try { - * const accountUrl = client.getAccountUrl(); - * } - * catch (e) { - * // No account URL exists, need to create account first - * } - * ``` - */ - - getAccountUrl () { - return this.api.getAccountUrl() - } - - /** - * Create a new account - * - * https://tools.ietf.org/html/rfc8555#section-7.3 - * - * @param {object} [data] Request data - * @returns {Promise} Account - * - * @example Create a new account - * ```js - * const account = await client.createAccount({ - * termsOfServiceAgreed: true - * }); - * ``` - * - * @example Create a new account with contact info - * ```js - * const account = await client.createAccount({ - * termsOfServiceAgreed: true, - * contact: ['mailto:test@example.com'] - * }); - * ``` - */ - - async createAccount (data = {}) { - try { - this.getAccountUrl() - - /* Account URL exists */ - debug('Account URL exists, returning updateAccount()') - return this.updateAccount(data) - } catch (e) { - const resp = await this.api.createAccount(data) - - /* HTTP 200: Account exists */ - if (resp.status === 200) { - debug('Account already exists (HTTP 200), returning updateAccount()') - return this.updateAccount(data) - } - - return resp.data - } - } - - /** - * Update existing account - * - * https://tools.ietf.org/html/rfc8555#section-7.3.2 - * - * @param {object} [data] Request data - * @returns {Promise} Account - * - * @example Update existing account - * ```js - * const account = await client.updateAccount({ - * contact: ['mailto:foo@example.com'] - * }); - * ``` - */ - - async updateAccount (data = {}) { - try { - this.api.getAccountUrl() - } catch (e) { - debug('No account URL found, returning createAccount()') - return this.createAccount(data) - } - - /* Remove data only applicable to createAccount() */ - if ('onlyReturnExisting' in data) { - delete data.onlyReturnExisting - } - - /* POST-as-GET */ - if (Object.keys(data).length === 0) { - data = null - } - - const resp = await this.api.updateAccount(data) - return resp.data - } - - /** - * Update account private key - * - * https://tools.ietf.org/html/rfc8555#section-7.3.5 - * - * @param {buffer|string} newAccountKey New PEM encoded private key - * @param {object} [data] Additional request data - * @returns {Promise} Account - * - * @example Update account private key - * ```js - * const newAccountKey = 'New private key goes here'; - * const result = await client.updateAccountKey(newAccountKey); - * ``` - */ - - async updateAccountKey (newAccountKey, data = {}) { - if (!Buffer.isBuffer(newAccountKey)) { - newAccountKey = Buffer.from(newAccountKey) - } - - const accountUrl = this.api.getAccountUrl() - - /* Create new HTTP and API clients using new key */ - const newHttpClient = new HttpClient(this.opts.directoryUrl, newAccountKey) - const newApiClient = new AcmeApi(newHttpClient, accountUrl) - - /* Get new JWK */ - data.account = accountUrl - data.oldKey = await this.http.getJwk() - - /* TODO: Backward-compatibility with draft-ietf-acme-12, remove this in a later release */ - data.newKey = await newHttpClient.getJwk() - - /* Get signed request body from new client */ - const url = await newHttpClient.getResourceUrl('keyChange') - const body = await newHttpClient.createSignedBody(url, data) - - /* Change key using old client */ - const resp = await this.api.updateAccountKey(body) - - /* Replace existing HTTP and API client */ - this.http = newHttpClient - this.api = newApiClient - - return resp.data - } - - /** - * Create a new order - * - * https://tools.ietf.org/html/rfc8555#section-7.4 - * - * @param {object} data Request data - * @returns {Promise} Order - * - * @example Create a new order - * ```js - * const order = await client.createOrder({ - * identifiers: [ - * { type: 'dns', value: 'example.com' }, - * { type: 'dns', value: 'test.example.com' } - * ] - * }); - * ``` - */ - - async createOrder (data) { - const resp = await this.api.createOrder(data) - - if (!resp.headers.location) { - throw new Error('Creating a new order did not return an order link') - } - - /* Add URL to response */ - resp.data.url = resp.headers.location - return resp.data - } - - /** - * Refresh order object from CA - * - * https://tools.ietf.org/html/rfc8555#section-7.4 - * - * @param {object} order Order object - * @returns {Promise} Order - * - * @example - * ```js - * const order = { ... }; // Previously created order object - * const result = await client.getOrder(order); - * ``` - */ - - async getOrder (order) { - if (!order.url) { - throw new Error('Unable to get order, URL not found') - } - - const resp = await this.api.getOrder(order.url) - - /* Add URL to response */ - resp.data.url = order.url - return resp.data - } - - /** - * Finalize order - * - * https://tools.ietf.org/html/rfc8555#section-7.4 - * - * @param {object} order Order object - * @param {buffer|string} csr PEM encoded Certificate Signing Request - * @returns {Promise} Order - * - * @example Finalize order - * ```js - * const order = { ... }; // Previously created order object - * const csr = { ... }; // Previously created Certificate Signing Request - * const result = await client.finalizeOrder(order, csr); - * ``` - */ - - async finalizeOrder (order, csr) { - if (!order.finalize) { - throw new Error('Unable to finalize order, URL not found') - } - - if (!Buffer.isBuffer(csr)) { - csr = Buffer.from(csr) - } - - const body = forge.getPemBody(csr) - const data = { csr: util.b64escape(body) } - - const resp = await this.api.finalizeOrder(order.finalize, data) - - /* Add URL to response */ - resp.data.url = order.url - return resp.data - } - - /** - * Get identifier authorizations from order - * - * https://tools.ietf.org/html/rfc8555#section-7.5 - * - * @param {object} order Order - * @returns {Promise} Authorizations - * - * @example Get identifier authorizations - * ```js - * const order = { ... }; // Previously created order object - * const authorizations = await client.getAuthorizations(order); - * - * authorizations.forEach((authz) => { - * const { challenges } = authz; - * }); - * ``` - */ - - async getAuthorizations (order) { - return Promise.map((order.authorizations || []), async (url) => { - const resp = await this.api.getAuthorization(url) - - /* Add URL to response */ - resp.data.url = url - return resp.data - }) - } - - /** - * Deactivate identifier authorization - * - * https://tools.ietf.org/html/rfc8555#section-7.5.2 - * - * @param {object} authz Identifier authorization - * @returns {Promise} Authorization - * - * @example Deactivate identifier authorization - * ```js - * const authz = { ... }; // Identifier authorization resolved from previously created order - * const result = await client.deactivateAuthorization(authz); - * ``` - */ - - async deactivateAuthorization (authz) { - if (!authz.url) { - throw new Error('Unable to deactivate identifier authorization, URL not found') - } - - const data = { - status: 'deactivated' - } - - const resp = await this.api.updateAuthorization(authz.url, data) - - /* Add URL to response */ - resp.data.url = authz.url - return resp.data - } - - /** - * Get key authorization for ACME challenge - * - * https://tools.ietf.org/html/rfc8555#section-8.1 - * - * @param {object} challenge Challenge object returned by API - * @returns {Promise} Key authorization - * - * @example Get challenge key authorization - * ```js - * const challenge = { ... }; // Challenge from previously resolved identifier authorization - * const key = await client.getChallengeKeyAuthorization(challenge); - * - * // Write key somewhere to satisfy challenge - * ``` - */ - - async getChallengeKeyAuthorization (challenge) { - const jwk = await this.http.getJwk() - const keysum = crypto.createHash('sha256').update(JSON.stringify(jwk)) - const thumbprint = util.b64escape(keysum.digest('base64')) - const result = `${challenge.token}.${thumbprint}` - - /** - * https://tools.ietf.org/html/rfc8555#section-8.3 - */ - - if (challenge.type === 'http-01') { - return result - } - - /** - * https://tools.ietf.org/html/rfc8555#section-8.4 - * https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01 - */ - - if ((challenge.type === 'dns-01') || (challenge.type === 'tls-alpn-01')) { - const shasum = crypto.createHash('sha256').update(result) - return util.b64escape(shasum.digest('base64')) - } - - throw new Error(`Unable to produce key authorization, unknown challenge type: ${challenge.type}`) - } - - /** - * Verify that ACME challenge is satisfied - * - * @param {object} authz Identifier authorization - * @param {object} challenge Authorization challenge - * @returns {Promise} - * - * @example Verify satisfied ACME challenge - * ```js - * const authz = { ... }; // Identifier authorization - * const challenge = { ... }; // Satisfied challenge - * await client.verifyChallenge(authz, challenge); - * ``` - */ - - async verifyChallenge (authz, challenge) { - if (!authz.url || !challenge.url) { - throw new Error('Unable to verify ACME challenge, URL not found') - } - - if (typeof verify[challenge.type] === 'undefined') { - throw new Error(`Unable to verify ACME challenge, unknown type: ${challenge.type}`) - } - - const keyAuthorization = await this.getChallengeKeyAuthorization(challenge) - - const verifyFn = async () => { - await verify[challenge.type](authz, challenge, keyAuthorization) - } - - debug('Waiting for ACME challenge verification', this.backoffOpts) - return util.retry(verifyFn, this.backoffOpts) - } - - /** - * Notify CA that challenge has been completed - * - * https://tools.ietf.org/html/rfc8555#section-7.5.1 - * - * @param {object} challenge Challenge object returned by API - * @returns {Promise} Challenge - * - * @example Notify CA that challenge has been completed - * ```js - * const challenge = { ... }; // Satisfied challenge - * const result = await client.completeChallenge(challenge); - * ``` - */ - - async completeChallenge (challenge) { - const resp = await this.api.completeChallenge(challenge.url, {}) - return resp.data - } - - /** - * Wait for ACME provider to verify status on a order, authorization or challenge - * - * https://tools.ietf.org/html/rfc8555#section-7.5.1 - * - * @param {object} item An order, authorization or challenge object - * @returns {Promise} Valid order, authorization or challenge - * - * @example Wait for valid challenge status - * ```js - * const challenge = { ... }; - * await client.waitForValidStatus(challenge); - * ``` - * - * @example Wait for valid authoriation status - * ```js - * const authz = { ... }; - * await client.waitForValidStatus(authz); - * ``` - * - * @example Wait for valid order status - * ```js - * const order = { ... }; - * await client.waitForValidStatus(order); - * ``` - */ - - async waitForValidStatus (item) { - if (!item.url) { - throw new Error('Unable to verify status of item, URL not found') - } - - const verifyFn = async (abort) => { - const resp = await this.api.apiRequest(item.url, null, [200]) - - /* Verify status */ - debug(`Item has status: ${resp.data.status}`) - - if (resp.data.status === 'invalid') { - abort() - throw new Error(util.formatResponseError(resp)) - } else if (resp.data.status === 'pending') { - throw new Error('Operation is pending') - } else if (resp.data.status === 'valid') { - return resp.data - } - - throw new Error(`Unexpected item status: ${resp.data.status}`) - } - - debug(`Waiting for valid status from: ${item.url}`, this.backoffOpts) - return util.retry(verifyFn, this.backoffOpts) - } - - /** - * Get certificate from ACME order - * - * https://tools.ietf.org/html/rfc8555#section-7.4.2 - * - * @param {object} order Order object - * @param {string} [preferredChain] Indicate which certificate chain is preferred if a CA offers multiple, by exact issuer common name, default: `null` - * @returns {Promise} Certificate - * - * @example Get certificate - * ```js - * const order = { ... }; // Previously created order - * const certificate = await client.getCertificate(order); - * ``` - * - * @example Get certificate with preferred chain - * ```js - * const order = { ... }; // Previously created order - * const certificate = await client.getCertificate(order, 'DST Root CA X3'); - * ``` - */ - - async getCertificate (order, preferredChain = null) { - if (order.status !== 'valid') { - order = await this.waitForValidStatus(order) - } - - if (!order.certificate) { - throw new Error('Unable to download certificate, URL not found') - } - - const resp = await this.api.apiRequest(order.certificate, null, [200]) - - /* Handle alternate certificate chains */ - if (preferredChain && resp.headers.link) { - const alternateLinks = util.parseLinkHeader(resp.headers.link) - const alternates = await Promise.map(alternateLinks, async (link) => this.api.apiRequest(link, null, [200])) - const certificates = [resp].concat(alternates).map((c) => c.data) - - return util.findCertificateChainForIssuer(certificates, preferredChain) - } - - /* Return default certificate chain */ - return resp.data - } - - /** - * Revoke certificate - * - * https://tools.ietf.org/html/rfc8555#section-7.6 - * - * @param {buffer|string} cert PEM encoded certificate - * @param {object} [data] Additional request data - * @returns {Promise} - * - * @example Revoke certificate - * ```js - * const certificate = { ... }; // Previously created certificate - * const result = await client.revokeCertificate(certificate); - * ``` - * - * @example Revoke certificate with reason - * ```js - * const certificate = { ... }; // Previously created certificate - * const result = await client.revokeCertificate(certificate, { - * reason: 4 - * }); - * ``` - */ - - async revokeCertificate (cert, data = {}) { - const body = forge.getPemBody(cert) - data.certificate = util.b64escape(body) - - const resp = await this.api.revokeCert(data) - return resp.data - } - - /** - * Auto mode - * - * @param {object} opts - * @param {buffer|string} opts.csr Certificate Signing Request - * @param {function} opts.challengeCreateFn Function returning Promise triggered before completing ACME challenge - * @param {function} opts.challengeRemoveFn Function returning Promise triggered after completing ACME challenge - * @param {string} [opts.email] Account email address - * @param {boolean} [opts.termsOfServiceAgreed] Agree to Terms of Service, default: `false` - * @param {boolean} [opts.skipChallengeVerification] Skip internal challenge verification before notifying ACME provider, default: `false` - * @param {string[]} [opts.challengePriority] Array defining challenge type priority, default: `['http-01', 'dns-01']` - * @param {string} [opts.preferredChain] Indicate which certificate chain is preferred if a CA offers multiple, by exact issuer common name, default: `null` - * @returns {Promise} Certificate - * - * @example Order a certificate using auto mode - * ```js - * const [certificateKey, certificateRequest] = await acme.forge.createCsr({ - * commonName: 'test.example.com' - * }); - * - * const certificate = await client.auto({ - * csr: certificateRequest, - * email: 'test@example.com', - * termsOfServiceAgreed: true, - * challengeCreateFn: async (authz, challenge, keyAuthorization) => { - * // Satisfy challenge here - * }, - * challengeRemoveFn: async (authz, challenge, keyAuthorization) => { - * // Clean up challenge here - * } - * }); - * ``` - * - * @example Order a certificate using auto mode with preferred chain - * ```js - * const [certificateKey, certificateRequest] = await acme.forge.createCsr({ - * commonName: 'test.example.com' - * }); - * - * const certificate = await client.auto({ - * csr: certificateRequest, - * email: 'test@example.com', - * termsOfServiceAgreed: true, - * preferredChain: 'DST Root CA X3', - * challengeCreateFn: async () => {}, - * challengeRemoveFn: async () => {} - * }); - * ``` - */ - - auto (opts) { - return auto(this, opts) - } -} - -/* Export client */ -module.exports = AcmeClient diff --git a/packages/certd/src/acme/crypto/forge.cjs b/packages/certd/src/acme/crypto/forge.cjs deleted file mode 100644 index 90cce27c..00000000 --- a/packages/certd/src/acme/crypto/forge.cjs +++ /dev/null @@ -1,445 +0,0 @@ -/** - * node-forge crypto engine - * - * @namespace forge - */ - -const net = require('net'); -const Promise = require('bluebird'); -const forge = require('node-forge'); - -const generateKeyPair = Promise.promisify(forge.pki.rsa.generateKeyPair); - - -/** - * Attempt to parse forge object from PEM encoded string - * - * @private - * @param {string} input PEM string - * @return {object} - */ - -function forgeObjectFromPem(input) { - const msg = forge.pem.decode(input)[0]; - let result; - - switch (msg.type) { - case 'PRIVATE KEY': - case 'RSA PRIVATE KEY': - result = forge.pki.privateKeyFromPem(input); - break; - - case 'PUBLIC KEY': - case 'RSA PUBLIC KEY': - result = forge.pki.publicKeyFromPem(input); - break; - - case 'CERTIFICATE': - case 'X509 CERTIFICATE': - case 'TRUSTED CERTIFICATE': - result = forge.pki.certificateFromPem(input).publicKey; - break; - - case 'CERTIFICATE REQUEST': - result = forge.pki.certificationRequestFromPem(input).publicKey; - break; - - default: - throw new Error('Unable to detect forge message type'); - } - - return result; -} - - -/** - * Parse domain names from a certificate or CSR - * - * @private - * @param {object} obj Forge certificate or CSR - * @returns {object} {commonName, altNames} - */ - -function parseDomains(obj) { - let commonName = null; - let altNames = []; - let altNamesDict = []; - - const commonNameObject = (obj.subject.attributes || []).find((a) => a.name === 'commonName'); - const rootAltNames = (obj.extensions || []).find((e) => 'altNames' in e); - const rootExtensions = (obj.attributes || []).find((a) => 'extensions' in a); - - if (rootAltNames && rootAltNames.altNames && rootAltNames.altNames.length) { - altNamesDict = rootAltNames.altNames; - } - else if (rootExtensions && rootExtensions.extensions && rootExtensions.extensions.length) { - const extAltNames = rootExtensions.extensions.find((e) => 'altNames' in e); - - if (extAltNames && extAltNames.altNames && extAltNames.altNames.length) { - altNamesDict = extAltNames.altNames; - } - } - - if (commonNameObject) { - commonName = commonNameObject.value; - } - - if (altNamesDict) { - altNames = altNamesDict.map((a) => a.value); - } - - return { - commonName, - altNames - }; -} - - -/** - * Generate a private RSA key - * - * @param {number} [size] Size of the key, default: `2048` - * @returns {Promise} PEM encoded private RSA key - * - * @example Generate private RSA key - * ```js - * const privateKey = await acme.forge.createPrivateKey(); - * ``` - * - * @example Private RSA key with defined size - * ```js - * const privateKey = await acme.forge.createPrivateKey(4096); - * ``` - */ - -async function createPrivateKey(size = 2048) { - const keyPair = await generateKeyPair({ bits: size }); - const pemKey = forge.pki.privateKeyToPem(keyPair.privateKey); - return Buffer.from(pemKey); -} - -exports.createPrivateKey = createPrivateKey; - - -/** - * Create public key from a private RSA key - * - * @param {buffer|string} key PEM encoded private RSA key - * @returns {Promise} PEM encoded public RSA key - * - * @example Create public key - * ```js - * const publicKey = await acme.forge.createPublicKey(privateKey); - * ``` - */ - -exports.createPublicKey = async function(key) { - const privateKey = forge.pki.privateKeyFromPem(key); - const publicKey = forge.pki.rsa.setPublicKey(privateKey.n, privateKey.e); - const pemKey = forge.pki.publicKeyToPem(publicKey); - return Buffer.from(pemKey); -}; - - -/** - * Parse body of PEM encoded object form buffer or string - * If multiple objects are chained, the first body will be returned - * - * @param {buffer|string} str PEM encoded buffer or string - * @returns {string} PEM body - */ - -exports.getPemBody = (str) => { - const msg = forge.pem.decode(str)[0]; - return forge.util.encode64(msg.body); -}; - - -/** - * Split chain of PEM encoded objects from buffer or string into array - * - * @param {buffer|string} str PEM encoded buffer or string - * @returns {string[]} Array of PEM bodies - */ - -exports.splitPemChain = (str) => forge.pem.decode(str).map(forge.pem.encode); - - -/** - * Get modulus - * - * @param {buffer|string} input PEM encoded private key, certificate or CSR - * @returns {Promise} Modulus - * - * @example Get modulus - * ```js - * const m1 = await acme.forge.getModulus(privateKey); - * const m2 = await acme.forge.getModulus(certificate); - * const m3 = await acme.forge.getModulus(certificateRequest); - * ``` - */ - -exports.getModulus = async function(input) { - if (!Buffer.isBuffer(input)) { - input = Buffer.from(input); - } - - const obj = forgeObjectFromPem(input); - return Buffer.from(forge.util.hexToBytes(obj.n.toString(16)), 'binary'); -}; - - -/** - * Get public exponent - * - * @param {buffer|string} input PEM encoded private key, certificate or CSR - * @returns {Promise} Exponent - * - * @example Get public exponent - * ```js - * const e1 = await acme.forge.getPublicExponent(privateKey); - * const e2 = await acme.forge.getPublicExponent(certificate); - * const e3 = await acme.forge.getPublicExponent(certificateRequest); - * ``` - */ - -exports.getPublicExponent = async function(input) { - if (!Buffer.isBuffer(input)) { - input = Buffer.from(input); - } - - const obj = forgeObjectFromPem(input); - return Buffer.from(forge.util.hexToBytes(obj.e.toString(16)), 'binary'); -}; - - -/** - * Read domains from a Certificate Signing Request - * - * @param {buffer|string} csr PEM encoded Certificate Signing Request - * @returns {Promise} {commonName, altNames} - * - * @example Read Certificate Signing Request domains - * ```js - * const { commonName, altNames } = await acme.forge.readCsrDomains(certificateRequest); - * - * console.log(`Common name: ${commonName}`); - * console.log(`Alt names: ${altNames.join(', ')}`); - * ``` - */ - -exports.readCsrDomains = async function(csr) { - if (!Buffer.isBuffer(csr)) { - csr = Buffer.from(csr); - } - - const obj = forge.pki.certificationRequestFromPem(csr); - return parseDomains(obj); -}; - - -/** - * Read information from a certificate - * - * @param {buffer|string} cert PEM encoded certificate - * @returns {Promise} Certificate info - * - * @example Read certificate information - * ```js - * const info = await acme.forge.readCertificateInfo(certificate); - * const { commonName, altNames } = info.domains; - * - * console.log(`Not after: ${info.notAfter}`); - * console.log(`Not before: ${info.notBefore}`); - * - * console.log(`Common name: ${commonName}`); - * console.log(`Alt names: ${altNames.join(', ')}`); - * ``` - */ - -exports.readCertificateInfo = async function(cert) { - if (!Buffer.isBuffer(cert)) { - cert = Buffer.from(cert); - } - - const obj = forge.pki.certificateFromPem(cert); - const issuerCn = (obj.issuer.attributes || []).find((a) => a.name === 'commonName'); - - return { - issuer: { - commonName: issuerCn ? issuerCn.value : null - }, - domains: parseDomains(obj), - notAfter: obj.validity.notAfter, - notBefore: obj.validity.notBefore - }; -}; - - -/** - * Determine ASN.1 type for CSR subject short name - * Note: https://tools.ietf.org/html/rfc5280 - * - * @private - * @param {string} shortName CSR subject short name - * @returns {forge.asn1.Type} ASN.1 type - */ - -function getCsrValueTagClass(shortName) { - switch (shortName) { - case 'C': - return forge.asn1.Type.PRINTABLESTRING; - case 'E': - return forge.asn1.Type.IA5STRING; - default: - return forge.asn1.Type.UTF8; - } -} - - -/** - * Create array of short names and values for Certificate Signing Request subjects - * - * @private - * @param {object} subjectObj Key-value of short names and values - * @returns {object[]} Certificate Signing Request subject array - */ - -function createCsrSubject(subjectObj) { - return Object.entries(subjectObj).reduce((result, [shortName, value]) => { - if (value) { - const valueTagClass = getCsrValueTagClass(shortName); - result.push({ shortName, value, valueTagClass }); - } - - return result; - }, []); -} - - -/** - * Create array of alt names for Certificate Signing Requests - * Note: https://github.com/digitalbazaar/forge/blob/dfdde475677a8a25c851e33e8f81dca60d90cfb9/lib/x509.js#L1444-L1454 - * - * @private - * @param {string[]} altNames Alt names - * @returns {object[]} Certificate Signing Request alt names array - */ - -function formatCsrAltNames(altNames) { - return altNames.map((value) => { - const type = net.isIP(value) ? 7 : 2; - return { type, value }; - }); -} - - -/** - * Create a Certificate Signing Request - * - * @param {object} data - * @param {number} [data.keySize] Size of newly created private key, default: `2048` - * @param {string} [data.commonName] - * @param {array} [data.altNames] default: `[]` - * @param {string} [data.country] - * @param {string} [data.state] - * @param {string} [data.locality] - * @param {string} [data.organization] - * @param {string} [data.organizationUnit] - * @param {string} [data.emailAddress] - * @param {buffer|string} [key] CSR private key - * @returns {Promise} [privateKey, certificateSigningRequest] - * - * @example Create a Certificate Signing Request - * ```js - * const [certificateKey, certificateRequest] = await acme.forge.createCsr({ - * commonName: 'test.example.com' - * }); - * ``` - * - * @example Certificate Signing Request with both common and alternative names - * ```js - * const [certificateKey, certificateRequest] = await acme.forge.createCsr({ - * keySize: 4096, - * commonName: 'test.example.com', - * altNames: ['foo.example.com', 'bar.example.com'] - * }); - * ``` - * - * @example Certificate Signing Request with additional information - * ```js - * const [certificateKey, certificateRequest] = await acme.forge.createCsr({ - * commonName: 'test.example.com', - * country: 'US', - * state: 'California', - * locality: 'Los Angeles', - * organization: 'The Company Inc.', - * organizationUnit: 'IT Department', - * emailAddress: 'contact@example.com' - * }); - * ``` - * - * @example Certificate Signing Request with predefined private key - * ```js - * const certificateKey = await acme.forge.createPrivateKey(); - * - * const [, certificateRequest] = await acme.forge.createCsr({ - * commonName: 'test.example.com' - * }, certificateKey); - */ - -exports.createCsr = async function(data, key = null) { - if (!key) { - key = await createPrivateKey(data.keySize); - } - else if (!Buffer.isBuffer(key)) { - key = Buffer.from(key); - } - - if (typeof data.altNames === 'undefined') { - data.altNames = []; - } - - const csr = forge.pki.createCertificationRequest(); - - /* Public key */ - const privateKey = forge.pki.privateKeyFromPem(key); - const publicKey = forge.pki.rsa.setPublicKey(privateKey.n, privateKey.e); - csr.publicKey = publicKey; - - /* Ensure subject common name is present in SAN - https://cabforum.org/wp-content/uploads/BRv1.2.3.pdf */ - if (data.commonName && !data.altNames.includes(data.commonName)) { - data.altNames.unshift(data.commonName); - } - - /* Subject */ - const subject = createCsrSubject({ - CN: data.commonName, - C: data.country, - ST: data.state, - L: data.locality, - O: data.organization, - OU: data.organizationUnit, - E: data.emailAddress - }); - - csr.setSubject(subject); - - /* SAN extension */ - if (data.altNames.length) { - csr.setAttributes([{ - name: 'extensionRequest', - extensions: [{ - name: 'subjectAltName', - altNames: formatCsrAltNames(data.altNames) - }] - }]); - } - - /* Sign CSR */ - csr.sign(privateKey); - - /* Done */ - const pemCsr = forge.pki.certificationRequestToPem(csr); - return [key, Buffer.from(pemCsr)]; -}; diff --git a/packages/certd/src/acme/http.cjs b/packages/certd/src/acme/http.cjs deleted file mode 100644 index 14c915cb..00000000 --- a/packages/certd/src/acme/http.cjs +++ /dev/null @@ -1,228 +0,0 @@ -/** - * ACME HTTP client - */ - -const crypto = require('crypto') -const debug = require('debug')('acme-client') -const axios = require('./axios.cjs') -const util = require('./util.cjs') -const forge = require('./crypto/forge.cjs') - -/** - * ACME HTTP client - * - * @class - * @param {string} directoryUrl ACME directory URL - * @param {buffer} accountKey PEM encoded account private key - */ - -class HttpClient { - constructor (directoryUrl, accountKey) { - this.directoryUrl = directoryUrl - this.accountKey = accountKey - - this.maxBadNonceRetries = 5 - this.directory = null - this.jwk = null - } - - /** - * HTTP request - * - * @param {string} url HTTP URL - * @param {string} method HTTP method - * @param {object} [opts] Request options - * @returns {Promise} HTTP response - */ - - async request (url, method, opts = {}) { - opts.url = url - opts.method = method - opts.validateStatus = null - - /* Headers */ - if (typeof opts.headers === 'undefined') { - opts.headers = {} - } - - opts.headers['Content-Type'] = 'application/jose+json' - - /* Request */ - debug(`HTTP request: ${method} ${url}`) - const resp = await axios.request(opts) - - debug(`RESP ${resp.status} ${method} ${url}`) - return resp - } - - /** - * Ensure provider directory exists - * - * https://tools.ietf.org/html/rfc8555#section-7.1.1 - * - * @returns {Promise} - */ - - async getDirectory () { - if (!this.directory) { - const resp = await this.request(this.directoryUrl, 'get') - this.directory = resp.data - } - } - - /** - * Get JSON Web Key - * - * @returns {Promise} {e, kty, n} - */ - - async getJwk () { - if (this.jwk) { - return this.jwk - } - - const exponent = await forge.getPublicExponent(this.accountKey) - const modulus = await forge.getModulus(this.accountKey) - - this.jwk = { - e: util.b64encode(exponent), - kty: 'RSA', - n: util.b64encode(modulus) - } - - return this.jwk - } - - /** - * Get nonce from directory API endpoint - * - * https://tools.ietf.org/html/rfc8555#section-7.2 - * - * @returns {Promise} nonce - */ - - async getNonce () { - const url = await this.getResourceUrl('newNonce') - const resp = await this.request(url, 'head') - - if (!resp.headers['replay-nonce']) { - throw new Error('Failed to get nonce from ACME provider') - } - - return resp.headers['replay-nonce'] - } - - /** - * Get URL for a directory resource - * - * @param {string} resource API resource name - * @returns {Promise} URL - */ - - async getResourceUrl (resource) { - await this.getDirectory() - - if (!this.directory[resource]) { - throw new Error(`Could not resolve URL for API resource: "${resource}"`) - } - - return this.directory[resource] - } - - /** - * Get directory meta field - * - * @param {string} field Meta field name - * @returns {Promise} Meta field value - */ - - async getMetaField (field) { - await this.getDirectory() - - if (('meta' in this.directory) && (field in this.directory.meta)) { - return this.directory.meta[field] - } - - return null - } - - /** - * Create signed HTTP request body - * - * @param {string} url Request URL - * @param {object} payload Request payload - * @param {string} [nonce] Request nonce - * @param {string} [kid] Request KID - * @returns {Promise} Signed HTTP request body - */ - - async createSignedBody (url, payload = null, nonce = null, kid = null) { - /* JWS header */ - const header = { - url, - alg: 'RS256' - } - - if (nonce) { - debug(`Using nonce: ${nonce}`) - header.nonce = nonce - } - - /* KID or JWK */ - if (kid) { - header.kid = kid - } else { - header.jwk = await this.getJwk() - } - - /* Request payload */ - const result = { - payload: payload ? util.b64encode(JSON.stringify(payload)) : '', - protected: util.b64encode(JSON.stringify(header)) - } - - /* Signature */ - const signer = crypto.createSign('RSA-SHA256').update(`${result.protected}.${result.payload}`, 'utf8') - result.signature = util.b64escape(signer.sign(this.accountKey, 'base64')) - - return result - } - - /** - * Signed HTTP request - * - * https://tools.ietf.org/html/rfc8555#section-6.2 - * - * @param {string} url Request URL - * @param {object} payload Request payload - * @param {string} [kid] Request KID - * @param {string} [nonce] Request anti-replay nonce - * @param {number} [attempts] Request attempt counter - * @returns {Promise} HTTP response - */ - - async signedRequest (url, payload, kid = null, nonce = null, attempts = 0) { - if (!nonce) { - nonce = await this.getNonce() - } - - /* Sign body and send request */ - const data = await this.createSignedBody(url, payload, nonce, kid) - const resp = await this.request(url, 'post', { data }) - - /* Retry on bad nonce - https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-6.4 */ - if (resp.data && resp.data.type && (resp.status === 400) && (resp.data.type === 'urn:ietf:params:acme:error:badNonce') && (attempts < this.maxBadNonceRetries)) { - const newNonce = resp.headers['replay-nonce'] || null - attempts += 1 - - debug(`Caught invalid nonce error, retrying (${attempts}/${this.maxBadNonceRetries}) signed request to: ${url}`) - return this.signedRequest(url, payload, kid, newNonce, attempts) - } - - /* Return response */ - return resp - } -} - -/* Export client */ -module.exports = HttpClient diff --git a/packages/certd/src/acme/index.cjs b/packages/certd/src/acme/index.cjs deleted file mode 100644 index e349fb07..00000000 --- a/packages/certd/src/acme/index.cjs +++ /dev/null @@ -1,28 +0,0 @@ -/** - * acme-client - */ - -exports.Client = require('./client.cjs') - -/** - * Directory URLs - */ - -exports.directory = { - letsencrypt: { - staging: 'https://acme-staging-v02.api.letsencrypt.org/directory', - production: 'https://acme-v02.api.letsencrypt.org/directory' - } -} - -/** - * Crypto - */ - -exports.forge = require('./crypto/forge.cjs') - -/** - * Axios - */ - -exports.axios = require('./axios.cjs') diff --git a/packages/certd/src/acme/util.cjs b/packages/certd/src/acme/util.cjs deleted file mode 100644 index 1d8c3ff7..00000000 --- a/packages/certd/src/acme/util.cjs +++ /dev/null @@ -1,158 +0,0 @@ -/** - * Utility methods - */ -const Promise = require('bluebird') -const Backoff = require('backo2') -const debug = require('debug')('acme-client') -const forge = require('./crypto/forge.cjs') - -/** - * Retry promise - * - * @param {function} fn Function returning promise that should be retried - * @param {number} attempts Maximum number of attempts - * @param {Backoff} backoff Backoff instance - * @returns {Promise} - */ - -async function retryPromise (fn, attempts, backoff) { - let aborted = false - - try { - const data = await fn(() => { aborted = true }) - return data - } catch (e) { - if (aborted || ((backoff.attempts + 1) >= attempts)) { - throw e - } - - const duration = backoff.duration() - debug(`Promise rejected attempt #${backoff.attempts}, retrying in ${duration}ms: ${e.message}`) - - await Promise.delay(duration) - return retryPromise(fn, attempts, backoff) - } -} - -/** - * Retry promise - * - * @param {function} fn Function returning promise that should be retried - * @param {object} [backoffOpts] Backoff options - * @param {number} [backoffOpts.attempts] Maximum number of attempts, default: `5` - * @param {number} [backoffOpts.min] Minimum attempt delay in milliseconds, default: `5000` - * @param {number} [backoffOpts.max] Maximum attempt delay in milliseconds, default: `30000` - * @returns {Promise} - */ - -function retry (fn, { attempts = 5, min = 5000, max = 30000 } = {}) { - const backoff = new Backoff({ min, max }) - return retryPromise(fn, attempts, backoff) -} - -/** - * Escape base64 encoded string - * - * @param {string} str Base64 encoded string - * @returns {string} Escaped string - */ - -function b64escape (str) { - return str.replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=/g, '') -} - -/** - * Base64 encode and escape buffer or string - * - * @param {buffer|string} str Buffer or string to be encoded - * @returns {string} Escaped base64 encoded string - */ - -function b64encode (str) { - const buf = Buffer.isBuffer(str) ? str : Buffer.from(str) - return b64escape(buf.toString('base64')) -} - -/** - * Parse URLs from link header - * - * @param {string} header Link header contents - * @param {string} rel Link relation, default: `alternate` - * @returns {array} Array of URLs - */ - -function parseLinkHeader (header, rel = 'alternate') { - const relRe = new RegExp(`\\s*rel\\s*=\\s*"?${rel}"?`, 'i') - - const results = (header || '').split(/,\s* { - const [, linkUrl, linkParts] = link.match(/]*)>;(.*)/) || [] - return (linkUrl && linkParts && linkParts.match(relRe)) ? linkUrl : null - }) - - return results.filter((r) => r) -} - -/** - * Find certificate chain with preferred issuer - * If issuer can not be located, the first certificate will be returned - * - * @param {array} certificates Array of PEM encoded certificate chains - * @param {string} issuer Preferred certificate issuer - * @returns {Promise} PEM encoded certificate chain - */ - -async function findCertificateChainForIssuer (chains, issuer) { - try { - return await Promise.any(chains.map(async (chain) => { - /* Look up all issuers */ - const certs = forge.splitPemChain(chain) - const infoCollection = await Promise.map(certs, forge.readCertificateInfo) - const issuerCollection = infoCollection.map((i) => i.issuer.commonName) - - /* Found match, return it */ - if (issuerCollection.includes(issuer)) { - debug(`Found matching certificate for preferred issuer="${issuer}", issuers=${JSON.stringify(issuerCollection)}`) - return chain - } - - /* No match, throw error */ - debug(`Unable to match certificate for preferred issuer="${issuer}", issuers=${JSON.stringify(issuerCollection)}`) - throw new Error('Certificate issuer mismatch') - })) - } catch (e) { - /* No certificates matched, return default */ - debug(`Found no match in ${chains.length} certificate chains for preferred issuer="${issuer}", returning default certificate chain`) - return chains[0] - } -} - -/** - * Find and format error in response object - * - * @param {object} resp HTTP response - * @returns {string} Error message - */ - -function formatResponseError (resp) { - let result - - if (resp.data.error) { - result = resp.data.error.detail || resp.data.error - } else { - result = resp.data.detail || JSON.stringify(resp.data) - } - - return result.replace(/\n/g, '') -} - -/* Export utils */ -module.exports = { - retry, - b64escape, - b64encode, - parseLinkHeader, - findCertificateChainForIssuer, - formatResponseError -} diff --git a/packages/certd/src/acme/verify.cjs b/packages/certd/src/acme/verify.cjs deleted file mode 100644 index b4fef4da..00000000 --- a/packages/certd/src/acme/verify.cjs +++ /dev/null @@ -1,90 +0,0 @@ -/** - * ACME challenge verification - */ - -const Promise = require('bluebird') -const dns = Promise.promisifyAll(require('dns')) -const debug = require('debug')('acme-client') -const axios = require('./axios.cjs') - -/** - * Verify ACME HTTP challenge - * - * https://tools.ietf.org/html/rfc8555#section-8.3 - * - * @param {object} authz Identifier authorization - * @param {object} challenge Authorization challenge - * @param {string} keyAuthorization Challenge key authorization - * @param {string} [suffix] URL suffix - * @returns {Promise} - */ - -async function verifyHttpChallenge (authz, challenge, keyAuthorization, suffix = `/.well-known/acme-challenge/${challenge.token}`) { - const httpPort = axios.defaults.acmeSettings.httpChallengePort || 80 - const challengeUrl = `http://${authz.identifier.value}:${httpPort}${suffix}` - - debug(`Sending HTTP query to ${authz.identifier.value}, suffix: ${suffix}, port: ${httpPort}`) - const resp = await axios.get(challengeUrl) - const data = (resp.data || '').replace(/\s+$/, '') - - debug(`Query successful, HTTP status code: ${resp.status}`) - - if (!data || (data !== keyAuthorization)) { - throw new Error(`Authorization not found in HTTP response from ${authz.identifier.value}`) - } - - debug(`Key authorization match for ${challenge.type}/${authz.identifier.value}, ACME challenge verified`) - return true -} - -/** - * Verify ACME DNS challenge - * - * https://tools.ietf.org/html/rfc8555#section-8.4 - * - * @param {object} authz Identifier authorization - * @param {object} challenge Authorization challenge - * @param {string} keyAuthorization Challenge key authorization - * @param {string} [prefix] DNS prefix - * @returns {Promise} - */ - -async function verifyDnsChallenge (authz, challenge, keyAuthorization, prefix = '_acme-challenge.') { - debug(`Resolving DNS TXT records for ${authz.identifier.value}, prefix: ${prefix}`) - let challengeRecord = `${prefix}${authz.identifier.value}` - - try { - /* Attempt CNAME record first */ - debug(`Checking CNAME for record ${challengeRecord}`) - const cnameRecords = await dns.resolveCnameAsync(challengeRecord) - - if (cnameRecords.length) { - debug(`CNAME found at ${challengeRecord}, new challenge record: ${cnameRecords[0]}`) - challengeRecord = cnameRecords[0] - } - } catch (e) { - debug(`No CNAME found for record ${challengeRecord}`) - } - - /* Read TXT record */ - const result = await dns.resolveTxtAsync(challengeRecord) - const records = [].concat(...result) - - debug(`Query successful, found ${records.length} DNS TXT records`) - - if (records.indexOf(keyAuthorization) === -1) { - throw new Error(`Authorization not found in DNS TXT records for ${authz.identifier.value}`) - } - - debug(`Key authorization match for ${challenge.type}/${authz.identifier.value}, ACME challenge verified`) - return true -} - -/** - * Export API - */ - -module.exports = { - 'http-01': verifyHttpChallenge, - 'dns-01': verifyDnsChallenge -} diff --git a/packages/certd/src/dns-provider/dns-provider.js b/packages/certd/src/dns-provider/dns-provider.js index 21da87bc..7754403f 100644 --- a/packages/certd/src/dns-provider/dns-provider.js +++ b/packages/certd/src/dns-provider/dns-provider.js @@ -3,7 +3,7 @@ export class DnsProvider { } - removeRecord (dnsRecord, type) { + removeRecord (dnsRecord, type, recordValue) { } } diff --git a/packages/certd/src/dns-provider/impl/aliyun.js b/packages/certd/src/dns-provider/impl/aliyun.js index 2d28bd59..a33fc295 100644 --- a/packages/certd/src/dns-provider/impl/aliyun.js +++ b/packages/certd/src/dns-provider/impl/aliyun.js @@ -60,6 +60,7 @@ export default class AliyunDnsProvider extends DnsProvider { } async createRecord (dnsRecord, type, recordValue) { + log.info('添加域名解析:', dnsRecord, recordValue) const domain = await this.matchDomain(dnsRecord) const rr = dnsRecord.replace('.' + domain, '') @@ -77,6 +78,7 @@ export default class AliyunDnsProvider extends DnsProvider { try { const ret = await this.client.request('AddDomainRecord', params, requestOption) + console.log('添加域名解析成功:', dnsRecord, recordValue, ret.RecordId) return ret.RecordId } catch (e) { // e.code === 'DomainRecordDuplicate' @@ -101,7 +103,7 @@ export default class AliyunDnsProvider extends DnsProvider { } const ret = await this.client.request('DeleteDomainRecord', params, requestOption) - log.info('delete record success:', ret.RecordId) + log.info('删除域名解析成功:', dnsRecord, value, ret.RecordId) return ret.RecordId } } diff --git a/packages/certd/src/index.js b/packages/certd/src/index.js index 99131f71..0b1a578b 100644 --- a/packages/certd/src/index.js +++ b/packages/certd/src/index.js @@ -40,6 +40,9 @@ export class Certd { if (options == null) { options = this.options } + if (options.args == null) { + options.args = {} + } let oldCert try { oldCert = this.readCurrentCert(options.cert.email, options.cert.domains) @@ -71,13 +74,11 @@ export class Certd { } async doCertApply (options) { - const accessProviders = options.accessProviders - const providerOptions = accessProviders[options.cert.challenge.dnsProvider] - const dnsProvider = await DnsProviderFactory.createByType(providerOptions.providerType, providerOptions) + const dnsProvider = await this.createDnsProvider(options) const cert = await this.acme.order({ email: options.cert.email, domains: options.cert.domains, - dnsProvider: dnsProvider, + dnsProvider, csrInfo: options.cert.csrInfo }) @@ -92,6 +93,12 @@ export class Certd { } } + async createDnsProvider (options) { + const accessProviders = options.accessProviders + const providerOptions = accessProviders[options.cert.challenge.dnsProvider] + return await DnsProviderFactory.createByType(providerOptions.providerType, providerOptions) + } + writeCert (email, domains, cert) { const certFilesRootDir = this.buildCertDir(email, domains) const dirPath = path.join(certFilesRootDir, dayjs().format('YYYY.MM.DD.HHmmss')) diff --git a/packages/certd/test/index.test.js b/packages/certd/test/index.test.js index 0f63c3f1..9360b6a1 100644 --- a/packages/certd/test/index.test.js +++ b/packages/certd/test/index.test.js @@ -14,8 +14,9 @@ describe('Certd', function () { const certd = new Certd() certd.writeCert('xiaojunnuo@qq.com', ['*.domain.cn'], { csr: 'csr', crt: 'aaa', key: 'bbb' }) }) - it('#certApply', async function () { + it('#申请证书-aliyun', async function () { this.timeout(300000) + options.args = { forceCert: true } const certd = new Certd() const cert = await certd.certApply(options) expect(cert).ok @@ -24,7 +25,6 @@ describe('Certd', function () { expect(cert.detail).ok expect(cert.expires).ok }) - it('#readCurrentCert', async function () { const certd = new Certd() const cert = certd.readCurrentCert('xiaojunnuo@qq.com', ['*.docmirror.cn']) diff --git a/packages/certd/test/options.js b/packages/certd/test/options.js index 381fbe35..a6d61ab8 100644 --- a/packages/certd/test/options.js +++ b/packages/certd/test/options.js @@ -31,55 +31,7 @@ const defaultOptions = { organizationUnit: 'IT Department', emailAddress: 'xiaojunnuo@qq.com' } - }, - deploy: [ - { - deployName: '流程1-部署到阿里云系列产品', - tasks: [ - { - name: '上传证书到云', - taskType: 'uploadCertToCloud', - certStore: 'aliyun' - }, - { - name: '部署证书到SLB', - taskType: 'deployCertToAliyunSLB', - certStore: 'aliyun' - }, - { - name: '部署证书到阿里云集群Ingress', - taskType: 'deployCertToAliyunK8sIngress', - certStore: 'aliyun' - } - ] - }, - { - deployName: '流程2-部署到nginx服务器', - tasks: [ - { - name: '上传证书到服务器,并重启nginx', - taskType: 'sshAndExecute', - ssh: 'myLinux', - upload: [ - { from: '{certPath}', to: '/xxx/xxx/xxx.cert.pem' }, - { from: '{keyPath}', to: '/xxx/xxx/xxx.key' } - ], - script: 'sudo systemctl restart nginx' - } - ] - }, - { - deployName: '流程3-触发jenkins任务', - tasks: [ - { - name: '触发jenkins任务', - taskType: 'sshAndExecute', - ssh: 'myLinux', - script: 'sudo systemctl restart nginx' - } - ] - } - ] + } } _.merge(defaultOptions, optionsPrivate) diff --git a/packages/executor/src/index.js b/packages/executor/src/index.js index 6dde91ec..10992078 100644 --- a/packages/executor/src/index.js +++ b/packages/executor/src/index.js @@ -33,6 +33,15 @@ export class Executor { } async run (options, args) { + try { + return this.doRun(options, args) + } catch (e) { + logger.error('任务执行出错:', e) + throw e + } + } + + async doRun (options, args) { if (args != null) { _.merge(options.args, args) } diff --git a/packages/executor/test/options.js b/packages/executor/test/options.js index 32dd52e9..6c195019 100644 --- a/packages/executor/test/options.js +++ b/packages/executor/test/options.js @@ -40,6 +40,7 @@ const defaultOptions = { deploy: [ { name: '流程1-部署到阿里云系列产品', + disabled: true, tasks: [ { name: '上传证书到云', @@ -79,6 +80,7 @@ const defaultOptions = { }, { deployName: '流程3-触发jenkins任务', + disabled: true, tasks: [ { name: '触发jenkins任务',