From 19cc513174f044a7166aac5188f90f6e58e5e072 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 21 May 2019 00:37:07 -0600 Subject: [PATCH] WIP almost working --- app/index.html | 19 +-- app/js/bluecrypt-acme.js | 268 +++++++++++++++++---------------- app/js/greenlock.js | 314 +++++++++++++++++++++------------------ index.html | 14 +- 4 files changed, 308 insertions(+), 307 deletions(-) diff --git a/app/index.html b/app/index.html index 4218741..72771c3 100644 --- a/app/index.html +++ b/app/index.html @@ -33,13 +33,8 @@ - - - - - - - + + @@ -342,14 +337,8 @@
- - - - - - - - + + diff --git a/app/js/bluecrypt-acme.js b/app/js/bluecrypt-acme.js index 8984883..8d58e6e 100644 --- a/app/js/bluecrypt-acme.js +++ b/app/js/bluecrypt-acme.js @@ -2,8 +2,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -; -(function (exports) { +;(function (exports) { var Enc = exports.Enc = {}; @@ -1962,7 +1961,12 @@ ACME._getChallenges = function (me, options, authUrl) { , payload: '' , url: authUrl }).then(function (resp) { - return resp.body; + // Pre-emptive rather than lazy for interfaces that need to show the challenges to the user first + return ACME._challengesToAuth(me, options, resp.body, false).then(function (auths) { + resp.body._rawChallenges = resp.body.challenges; + resp.body.challenges = auths; + return resp.body; + }); }); }; ACME._wait = function wait(ms) { @@ -1987,12 +1991,6 @@ ACME._testChallengeOptions = function () { "token": "test-" + chToken + "-1", "_wildcard": true } - , { - "type": "tls-sni-01", - "status": "pending", - "url": "https://acme-staging-v02.example.com/2", - "token": "test-" + chToken + "-2" - } , { "type": "tls-alpn-01", "status": "pending", @@ -2010,47 +2008,49 @@ ACME._testChallenges = function (me, options) { challenges = challenges.filter(function (ch) { return ch._wildcard; }); } - var challenge = ACME._chooseChallenge(options, { challenges: challenges }); - if (!challenge) { - // For example, wildcards require dns-01 and, if we don't have that, we have to bail - var enabled = Object.keys(options.challenges).join(', ') || 'none'; - var suitable = challenges.map(function (r) { return r.type; }).join(', ') || 'none'; - return Promise.reject(new Error( - "None of the challenge types that you've enabled ( " + enabled + " )" - + " are suitable for validating the domain you've selected (" + identifierValue + ")." - + " You must enable one of ( " + suitable + " )." - )); - } - - // TODO remove skipChallengeTest - if (me.skipDryRun || me.skipChallengeTest) { - return null; - } - - if ('dns-01' === challenge.type) { - // Give the nameservers a moment to propagate - CHECK_DELAY = 1.5 * 1000; - } - - return Promise.resolve().then(function () { - var results = { + // The dry-run comes first in the spirit of "fail fast" + // (and protecting against challenge failure rate limits) + var dryrun = true; + var resp = { + body: { identifier: { type: "dns" , value: identifierValue.replace(/^\*\./, '') } - , challenges: [ challenge ] + , challenges: challenges , expires: new Date(Date.now() + (60 * 1000)).toISOString() , wildcard: identifierValue.includes('*.') || undefined - }; + } + }; + return ACME._challengesToAuth(me, options, resp.body, dryrun).then(function (auths) { + resp.body._rawChallenges = resp.body.challenges; + resp.body.challenges = auths; - // The dry-run comes first in the spirit of "fail fast" - // (and protecting against challenge failure rate limits) - var dryrun = true; - return ACME._challengeToAuth(me, options, results, challenge, dryrun).then(function (auth) { - if (!me._canUse[auth.type]) { return; } - return ACME._setChallenge(me, options, auth).then(function () { - return auth; - }); + var auth = ACME._chooseAuth(options, resp.body.challenges); + if (!auth) { + // For example, wildcards require dns-01 and, if we don't have that, we have to bail + var enabled = Object.keys(options.challenges).join(', ') || 'none'; + var suitable = resp.body.challenges.map(function (r) { return r.type; }).join(', ') || 'none'; + return Promise.reject(new Error( + "None of the challenge types that you've enabled ( " + enabled + " )" + + " are suitable for validating the domain you've selected (" + identifierValue + ")." + + " You must enable one of ( " + suitable + " )." + )); + } + + // TODO remove skipChallengeTest + if (me.skipDryRun || me.skipChallengeTest) { + return null; + } + + if ('dns-01' === auth.type) { + // Give the nameservers a moment to propagate + CHECK_DELAY = 1.5 * 1000; + } + + if (!me._canUse[auth.type]) { return; } + return ACME._setChallenge(me, options, auth).then(function () { + return auth; }); }); })).then(function (auths) { @@ -2067,93 +2067,88 @@ ACME._testChallenges = function (me, options) { }); }); }; -ACME._chooseChallenge = function(options, results) { +ACME._chooseAuth = function(options, auths) { // For each of the challenge types that we support - var challenge; + var auth; var challengeTypes = Object.keys(options.challenges); // ordered from most to least preferred - challengeTypes = [ 'tls-alpn-01', 'http-01', 'dns-01' ].filter(function (chType) { + challengeTypes = (options.challengePriority||[ 'tls-alpn-01', 'http-01', 'dns-01' ]).filter(function (chType) { return challengeTypes.includes(chType); }); - /* - // Lot's of error checking to inform the user of mistakes - if (!(options.challengeTypes||[]).length) { - options.challengeTypes = Object.keys(options.challenges||{}); - } - if (!options.challengeTypes.length) { - options.challengeTypes = [ options.challengeType ].filter(Boolean); - } - if (options.challengeType) { - options.challengeTypes.sort(function (a, b) { - if (a === options.challengeType) { return -1; } - if (b === options.challengeType) { return 1; } - return 0; - }); - if (options.challengeType !== options.challengeTypes[0]) { - return Promise.reject(new Error("options.challengeType is '" + options.challengeType + "'," - + " which does not exist in the supplied types '" + options.challengeTypes.join(',') + "'")); - } - } - // TODO check that all challengeTypes are represented in challenges - if (!options.challengeTypes.length) { - return Promise.reject(new Error("options.challengeTypes (string array) must be specified" - + " (and in order of preferential priority).")); - } - */ challengeTypes.some(function (chType) { // And for each of the challenge types that are allowed - return results.challenges.some(function (ch) { + return auths.some(function (ch) { // Check to see if there are any matches if (ch.type === chType) { - challenge = ch; + auth = ch; return true; } }); }); - return challenge; + return auth; }; -ACME._challengeToAuth = function (me, options, request, challenge, dryrun) { +ACME._challengesToAuth = function (me, options, request, dryrun) { // we don't poison the dns cache with our dummy request var dnsPrefix = ACME.challengePrefixes['dns-01']; if (dryrun) { dnsPrefix = dnsPrefix.replace('acme-challenge', 'greenlock-dryrun-' + ACME._prnd(4)); } + var challengeTypes = Object.keys(options.challenges); - var auth = {}; - - // straight copy from the new order response - // { identifier, status, expires, challenges, wildcard } - Object.keys(request).forEach(function (key) { - auth[key] = request[key]; - }); - - // copy from the challenge we've chosen - // { type, status, url, token } - // (note the duplicate status overwrites the one above, but they should be the same) - Object.keys(challenge).forEach(function (key) { - // don't confused devs with the id url - auth[key] = challenge[key]; - }); - - // batteries-included helpers - auth.hostname = auth.identifier.value; - // because I'm not 100% clear if the wildcard identifier does or doesn't have the leading *. in all cases - auth.altname = ACME._untame(auth.identifier.value, auth.wildcard); return ACME._importKeypair(me, options.accountKeypair).then(function (pair) { return me.Keypairs.thumbprint({ jwk: pair.public }).then(function (thumb) { - auth.thumbprint = thumb; - // keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey)) - auth.keyAuthorization = challenge.token + '.' + auth.thumbprint; - // conflicts with ACME challenge id url is already in use, so we call this challengeUrl instead - // TODO auth.http01Url ? - auth.challengeUrl = 'http://' + auth.identifier.value + ACME.challengePrefixes['http-01'] + '/' + auth.token; - auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', ''); + return Promise.all(request.challenges.map(function (challenge) { + // Don't do extra work for challenges that we can't satisfy + if (!challengeTypes.includes(challenge.type)) { + return null; + } - return Crypto._sha('sha256', auth.keyAuthorization).then(function (hash) { - auth.dnsAuthorization = hash; - return auth; + var auth = {}; + + // straight copy from the new order response + // { identifier, status, expires, challenges, wildcard } + Object.keys(request).forEach(function (key) { + auth[key] = request[key]; + }); + + // copy from the challenge we've chosen + // { type, status, url, token } + // (note the duplicate status overwrites the one above, but they should be the same) + Object.keys(challenge).forEach(function (key) { + // don't confused devs with the id url + auth[key] = challenge[key]; + }); + + // batteries-included helpers + auth.hostname = auth.identifier.value; + // because I'm not 100% clear if the wildcard identifier does or doesn't have the leading *. in all cases + auth.altname = ACME._untame(auth.identifier.value, auth.wildcard); + + auth.thumbprint = thumb; + // keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey)) + auth.keyAuthorization = challenge.token + '.' + auth.thumbprint; + + if ('http-01' === auth.type) { + // conflicts with ACME challenge id url is already in use, so we call this challengeUrl instead + // TODO auth.http01Url ? + auth.challengeUrl = 'http://' + auth.identifier.value + ACME.challengePrefixes['http-01'] + '/' + auth.token; + return auth; + } + + if ('dns-01' !== auth.type) { + return auth; + } + + return Crypto._sha('sha256', auth.keyAuthorization).then(function (hash) { + auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', ''); + auth.dnsAuthorization = hash; + auth.keyAuthorizationDigest = hash; + return auth; + }); + })).then(function (auths) { + return auths.filter(Boolean); }); }); }); @@ -2241,18 +2236,21 @@ ACME._postChallenge = function (me, options, auth) { return resp.body; } - var errmsg; - if (!resp.body.status) { - errmsg = "[acme-v2] (E_STATE_EMPTY) empty challenge state for '" + altname + "':"; - } - else if ('invalid' === resp.body.status) { - errmsg = "[acme-v2] (E_STATE_INVALID) challenge state for '" + altname + "': '" + resp.body.status + "'"; - } - else { - errmsg = "[acme-v2] (E_STATE_UKN) challenge state for '" + altname + "': '" + resp.body.status + "'"; + var err; + if (resp.body.error && resp.body.error.detail) { + err = new Error("[acme-v2] " + auth.altname + " state:" + resp.body.status + " " + resp.body.error.detail); + err.auth = auth; + err.altname = auth.altname; + err.type = auth.type; + err.urn = resp.body.error.type; + err.code = ('invalid' === resp.body.status) ? 'E_CHALLENGE_INVALID' : 'E_CHALLENGE_UNKNOWN'; + err.uri = resp.body.url; + } else { + err = new Error("[acme-v2] " + auth.altname + " (E_STATE_UKN): " + JSON.stringify(resp.body, null, 2)); + err.code = 'E_CHALLENGE_UNKNOWN'; } - return Promise.reject(new Error(errmsg)); + return Promise.reject(err); }); } @@ -2312,34 +2310,32 @@ ACME._setChallenge = function (me, options, auth) { ACME._setChallengesAll = function (me, options) { var order = options.order; var setAuths = order.authorizations.slice(0); - var challenges = order.challenges; + var claims = order.claims.slice(0); var validAuths = []; var auths = []; function setNext() { var authUrl = setAuths.shift(); - var results = challenges.shift(); + var claim = claims.shift(); if (!authUrl) { return; } - // var domain = options.domains[i]; // results.identifier.value + // var domain = options.domains[i]; // claim.identifier.value // If it's already valid, we're golden it regardless - if (results.challenges.some(function (ch) { return 'valid' === ch.status; })) { + if (claim.challenges.some(function (ch) { return 'valid' === ch.status; })) { return setNext(); } - var challenge = ACME._chooseChallenge(options, results); - if (!challenge) { + var auth = ACME._chooseAuth(options, claim.challenges); + if (!auth) { // For example, wildcards require dns-01 and, if we don't have that, we have to bail return Promise.reject(new Error( "Server didn't offer any challenge we can handle for '" + options.domains.join() + "'." )); } - return ACME._challengeToAuth(me, options, results, challenge, false).then(function (auth) { - auths.push(auth); - return ACME._setChallenge(me, options, auth).then(setNext); - }); + auths.push(auth); + return ACME._setChallenge(me, options, auth).then(setNext); } function checkNext() { @@ -2358,6 +2354,7 @@ ACME._setChallengesAll = function (me, options) { }).then(checkNext); } + // Actually sets the challenge via ACME function challengeNext() { var auth = validAuths.shift(); if (!auth) { return; } @@ -2519,7 +2516,7 @@ ACME._createOrder = function (me, options) { if (me.debug) { console.debug('[ordered]', location); } // the account id url if (me.debug) { console.debug(resp); } - if (!options.authorizations) { + if (!order.authorizations) { return Promise.reject(new Error( "[acme-v2.js] authorizations were not fetched for '" + options.domains.join() + "':\n" + JSON.stringify(resp.body) @@ -2528,25 +2525,24 @@ ACME._createOrder = function (me, options) { return order; }).then(function (order) { - var challenges = []; + var claims = []; if (me.debug) { console.debug("[acme-v2] POST newOrder has authorizations"); } var challengeAuths = order.authorizations.slice(0); function getNext() { var authUrl = challengeAuths.shift(); - if (!authUrl) { return challenges; } + if (!authUrl) { return claims; } - return ACME._getChallenges(me, options, authUrl).then(function (results) { - // var domain = options.domains[i]; // results.identifier.value - challenges.push(results); + return ACME._getChallenges(me, options, authUrl).then(function (claim) { + // var domain = options.domains[i]; // claim.identifier.value + claims.push(claim); return getNext(); }); } return getNext().then(function () { - order.challenges = challenges; + order.claims = claims; options.order = order; - console.log('DEBUG 2 order (too much info for challenges?):', order); return order; }); }); @@ -2684,10 +2680,12 @@ ACME.create = function create(me) { } }; me.orders = { - create: function (options) { + // create + get challlenges + request: function (options) { return ACME._createOrder(me, options); } - , finalize: function (options) { + // set challenges, check challenges, finalize order, return order + , complete: function (options) { return ACME._finalizeOrder(me, options); } }; diff --git a/app/js/greenlock.js b/app/js/greenlock.js index a80ab03..944b1cd 100644 --- a/app/js/greenlock.js +++ b/app/js/greenlock.js @@ -2,6 +2,7 @@ 'use strict'; /*global URLSearchParams,Headers*/ + var PromiseA = window.Promise; var VERSION = '2'; // ACME recommends ECDSA P-256, but RSA 2048 is still required by some old servers (like what replicated.io uses ) // ECDSA P-384, P-521, and RSA 3072, 4096 are NOT recommend standards (and not properly supported) @@ -15,11 +16,32 @@ var $qs = function (s) { return window.document.querySelector(s); }; var $qsa = function (s) { return window.document.querySelectorAll(s); }; var acme; - var accountStuff; var info = {}; var steps = {}; var i = 1; var apiUrl = 'https://acme-{{env}}.api.letsencrypt.org/directory'; + var challenges = { + 'http-01': { + set: function (auth) { + console.log('Chose http-01 for', auth.altname, auth); + return Promise.resolve(); + } + , remove: function (auth) { + console.log('Can remove http-01 for', auth.altname, auth); + return Promise.resolve(); + } + } + , 'dns-01': { + set: function (auth) { + console.log('Chose dns-01 for', auth.altname, auth); + return Promise.resolve(); + } + , remove: function (auth) { + console.log('Can remove dns-01 for', auth.altname, auth); + return Promise.resolve(); + } + } + }; function updateApiType() { console.log("type updated"); @@ -59,8 +81,29 @@ i += 1; return PromiseA.resolve(steps[j].submit(ev)).catch(function (err) { + var ourfault = true; console.error(err); - window.alert("Something went wrong. It's our fault not yours. Please email aj@rootprojects.org and let him know that 'step " + j + "' failed."); + console.error(Object.keys(err)); + if ('E_CHALLENGE_INVALID' === err.code) { + if ('dns-01' === err.type) { + ourfault = false; + window.alert("It looks like the DNS record you set for " + + err.altname + " was incorrect or did not propagate. " + + "The error message was '" + err.message + "'"); + } else if ('http-01' === err.type) { + ourfault = false; + window.alert("It looks like the file you uploaded for " + + err.altname + " was incorrect or could not be downloaded. " + + "The error message was '" + err.message + "'"); + } + } + + if (ourfault) { + err.auth = undefined; + window.alert("Something went wrong. It's probably our fault, not yours." + + " Please email aj@rootprojects.org to let him know. The error message is: \n" + + JSON.stringify(err, null, 2)); + } }); } @@ -178,7 +221,8 @@ $qs('.js-acme-form-domains').hidden = false; }; steps[1].submit = function () { - info.identifiers = $qs('.js-acme-domains').value.split(/\s*,\s*/g).map(function (hostname) { + info.domains = $qs('.js-acme-domains').value.replace(/https?:\/\//g, ' ').replace(/,/g, ' ').trim().split(/\s+/g); + info.identifiers = info.domains.map(function (hostname) { return { type: 'dns', value: hostname.toLowerCase().trim() }; }).slice(0,1); //Disable multiple values for now. We'll just take the first and work with it. info.identifiers.sort(function (a, b) { @@ -204,12 +248,14 @@ steps[2].submit = function () { var email = $qs('.js-acme-account-email').value.toLowerCase().trim(); + info.email = email; info.contact = [ 'mailto:' + email ]; info.agree = $qs('.js-acme-account-tos').checked; //info.greenlockAgree = $qs('.js-gl-tos').checked; + info.domains = info.identifiers.map(function (ident) { return ident.value; }); // TODO ping with version and account creation - setTimeout(saveContact, 100, email, info.identifiers.map(function (ident) { return ident.value; })); + setTimeout(saveContact, 100, email, info.domains); function checkTos(tos) { if (info.agree) { @@ -227,10 +273,10 @@ , accountKeypair: { privateKeyJwk: jwk } }).then(function (account) { console.log("account created result:", account); - accountStuff.account = account; - accountStuff.privateJwk = jwk; - accountStuff.email = email; - accountStuff.acme = acme; // TODO XXX remove + info.account = account; + info.privateJwk = jwk; + info.email = email; + info.acme = acme; // TODO XXX remove }).catch(function (err) { console.error("A bad thing happened:"); console.error(err); @@ -241,116 +287,104 @@ }); }); }).then(function () { - var jwk = accountStuff.privateJwk; - var account = accountStuff.account; + var jwk = info.privateJwk; + var account = info.account; - return acme.orders.create({ + return acme.orders.request({ account: account , accountKeypair: { privateKeyJwk: jwk } - , identifiers: info.identifiers + , domains: info.domains + , challenges: challenges }).then(function (order) { - return acme.orders.create({ - signedOrder: signedOrder - }).then(function (order) { - accountStuff.order = order; - var claims = order.challenges; - console.log('claims:'); - console.log(claims); + info.order = order; - var obj = { 'dns-01': [], 'http-01': [], 'wildcard': [] }; - info.challenges = obj; - var map = { - 'http-01': '.js-acme-verification-http-01' - , 'dns-01': '.js-acme-verification-dns-01' - , 'wildcard': '.js-acme-verification-wildcard' - }; - options.challengePriority = [ 'http-01', 'dns-01' ]; + var claims = order.claims; + console.log('claims:'); + console.log(claims); - // TODO make Promise-friendly - return PromiseA.all(claims.map(function (claim) { - var hostname = claim.identifier.value; - return PromiseA.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.value - , 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 - }; + var obj = { 'dns-01': [], 'http-01': [], 'wildcard': [] }; + info.challenges = obj; + /* + var map = { + 'http-01': '.js-acme-verification-http-01' + , 'dns-01': '.js-acme-verification-dns-01' + , 'wildcard': '.js-acme-verification-wildcard' + }; + */ - console.log(''); - console.log('CHALLENGE'); - console.log(claim); - console.log(c); - console.log(data); - console.log(''); + claims.forEach(function (claim) { + console.log("Challenge (claim):"); + console.log(claim); + var hostname = claim.identifier.value; + claim.challenges.forEach(function (c) { + var auth = c; + var data = { + type: c.type + , hostname: hostname + , url: c.url + , token: c.token + , httpPath: auth.challengeUrl + , httpAuth: auth.keyAuthorization + , dnsType: 'TXT' + , dnsHost: auth.dnsHost + , dnsAnswer: auth.keyAuthorizationDigest + }; - if (claim.wildcard) { - obj.wildcard.push(data); - let verification = $qs(".js-acme-verification-wildcard"); - verification.querySelector(".js-acme-ver-hostname").innerHTML = data.hostname; - verification.querySelector(".js-acme-ver-txt-host").innerHTML = data.dnsHost; - verification.querySelector(".js-acme-ver-txt-value").innerHTML = data.dnsAnswer; + console.log(''); + console.log('CHALLENGE'); + console.log(claim); + console.log(c); + console.log(data); + console.log(''); - } else if(obj[data.type]) { + var verification; + if (claim.wildcard) { + obj.wildcard.push(data); + verification = $qs(".js-acme-verification-wildcard"); + verification.querySelector(".js-acme-ver-hostname").innerHTML = data.hostname; + verification.querySelector(".js-acme-ver-txt-host").innerHTML = data.dnsHost; + verification.querySelector(".js-acme-ver-txt-value").innerHTML = data.dnsAnswer; - obj[data.type].push(data); + } else if(obj[data.type]) { - if ('dns-01' === data.type) { - let verification = $qs(".js-acme-verification-dns-01"); - verification.querySelector(".js-acme-ver-hostname").innerHTML = data.hostname; - verification.querySelector(".js-acme-ver-txt-host").innerHTML = data.dnsHost; - verification.querySelector(".js-acme-ver-txt-value").innerHTML = data.dnsAnswer; - } else if ('http-01' === data.type) { - $qs(".js-acme-ver-file-location").innerHTML = data.httpPath.split("/").slice(-1); - $qs(".js-acme-ver-content").innerHTML = data.httpAuth; - $qs(".js-acme-ver-uri").innerHTML = data.httpPath; - $qs(".js-download-verify-link").href = - "data:text/octet-stream;base64," + window.btoa(data.httpAuth); - $qs(".js-download-verify-link").download = data.httpPath.split("/").slice(-1); - } - } + obj[data.type].push(data); - }); - - })); - })).then(function () { - - // hide wildcard if no wildcard - // hide http-01 and dns-01 if only wildcard - if (!obj.wildcard.length) { - $qs('.js-acme-wildcard-challenges').hidden = true; + if ('dns-01' === data.type) { + verification = $qs(".js-acme-verification-dns-01"); + verification.querySelector(".js-acme-ver-hostname").innerHTML = data.hostname; + verification.querySelector(".js-acme-ver-txt-host").innerHTML = data.dnsHost; + verification.querySelector(".js-acme-ver-txt-value").innerHTML = data.dnsAnswer; + } else if ('http-01' === data.type) { + $qs(".js-acme-ver-file-location").innerHTML = data.httpPath.split("/").slice(-1); + $qs(".js-acme-ver-content").innerHTML = data.httpAuth; + $qs(".js-acme-ver-uri").innerHTML = data.httpPath; + $qs(".js-download-verify-link").href = + "data:text/octet-stream;base64," + window.btoa(data.httpAuth); + $qs(".js-download-verify-link").download = data.httpPath.split("/").slice(-1); + } } - if (!obj['http-01'].length) { - $qs('.js-acme-challenges').hidden = true; - } - - updateChallengeType(); - - console.log("MAGIC STEP NUMBER in 2 is:", i); - steps[i](); }); - }); + + // hide wildcard if no wildcard + // hide http-01 and dns-01 if only wildcard + if (!obj.wildcard.length) { + $qs('.js-acme-wildcard-challenges').hidden = true; + } + if (!obj['http-01'].length) { + $qs('.js-acme-challenges').hidden = true; + } + + updateChallengeType(); + + console.log("MAGIC STEP NUMBER in 2 is:", i); + steps[i](); }); }).catch(function (err) { console.error('Step \'\' Error:'); console.error(err, err.stack); - window.alert("An error happened at Step " + i + ", but it's not your fault. Email aj@rootprojects.org and let him know."); + window.alert("An error happened (but it's not your fault)." + + " Email aj@rootprojects.org to let him know that 'order and get challenges' failed."); }); }; @@ -360,58 +394,44 @@ $qs('.js-acme-form-challenges').hidden = false; }; steps[3].submit = function () { - options.challengeTypes = [ 'dns-01' ]; + var challengePriority = [ 'dns-01' ]; if ('http-01' === $qs('.js-acme-challenge-type:checked').value) { - options.challengeTypes.unshift('http-01'); + challengePriority.unshift('http-01'); } - console.log('primary challenge type is:', options.challengeTypes[0]); + console.log('primary challenge type is:', challengePriority[0]); - return getAccountKeypair(email).then(function (jwk) { + steps[i](); + return getAccountKeypair(info.email).then(function (jwk) { // for now just show the next page immediately (its a spinner) // TODO put a test challenge in the list // TODO warn about wait-time if DNS - steps[i](); - return getServerKeypair().then(function () { - return acme.orders.finalize({ - account: accountStuff.account + return getServerKeypair().then(function (serverJwk) { + return acme.orders.complete({ + account: info.account , accountKeypair: { privateKeyJwk: jwk } - , order: accountStuff.order - , domainKeypair: 'TODO' - }); - }).then(function (certs) { - console.log('WINNING!'); - console.log(certs); - $qs('#js-fullchain').innerHTML = certs; - $qs("#js-download-fullchain-link").href = - "data:text/octet-stream;base64," + window.btoa(certs); + , order: info.order + , domains: info.domains + , domainKeypair: { privateKeyJwk: serverJwk } + , challengePriority: challengePriority + , challenges: challenges + }).then(function (certs) { + return Keypairs.export({ jwk: serverJwk }).then(function (keyPem) { + console.log('WINNING!'); + console.log(certs); + $qs('#js-fullchain').innerHTML = [ + certs.cert.trim() + "\n" + , certs.chain + "\n" + ].join("\n"); + $qs("#js-download-fullchain-link").href = + "data:text/octet-stream;base64," + window.btoa(certs); - var wcOpts; - var pemName; - if (/^R/.test(info.serverJwk.kty)) { - pemName = 'RSA'; - wcOpts = { name: "RSASSA-PKCS1-v1_5", hash: { name: "SHA-256" } }; - } else { - pemName = 'EC'; - wcOpts = { name: "ECDSA", namedCurve: "P-256" }; - } - return crypto.subtle.importKey( - "jwk" - , info.serverJwk - , wcOpts - , true - , ["sign"] - ).then(function (privateKey) { - return window.crypto.subtle.exportKey("pkcs8", privateKey); - }).then (function (keydata) { - var pem = spkiToPEM(keydata, pemName); - $qs('#js-privkey').innerHTML = pem; - $qs("#js-download-privkey-link").href = - "data:text/octet-stream;base64," + window.btoa(pem); - steps[i](); + $qs('#js-privkey').innerHTML = keyPem; + $qs("#js-download-privkey-link").href = + "data:text/octet-stream;base64," + window.btoa(keyPem); + submitForm(); + }); }); }); - }).then(function () { - return submitForm(); }); }; @@ -424,11 +444,7 @@ steps[4].submit = function () { console.log('Congrats! Auto advancing...'); - - }).catch(function (err) { - console.error(err.toString()); - window.alert("An error happened in the final step, but it's not your fault. Email aj@rootprojects.org and let him know."); - }); + window.alert("An error happened in the final step, but it's not your fault. Email aj@rootprojects.org and let him know."); }; steps[5] = function () { @@ -436,6 +452,8 @@ hideForms(); $qs('.js-acme-form-download').hidden = false; }; + + // The kickoff steps[1](); var params = new URLSearchParams(window.location.search); @@ -481,8 +499,8 @@ return true; } - return testRsaSupport().then(function () { - console.info('[crypto] RSA is supported'); + return testEcdsaSupport().then(function () { + console.info('[crypto] ECDSA is supported'); }).catch(function (err) { console.error('[crypto] could not use either RSA nor EC.'); console.error(err); diff --git a/index.html b/index.html index 18b177e..6874cc6 100644 --- a/index.html +++ b/index.html @@ -23,15 +23,10 @@ - - - - - - - - + + +