diff --git a/.eslintrc b/.eslintrc index 9b9ef8b..ed72354 100644 --- a/.eslintrc +++ b/.eslintrc @@ -77,7 +77,7 @@ "eqeqeq": "error", "grouped-accessor-pairs": "warn", "guard-for-in": "warn", - "max-classes-per-file": ["error", 1], + "max-classes-per-file": "off", "max-lines-per-function": "off", "no-alert": "error", "no-caller": "error", @@ -359,7 +359,7 @@ "lodash/chain-style": ["error", "as-needed"], "lodash/chaining": ["error", "always", 3], "lodash/consistent-compose": ["error", "flow"], - "lodash/identity-shorthand": ["error", "always"], + "lodash/identity-shorthand": "off", "lodash/import-scope": "off", "lodash/matches-prop-shorthand": ["error", "always"], "lodash/matches-shorthand": ["error", "always", 3], diff --git a/index.js b/index.js index e69de29..b67fe7f 100644 --- a/index.js +++ b/index.js @@ -0,0 +1,4 @@ +const LetsEncryptManager = require('./lib/LetsEncryptManager'); + +module.exports = LetsEncryptManager; + diff --git a/lib/AccountsManager.js b/lib/AccountsManager.js new file mode 100644 index 0000000..614ec81 --- /dev/null +++ b/lib/AccountsManager.js @@ -0,0 +1,107 @@ +const _ = require('lodash'), + async = require('async'), + + { generateKey, formatKey, jwkToKey } = require('./util/crypto'), + { AcmeError } = require('./util/errors'); + +class AccountsManager { + constructor (manager) { + this.manager = manager; + } + + _register (key, email, tos, done) { + let payload = { + termsOfServiceAgreed: tos, + contact: [ + `mailto:${email}` + ] + }, + protectedHeader = { + jwk: key.toJWK(false) + }; + + this.manager.requester.signedRequest({ + resourceName: 'newAccount', + key: key, + payload: payload, + method: 'POST', + protectedHeader: protectedHeader + }, + (err, result) => { + if (err) { return done(err); } + + if (_.get(result, 'data.status') !== 'valid') { + return done(new AcmeError('Error creating new account', result)); + } + + let account = { + id: result.location, + meta: _.pick(result.data, ['contact', 'initialIp', 'createdAt', 'status']) + }; + + return done(null, { account: account, nonce: result.nonce }); + }); + } + + registerAccount (email, tos, done) { + async.auto({ + key: (next) => { + next(null, generateKey()); + }, + register: ['key', (results, next) => { + this._register(results.key, email, tos, next); + }], + storeKeypair: ['register', (results, next) => { + let opts = { email }, + keypair = formatKey(results.key); + + this.manager.store.accounts.setKeypair(opts, keypair, next); + }], + store: ['storeKeypair', (results, next) => { + let opts = { email }, + formattedKey = formatKey(results.key), + accountId = _.get(results.register, 'account.id'), + registration = { + keypair: formattedKey, + receipt: { + accountId: accountId, + meta: _.get(results.register, 'account.meta', {}) + } + }; + + this.manager.store.accounts.set(opts, registration, (err) => { + if (err) { return next(err); } + + return next(null, { keypair: formattedKey, accountId: accountId }); + }); + }] + }, + (err, results) => { + if (err) { return done(err); } + + return done(null, { + key: results.key, + id: _.get(results.register, 'account.id') + }); + }); + } + + + getAccount (email, tos, done) { + this.manager.store.accounts.check({ email }, (err, data) => { + if (err) { return done(err); } + + if (data && data.keypair) { + return done(null, { + key: jwkToKey(data.keypair.privateKeyJwk), + id: _.get(data, 'receipt.accountId') + }); + } + + this.registerAccount(email, tos, done); + }); + } +} + +module.exports = AccountsManager; + diff --git a/lib/CertificateManager.js b/lib/CertificateManager.js new file mode 100644 index 0000000..da8a993 --- /dev/null +++ b/lib/CertificateManager.js @@ -0,0 +1,155 @@ +const _ = require('lodash'), + async = require('async'), + moment = require('moment'), + { pki } = require('node-forge'), + + Order = require('./Order'), + { generateKey, formatKey, generateCSR } = require('./util/crypto'), + { LCMError } = require('./util/errors'); + +class CertificateManager { + constructor (manager) { + this.manager = manager; + } + + static getCertificateExpiry (certificate) { + let pem = pki.certificateFromPem(certificate); + + return moment.utc(pem.validity.notAfter); + } + + static certificateRequiresRenewal (certificate, validityThreshold) { + let expiry = CertificateManager.getCertificateExpiry(certificate); + + return expiry.diff(moment.utc(), 'd') < validityThreshold; + } + + static formatDownloadedCertificate (downloadedCertificate) { + let regex = /^-+BEGIN CERTIFICATE[\s\S]+-+END CERTIFICATE-+[\n\r]{1}/, + certificate, + chain, + match; + + downloadedCertificate = downloadedCertificate.trim(); + match = downloadedCertificate.match(regex); + + certificate = match.slice()[0]; + chain = downloadedCertificate.substring(certificate.length + 1); + + return { + certificate: certificate.trim(), + chain: chain.trim() + }; + } + + get (domain, email, tnc, done) { + this.manager.store.certificates.check({ domains: [domain] }, (err, certificate) => { + if (err) { return done(err); } + + if (!certificate) { + return this.register(domain, email, tnc, (err, issuedCertificate) => { + if (err) { return done(err); } + + return done(null, issuedCertificate, { + issued: true, + renewed: false + }); + }); + } + + let renew; + + try { + renew = CertificateManager.certificateRequiresRenewal(certificate.cert, 30); + } + catch (certificateParseError) { + return done(new LCMError('Error parsing certificate', { + domain + })); + } + + if (renew) { + return this.register(domain, email, tnc, (err, renewedCertificate) => { + if (err) { return done(err); } + + return done(null, renewedCertificate, { + issued: true, + renewed: true + }); + }); + } + + return done(null, _.pick(certificate, ['cert', 'chain', 'privkey']), { issued: false, renewed: false }); + }); + } + + _getKeypair (domain, email, done) { + this.manager.store.certificates.checkKeypair({ domains: [domain] }, (err, keypair) => { + if (err) { return done(err); } + + if (keypair) { return done(null, keypair); } + + let generatedKeypair = formatKey(generateKey()); + + this.manager.store.certificates.setKeypair({ + domains: [domain], + email: email + }, generatedKeypair, + (keypairStoreErr) => { + if (keypairStoreErr) { return done(err); } + + return done(null, generatedKeypair); + }); + }); + } + + register (domain, email, tos, done) { + async.auto({ + keypair: (next) => { + this._getKeypair(domain, email, next); + }, + account: (next) => { + this.manager.accountsManager.getAccount(email, tos, next); + }, + csr: ['keypair', (results, next) => { + let { privateKeyPem, publicKeyPem } = results.keypair; + + return next(null, generateCSR(privateKeyPem, publicKeyPem, domain)); + }], + order: ['csr', 'account', (results, next) => { + let order = new Order(results.account, domain, results.csr, this.manager.requester, { + type: this.manager.challengeType, + manager: this.manager.challengeManager + }); + + order.procure((err, downloadedCertificate) => { + if (err) { return next(err); } + + return next(null, CertificateManager.formatDownloadedCertificate(downloadedCertificate)); + }); + }], + store: ['order', (results, next) => { + this.manager.store.certificates.set({ + pems: { + cert: results.order.certificate, + chain: results.order.chain, + privkey: results.keypair.privateKeyPem + }, + domains: [domain], + email: email + }, next); + }] + }, + (err, results) => { + if (err) { return done(err); } + + let { chain, certificate: cert } = results.order, + { privateKeyPem: privkey } = results.keypair; + + return done(null, { chain, cert, privkey }); + }); + } +} + +module.exports = CertificateManager; + diff --git a/lib/LetsEncryptManager.js b/lib/LetsEncryptManager.js new file mode 100644 index 0000000..d2f0269 --- /dev/null +++ b/lib/LetsEncryptManager.js @@ -0,0 +1,102 @@ +const _ = require('lodash'), + { isEmail, isFQDN } = require('validator'), + + AccountsManager = require('./AccountsManager'), + CertificateManager = require('./CertificateManager'), + + Requester = require('./util/requester'), + { LCMError } = require('./util/errors'), + + LE_ENVIRONMENTS = { + staging: { + directoryUrl: 'https://acme-staging-v02.api.letsencrypt.org/directory' + }, + production: { + directoryUrl: 'https://acme-v02.api.letsencrypt.org/directory' + } + }, + + DEFAULT_ENVIRONMENT = 'staging'; + +class LetsEncryptManager { + constructor (options) { + this.environment = _.get(LE_ENVIRONMENTS, options.environment, LE_ENVIRONMENTS[DEFAULT_ENVIRONMENT]); + this.directoryUrl = options.directoryUrl || this.environment.directoryUrl; + + this.requester = new Requester({ directoryUrl: this.directoryUrl }); + + this.challengeType = options.challengeType || 'http-01'; + + this.accountsManager = new AccountsManager(this); + this.certificateManager = new CertificateManager(this); + + this.store = options.store.create(); + this.challengeManager = options.challenges[this.challengeType].create(); + } + + register (domain, email, tnc, done) { + if (!isFQDN(domain)) { + return done(new LCMError('Invalid domain', { + domain + })); + } + + if (!isEmail(email)) { + return done(new LCMError('Invalid email', { + email + })); + } + + if (tnc !== true) { + return done(new LCMError('Terms not accepted')); + } + + this.certificateManager.get(domain, email, tnc, done); + } + + middleware () { + const PREFIX = '/.well-known/acme-challenge/', + ChallengeManager = this.challengeManager; + + return function (req, res, next) { + if (!req.url.startsWith(PREFIX)) { + return next(); + } + + let split = req.url.split('/'), + token = split.pop(), + hostname = req.hostname; + + // if the url ends with a / + // unlikely, but still + if (!token) { token = split.pop(); } + + // token not found, let the service deal with the request + if (!token) { return next(); } + + ChallengeManager.get({}, hostname, token, (err, challenge) => { + if (err) { + res.set('content-type', 'text/plain; charset=utf8'); + res.status(500); + + return res.send('Something went wrong'); + } + + if (!challenge) { + res.set('content-type', 'text/plain; charset=utf8'); + res.status(404); + + return res.send('Not found'); + } + + res.set('content-type', 'application/octet-stream'); + res.status(200); + + return res.send(challenge); + }); + }; + } +} + +module.exports = LetsEncryptManager; + diff --git a/lib/Order.js b/lib/Order.js new file mode 100644 index 0000000..930e5f1 --- /dev/null +++ b/lib/Order.js @@ -0,0 +1,358 @@ +const _ = require('lodash'), + async = require('async'), + + { AcmeError, LCMError } = require('./util/errors'); + +class Order { + constructor (account, domain, csr, requester, challenge, config) { + this.account = account; + this.domain = domain; + this.csr = csr; + + this.config = _.assign({ + timeout: 500, + maxRetries: 10 + }, config); + + this.requester = requester; + this.challenge = challenge; + this.nonce = undefined; + this.order = {}; + } + + static _parseOrder (response) { + let url = _.get(response, 'location'), + data = _.get(response, 'data', {}), + { status, finalize, certificate } = data, + authorization = _.get(data, 'authorizations.0'), + orderData = _.pickBy({ + url, + status, + finalize, + certificate, + authorization + }, _.identity); + + return orderData; + } + + _request (options, done) { + _.assign(options, { + key: this.account.key, + accountId: this.account.id, + nonce: this.nonce + }); + + this.nonce = undefined; + + this.requester.signedRequest(options, (err, response) => { + if (err) { return done(err); } + + this.nonce = _.get(response, 'nonce'); + + return done(null, response); + }); + } + + _orderRequest (options, done) { + this._request(options, (err, response) => { + if (err) { return done(err); } + + let data = _.get(response, 'data', {}), + { status } = data; + + if (status === 'invalid') { + return done(new AcmeError('Order failed, status invalid', { response })); + } + + if (status === 'deactivated') { + return done(new AcmeError('Cannot complete order. Authorization has been deactivated', { response })); + } + + if (response.statusCode >= 400 || (data.type && data.detail) || data.error) { + return done(new AcmeError('Received error from server', { response })); + } + + return done(null, response, Order._parseOrder(response)); + }); + } + + _placeOrder (done) { + this._orderRequest({ + resourceName: 'newOrder', + payload: { + identifiers: [ + { + type: 'dns', + value: this.domain + } + ] + }, + method: 'POST' + }, done); + } + + _checkOrder (done) { + this._orderRequest({ + url: this.order.url, + payload: '', + method: 'POST' + }, done); + } + + _fetchChallenge (done) { + this._request({ + url: this.order.authorization, + payload: '', + method: 'POST' + }, done); + } + + _acceptChallenge (done) { + let { url } = _.get(this.authorization, 'challenge', {}); + + this._request({ + url: url, + payload: {}, + method: 'POST' + }, done); + } + + _finalize (done) { + this._orderRequest({ + url: this.order.finalize, + payload: { + csr: this.csr + }, + method: 'POST' + }, done); + } + + _downloadCertificate (done) { + this._request({ + url: this.order.certificate, + payload: '', + requestHeaders: { + accept: 'application/pem-certificate-chain' + }, + method: 'POST' + }, done); + } + + _deactivateAuthorization (done) { + this._request({ + url: this.order.authorization, + payload: { + status: 'deactivated' + }, + method: 'POST' + }, done); + } + + procure (done) { + async.series({ + order: (next) => { + this._placeOrder((err, response, order) => { + if (err) { return next(err); } + + _.assign(this.order, order); + + return next(); + }); + }, + fetchChallenge: (next) => { + // Skip if order status is not pending + if (this.order.status !== 'pending') { return next(); } + + this._fetchChallenge((err, response) => { + if (err) { return next(err); } + + let data = _.get(response, 'data', {}); + + if (response.statusCode >= 400 || (data.type && data.detail) || data.error) { + return next(new AcmeError('Error fetching challenge', { response })); + } + + this.authorization = { + status: data.status, + challenge: _.find(data.challenges, { type: this.challenge.type }) + }; + + return next(); + }); + }, + acceptChallenge: (next) => { + // Skip if order is not pending + if (this.order.status !== 'pending') { return next(); } + + // Authorization and challenge don't exist. Skip. + // This is likely because challenge was not fetched in the + // previous step. + // This could be because of an error, or because + // the order status was not "pending". Skip to the + // next step to check for order status. + if (!this.authorization) { return next(); } + + // If authorization status is not "pending", skip to next step. + // Either authorization has suceeded, or it has failed (for any reason). + // Check order status in next step to decide what to do next. + if (this.authorization.status !== 'pending') { return next(); } + + let { token, status } = _.get(this.authorization, 'challenge', {}), + authzKey; + + // If challenge status is not "pending", skip to next step. + if (status !== 'pending') { return next(); } + + authzKey = `${token}.${this.account.key.thumbprint}`; + + this.challenge.manager.set({}, this.domain, token, authzKey, (err) => { + if (err) { return next(err); } + + this._acceptChallenge((err, response) => { + if (err) { return next(err); } + + let data = _.get(response, 'data', {}); + + if (response.statusCode >= 400 || (data.type && data.detail) || data.error) { + return next(new AcmeError('Error accepting ACME challenge', { response })); + } + + return next(); + }); + }); + }, + pollStatusReady: (next) => { + let counter = 0, + max = this.config.maxRetries; + + async.whilst((cb) => { + let keepPolling = (this.order.status === 'pending' && counter < max); + + counter++; + + return cb(null, keepPolling); + }, + (cb) => { + setTimeout(this._checkOrder.bind(this), this.config.timeout, (err, response, order) => { + if (err) { return cb(err); } + + _.assign(this.order, order); + + return cb(); + }); + }, + (err) => { + if (err) { return next(err); } + + if (this.order.status === 'pending' || counter >= max) { + return next(new LCMError(`Order stuck in pending. Bailing out after ${max} retries`, { + acme: { status: this.order.status } + })); + } + + return next(); + }); + }, + finalize: (next) => { + // If status is valid then go download the certificate. + // If status is processing, then keep polling order status. + if (this.order.status === 'valid' || this.order.status === 'processing') { return next(); } + + // Can only finalize order in ready state. + // Highly unlikely that order can be at any state other than "ready" at this point. + if (this.order.status !== 'ready') { + return next(new AcmeError('Cannot finalize order. Status needs to be "ready" to finalize.', { + acme: { orderStatus: this.order.status } + })); + } + + this._finalize((err, response, order) => { + // Order is in a state where it can't be finalized. It is highly unlikely + // that the order is "pending" at this stage. + // So the order is either in "processing" or "valid" states. + // The order cached locally has likely diverged from the actual order + // so can't rely on it till we check again. + // Skip to the next step which will poll order status before + // deciding the next step. + if (err && err.options.acme.friendlyError === 'orderNotReady') { + return next(); + } + + if (err) { return next(err); } + + _.assign(this.order, order); + + return next(); + }); + }, + pollStatusValid: (next) => { + // If order status is "valid" then certificate is available. Go download. + if (this.order.status === 'valid') { return next(); } + + let counter = 0, + max = this.config.maxRetries; + + // Use async.doWhilst instead of async.whilst as we need to check + // order status online as we can't trust the order cached locally + // at this point. + async.doWhilst((cb) => { + setTimeout(this._checkOrder.bind(this), this.config.timeout, (err, response, order) => { + if (err) { return cb(err); } + + _.assign(this.order, order); + + return cb(); + }); + }, + (cb) => { + let keepPolling = (this.order.status === 'processing' && counter < max); + + counter++; + + return cb(null, keepPolling); + }, + (err) => { + if (err) { return next(err); } + + if (this.order.status !== 'valid' || counter >= max) { + return next(new LCMError(`Order stuck. Bailing out after ${max} retries`, { + acme: { status: this.order.status } + })); + } + + return next(); + }); + }, + download: (next) => { + if (this.order.status !== 'valid') { + return next(new AcmeError('Cannot download certificate if order status is not "valid"', { + acme: { status: this.order.status } + })); + } + + this._downloadCertificate((err, response) => { + if (err) { return next(err); } + + if (response.statusCode !== 200) { + return next(new AcmeError('Error downloading certificate', { response })); + } + + return next(null, response.data); + }); + } + }, + (err, results) => { + if (_.get(this.authorization, 'challenge.token')) { + this.challenge.manager.remove({}, this.domain, this.authorization.challenge.token, _.noop); + } + + if (err) { return done(err); } + + return done(null, results.download); + }); + } +} + +module.exports = Order; + diff --git a/lib/manager.js b/lib/manager.js deleted file mode 100644 index e69de29..0000000 diff --git a/lib/util/crypto.js b/lib/util/crypto.js new file mode 100644 index 0000000..c14b0ec --- /dev/null +++ b/lib/util/crypto.js @@ -0,0 +1,75 @@ +const { pki } = require('node-forge'), + forge = require('node-forge'), + { JWK } = require('jose'); + +function generateKey () { + return JWK.generateSync('RSA', 4096, { use: 'sig', alg: 'RS256' }); +} + +function formatKey (key) { + return { + privateKeyPem: key.toPEM(true), + publicKeyPem: key.toPEM(false), + privateKeyJwk: key.toJWK(true), + publicKeyJwk: key.toJWK(false) + }; +} + +function jwkToKey (jwk) { + return JWK.asKey(jwk); +} + +function _urlSafeDer (der) { + return Buffer.from(der.bytes(), 'binary') + .toString('base64') + .replace(/[=]/g, '') + .replace(/\+/g, '-') + .replace(/\//g, '_'); +} + +function generateCSR (privateKeyPem, publicKeyPem, domain) { + let privateKey = pki.privateKeyFromPem(privateKeyPem), + publicKey = pki.publicKeyFromPem(publicKeyPem), + csr = pki.createCertificationRequest(), + pem, + der; + + csr.publicKey = publicKey; + csr.setSubject([ + { + name: 'commonName', + value: domain + } + ]); + + csr.setAttributes([ + { + name: 'extensionRequest', + extensions: [ + { + name: 'subjectAltName', + altNames: [ + { + type: 2, + value: domain + } + ] + } + ] + } + ]); + + csr.sign(privateKey, forge.md.sha256.create()); + pem = pki.certificationRequestToPem(csr); + der = pki.pemToDer(pem); + + return _urlSafeDer(der); +} + +module.exports = { + generateKey, + formatKey, + jwkToKey, + generateCSR +}; + diff --git a/lib/util/errors.js b/lib/util/errors.js new file mode 100644 index 0000000..a915019 --- /dev/null +++ b/lib/util/errors.js @@ -0,0 +1,44 @@ +const _ = require('lodash'); + +class LCMError extends Error { + constructor (message, options) { + super(message); + this.name = this.constructor.name; + this.options = options; + + Error.captureStackTrace(this, this.constructor); + } +} + +class AcmeError extends LCMError { + constructor (message, options = {}) { + let { response } = options; + + if (_.isPlainObject(response)) { + delete options.response; + + let { statusCode } = response, + data = _.get(response, 'data', {}), + errorObject = data.error || data, + { type: error, detail, status: orderStatus, subproblems: subProblems } = errorObject, + friendlyError = error && error.split ? error.split(':').pop() : undefined; + + options.acme = _.pickBy({ + error, + friendlyError, + detail, + subProblems, + statusCode, + orderStatus + }, _.identity); + } + + super(message, options); + } +} + +module.exports = { + LCMError, + AcmeError +}; + diff --git a/lib/util/requester.js b/lib/util/requester.js new file mode 100644 index 0000000..e075b4f --- /dev/null +++ b/lib/util/requester.js @@ -0,0 +1,130 @@ +const _ = require('lodash'), + async = require('async'), + request = require('postman-request'), + { JWS } = require('jose'), + + { LCMError } = require('./errors'); + +class Requester { + constructor (options) { + this.requester = request.defaults({ + forever: true + }); + this.directoryUrl = options.directoryUrl; + this.directory = undefined; + } + + _request (options, done) { + this.requester(options, (err, response, body) => { + if (err) { return done(err); } + + let result = { + nonce: response.headers['replay-nonce'], + retryAfter: response.headers['retry-after'], + location: response.headers.location, + statusCode: response.statusCode, + data: body + }; + + return done(null, result); + }); + } + + _getDirectory (done) { + this.request({ method: 'get', url: this.directoryUrl, json: true }, (err, result) => { + if (err) { return done(err); } + + return done(null, result.data); + }); + } + + _getResourceUrl (name, done) { + if (_.isPlainObject(this.directory)) { + return done(null, this.directory[name]); + } + + this._getDirectory((err, directory) => { + if (err) { return done(err); } + + this.directory = directory; + + return done(null, directory[name]); + }); + } + + _getNonce (done) { + this._getResourceUrl('newNonce', (err, url) => { + if (err) { return done(err); } + + this.request({ method: 'get', url: url }, (err, result) => { + if (err) { return done(err); } + + return done(null, result.nonce); + }); + }); + } + + _makeSignedRequest (options, done) { + let { key, payload, nonce, url, accountId, method, protectedHeader = {}, requestHeaders = {} } = options; + + async.auto({ + nonce: (next) => { + if (nonce) { return next(null, nonce); } + + this._getNonce(next); + }, + request: ['nonce', (results, next) => { + let requestOptions = { + method: method, + url: url, + headers: _.assign({ + 'content-type': 'application/jose+json' + }, requestHeaders), + json: true + }; + + _.assign(protectedHeader, { nonce: results.nonce, url: url }); + + if (accountId) { protectedHeader.kid = accountId; } + + requestOptions.body = JWS.sign.flattened(payload, key, protectedHeader); + + this._request(requestOptions, next); + }] + }, + (err, results) => { + if (err) { return done(err); } + + return done(null, results.request); + }); + } + + _makeSignedResourceRequest (resourceName, options, done) { + this._getResourceUrl(resourceName, (err, url) => { + if (err) { return done(err); } + + options.url = url; + + this._makeSignedRequest(options, done); + }); + } + + signedRequest (options, done) { + if (options.url) { + return this._makeSignedRequest(options, done); + } + + if (options.resourceName) { + return this._makeSignedResourceRequest(options.resourceName, _.omit(options, ['resourceName']), done); + } + + return done(new LCMError('Require either url or resource name to make signed request')); + } + + request (options, done) { + return this._request(options, done); + } +} + +module.exports = Requester; + diff --git a/package-lock.json b/package-lock.json index 9a4aa7f..8197ab7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "letsencrypt-manager", - "version": "0.0.0", + "version": "0.0.0-alpha.11", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -263,6 +263,11 @@ "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", "dev": true }, + "@panva/asn1.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz", + "integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==" + }, "@postman/form-data": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@postman/form-data/-/form-data-3.1.0.tgz", @@ -1130,9 +1135,9 @@ } }, "eslint-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.0.0.tgz", - "integrity": "sha512-0HCPuJv+7Wv1bACm8y5/ECVfYdfsAm9xmVb7saeFlxjPYALefjhbYoCkBjPdPzGH8wWyTpAez82Fh3VKYEZ8OA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", "dev": true, "requires": { "eslint-visitor-keys": "^1.1.0" @@ -1895,6 +1900,14 @@ "iterate-iterator": "^1.0.1" } }, + "jose": { + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/jose/-/jose-1.27.1.tgz", + "integrity": "sha512-VyHM6IJPw0TTGqHVNlPWg16/ASDPAmcChcLqSb3WNBvwWFoWPeFqlmAUCm8/oIG1GjZwAlUDuRKFfycowarcVA==", + "requires": { + "@panva/asn1.js": "^1.0.0" + } + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2278,6 +2291,11 @@ } } }, + "moment": { + "version": "2.26.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.26.0.tgz", + "integrity": "sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw==" + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -2321,6 +2339,11 @@ "propagate": "^2.0.0" } }, + "node-forge": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.1.tgz", + "integrity": "sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ==" + }, "node-preload": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", @@ -3377,6 +3400,11 @@ "integrity": "sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==", "dev": true }, + "validator": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.1.1.tgz", + "integrity": "sha512-8GfPiwzzRoWTg7OV1zva1KvrSemuMkv07MA9TTl91hfhe+wKrsrgVN4H2QSFd/U/FhiU3iWPYVgvbsOGwhyFWw==" + }, "verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", diff --git a/package.json b/package.json index 6ff6280..309ca51 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "letsencrypt-manager", - "version": "0.0.0", + "version": "0.1.0-alpha.7", "description": "Node module for issuing and managing certificates from letsencrypt", "homepage": "https://github.com/postmanlabs/letsencrypt-manager", "bugs": "https://github.com/postmanlabs/letsencrypt-manager/issues", @@ -22,8 +22,12 @@ "license": "Apache-2.0", "dependencies": { "async": "3.2.0", + "jose": "1.27.1", "lodash": "4.17.15", - "postman-request": "2.88.1-postman.23" + "moment": "2.26.0", + "node-forge": "0.9.1", + "postman-request": "2.88.1-postman.23", + "validator": "13.1.1" }, "devDependencies": { "chai": "4.2.0", diff --git a/test.js b/test.js new file mode 100644 index 0000000..6d6fd37 --- /dev/null +++ b/test.js @@ -0,0 +1,106 @@ +const Manager = require('./index'); +const request = require('postman-request'); +const async = require('async'); +const _ = require('lodash'); + +const Store = { + create () { + return { + accounts: { + setKeypair (opts, keypair, done) { return done(); }, + checkKeypair (opts, done) { return done(); }, + check (opts, done) { return done(); }, + set (opts, reg, done) { return done(); } + }, + certificates: { + setKeypair (opts, keypair, done) { return done(); }, + checkKeypair (opts, done) { return done(); }, + check (opts, done) { return done(); }, + set (opts, done) { return done(); } + } + }; + } +}; + +const Challenge = { + create () { + return { + get (args, domain, token, done) { + return done(); + }, + set (args, domain, token, secret, done) { + request.put({ + url: `https://api.cloudflare.com/client/v4/accounts/476517b9bdc1bbf3f99b1a542b42061c/storage/kv/namespaces/43db20d755044626955821c7dc7612bd/values/${token}`, + headers: { + 'content-type': 'application/json', + authorization: 'Bearer hx0u4fRV9c80yfDJ4CqQL31JHzUuD8-8ZMP6TErg' + }, + body: secret + }, done); + }, + remove () {} + }; + } +}; + +const accStr = '{"keypair":{"privateKeyPem":"-----BEGIN PRIVATE KEY-----\\n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\\n/BM8lUprIIbVOJMtQD4e5ek7K54GuZcS\\n-----END PRIVATE KEY-----\\n","publicKeyPem":"-----BEGIN PUBLIC KEY-----\\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp71naXT5bei7zZTDjmpG\\nplewbz9QIZvaf+HPDcv9LYFeISlvmvYKcAX8wylFpILKfWA1DfsEB8VuKR2llcpr\\n2ztUYPb6bke3aaz+w6hmKsmguuhxx+miB89641M8m1A4Eo3HOLQqIxQk8kqlKK2Y\\nHfmhVuSeyrNU/bOsn2TOWUOQjgB8+fzAXqwy0PfvIT8w0PYEZk+dfpkIgsntAnYr\\nUkehHYBSzgBMq5b8Z3r49mv13yXd2G0PFHrCBtNuwoeEfNA+cUuG1pOLucba9ZAZ\\nlwXmvzyhXLpY/++Q37meIB5NAqmXCDMV+Z3SkGV76SOJ+SDWYD+INJv8+eEtZ9qc\\nLa1fKxZ/oaWk5UefWsy02B840Us89BjpEH467L8FQEtKuDJn86vnzzUoYD3vM2ZV\\nWrSo4LtncAbDqf71N2M872Hq1RVIwDt7uHjeoDpZSadKvL5c+v3r8opMGseeltXJ\\nXDP2k21lvu+2NeWEU0RX8i/t8rpbHczeya+e3vXrGJytc4Hv4E/f769h+86vTBAM\\nHEv2TiBqJOXtxa1a3iPgFsRbu/B5LfRk+HwLjzCCStVrqbCofDzdY3Sxc49YkgfH\\nyxZDw9yCy2etoZSTowS96iQMbH1uAPLsQ3Zzy1KdOXeqCYKxjPkCDRQuIRu99nNL\\n2hN7mC/JlwIrvQkkwuhY4R0CAwEAAQ==\\n-----END PUBLIC KEY-----","privateKeyJwk":{"e":"AQAB","n":"p71naXT5bei7zZTDjmpGplewbz9QIZvaf-HPDcv9LYFeISlvmvYKcAX8wylFpILKfWA1DfsEB8VuKR2llcpr2ztUYPb6bke3aaz-w6hmKsmguuhxx-miB89641M8m1A4Eo3HOLQqIxQk8kqlKK2YHfmhVuSeyrNU_bOsn2TOWUOQjgB8-fzAXqwy0PfvIT8w0PYEZk-dfpkIgsntAnYrUkehHYBSzgBMq5b8Z3r49mv13yXd2G0PFHrCBtNuwoeEfNA-cUuG1pOLucba9ZAZlwXmvzyhXLpY_--Q37meIB5NAqmXCDMV-Z3SkGV76SOJ-SDWYD-INJv8-eEtZ9qcLa1fKxZ_oaWk5UefWsy02B840Us89BjpEH467L8FQEtKuDJn86vnzzUoYD3vM2ZVWrSo4LtncAbDqf71N2M872Hq1RVIwDt7uHjeoDpZSadKvL5c-v3r8opMGseeltXJXDP2k21lvu-2NeWEU0RX8i_t8rpbHczeya-e3vXrGJytc4Hv4E_f769h-86vTBAMHEv2TiBqJOXtxa1a3iPgFsRbu_B5LfRk-HwLjzCCStVrqbCofDzdY3Sxc49YkgfHyxZDw9yCy2etoZSTowS96iQMbH1uAPLsQ3Zzy1KdOXeqCYKxjPkCDRQuIRu99nNL2hN7mC_JlwIrvQkkwuhY4R0","d":"okCapC6LC16hMtWgZ8SzPDn8J0_l0eyw6RxirSTRbFAD3i33j7xJEl0oRxtICz6cYbRK_Y_-MQovacQMpJzdwX1dPEIKsdvbNXQqJtZgpSjNuZ4YWVjL66QtaXDdYhqxcnkD6PyU-cOFjXzYz0VDrFMIVkXkt4vKPXGsG65lz4CEdmdQfHuZFLqamPoKrG00ahxe2xxN6uRakDCEeuoVM4yn8yJqdmthvmTZF7ui0LJTbZtyQOQVPykTP4kLTU3KwPSqQerdrfego34P1JKjviruLbOXHlv-tOplh5NDuiu1MEsdFchcjEy2HgicWEKRNpMl6onp1YHBrW-7_QZAJHj4NtWMmvSwIgavyMHey8WiLWxIkahOMpWKaHozuaUbsNhgNsxR__1rb-1mbxZZE3ixUWpWvi_lyFN2LYtK-YKiA1J6tE3IubnKbnKePz6eWh9Vaa9_8TImXtO5OTnSQDKrJHDIjg4yG9LPE3m5SYC0Jh0aVaw7E3eO2d44EH-mZK7VgxkjAUYxU7vf22PDBLOeZ3yA7AvxijafrsodldcNYmwxyLueD_bTvhruXJ6thvMdjk-HEnaU6JupCMkoLcLWTBtylWdpVGjvtwV_Q9uqVStZqu8J7uGgBYHmhO3oI26N5Kylpns_e4ovcHP-K5waVz0qDyBiJlDN_HU_7B0","p":"3l1mxsvxXFttrF-q1xj0gtvejkYh2jv8ZTMSqNcoyFBrLTRq5ZsQ12kh9rDVdloiHm6VRHMbkn_RKglgfy2UJsjwD90xjGpup9wfU3FVYEQvz5fGHZQsqkDxeqzSqdBgY8r8Tvp5Zs8-pcgQeww_rOQ9EhGal7Qp-Vx1RQAQyYRHWwsK_4VR2zrBYwzi2QvBxTn0Kptk1zVcqMTUooSb9F_KlJXrRC3LRAaeiBRI7Io4udlIq4zPdJx-qwcSZ9cD9RCIqO9sGiDcdrnuwD2F-FjKTupGxqHFOFahmm2ai8W6zSVdPfk89HN4LcgARIMSnm2ZoD6qSba0vb12aeGWiw","q":"wRzEiM5FTWv95p7R4OTuaVjxJSsiNz8G9UzA2U1FrZvVRK3ajExpRgIYj6xR4Ds2cozYZQksaW0lUwDHKe_IHP0IfuNZswqFqn_6AqqlMSCknVPobG_jChaNsDSkCOf2TiTJC21KmpXUBOfkwVpyxG-CAKCRLINwf2PGjChfnVfD2YsheT7RuWB6v_CMCNy1rzDmf1m54zSOFr8qYcW0EJy50FXY-uu_NTfIVTC8c2TM9UGymaHSGUJYqfNAOQEgDpp_uGiju2Fwkt4kdnyIhQ1n1m2B4mL3LvZtUzIyqj2rbgcp9SsiTLtrgRirE6HdFt2T61UBDj3dy6_eu2cD9w","dp":"X2x9mnoSjau-mOZFDiFrbGymSS_XfyuW2Gbebw5GNhAsri18YiP1SjuXk6G3OR7c3WpfLAypnGi1ffCgNHN-zFYWjvhBNxzFd8uCtfBAzxrz2YYsQq0_raBJAHgBv1Xox2podAwQ5QmyvLnqOKO8_5jCjGp5iPiLurLusDmi-peFd5B5m8W-pvHD45m4D6R39wczvMUKtT_1D4fIKCX4NxqUeMcNOyjbvcmJZqQt1zRJu5d_E_9NV6GAyiwyvdULobMCslxsa5oGgfywdFbykuynhM4MqVjqpY6nsvmuBKVJ0JdkOCApBE45GIxncP8-kjY-YwIBWbjKP1J66l0gmQ","dq":"mvZox5cuiZg1IJElviA3KJZUvAzG4ipTfRhgUH_vgAbitsAD9j2C5MjdyLzw3szlPjrDkyAvYVJSdy7So1Zk8M4juR7bZg71gAI-L1GcSbX2IsGoawvF1tyOSm2TjAoH3mEqa18CVNYSUvV63aWlHviUC8jNdjjrV25Lb0EeqPY6lSFOdSrxxJ4lIkz3Mk1aBgFplZv8zNPpiVtRuS-a9RGvEf0xpvjycNQYkDQRpU-oIM7dcOcn31kcKRSn3paGkPFtuB9P9BeW7GQSQnpgWtSNFMa_SVk9HZRl2jMX8QCXxi09gB1i4swq0mdnMNPcxHJ-NoP52uHOT5sLUrqxtQ","qi":"zOXglUaQDnN52WPstDZNSPiNBfZFi3-eVg0y6jcE6mi72ZfR0PwE7_P8qCcTFFyZ6d5RD6MwAiZ8f5EyHIIvaYYZgS_BT1CMNzAoprwOMRTPbF4otEdphT2aZ-Pxj8RfG5XDNhTUeKLfsUM_o6_b5Ve5ADAs-EpxPDX38BcTyEDP2t_Gu35oyOwpjrhkWidvjjq66L5P1mqKVQouf_GxwOtqcuX_fY9ZSMnOVO10CO2JuIkoc98BSuM1fJwqp5OnGEJl8uoimX01otv0ON837ieaxKajZjHktWPMx5dI_uX-JsgVBOfF1fwTPJVKayCG1TiTLUA-HuXpOyueBrmXEg","kty":"RSA","kid":"xvePq6aD_g2OT9gLPFVVq3-Fg1gcvm6jKIn67FTlD3M","alg":"RS256","use":"sig"},"publicKeyJwk":{"e":"AQAB","n":"p71naXT5bei7zZTDjmpGplewbz9QIZvaf-HPDcv9LYFeISlvmvYKcAX8wylFpILKfWA1DfsEB8VuKR2llcpr2ztUYPb6bke3aaz-w6hmKsmguuhxx-miB89641M8m1A4Eo3HOLQqIxQk8kqlKK2YHfmhVuSeyrNU_bOsn2TOWUOQjgB8-fzAXqwy0PfvIT8w0PYEZk-dfpkIgsntAnYrUkehHYBSzgBMq5b8Z3r49mv13yXd2G0PFHrCBtNuwoeEfNA-cUuG1pOLucba9ZAZlwXmvzyhXLpY_--Q37meIB5NAqmXCDMV-Z3SkGV76SOJ-SDWYD-INJv8-eEtZ9qcLa1fKxZ_oaWk5UefWsy02B840Us89BjpEH467L8FQEtKuDJn86vnzzUoYD3vM2ZVWrSo4LtncAbDqf71N2M872Hq1RVIwDt7uHjeoDpZSadKvL5c-v3r8opMGseeltXJXDP2k21lvu-2NeWEU0RX8i_t8rpbHczeya-e3vXrGJytc4Hv4E_f769h-86vTBAMHEv2TiBqJOXtxa1a3iPgFsRbu_B5LfRk-HwLjzCCStVrqbCofDzdY3Sxc49YkgfHyxZDw9yCy2etoZSTowS96iQMbH1uAPLsQ3Zzy1KdOXeqCYKxjPkCDRQuIRu99nNL2hN7mC_JlwIrvQkkwuhY4R0","kty":"RSA","kid":"xvePq6aD_g2OT9gLPFVVq3-Fg1gcvm6jKIn67FTlD3M","alg":"RS256","use":"sig"}},"receipt":{"accountId":"https://acme-staging-v02.api.letsencrypt.org/acme/acct/14299679"}}' + + +const account = JSON.parse(accStr); + +const options = { + environment: 'staging', + challengeType: 'http-01', + challenges: { 'http-01': Challenge }, + store: Store +}; + +const manager = new Manager(options); + +function getCert (domain, email, done) { + manager.register(domain, email, true, done); +} + +function asyncGetCert (domain, times, done) { + const asyncStore = { + create () { + let store = Store.create(); + + store.accounts.check = (opts, done) => { + return done(null, account); + } + + return store; + } + }; + + const asyncOptions = _.assign({ store: asyncStore }, _.omit(options, ['store'])); + const asyncManager = new Manager(asyncOptions); + + async.times(times, function (counter, next) { + console.log('counter:', counter); + asyncManager.register(domain, 'letest@elssar.space', true, (err, certs, meta) => { + console.log('Counter:', counter, Boolean(err), Boolean(certs), Boolean(meta)); + + return next(null, { certs, err }); + }) + }, done); +} + +function cb (err, result) { + console.log('done!'); + global.result = { err, result }; +} + +function done (err, result) { + console.log('done!'); + + global.result = result; +} + +module.exports.getCert = getCert; +module.exports.cb = cb; +module.exports.asyncGetCert = asyncGetCert; +module.exports.manager = manager; +module.exports.done = done; +