reference implementation

This commit is contained in:
AJ ONeal 2016-08-13 14:48:55 -06:00
parent e120d2dcc2
commit 3083130ccc
4 changed files with 677 additions and 47 deletions

View File

@ -22,3 +22,20 @@ API
* check(opts, cb)
* set(opts, reg, 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`.
**check**
`checkKeypair` may be called with any of `email`, `accountId`, and `keypair` - which will
contain only `publicKeyPem` and `publicKeyJwk`.

362
index.js
View File

@ -4,87 +4,355 @@ module.exports.create = function (options) {
var crypto = require('crypto');
var defaults = {};
var memDb = {
accountKeypairs: {}
, certificateKeypairs: {}
, accountIndices: {}
, certIndices: {}
, certificates: {}
, accounts: {}
, accountCerts: {}
};
var accounts = {
checkKeypair: function (opts, cb) {
// Accounts
setKeypair: function (opts, keypair, cb) {
// opts.email // non-optional
// opts.keypair // non-optional
if (!opts.email) {
cb(new Error("MUST use email when setting Keypair"));
return;
}
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.accounts[accountId]);
}
// Accounts
, checkKeypair: function (opts, cb) {
// opts.email // optional
// opts.accountId // optional
// check db and return null or keypair object with one of privateKeyPem or privateKeyJwk
cb(null, { privateKeyPem: '...', privateKeyJwk: {} });
}
, setKeypair: function (opts, keypair, cb) {
// opts.email // optional
// opts.accountId // optional
// SAVE to db (as PEM and/or JWK) and index each domain in domains to this keypair
cb(null, keypair);
}
, check: function (opts, cb) {
// opts.email // optional
// opts.accountId // optional
// opts.domains // optional
// return account from db if it exists, otherwise null
cb(null, { id: '...', keypair: { privateKeyJwk: {} }, domains: [] });
var keypair = opts.keypair || {};
var index;
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
, set: function (opts, reg, cb) {
// opts.email
// reg.keypair
// reg.receipt // response from acme server
cb(null, { id: '...', email: opts.email, keypair: reg.keypair, receipt: reg.receipt });
var keypair = reg.keypair || opts.keypair || {};
var accountId;
var index;
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 = {
checkKeypair: function (opts, cb) {
// Certificates
setKeypair: function (opts, keypair, cb) {
// opts.domains
// check db and return null or keypair object with one of privateKeyPem or privateKeyJwk
cb(null, { privateKeyPem: '...', privateKeyJwk: {} });
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;
}
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]);
}
, setKeypair: function (opts, keypair, cb) {
// Certificates
, checkKeypair: function (opts, cb) {
// opts.domains
// SAVE to db (as PEM and/or JWK) and index each domain in domains to this keypair
cb(null, keypair);
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]);
}
// Certificates
, set: function (opts, certs, cb) {
// opts.domains
// opts.email // optional
// opts.accountId // optional
// certs.privkey
// certs.cert
// certs.chain
var index;
var accountId;
var account;
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
// 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: '...' });
}
, set: function (opts, pems, cb) {
// opts.domains
// opts.email // optional
// opts.accountId // optional
// pems.privkey
// pems.cert
// pems.chain
// SAVE to the database, index the email address, the accountId, and alias the domains
cb(null, pems);
var subject;
var subjects;
var accountId;
if (opts.domains) {
subject = memDb.certIndices[opts.domains[0]];
cb(null, memDb.certificates[subject]);
return;
}
if (opts.accountId) {
accountId = memDb.accountIndices[opts.accountId];
}
else if (opts.email) {
accountId = memDb.accountIndices[opts.email];
}
subjects = memDb.accountCerts[accountId] || [];
cb(null, subjects.map(function (subject) {
subject = memDb.certIndices[subject];
return memDb.certificates[subject] || null ;
}));
}
};
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;
}

342
tests/basic.js Normal file
View File

@ -0,0 +1,342 @@
'use strict';
var PromiseA = require('bluebird');
var leStore = PromiseA.promisifyAll(require('../').create({
debug: true
}));
leStore.accounts = PromiseA.promisifyAll(leStore.accounts);
leStore.certificates = PromiseA.promisifyAll(leStore.certificates);
// fixtures
var doesntExist = {
email: 'e@gmail.co'
, accountId: 'eee'
};
var goodGuy = {
email: 'goodguy@gmail.com'
, keypair: {
privateKeyPem: 'PRIVKEY.PEM', privateKeyJwk: { e: 'EXPO', n: 'MODULO' }
, publicKeyPem: 'PUBKEY.PEM'/*, publicKeyJwk: would be reduntdant */
}
};
var tests = [
//
// SANITY CHECKS
//
// SANITY test that an unregistered email returns no results
function () {
return leStore.accounts.checkKeypairAsync({
email: doesntExist.email
}).then(function (keypair) {
if (null !== keypair) {
throw new Error("Should return `null` when keypair does not exist by `email`.");
}
});
}
// SANITY test that an unregistered account id returns no results
, function () {
return leStore.accounts.checkAsync({
accountId: doesntExist.accountId
}).then(function (account) {
if (null !== account) {
throw new Error("Should return `null` when account does not exist by `accountId`.");
}
});
}
// SANITY test that an unregistered email returns no results
, function () {
return leStore.accounts.checkAsync({
email: doesntExist.email
}).then(function (account) {
if (null !== account) {
throw new Error("Should return `null` when account does not exist by `accountId`.");
}
});
}
//
// Creating Account Keypairs
//
// Register a private key to an email
// and make sure agreeTos remains falsey
, function () {
return leStore.accounts.setKeypairAsync(goodGuy, goodGuy.keypair);
}
, function () {
return leStore.accounts.checkKeypairAsync({
email: goodGuy.email
}).then(function (keypair) {
if (!keypair) {
throw new Error("should return saved keypair");
}
if (goodGuy.keypair.privateKeyPem !== keypair.privateKeyPem) {
if (keypair.privateKeyJwk) {
throw new Error("Error in test itself (not your fault). TODO: implement checking privateKeyJwk.");
}
throw new Error("agreeTos should return false or null because it was not set.");
}
});
}
//
// Creating Accounts
//
// create a new account
, function () {
var account = {
receipt: {}
, agreeTos: true
};
return leStore.accounts.setAsync(goodGuy, account).then(function (account) {
if (!account || !account.id || !account.email) {
throw new Error('accounts.set should return the object with its new `id` attached');
}
goodGuy.accountId = account.id;
});
}
// get by account id
, function () {
return leStore.accounts.checkAsync({
accountId: goodGuy.accountId
}).then(function (account) {
if (!account) {
throw new Error("Did not find account.");
}
else if (!account.keypair) {
throw new Error("Account did not have a keypair.");
}
else if (goodGuy.keypair.privateKeyPem !== account.keypair.privateKeyPem) {
if (account.keypair.privateKeyJwk) {
throw new Error("Error in test itself (not your fault). TODO: implement checking privateKeyJwk.");
}
throw new Error("agreeTos should return false or null because it was not set.");
}
if (!account.email) {
throw new Error("should have returned email");
}
if (!account.agreeTos) {
throw new Error("should have returned agreeTos");
}
if (!account.receipt) {
throw new Error("should have returned receipt");
}
});
}
// get by email
, function () {
return leStore.accounts.checkAsync({
email: goodGuy.email
}).then(function (account) {
if (goodGuy.keypair.privateKeyPem !== account.keypair.privateKeyPem) {
if (account.keypair.privateKeyJwk) {
throw new Error("Error in test itself (not your fault). TODO: implement checking privateKeyJwk.");
}
throw new Error("agreeTos should return false or null because it was not set.");
}
if (!account.email) {
throw new Error("should have returned email");
}
if (!account.agreeTos) {
throw new Error("should have returned agreeTos");
}
if (!account.receipt) {
throw new Error("should have returned receipt");
}
});
}
// Test that id and accountId are ignored
// and that arbitrary keys are stored
, function () {
var rnd = require('crypto').randomBytes(8).toString('hex');
var opts = {
accountId: '_account_id'
, id: '__account_id'
, email: 'john.doe@gmail.com'
, agreeTos: 'TOS_URL'
};
var account = {
keypair: { privateKeyJwk: {}, privateKeyPem: 'PEM2', publicKeyPem: 'PUBPEM2' }
, receipt: {}
};
account[rnd] = rnd;
return leStore.accounts.setKeypairAsync(opts, account.keypair).then(function () {
return leStore.accounts.setAsync(opts, account).then(function (account) {
if ('_account_id' === account.id || '__account_id' === account.id) {
throw new Error("Should create `id` deterministically from email or public key, not the given `accountId` or `id`.");
}
if ('john.doe@gmail.com' !== account.email) {
throw new Error("Should return the same email that was stored.");
}
if ('TOS_URL' !== account.agreeTos) {
throw new Error("Should return the same string for the tosUrl in agreeTos as was stored.");
}
if ('PEM2' !== account.keypair.privateKeyPem) {
throw new Error("Should return the same privateKey that was stored.");
}
if (rnd !== account[rnd]) {
throw new Error("Should save and restore arbitrary keys.");
}
});
});
}
// test lots of stuff
, function () {
return leStore.accounts.checkAsync({
accountId: goodGuy.accountId
}).then(function (account) {
if (!account
|| !account.agreeTos
|| account.email !== goodGuy.email
|| goodGuy.keypair.privateKeyPem !== account.keypair.privateKeyPem
) {
throw new Error("Should return the same account that was saved when retrieved using `accountId`.");
}
});
}
, function () {
return leStore.accounts.checkAsync({
email: goodGuy.email
}).then(function (account) {
if (!account
|| !account.agreeTos
|| account.email !== goodGuy.email
|| goodGuy.keypair.privateKeyPem !== account.keypair.privateKeyPem
) {
throw new Error("Should return the same account that was saved when retrieved using `accountId`.");
}
});
}
//
// Save a cert
//
, function () {
var certOpts = {
domains: [ 'example.com', 'www.example.com', 'foo.net', 'bar.foo.net' ]
, email: goodGuy.email
, certs: {
cert: 'CERT_A.PEM'
, privkey: 'PRIVKEY_A.PEM'
, chain: 'CHAIN_A.PEM'
// TODO issuedAt, expiresAt?
}
};
return leStore.certificates.setAsync(certOpts, certOpts.certs);
}
// and another
, function () {
var certOpts = {
domains: [ 'foo.com', 'www.foo.com', 'baz.net', 'bar.baz.net' ]
, accountId: goodGuy.accountId
, certs: {
cert: 'CERT_B.PEM'
, privkey: 'PRIVKEY_B.PEM'
, chain: 'CHAIN_B.PEM'
}
};
return leStore.certificates.setAsync(certOpts, certOpts.certs);
}
// basic test (set by email)
, function () {
var certOpts = {
domains: [ 'example.com' ]
};
return leStore.certificates.checkAsync(certOpts).then(function (certs) {
if (!certs || certs.privkey !== 'PRIVKEY_A.PEM') {
throw new Error("should have correct certs for example.com (set by email)");
}
});
}
// basic test (set by accountId)
, function () {
var certOpts = {
domains: [ 'example.com' ]
};
return leStore.certificates.checkAsync(certOpts).then(function (certs) {
if (!certs || certs.privkey !== 'PRIVKEY_A.PEM') {
throw new Error("should have correct certs for example.com (set by email)");
}
});
}
// altnames test
, function () {
var certOpts = {
domains: [ 'bar.foo.net' ]
};
return leStore.certificates.checkAsync(certOpts).then(function (certs) {
if (!certs || certs.privkey !== 'PRIVKEY_A.PEM') {
throw new Error("should have correct certs for bar.foo.net (one of the example.com altnames)");
}
});
}
// altnames test
, function () {
var certOpts = {
domains: [ 'baz.net' ]
};
return leStore.certificates.checkAsync(certOpts).then(function (certs) {
if (!certs || certs.privkey !== 'PRIVKEY_B.PEM') {
throw new Error("should have correct certs for baz.net (one of the foo.com altnames)");
}
});
}
];
function run() {
var test = tests.shift();
if (!test) {
console.info('All tests passed');
return;
}
test().then(run);
}
run();

3
tests/relationships.js Normal file
View File

@ -0,0 +1,3 @@
'use strict';
throw new Error("Tests not implemented");