Compare commits

..

21 Commits

Author SHA1 Message Date
AJ ONeal 2d7a117197 Update 'README.md' 2019-05-16 07:49:06 +00:00
AJ ONeal 3c2778a67e Update 'README.md' 2019-05-16 07:47:19 +00:00
AJ ONeal f4c1aa6906 Merge branch 'master' of ssh://git.coolaj86.com:22042/coolaj86/le-store-SPEC.js 2019-03-30 10:00:52 -06:00
AJ ONeal 974b3707c1 Merge branch 'tests' of ssh://git.coolaj86.com/CaptEmulation/le-store-SPEC.js 2019-03-30 10:00:34 -06:00
AJ ONeal 06c78a6a10 A Root Project 2019-03-30 15:51:51 +00:00
CaptEmulation c58785f6e3 Updates from writing my own store 2019-02-17 11:37:22 -06:00
AJ ONeal 766eaa3eed update link 2018-10-02 23:23:01 +00:00
AJ ONeal 2ac41d914a Merge branch 'patch-1' of https://github.com/jotto/le-store-SPEC 2018-04-30 09:17:00 -06:00
AJ ONeal 359755901d update sponsorship 2018-04-30 09:15:00 -06:00
AJ ONeal 7f082d728b auto-update banner 2016-12-30 02:39:38 -07:00
AJ ONeal f4ccef647b auto-update ad 2016-12-30 02:22:26 -07:00
AJ ONeal 10764c4f56 Update README.md 2016-11-25 10:31:29 -07:00
Jonathan Otto 73f6cd5ab2 Fix accounts.setKeypair undefined return value 2016-11-15 15:14:52 -08:00
AJ ONeal a52e2aeb8e Merge branch 'tests' 2016-09-02 16:38:23 -06:00
AJ ONeal abc665fa81 merge readme 2016-09-02 14:26:55 -06:00
AJ ONeal 434bde67dc merge with tests branch 2016-08-13 15:45:17 -06:00
AJ ONeal b5f44afec6 note usage of tests branch 2016-08-13 15:25:43 -06:00
AJ ONeal c7d2806fe7 note how to run tests 2016-08-13 15:21:29 -06:00
AJ ONeal d657afbac9 show test #, stack, and stringified test 2016-08-13 15:04:33 -06:00
AJ ONeal 43c830482c varname fix 2016-08-13 14:49:21 -06:00
AJ ONeal 3083130ccc reference implementation 2016-08-13 14:48:55 -06:00
2 changed files with 292 additions and 143 deletions

101
README.md
View File

