diff --git a/index.html b/index.html index cb57b1e..92a4bbb 100644 --- a/index.html +++ b/index.html @@ -25,10 +25,12 @@
+
+ -
-
+
Verify Wildcard Domains: - +
diff --git a/js/app.js b/js/app.js index c8b5802..18daa54 100644 --- a/js/app.js +++ b/js/app.js @@ -18,13 +18,32 @@ }); } + function submitForm(ev) { + steps[i].submit(ev); + i += 1; + } $qsa('.js-acme-form').forEach(function ($el) { $el.addEventListener('submit', function (ev) { ev.preventDefault(); - steps[i].submit(ev); - i += 1; + submitForm(ev); }); }); + function updateChallengeType() { + var input = this || $qs('.js-acme-challenge-type'); + console.log('ch type radio:', input.value); + $qs('.js-acme-table-wildcard').hidden = true; + $qs('.js-acme-table-http-01').hidden = true; + $qs('.js-acme-table-dns-01').hidden = true; + if (info.challenges.wildcard) { + $qs('.js-acme-table-wildcard').hidden = false; + } + if (info.challenges[input.value]) { + $qs('.js-acme-table-' + input.value).hidden = false; + } + } + $qsa('.js-acme-challenge-type').forEach(function ($el) { + $el.addEventListener('change', updateChallengeType); + }); steps[1] = function () { hideForms(); @@ -85,6 +104,7 @@ console.log('account jwk:'); console.log(jwk); delete jwk.key_ops; + info.jwk = jwk; return BACME.accounts.sign({ jwk: jwk , contacts: [ 'mailto:' + email ] @@ -117,6 +137,7 @@ return p2.then(function (_kid) { kid = _kid; + info.kid = kid; return BACME.orders.sign({ jwk: jwk , identifiers: info.identifiers @@ -124,12 +145,95 @@ }).then(function (signedOrder) { return BACME.orders.create({ signedOrder: signedOrder - }).then(function (/*challengeIndexes*/) { - return BACME.challenges.all().then(function (challenges) { - console.log('challenges:'); - console.log(challenges); - // TODO populate challenges in table - steps[i](); + }).then(function (order) { + info.finalizeUrl = order.finalize; + return BACME.thumbprint({ jwk: jwk }).then(function (thumbprint) { + return BACME.challenges.all().then(function (claims) { + console.log('claims:'); + console.log(claims); + var obj = { 'dns-01': [], 'http-01': [], 'wildcard': [] }; + var map = { + 'http-01': '.js-acme-table-http-01' + , 'dns-01': '.js-acme-table-dns-01' + , 'wildcard': '.js-acme-table-wildcard' + } + var tpls = {}; + info.challenges = obj; + Object.keys(map).forEach(function (k) { + var sel = map[k] + ' tbody'; + console.log(sel); + tpls[k] = $qs(sel).innerHTML; + $qs(map[k] + ' tbody').innerHTML = ''; + }); + + // TODO make Promise-friendly + return Promise.all(claims.map(function (claim) { + var hostname = claim.identifier.value; + return Promise.all(claim.challenges.map(function (c) { + var keyAuth = BACME.challenges['http-01']({ + token: c.token + , thumbprint: thumbprint + , challengeDomain: hostname + }); + return BACME.challenges['dns-01']({ + keyAuth: keyAuth + , challengeDomain: hostname + }).then(function (dnsAuth) { + var data = { + type: c.type + , hostname: hostname + , url: c.url + , token: c.token + , keyAuthorization: keyAuth + , httpPath: keyAuth.path + , httpAuth: keyAuth.value + , dnsType: dnsAuth.type + , dnsHost: dnsAuth.host + , dnsAnswer: dnsAuth.answer + }; + + obj[c.type].push(data); + console.log(''); + console.log('CHALLENGE'); + console.log(claim); + console.log(c); + console.log(data); + console.log(''); + + if (claim.wildcard) { + obj.wildcard.push(data); + $qs(map.wildcard).innerHTML += ''; + } else { + obj[data.type].push(data); + if ('dns-01' === data.type) { + $qs(map[data.type]).innerHTML += ''; + } else if ('http-01' === data.type) { + $qs(map[data.type]).innerHTML += ''; + } else { + throw new Error('Unexpected type: ' + data.type); + } + } + + }); + + })); + })).then(function () { + + // hide wildcard if no wildcard + // hide http-01 and dns-01 if only wildcard + if (!obj.wildcard.length) { + $qs('.js-acme-wildcard').hidden = true; + } + if (!obj['http-01'].length) { + $qs('.js-acme-challenges').hidden = true; + } + + updateChallengeType(); + + steps[i](); + }); + + }); }); }); }); @@ -144,11 +248,72 @@ hideForms(); $qs('.js-acme-form-challenges').hidden = false; }; + steps[3].submit = function () { + var chType = $qs('.js-acme-challenge-type').value; + var ps = []; + // do each wildcard, if any + // do each challenge, by selected type only + [ 'wildcard', chType].forEach(function (typ) { + info.challenges[typ].forEach(function (ch) { + // { jwk, challengeUrl, accountId (kid) } + ps.push(BACME.challenges.accept({ + jwk: info.jwk + , challengeUrl: ch.url + , accountId: info.kid + })); + }); + }); + + return Promise.all(ps).then(function (results) { + console.log('challenge status:', results); + var polls = results.slice(0); + var allsWell = true; + + function checkPolls() { + return new Promise(function (resolve) { + setTimeout(resolve, 1000); + }).then(function () { + return Promise.all(polls.map(function (poll) { + return BACME.challenges.check({ challengePollUrl: poll.url }); + })).then(function () { + polls = polls.filter(function (poll) { + //return 'valid' !== poll.status && 'invalid' !== poll.status; + if ('pending' === poll.status) { + return true; + } + if ('valid' !== poll.status) { + allsWell = false; + console.warn('BAD POLL STATUS', poll); + } + // TODO show status in HTML + }); + + if (polls.length) { + return checkPolls(); + } + return true; + }); + }); + } + + return checkPolls().then(function () { + if (allsWell) { + return submitForm(); + } + }); + }); + }; + + // spinner steps[4] = function () { hideForms(); $qs('.js-acme-form-poll').hidden = false; } + steps[4].submit = function () { + console.log('Congrats! Auto advancing...'); + return BACME.order + }; steps[5] = function () { hideForms(); diff --git a/js/bacme.js b/js/bacme.js index 1cbf4a0..18b31ef 100644 --- a/js/bacme.js +++ b/js/bacme.js @@ -289,11 +289,10 @@ BACME.orders = {}; BACME.orders.sign = function (opts) { var payload64 = BACME._jsto64({ identifiers: opts.identifiers }); - var protected64 = BACME._jsto64( - { nonce: nonce, alg: 'ES256', url: orderUrl, kid: opts.kid } - ); - return BACME._importKey(opts.jwk).then(function (abstractKey) { + var protected64 = BACME._jsto64( + { nonce: nonce, alg: abstractKey.meta.alg/*'ES256'*/, url: orderUrl, kid: opts.kid } + ); console.log('abstractKey:'); console.log(abstractKey); return BACME._sign({ @@ -378,7 +377,16 @@ BACME.challenges.view = function () { BACME._logBody(result); - return { token: challenge.token, url: challenge.url, domain: result.identifier.value, challenges: result.challenges }; + return { + challenges: result.challenges + , expires: result.expires + , identifier: result.identifier + , status: result.status + , wildcard: result.wildcard + //, token: challenge.token + //, url: challenge.url + //, domain: result.identifier.value, + }; }); }); }; @@ -420,12 +428,13 @@ BACME.thumbprint = function (opts) { }); }; -BACME.challenges['http-01'] = function () { +// { token, thumbprint, challengeDomain } +BACME.challenges['http-01'] = function (opts) { // The contents of the key authorization file - keyAuth = token + '.' + thumbprint; + keyAuth = opts.token + '.' + opts.thumbprint; // Where the key authorization file goes - httpPath = 'http://' + challengeDomain + '/.well-known/acme-challenge/' + token; + httpPath = 'http://' + opts.challengeDomain + '/.well-known/acme-challenge/' + opts.token; console.log("echo '" + keyAuth + "' > '" + httpPath + "'"); @@ -435,16 +444,17 @@ BACME.challenges['http-01'] = function () { }; }; -BACME.challenges['dns-01'] = function () { +// { keyAuth } +BACME.challenges['dns-01'] = function (opts) { return window.crypto.subtle.digest( { name: "SHA-256", } - , textEncoder.encode(keyAuth) + , textEncoder.encode(opts.keyAuth) ).then(function(hash){ dnsAuth = btoa(Array.prototype.map.call(new Uint8Array(hash), function (ch) { return String.fromCharCode(ch); }).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); - dnsRecord = '_acme-challenge.' + challengeDomain; + dnsRecord = '_acme-challenge.' + opts.challengeDomain; console.log('DNS TXT Auth:'); // The name of the record @@ -462,55 +472,48 @@ BACME.challenges['dns-01'] = function () { var challengePollUrl; -BACME.challenges.accept = function () { +// { jwk, challengeUrl, accountId (kid) } +BACME.challenges.accept = function (opts) { var payload64 = BACME._jsto64( {} ); - var protected64 = BACME._jsto64( - { nonce: nonce, alg: 'ES256', url: challengeUrl, kid: accountId } - ); - nonce = null; - return window.crypto.subtle.sign( - { name: "ECDSA", hash: { name: "SHA-256" } } - , accountKeypair.privateKey - , textEncoder.encode(protected64 + '.' + payload64) - ).then(function (signature) { - - var sig64 = btoa(Array.prototype.map.call(new Uint8Array(signature), function (ch) { - return String.fromCharCode(ch); - }).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); - - var body = { - protected: protected64 - , payload: payload64 - , signature: sig64 - }; + return BACME._import(opts.jwk).then(function (abstractKey) { + var protected64 = BACME._jsto64( + { nonce: nonce, alg: abstractKey.meta.alg/*'ES256'*/, url: opts.challengeUrl, kid: opts.accountId } + ); + return BACME._sign({ + abstractKey: abstractKey + , payload64: payload64 + , protected64: protected64 + }); + }).then(function (signedAccept) { return window.fetch( - challengeUrl + opts.challengeUrl , { mode: 'cors' , method: 'POST' , headers: { 'Content-Type': 'application/jose+json' } - , body: JSON.stringify(body) + , body: JSON.stringify(signedAccept) } ).then(function (resp) { BACME._logHeaders(resp); nonce = resp.headers.get('replay-nonce'); return resp.json().then(function (reply) { - challengePollUrl = reply.url; + challengePollUrl = reply.url; console.log('Challenge ACK:'); console.log(JSON.stringify(reply)); + return reply; }); }); }); }; -BACME.challenges.check = function () { - return window.fetch(challengePollUrl, { mode: 'cors' }).then(function (resp) { +BACME.challenges.check = function (opts) { + return window.fetch(opts.challengePollUrl, { mode: 'cors' }).then(function (resp) { BACME._logHeaders(resp); nonce = resp.headers.get('replay-nonce'); @@ -558,38 +561,30 @@ BACME.orders.generateCsr = function (keypair, domains) { var certificateUrl; -BACME.orders.finalize = function () { +// { csr, jwk, finalizeUrl, accountId } +BACME.orders.finalize = function (opts) { var payload64 = BACME._jsto64( - { csr: csr } - ); - - var protected64 = BACME._jsto64( - { nonce: nonce, alg: 'ES256', url: finalizeUrl, kid: accountId } + { csr: opts.csr } ); nonce = null; - return window.crypto.subtle.sign( - { name: "ECDSA", hash: { name: "SHA-256" } } - , accountKeypair.privateKey - , textEncoder.encode(protected64 + '.' + payload64) - ).then(function (signature) { - - var sig64 = btoa(Array.prototype.map.call(new Uint8Array(signature), function (ch) { - return String.fromCharCode(ch); - }).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); - - var body = { - protected: protected64 - , payload: payload64 - , signature: sig64 - }; + return BACME._import(opts.jwk).then(function (abstractKey) { + var protected64 = BACME._jsto64( + { nonce: nonce, alg: abstractKey.meta.alg/*'ES256'*/, url: opts.finalizeUrl, kid: opts.accountId } + ); + return BACME._sign({ + abstractKey: abstractKey + , payload64: payload64 + , protected64: protected64 + }); + }).then(function (signedFinal) { return window.fetch( finalizeUrl , { mode: 'cors' , method: 'POST' , headers: { 'Content-Type': 'application/jose+json' } - , body: JSON.stringify(body) + , body: JSON.stringify(signedFinal) } ).then(function (resp) { BACME._logHeaders(resp); @@ -598,6 +593,8 @@ BACME.orders.finalize = function () { return resp.json().then(function (reply) { certificateUrl = reply.certificate; BACME._logBody(reply); + + return reply; }); }); });
Hostname
' + data.hostname + '' + data.dnsHost + '' + data.dnsAnswer + '
' + data.hostname + '' + data.dnsHost + '' + data.dnsAnswer + '
' + data.hostname + '' + data.httpPath + '' + data.httpAuth + '