use ACME to create account

This commit is contained in:
AJ ONeal 2019-05-11 14:00:17 -06:00
parent b81ff7550b
commit 8b2e6e69d0
3 changed files with 103 additions and 39 deletions

View File

@ -10,6 +10,7 @@ try {
} }
var pkg = require('../package.json'); var pkg = require('../package.json');
var Keypairs = require('keypairs');
var crypto = require('crypto'); var crypto = require('crypto');
//var url = require('url'); //var url = require('url');
@ -422,7 +423,7 @@ controllers.newAccount = function (req, res) {
// TODO clean up error messages to be similar to ACME // TODO clean up error messages to be similar to ACME
// check if there's a public key // check if there's a public key
if (!req.jws || !req.jws.header.kid || !req.jws.header.jwk) { if (!req.jws || !req.jws.header.jwk) {
res.statusCode = 422; res.statusCode = 422;
res.send({ error: { message: "jws body was not present or could not be validated" } }); res.send({ error: { message: "jws body was not present or could not be validated" } });
return; return;
@ -448,7 +449,7 @@ controllers.newAccount = function (req, res) {
return verifyJws(req.jws.header.jwk, req.jws).then(function (verified) { return verifyJws(req.jws.header.jwk, req.jws).then(function (verified) {
if (!verified) { if (!verified) {
res.statusCode = 422; res.statusCode = 422;
res.send({ error: { message: "jws body was not present or could not be validated" } }); res.send({ error: { message: "jws body failed verification" } });
return; return;
} }
@ -483,7 +484,7 @@ controllers.newAccount = function (req, res) {
} }
res.statusCode = 201; res.statusCode = 201;
account = {}; account = {};
account._id = crypto.randomBytes(16).toString('base64'); account._id = toUrlSafe(crypto.randomBytes(16).toString('base64'));
// TODO be better about this // TODO be better about this
account.location = myBaseUrl + '/acme/accounts/' + account._id; account.location = myBaseUrl + '/acme/accounts/' + account._id;
account.thumb = thumb; account.thumb = thumb;
@ -620,12 +621,38 @@ function jwsEggspress(req, res, next) {
} }
var ua = req.headers['user-agent']; var ua = req.headers['user-agent'];
var trusted = false;
var vjwk; var vjwk;
var pubs; var pubs;
var kid = req.jws.header.kid;
var p = Promise.resolve();
if (!kid && !req.jws.header.jwk) {
res.send({ error: { message: "jws protected header must include either 'kid' or 'jwk'" } });
return;
}
if (req.jws.header.jwk) {
if (kid) {
// TODO kid and jwk are mutually exclusive
//res.send({ error: { message: "jws protected header must not include both 'kid' and 'jwk'" } });
//return;
}
kid = req.jws.header.jwk.kid;
p = Keypairs.thumbprint({ jwk: req.jws.header.jwk }).then(function (thumb) {
if (kid && kid !== thumb) {
res.send({ error: { message: "jwk included 'kid' for key id, but it did not match the key's thumbprint" } });
return;
}
kid = thumb;
req.jws.header.jwk.kid = thumb;
});
}
// Check if this is a key we already trust // Check if this is a key we already trust
DB.pubs.some(function (jwk) { DB.pubs.some(function (jwk) {
if (jwk.kid === req.jws.header.kid) { if (jwk.kid === kid) {
trusted = true;
vjwk = jwk; vjwk = jwk;
return true;
} }
}); });
@ -645,33 +672,32 @@ function jwsEggspress(req, res, next) {
}); });
} }
// Check if there aren't any keys that we trust p.then(function () {
// and this has signed itself, then make it a key we trust // Check if there aren't any keys that we trust
// (TODO: move this all to the new account function) // and this has signed itself, then make it a key we trust
if ((0 === pubs.length && req.jws.header.jwk)) { // (TODO: move this all to the new account function)
vjwk = req.jws.header.jwk; if (0 === pubs.length) { trusted = true; }
if (!vjwk.kid) { throw Error("Impossible: no key id"); } if (!vjwk) { vjwk = req.jws.header.jwk; }
} // Don't verify if it can't be verified
if (!vjwk) { return null; }
// Don't verify if it can't be verified // Run the verification
if (!vjwk) { return p.then(function () {
next(); return verifyJws(vjwk, req.jws).then(function (verified) {
return; if (true !== verified) { return null; }
}
// Run the verification // Mark as verified
return verifyJws(vjwk, req.jws).then(function (verified) { req.jws.verified = verified;
if (true !== verified) { return; } req.jws.trusted = trusted;
vjwk.useragent = ua;
// Mark as verified // (double check) DO NOT save if there are existing pubs
req.jws.verified = verified; if (0 !== pubs.length) { return null; }
vjwk.useragent = ua;
// (double check) DO NOT save if there are existing pubs DB.pubs.push(vjwk);
if (0 !== pubs.length) { return; } return keystore.set(vjwk.kid + PUBEXT, vjwk);
});
DB.pubs.push(vjwk); });
return keystore.set(vjwk.kid + PUBEXT, vjwk);
}).then(function () { }).then(function () {
next(); next();
}); });
@ -1001,9 +1027,12 @@ function handleApi() {
next(); next();
}); });
app.get('/acme/directory', function (req, res) { app.get('/acme/directory', function (req, res) {
var myBaseUrl = (req.connection.encrypted ? 'https' : 'http') + '://' + req.headers.host;
res.send({ res.send({
'new-nonce': '/acme/new-nonce' 'newNonce': '/acme/new-nonce'
, 'new-account': '/acme/new-acct' , 'newAccount': '/acme/new-acct'
// TODO link to the terms that the user selects
, 'meta': { 'termsOfService': myBaseUrl + '/acme/terms.html' }
}); });
}); });
app.head('/acme/new-nonce', controllers.newNonce); app.head('/acme/new-nonce', controllers.newNonce);