@ -1,99 +1,8 @@
# le-store-SPEC
# See [`greenlock-store-test`](https://git.rootprojects.org/root/greenlock-store-test.js)
The reference implementation, specification, template, and tests for creating an le-store- strategy.
That's the test.
The reference implementation is completely in-memory.
## Reference implementations
See [Help Wanted: Database Plugins (for saving certs)](https://github.com/Daplie/node-letsencrypt/issues/39)
How to create a custom strategy
===============================
READ THIS README:
Believe it or not, most of your answers are either right here
or in the comments in the sample code in `index.js`.
Now, let's say there's some new database AwesomeDB that
we want to make a plugin for, here's how we'd start:
```bash
# First create you repo on github or wherever
# Then clone it
git clone git@github.com:AwesomeDB/le-store-awesome.git
pushd le-store-awesome
# IMPORTANT: we pull in the 'template' branch, which has the skeleton code
git pull https://github.com/Daplie/le-store-SPEC.git template
git push
```
Or, if you already have some code and just need to merge in the tests:
```bash
git pull https://github.com/Daplie/le-store-SPEC.git tests
```
Next, Just run the tests
```
node tests/basic.js
```
Note: you should not modify the tests that come from the tests branch,
but rather create separate files for your own tests.
API
===
```
* getOptions()
* accounts.
* checkKeypair(opts, cb)
* setKeypair(opts, keypair, cb)
* check(opts, cb)
* set(opts, reg, cb)
* certificates.
* checkKeypair(opts, cb)
* setKeypair(opts, keypair, cb)
* check(opts, cb)
* set(opts, certs, cb)
```
Keypairs
--------
For convenience, the keypair object will always contain **both** PEM and JWK
versions of the private and/or public keys when being passed to the `*Keypair` functions.
**set**
`setKeypair` will always be called with `email` and **all three** forms of the keypair:
`privateKeyPem`, `publicKeyPem`, and `privateKeyJwk`. It's easy to generate `publicKeyJwk`
from `privateKeyJwk` because it is just a copy of the public fields `e` and `n`.
```
// keypair looks like this
{ privateKeyPem: '...'
, publicKeyPem: '...'
, privateKeyJwk: { ... }
}
```
**check**
`checkKeypair` may be called with any of `email`, `accountId`, and `keypair` - which will
contain only `publicKeyPem` and `publicKeyJwk`.
```
// opts looks like this
{
email: '...@...'
, accountId: '...'
, keypair: {
publicKeyPem: '...'
, publicKeyJwk: { ... }
}
}
```
* [`greenlock-store-fs`](https://git.rootprojects.org/root/greenlock-store-fs.js)
* [`greenlock-store-sequelize`](https://git.rootprojects.org/root/greenlock-store-sequelize.js)

332
index.js
View File

@ -4,108 +4,342 @@ module.exports.create = function (options) {
var crypto = require('crypto');
var defaults = {};
var memDb = {
accountKeypairs: {}
, certificateKeypairs: {}
, accountIndices: {}
, certIndices: {}
, certificates: {}
, accounts: {}
, accountCerts: {}
};
var accounts = {
// Accounts
setKeypair: function (opts, keypair, cb) {
// opts.email // optional
// opts.accountId // optional - same as returned from acounts.set(opts, reg)
// opts.email // non-optional
// opts.keypair // non-optional
if (!opts.email) {
cb(new Error("MUST use email when setting Keypair"));
return;
}
// SAVE to db (as PEM and/or JWK) and index each domain in domains to this keypair
// keypair = { privateKeyPem: '...', privateKeyJwk: { ... } }
cb(null, keypair);
if (!keypair.privateKeyJwk) {
cb(new Error("MUST use privateKeyJwk when setting Keypair"));
return;
}
if (!keypair.privateKeyPem) {
cb(new Error("MUST use privateKeyPem when setting Keypair"));
return;
}
if (!keypair.publicKeyPem) {
cb(new Error("MUST use publicKeyPem when setting Keypair"));
return;
}
var accountId = crypto.createHash('sha256').update(keypair.publicKeyPem).digest('hex');
memDb.accountIndices[accountId] = accountId;
memDb.accountIndices[opts.email] = accountId;
memDb.accountKeypairs[accountId] = keypair;
/*
{
id: accountId
// TODO nix accountId
, accountId: accountId
, email: opts.email
, keypair: keypair
};
*/
cb(null, memDb.accountKeypairs[accountId]);
}
// Accounts
, checkKeypair: function (opts, cb) {
// opts.email // optional
// opts.accountId // optional - same as returned from acounts.set(opts, reg)
// opts.accountId // optional
var keypair = opts.keypair || {};
var index;
// check db and return null or keypair object with one
// (or both) of privateKeyPem or privateKeyJwk
cb(null, { privateKeyPem: '...', privateKeyJwk: {} });
if (keypair.publicKeyPem) {
index = crypto.createHash('sha256').update(keypair.publicKeyPem).digest('hex');
index = memDb.accountIndices[index];
}
else if (keypair.publicKeyJwk) {
// TODO RSA.exportPublicPem(keypair);
cb(new Error("id from publicKeyJwk not yet implemented"));
return;
}
else if (opts.email) {
index = memDb.accountIndices[opts.email];
}
else {
cb(new Error("MUST supply email or keypair.publicKeyPem or keypair.publicKeyJwk"));
return;
}
cb(null, memDb.accountKeypairs[index] || null);
}
// Accounts
, check: function (opts, cb) {
// opts.email // optional
// opts.accountId // optional - same as returned from acounts.set(opts, reg)
// opts.domains // optional - same as set in certificates.set(opts, certs)
// return account from db if it exists, otherwise null
cb(null, { id: '...', keypair: { privateKeyJwk: {} }/*, domains: []*/ });
}
// Accounts
, set: function (opts, reg, cb) {
// opts.email
// reg.keypair
// reg.receipt // response from acme server
var keypair = reg.keypair || opts.keypair || {};
var accountId;
var index;
// You must implement a method to deterministically generate 'id'
// For example, you could do this:
// var id = crypto.createHash('sha256').update(reg.keypair.publicKeyPem).digest('hex');
cb(null, { id: '...', email: opts.email, keypair: reg.keypair, receipt: reg.receipt });
if (keypair.publicKeyPem) {
index = crypto.createHash('sha256').update(keypair.publicKeyPem).digest('hex');
index = memDb.accountIndices[index];
}
else if (keypair.publicKeyJwk) {
// TODO RSA.exportPublicPem(keypair);
cb(new Error("id from publicKeyJwk not yet implemented"));
return;
}
else if (opts.email) {
index = memDb.accountIndices[opts.email];
}
else {
cb(new Error("MUST supply email or keypair.publicKeyPem or keypair.publicKeyJwk"));
return;
}
accountId = memDb.accountIndices[index];
if (!accountId) {
cb(new Error("keypair was not previously set with email and keypair.publicKeyPem"));
return;
}
memDb.accounts[accountId] = {
id: accountId
// TODO nix accountId
, accountId: accountId
, email: opts.email
, keypair: keypair
, agreeTos: opts.agreeTos || reg.agreeTos
//, receipt: reg.receipt || opts.receipt
};
Object.keys(reg).forEach(function (key) {
memDb.accounts[accountId][key] = reg[key];
});
cb(null, memDb.accounts[accountId]);
}
// Accounts
, check: function (opts, cb) {
// opts.email // optional
// opts.accountId // optional
// opts.domains // optional
var keypair = opts.keypair || {};
var index;
var accountId;
var account;
if (opts.accountId) {
index = memDb.accountIndices[opts.accountId];
}
else if (keypair.publicKeyPem) {
index = crypto.createHash('sha256').update(keypair.publicKeyPem).digest('hex');
index = memDb.accountIndices[index];
}
else if (keypair.publicKeyJwk) {
// TODO RSA.exportPublicPem(keypair);
cb(new Error("id from publicKeyJwk not yet implemented"));
return;
}
else if (opts.email) {
index = memDb.accountIndices[opts.email];
}
else if (opts.domains && opts.domains[0]) {
index = memDb.accountIndices[opts.domains[0]];
}
else {
console.error(opts);
cb(new Error("MUST supply email or keypair.publicKeyPem or keypair.publicKeyJwk"));
return;
}
accountId = memDb.accountIndices[index];
if (!accountId) {
cb(null, null);
return;
}
account = JSON.parse(JSON.stringify(memDb.accounts[accountId] || null));
account.keypair = memDb.accountKeypairs[accountId] || null;
cb(null, account);
}
};
var certificates = {
// Certificates
setKeypair: function (opts, keypair, cb) {
// opts.domains - this is an array, but you nly need the first (or any) of them
// opts.domains
if (!opts.domains || !opts.domains.length) {
cb(new Error("MUST use domains when setting Keypair"));
return;
}
if (!opts.email) {
cb(new Error("MUST use email when setting Keypair"));
return;
}
if (!opts.accountId) {
cb(new Error("MUST use accountId when setting Keypair"));
return;
}
// SAVE to db (as PEM and/or JWK) and index each domain in domains to this keypair
cb(null, keypair);
if (!keypair.privateKeyJwk) {
cb(new Error("MUST use privateKeyJwk when setting Keypair"));
return;
}
if (!keypair.privateKeyPem) {
cb(new Error("MUST use privateKeyPem when setting Keypair"));
return;
}
if (!keypair.publicKeyPem) {
cb(new Error("MUST use publicKeyPem when setting Keypair"));
return;
}
var subject = opts.domains[0];
opts.domains.forEach(function (domain) {
memDb.certIndices[domain] = subject;
});
memDb.certKeypairs[subject] = keypair;
/*
{
subject: subject
, keypair: keypair
};
*/
cb(null, memDb.certKeypairs[subject]);
}
// Certificates
, checkKeypair: function (opts, cb) {
// opts.domains - this is an array, but you only need the first (or any) of them
// opts.domains
if (!opts.domains || !opts.domains.length) {
cb(new Error("MUST use domains when checking Keypair"));
return;
}
var domain = opts.domains[0];
var subject = memDb.certIndices[domain];
// check db and return null or keypair object with one of privateKeyPem or privateKeyJwk
cb(null, { privateKeyPem: '...', privateKeyJwk: {} });
cb(null, memDb.certKeypairs[subject]);
}
// Certificates
, set: function (opts, cb) {
// opts.domains
// opts.email // optional
// opts.accountId // optional
// opts.certs.privkey
// opts.certs.cert
// opts.certs.chain
var index;
var accountId;
var account;
var certs = opts.certs;
var subject = certs.subject || opts.domains[0];
var altnames = certs.altnames || opts.domains;
var accountCerts;
if (opts.accountId) {
index = opts.accountId;
}
else if (opts.email) {
index = opts.email;
}
else {
cb(new Error("MUST supply email or accountId"));
return;
}
accountId = memDb.accountIndices[index];
account = memDb.accounts[accountId];
if (!account) {
cb(new Error("account must exist"));
}
accountId = memDb.accountIndices[index];
if (!accountId) {
cb(new Error("keypair was not previously set with email and keypair.publicKeyPem"));
return;
}
memDb.certIndices[subject] = subject;
altnames.forEach(function (altname) {
memDb.certIndices[altname] = subject;
});
accountCerts = memDb.accountCerts[accountId] || {};
accountCerts[subject] = subject;
memDb.accountCerts[accountId] = accountCerts;
memDb.certificates[subject] = certs;
// SAVE to the database, index the email address, the accountId, and alias the domains
cb(null, certs);
}
// Certificates
, check: function (opts, cb) {
// You will be provided one of these (which should be tried in this order)
// opts.domains
// opts.email // optional
// opts.accountId // optional
var subject;
var subjects;
var accountId;
// return certificate PEMs from db if they exist, otherwise null
// optionally include expiresAt and issuedAt, if they are known exactly
// (otherwise they will be read from the cert itself later)
cb(null, { privkey: 'PEM', cert: 'PEM', chain: 'PEM', domains: [], accountId: '...' });
if (opts.domains) {
subject = memDb.certIndices[opts.domains[0]];
cb(null, memDb.certificates[subject]);
return;
}
// Certificates
, set: function (opts, cb) {
// opts.domains // each of these must be indexed
// opts.email // optional, should be indexed
// opts.accountId // optional - same as set by you in accounts.set(opts, keypair) above
// opts.certs.privkey
// opts.certs.cert
// opts.certs.chain
if (opts.accountId) {
accountId = memDb.accountIndices[opts.accountId];
}
else if (opts.email) {
accountId = memDb.accountIndices[opts.email];
}
// SAVE to the database, index the email address, the accountId, and alias the domains
cb(null, { privkey: 'PEM', cert: 'PEM', chain: 'PEM' });
subjects = memDb.accountCerts[accountId] || [];
cb(null, subjects.map(function (subject) {
subject = memDb.certIndices[subject];
return memDb.certificates[subject] || null ;
}));
}
};
@ -114,6 +348,12 @@ module.exports.create = function (options) {
return {
getOptions: function () {
Object.keys(defaults).forEach(function (key) {
if ('undefined' === typeof options[key]) {
options[key] = defaults[key];
}
});
// merge options with default settings and then return them
return options;
}