diff --git a/bin/telebitd.js b/bin/telebitd.js index 145d9e0..e98f403 100755 --- a/bin/telebitd.js +++ b/bin/telebitd.js @@ -10,6 +10,7 @@ try { } var pkg = require('../package.json'); +var Keypairs = require('keypairs'); var crypto = require('crypto'); //var url = require('url'); @@ -422,7 +423,7 @@ controllers.newAccount = function (req, res) { // TODO clean up error messages to be similar to ACME // 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.send({ error: { message: "jws body was not present or could not be validated" } }); return; @@ -448,7 +449,7 @@ controllers.newAccount = function (req, res) { return verifyJws(req.jws.header.jwk, req.jws).then(function (verified) { if (!verified) { 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; } @@ -483,7 +484,7 @@ controllers.newAccount = function (req, res) { } res.statusCode = 201; account = {}; - account._id = crypto.randomBytes(16).toString('base64'); + account._id = toUrlSafe(crypto.randomBytes(16).toString('base64')); // TODO be better about this account.location = myBaseUrl + '/acme/accounts/' + account._id; account.thumb = thumb; @@ -620,12 +621,38 @@ function jwsEggspress(req, res, next) { } var ua = req.headers['user-agent']; + var trusted = false; var vjwk; 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 DB.pubs.some(function (jwk) { - if (jwk.kid === req.jws.header.kid) { + if (jwk.kid === kid) { + trusted = true; vjwk = jwk; + return true; } }); @@ -645,33 +672,32 @@ function jwsEggspress(req, res, next) { }); } - // Check if there aren't any keys that we trust - // and this has signed itself, then make it a key we trust - // (TODO: move this all to the new account function) - if ((0 === pubs.length && req.jws.header.jwk)) { - vjwk = req.jws.header.jwk; - if (!vjwk.kid) { throw Error("Impossible: no key id"); } - } + p.then(function () { + // Check if there aren't any keys that we trust + // and this has signed itself, then make it a key we trust + // (TODO: move this all to the new account function) + if (0 === pubs.length) { trusted = true; } + 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 - if (!vjwk) { - next(); - return; - } + // Run the verification + return p.then(function () { + return verifyJws(vjwk, req.jws).then(function (verified) { + if (true !== verified) { return null; } - // Run the verification - return verifyJws(vjwk, req.jws).then(function (verified) { - if (true !== verified) { return; } + // Mark as verified + req.jws.verified = verified; + req.jws.trusted = trusted; + vjwk.useragent = ua; - // Mark as verified - req.jws.verified = verified; - vjwk.useragent = ua; + // (double check) DO NOT save if there are existing pubs + if (0 !== pubs.length) { return null; } - // (double check) DO NOT save if there are existing pubs - if (0 !== pubs.length) { return; } - - DB.pubs.push(vjwk); - return keystore.set(vjwk.kid + PUBEXT, vjwk); + DB.pubs.push(vjwk); + return keystore.set(vjwk.kid + PUBEXT, vjwk); + }); + }); }).then(function () { next(); }); @@ -1001,9 +1027,12 @@ function handleApi() { next(); }); app.get('/acme/directory', function (req, res) { + var myBaseUrl = (req.connection.encrypted ? 'https' : 'http') + '://' + req.headers.host; res.send({ - 'new-nonce': '/acme/new-nonce' - , 'new-account': '/acme/new-acct' + 'newNonce': '/acme/new-nonce' + , '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); diff --git a/lib/admin/js/app.js b/lib/admin/js/app.js index 9d71672..1a06d3e 100644 --- a/lib/admin/js/app.js +++ b/lib/admin/js/app.js @@ -4,6 +4,7 @@ var Vue = window.Vue; var Telebit = window.TELEBIT; var Keypairs = window.Keypairs; +var ACME = window.ACME; var api = {}; /* @@ -516,25 +517,59 @@ function run(key) { // TODO protect key with passphrase (or QR code?) function getKey() { - var key; + var jwk; try { - key = JSON.parse(localStorage.getItem('key')); + jwk = JSON.parse(localStorage.getItem('key')); } catch(e) { // ignore } - if (key && key.kid && key.d) { - return Promise.resolve(key); + if (jwk && jwk.kid && jwk.d) { + return Promise.resolve(jwk); } return Keypairs.generate().then(function (pair) { - key = pair.private; - localStorage.setItem('key', JSON.stringify(key)); - return key; + jwk = pair.private; + localStorage.setItem('key', JSON.stringify(jwk)); + 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; -getKey().then(function (key) { - run(key); +requestAccount().then(function (jwk) { + run(jwk); setTimeout(function () { document.body.hidden = false; }, 50); diff --git a/lib/admin/js/bluecrypt-acme.js b/lib/admin/js/bluecrypt-acme.js index 2b20d66..e4a5996 100644 --- a/lib/admin/js/bluecrypt-acme.js +++ b/lib/admin/js/bluecrypt-acme.js @@ -2567,7 +2567,7 @@ ACME.create = function create(me) { // me.debug = true; me.challengePrefixes = ACME.challengePrefixes; 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._canUse = {}; if (!me._baseUrl) {