View File

@ -4,6 +4,7 @@
var Vue = window.Vue; var Vue = window.Vue;
var Telebit = window.TELEBIT; var Telebit = window.TELEBIT;
var Keypairs = window.Keypairs; var Keypairs = window.Keypairs;
var ACME = window.ACME;
var api = {}; var api = {};
/* /*
@ -516,25 +517,59 @@ function run(key) {
// TODO protect key with passphrase (or QR code?) // TODO protect key with passphrase (or QR code?)
function getKey() { function getKey() {
var key; var jwk;
try { try {
key = JSON.parse(localStorage.getItem('key')); jwk = JSON.parse(localStorage.getItem('key'));
} catch(e) { } catch(e) {
// ignore // ignore
} }
if (key && key.kid && key.d) { if (jwk && jwk.kid && jwk.d) {
return Promise.resolve(key); return Promise.resolve(jwk);
} }
return Keypairs.generate().then(function (pair) { return Keypairs.generate().then(function (pair) {
key = pair.private; jwk = pair.private;
localStorage.setItem('key', JSON.stringify(key)); localStorage.setItem('key', JSON.stringify(jwk));
return key; return jwk;
});
}
function getEmail() {
return Promise.resolve().then(function () {
var email = localStorage.getItem('email');
if (email) { return email; }
while (!email) {
email = window.prompt("Email address (device owner)?");
}
return email;
});
}
function requestAccount() {
return getKey().then(function (jwk) {
return getEmail().then(function(email) {
// creates new or returns existing
var acme = ACME.create({});
var url = window.location.protocol + '//' + window.location.host + '/acme/directory';
return acme.init(url).then(function () {
return acme.accounts.create({
agreeToTerms: function (tos) { return tos; }
, accountKeypair: { privateKeyJwk: jwk }
, email: email
}).then(function (account) {
console.log('account:');
console.log(account);
if (account.id) {
localStorage.setItem('email', email);
}
return jwk;
});
});
});
}); });
} }
window.api = api; window.api = api;
getKey().then(function (key) { requestAccount().then(function (jwk) {
run(key); run(jwk);
setTimeout(function () { setTimeout(function () {
document.body.hidden = false; document.body.hidden = false;
}, 50); }, 50);

View File

@ -2567,7 +2567,7 @@ ACME.create = function create(me) {
// me.debug = true; // me.debug = true;
me.challengePrefixes = ACME.challengePrefixes; me.challengePrefixes = ACME.challengePrefixes;
me.Keypairs = me.Keypairs || exports.Keypairs || require('keypairs').Keypairs; me.Keypairs = me.Keypairs || exports.Keypairs || require('keypairs').Keypairs;
me.CSR = me.CSR || exports.cSR || require('CSR').CSR; me.CSR = me.CSR || exports.CSR || require('CSR').CSR;
me._nonces = []; me._nonces = [];
me._canUse = {}; me._canUse = {};
if (!me._baseUrl) { if (!me._baseUrl) {