Compare commits
10 Commits
Author | SHA1 | Date |
---|---|---|
AJ ONeal | 7cdb42db7a | |
CaptEmulation | 37a7fd55b7 | |
AJ ONeal | e96ab294b6 | |
AJ ONeal | 7bdf8804a3 | |
AJ ONeal | 718e081495 | |
AJ ONeal | 258ad19f77 | |
AJ ONeal | 03a4db1034 | |
AJ ONeal | b0eff828bb | |
AJ ONeal | 00157ab870 | |
AJ ONeal | a0bda23683 |
101
README.md
101
README.md
|
@ -1,8 +1,99 @@
|
||||||
# See [`greenlock-store-test`](https://git.rootprojects.org/root/greenlock-store-test.js)
|
# le-store-SPEC
|
||||||
|
|
||||||
That's the test.
|
The reference implementation, specification, template, and tests for creating an le-store- strategy.
|
||||||
|
|
||||||
## Reference implementations
|
The reference implementation is completely in-memory.
|
||||||
|
|
||||||
* [`greenlock-store-fs`](https://git.rootprojects.org/root/greenlock-store-fs.js)
|
See [Help Wanted: Database Plugins (for saving certs)](https://github.com/Daplie/node-letsencrypt/issues/39)
|
||||||
* [`greenlock-store-sequelize`](https://git.rootprojects.org/root/greenlock-store-sequelize.js)
|
|
||||||
|
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: { ... }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
334
index.js
334
index.js
|
@ -4,342 +4,108 @@ module.exports.create = function (options) {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var crypto = require('crypto');
|
|
||||||
var defaults = {};
|
var defaults = {};
|
||||||
var memDb = {
|
|
||||||
accountKeypairs: {}
|
|
||||||
, certificateKeypairs: {}
|
|
||||||
, accountIndices: {}
|
|
||||||
, certIndices: {}
|
|
||||||
, certificates: {}
|
|
||||||
, accounts: {}
|
|
||||||
, accountCerts: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var accounts = {
|
var accounts = {
|
||||||
|
|
||||||
// Accounts
|
// Accounts
|
||||||
setKeypair: function (opts, keypair, cb) {
|
setKeypair: function (opts, keypair, cb) {
|
||||||
// opts.email // non-optional
|
// opts.email // optional
|
||||||
// opts.keypair // non-optional
|
// opts.accountId // optional - same as returned from acounts.set(opts, reg)
|
||||||
|
|
||||||
if (!opts.email) {
|
|
||||||
cb(new Error("MUST use email when setting Keypair"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!keypair.privateKeyJwk) {
|
// SAVE to db (as PEM and/or JWK) and index each domain in domains to this keypair
|
||||||
cb(new Error("MUST use privateKeyJwk when setting Keypair"));
|
// keypair = { privateKeyPem: '...', privateKeyJwk: { ... } }
|
||||||
return;
|
cb(null, keypair);
|
||||||
}
|
|
||||||
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
|
// Accounts
|
||||||
, checkKeypair: function (opts, cb) {
|
, checkKeypair: function (opts, cb) {
|
||||||
// opts.email // optional
|
// opts.email // optional
|
||||||
// opts.accountId // optional
|
// opts.accountId // optional - same as returned from acounts.set(opts, reg)
|
||||||
|
|
||||||
var keypair = opts.keypair || {};
|
|
||||||
var index;
|
|
||||||
|
|
||||||
if (keypair.publicKeyPem) {
|
// check db and return null or keypair object with one
|
||||||
index = crypto.createHash('sha256').update(keypair.publicKeyPem).digest('hex');
|
// (or both) of privateKeyPem or privateKeyJwk
|
||||||
index = memDb.accountIndices[index];
|
cb(null, { privateKeyPem: '...', privateKeyJwk: {} });
|
||||||
}
|
|
||||||
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
|
// Accounts
|
||||||
, set: function (opts, reg, cb) {
|
, set: function (opts, reg, cb) {
|
||||||
// opts.email
|
// opts.email
|
||||||
// reg.keypair
|
// reg.keypair
|
||||||
// reg.receipt // response from acme server
|
// reg.receipt // response from acme server
|
||||||
|
|
||||||
var keypair = reg.keypair || opts.keypair || {};
|
|
||||||
var accountId;
|
|
||||||
var index;
|
|
||||||
|
|
||||||
if (keypair.publicKeyPem) {
|
// You must implement a method to deterministically generate 'id'
|
||||||
index = crypto.createHash('sha256').update(keypair.publicKeyPem).digest('hex');
|
// For example, you could do this:
|
||||||
index = memDb.accountIndices[index];
|
// var id = crypto.createHash('sha256').update(reg.keypair.publicKeyPem).digest('hex');
|
||||||
}
|
cb(null, { id: '...', email: opts.email, keypair: reg.keypair, receipt: reg.receipt });
|
||||||
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 = {
|
var certificates = {
|
||||||
|
|
||||||
// Certificates
|
// Certificates
|
||||||
setKeypair: function (opts, keypair, cb) {
|
setKeypair: function (opts, keypair, cb) {
|
||||||
// opts.domains
|
// opts.domains - this is an array, but you nly need the first (or any) of them
|
||||||
|
|
||||||
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
|
||||||
if (!keypair.privateKeyJwk) {
|
cb(null, keypair);
|
||||||
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
|
// Certificates
|
||||||
, checkKeypair: function (opts, cb) {
|
, checkKeypair: function (opts, cb) {
|
||||||
// opts.domains
|
// opts.domains - this is an array, but you only need the first (or any) of them
|
||||||
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];
|
|
||||||
|
|
||||||
cb(null, memDb.certKeypairs[subject]);
|
// check db and return null or keypair object with one of privateKeyPem or privateKeyJwk
|
||||||
|
cb(null, { privateKeyPem: '...', privateKeyJwk: {} });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 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
|
// Certificates
|
||||||
, check: function (opts, cb) {
|
, check: function (opts, cb) {
|
||||||
// You will be provided one of these (which should be tried in this order)
|
// You will be provided one of these (which should be tried in this order)
|
||||||
// opts.domains
|
// opts.domains
|
||||||
// opts.email // optional
|
// opts.email // optional
|
||||||
// opts.accountId // optional
|
// opts.accountId // optional
|
||||||
var subject;
|
|
||||||
var subjects;
|
|
||||||
var accountId;
|
|
||||||
|
|
||||||
if (opts.domains) {
|
|
||||||
subject = memDb.certIndices[opts.domains[0]];
|
|
||||||
cb(null, memDb.certificates[subject]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.accountId) {
|
// return certificate PEMs from db if they exist, otherwise null
|
||||||
accountId = memDb.accountIndices[opts.accountId];
|
// optionally include expiresAt and issuedAt, if they are known exactly
|
||||||
}
|
// (otherwise they will be read from the cert itself later)
|
||||||
else if (opts.email) {
|
cb(null, { privkey: 'PEM', cert: 'PEM', chain: 'PEM', domains: [], accountId: '...' });
|
||||||
accountId = memDb.accountIndices[opts.email];
|
}
|
||||||
}
|
// 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
|
||||||
|
|
||||||
subjects = memDb.accountCerts[accountId] || [];
|
// opts.certs.privkey
|
||||||
cb(null, subjects.map(function (subject) {
|
// opts.certs.cert
|
||||||
subject = memDb.certIndices[subject];
|
// opts.certs.chain
|
||||||
return memDb.certificates[subject] || null ;
|
|
||||||
}));
|
|
||||||
|
// SAVE to the database, index the email address, the accountId, and alias the domains
|
||||||
|
cb(null, { privkey: 'PEM', cert: 'PEM', chain: 'PEM' });
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
@ -348,12 +114,6 @@ module.exports.create = function (options) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getOptions: function () {
|
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
|
// merge options with default settings and then return them
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue