Skip to content

[INC-263] Fallback to SHA-256 #11

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 62 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
0dee56d
Added jose module to dependencies
Jun 15, 2020
8abf6bd
Base manager implementation
Jun 15, 2020
1ea754e
Addded function to register account on LE via AccountsManager
Jun 15, 2020
0cf673b
Renamed lib/manager.js to lib/LetsEncryptManager.js
Jun 15, 2020
71974fa
Added CertificateManager and the ability to issue a certificate
Jun 15, 2020
89b8e08
Fixed bugs in certificate issuance
Jun 16, 2020
42a0437
Added support for resuing existing certificates and keypairs from the
Jun 16, 2020
fbaae54
Updated CertificateManager to format and store certificates
Jun 16, 2020
3128efc
Fixed the certificate pruning regular expression so that the linter stop
Jun 16, 2020
a286f7d
Store newly registered account and keypair in the store
Jun 16, 2020
d181846
Added express middleware to respond to http-01 challenge
Jun 16, 2020
de5d265
Fixed minor typos
Jun 16, 2020
5ef8c12
Merge branch 'feature/accounts' into develop
Jun 16, 2020
c1fab30
Fixed bug where the parent callback we being called when finalizing
Jun 16, 2020
0a61ee7
Merge branch 'feature/fixup' into develop
Jun 16, 2020
98a0ef5
Do not try to process invalid domains
Jun 16, 2020
96b57cf
Merge branch 'feature/verify-domain' into develop
Jun 16, 2020
4786e01
Fixed params while fetching / storing certificate keypair
Jun 16, 2020
36dead9
Fixed typo in LE middleware
Jun 16, 2020
ce0b7d4
Merge branch 'feature/use-keypair' into develop
Jun 16, 2020
08f28dc
Use account instead of keypair when fetching existing account
Jun 16, 2020
dbca4e5
Merge branch 'feature/use-account' into develop
Jun 16, 2020
934ee78
Do not store certificate keypair separately
Jun 16, 2020
4453840
Merge branch 'feature/no-store-cert-keypair' into develop
Jun 16, 2020
37b0076
Fixed bug in setting status code in middleware
Jun 16, 2020
c8e3278
Merge branch 'feature/fix-middleware' into develop
Jun 16, 2020
6ab918c
Store keypair when storing certificate
Jun 16, 2020
a797a2a
Merge branch 'feature/store-keypair' into develop
Jun 16, 2020
66efa28
Store keypair before storing certificate
Jun 16, 2020
3669aea
Merge branch 'feature/create-keypair-before-cert' into develop
Jun 16, 2020
c89772a
Detailed error logging in certificate manager
Jun 17, 2020
3cdf0ff
Merge branch 'feature/detailed-errors' into develop
Jun 17, 2020
7fc6f06
Change retries in Certificate Service to retry 10 times at 500ms inte…
Jun 17, 2020
f6abfae
Merge branch 'feature/fix-retries' into develop
Jun 17, 2020
4c8ffc2
Fixed retry logic for challenge verification and finalization
Jun 17, 2020
1cfcba6
Merge branch 'feature/fix-retry-handling' into develop
Jun 17, 2020
ed7212d
common requester
Jun 17, 2020
336b3ef
Added standard custom errors for the module
Jun 22, 2020
23c1a6e
Commonized all key and certificate code
Jun 22, 2020
45def7c
Added a common requester
Jun 22, 2020
a38961e
Split out order management from CertificateManager
Jun 22, 2020
621cab4
Use common modules in Account and LetsEncrypt managers
Jun 22, 2020
8fc331f
Updated dependencies
Jun 22, 2020
217d1a9
Renamed ACMEError to AcmeError
Jun 22, 2020
5481425
Renamed lib/util/keys.js to lib/util/crypto.js
Jun 22, 2020
72ff5d7
Fixed typo in CertificateManager._getKeypair
Jun 22, 2020
f70f6c8
Streamlined order processing
Jun 22, 2020
83a0502
Merge branch 'feature/common-requester' into develop
Jun 22, 2020
42eee45
Pass default threshold of 30 days to certificate renewal checker
Jun 22, 2020
0c98ebb
Merge branch 'feature/cert-validity' into develop
Jun 22, 2020
18331b5
Make ordering a certificate resilient to parallel orders
Jun 22, 2020
f82dbd1
Improved error response parsing
Jun 22, 2020
c044c7a
Merge branch 'feature/handle-parallel-orders' into develop
Jun 22, 2020
61e92d2
Store account keypair when creating a new account
Jun 23, 2020
140a77a
Merge branch 'feature/store-account-keypair' into develop
Jun 23, 2020
6e2ec30
Serve priv key with certificate
Jun 23, 2020
4181b05
Merge branch 'feature/serve-privkey-with-cert' into develop
Jun 23, 2020
c6a34bd
Send private key with certificate request for generated certificates
Jun 23, 2020
93ae32b
Merge branch 'feature/send-privkey-with-issued-certs' into develop
Jun 23, 2020
1bb644e
Fixed call to store while checking certificate keypair
Jun 23, 2020
1dacac6
Merge branch 'feature/fix-keypair-check' into develop
Jun 23, 2020
26a8292
[INC-263] Switch to SHA256 error
abhinav-ravi Sep 19, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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],
Expand Down
4 changes: 4 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const LetsEncryptManager = require('./lib/LetsEncryptManager');

module.exports = LetsEncryptManager;

107 changes: 107 additions & 0 deletions lib/AccountsManager.js
Original file line number Diff line number Diff line change
@@ -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;

155 changes: 155 additions & 0 deletions lib/CertificateManager.js
Original file line number Diff line number Diff line change
@@ -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;

102 changes: 102 additions & 0 deletions lib/LetsEncryptManager.js
Original file line number Diff line number Diff line change
@@ -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;

Loading