Browse Source

WIP almost working

master
AJ ONeal 5 years ago
parent
commit
2ca4c984f8
  1. 19
      app/index.html
  2. 268
      app/js/bluecrypt-acme.js
  3. 330
      app/js/greenlock.js
  4. 14
      index.html

19
app/index.html

@ -33,13 +33,8 @@
<link rel="preload" href="./fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7l.woff2" as="font" crossorigin="anonymous"> <link rel="preload" href="./fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7l.woff2" as="font" crossorigin="anonymous">
<link rel="preload" href="./fonts/HI_SiYsKILxRpg3hIP6sJ7fM7PqlPevW.woff2" as="font" crossorigin="anonymous"> <link rel="preload" href="./fonts/HI_SiYsKILxRpg3hIP6sJ7fM7PqlPevW.woff2" as="font" crossorigin="anonymous">
<link rel="preload" href="./js/bacme.js" as="script"> <link rel="preload" href="./js/bluecrypt-acme.js" as="script">
<link rel="preload" href="./js/app.js" as="script"> <link rel="preload" href="./js/greenlock.js" as="script">
<link rel="preload" href="./js/pkijs.org/v1.3.33/common.js" as="script">
<link rel="preload" href="./js/pkijs.org/v1.3.33/asn1.js" as="script">
<link rel="preload" href="./js/pkijs.org/v1.3.33/x509_schema.js" as="script">
<link rel="preload" href="./js/pkijs.org/v1.3.33/x509_simpl.js" as="script">
<link rel="preload" href="./js/browser-csr/v1.0.0-alpha/csr.js" as="script">
</head> </head>
<body hidden> <body hidden>
@ -342,14 +337,8 @@
<br> <br>
<script src="./js/bacme.js"></script> <script src="./js/bluecrypt-acme.js"></script>
<script src="./js/app.js"></script> <script src="./js/greenlock.js"></script>
<script src="./js/pkijs.org/v1.3.33/common.js"></script>
<script src="./js/pkijs.org/v1.3.33/asn1.js"></script>
<script src="./js/pkijs.org/v1.3.33/x509_schema.js"></script>
<script src="./js/pkijs.org/v1.3.33/x509_simpl.js"></script>
<script src="./js/browser-csr/v1.0.0-alpha/csr.js"></script>
<!-- Global site tag (gtag.js) - Google Analytics --> <!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-118745161-2"></script> <script async src="https://www.googletagmanager.com/gtag/js?id=UA-118745161-2"></script>

268
app/js/bluecrypt-acme.js

@ -2,8 +2,7 @@
/* This Source Code Form is subject to the terms of the Mozilla Public /* 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 * 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/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
; ;(function (exports) {
(function (exports) {
var Enc = exports.Enc = {}; var Enc = exports.Enc = {};
@ -1962,7 +1961,12 @@ ACME._getChallenges = function (me, options, authUrl) {
, payload: '' , payload: ''
, url: authUrl , url: authUrl
}).then(function (resp) { }).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) { ACME._wait = function wait(ms) {
@ -1987,12 +1991,6 @@ ACME._testChallengeOptions = function () {
"token": "test-" + chToken + "-1", "token": "test-" + chToken + "-1",
"_wildcard": true "_wildcard": true
} }
, {
"type": "tls-sni-01",
"status": "pending",
"url": "https://acme-staging-v02.example.com/2",
"token": "test-" + chToken + "-2"
}
, { , {
"type": "tls-alpn-01", "type": "tls-alpn-01",
"status": "pending", "status": "pending",
@ -2010,47 +2008,49 @@ ACME._testChallenges = function (me, options) {
challenges = challenges.filter(function (ch) { return ch._wildcard; }); challenges = challenges.filter(function (ch) { return ch._wildcard; });
} }
var challenge = ACME._chooseChallenge(options, { challenges: challenges }); // The dry-run comes first in the spirit of "fail fast"
if (!challenge) { // (and protecting against challenge failure rate limits)
// For example, wildcards require dns-01 and, if we don't have that, we have to bail var dryrun = true;
var enabled = Object.keys(options.challenges).join(', ') || 'none'; var resp = {
var suitable = challenges.map(function (r) { return r.type; }).join(', ') || 'none'; body: {
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 = {
identifier: { identifier: {
type: "dns" type: "dns"
, value: identifierValue.replace(/^\*\./, '') , value: identifierValue.replace(/^\*\./, '')
} }
, challenges: [ challenge ] , challenges: challenges
, expires: new Date(Date.now() + (60 * 1000)).toISOString() , expires: new Date(Date.now() + (60 * 1000)).toISOString()
, wildcard: identifierValue.includes('*.') || undefined , 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;
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 + " )."
));
}
// The dry-run comes first in the spirit of "fail fast" // TODO remove skipChallengeTest
// (and protecting against challenge failure rate limits) if (me.skipDryRun || me.skipChallengeTest) {
var dryrun = true; return null;
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 () { if ('dns-01' === auth.type) {
return auth; // 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) { })).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 // For each of the challenge types that we support
var challenge; var auth;
var challengeTypes = Object.keys(options.challenges); var challengeTypes = Object.keys(options.challenges);
// ordered from most to least preferred // 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); 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) { challengeTypes.some(function (chType) {
// And for each of the challenge types that are allowed // 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 // Check to see if there are any matches
if (ch.type === chType) { if (ch.type === chType) {
challenge = ch; auth = ch;
return true; 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 // we don't poison the dns cache with our dummy request
var dnsPrefix = ACME.challengePrefixes['dns-01']; var dnsPrefix = ACME.challengePrefixes['dns-01'];
if (dryrun) { if (dryrun) {
dnsPrefix = dnsPrefix.replace('acme-challenge', 'greenlock-dryrun-' + ACME._prnd(4)); dnsPrefix = dnsPrefix.replace('acme-challenge', 'greenlock-dryrun-' + ACME._prnd(4));
} }
var challengeTypes = Object.keys(options.challenges);
var auth = {}; return ACME._importKeypair(me, options.accountKeypair).then(function (pair) {
return me.Keypairs.thumbprint({ jwk: pair.public }).then(function (thumb) {
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;
}
// straight copy from the new order response var auth = {};
// { identifier, status, expires, challenges, wildcard }
Object.keys(request).forEach(function (key) {
auth[key] = request[key];
});
// copy from the challenge we've chosen // straight copy from the new order response
// { type, status, url, token } // { identifier, status, expires, challenges, wildcard }
// (note the duplicate status overwrites the one above, but they should be the same) Object.keys(request).forEach(function (key) {
Object.keys(challenge).forEach(function (key) { auth[key] = request[key];
// don't confused devs with the id url });
auth[key] = challenge[key];
});
// batteries-included helpers // copy from the challenge we've chosen
auth.hostname = auth.identifier.value; // { type, status, url, token }
// because I'm not 100% clear if the wildcard identifier does or doesn't have the leading *. in all cases // (note the duplicate status overwrites the one above, but they should be the same)
auth.altname = ACME._untame(auth.identifier.value, auth.wildcard); Object.keys(challenge).forEach(function (key) {
return ACME._importKeypair(me, options.accountKeypair).then(function (pair) { // don't confused devs with the id url
return me.Keypairs.thumbprint({ jwk: pair.public }).then(function (thumb) { auth[key] = challenge[key];
auth.thumbprint = thumb; });
// keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey))
auth.keyAuthorization = challenge.token + '.' + auth.thumbprint; // batteries-included helpers
// conflicts with ACME challenge id url is already in use, so we call this challengeUrl instead auth.hostname = auth.identifier.value;
// TODO auth.http01Url ? // because I'm not 100% clear if the wildcard identifier does or doesn't have the leading *. in all cases
auth.challengeUrl = 'http://' + auth.identifier.value + ACME.challengePrefixes['http-01'] + '/' + auth.token; auth.altname = ACME._untame(auth.identifier.value, auth.wildcard);
auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', '');
auth.thumbprint = thumb;
return Crypto._sha('sha256', auth.keyAuthorization).then(function (hash) { // keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey))
auth.dnsAuthorization = hash; auth.keyAuthorization = challenge.token + '.' + auth.thumbprint;
return auth;
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; return resp.body;
} }
var errmsg; var err;
if (!resp.body.status) { if (resp.body.error && resp.body.error.detail) {
errmsg = "[acme-v2] (E_STATE_EMPTY) empty challenge state for '" + altname + "':"; err = new Error("[acme-v2] " + auth.altname + " state:" + resp.body.status + " " + resp.body.error.detail);
} err.auth = auth;
else if ('invalid' === resp.body.status) { err.altname = auth.altname;
errmsg = "[acme-v2] (E_STATE_INVALID) challenge state for '" + altname + "': '" + resp.body.status + "'"; err.type = auth.type;
} err.urn = resp.body.error.type;
else { err.code = ('invalid' === resp.body.status) ? 'E_CHALLENGE_INVALID' : 'E_CHALLENGE_UNKNOWN';
errmsg = "[acme-v2] (E_STATE_UKN) challenge state for '" + altname + "': '" + resp.body.status + "'"; 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) { ACME._setChallengesAll = function (me, options) {
var order = options.order; var order = options.order;
var setAuths = order.authorizations.slice(0); var setAuths = order.authorizations.slice(0);
var challenges = order.challenges; var claims = order.claims.slice(0);
var validAuths = []; var validAuths = [];
var auths = []; var auths = [];
function setNext() { function setNext() {
var authUrl = setAuths.shift(); var authUrl = setAuths.shift();
var results = challenges.shift(); var claim = claims.shift();
if (!authUrl) { return; } 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 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(); return setNext();
} }
var challenge = ACME._chooseChallenge(options, results); var auth = ACME._chooseAuth(options, claim.challenges);
if (!challenge) { if (!auth) {
// For example, wildcards require dns-01 and, if we don't have that, we have to bail // For example, wildcards require dns-01 and, if we don't have that, we have to bail
return Promise.reject(new Error( return Promise.reject(new Error(
"Server didn't offer any challenge we can handle for '" + options.domains.join() + "'." "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);
auths.push(auth); return ACME._setChallenge(me, options, auth).then(setNext);
return ACME._setChallenge(me, options, auth).then(setNext);
});
} }
function checkNext() { function checkNext() {
@ -2358,6 +2354,7 @@ ACME._setChallengesAll = function (me, options) {
}).then(checkNext); }).then(checkNext);
} }
// Actually sets the challenge via ACME
function challengeNext() { function challengeNext() {
var auth = validAuths.shift(); var auth = validAuths.shift();
if (!auth) { return; } 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('[ordered]', location); } // the account id url
if (me.debug) { console.debug(resp); } if (me.debug) { console.debug(resp); }
if (!options.authorizations) { if (!order.authorizations) {
return Promise.reject(new Error( return Promise.reject(new Error(
"[acme-v2.js] authorizations were not fetched for '" + options.domains.join() + "':\n" "[acme-v2.js] authorizations were not fetched for '" + options.domains.join() + "':\n"
+ JSON.stringify(resp.body) + JSON.stringify(resp.body)
@ -2528,25 +2525,24 @@ ACME._createOrder = function (me, options) {
return order; return order;
}).then(function (order) { }).then(function (order) {
var challenges = []; var claims = [];
if (me.debug) { console.debug("[acme-v2] POST newOrder has authorizations"); } if (me.debug) { console.debug("[acme-v2] POST newOrder has authorizations"); }
var challengeAuths = order.authorizations.slice(0); var challengeAuths = order.authorizations.slice(0);
function getNext() { function getNext() {
var authUrl = challengeAuths.shift(); var authUrl = challengeAuths.shift();
if (!authUrl) { return challenges; } if (!authUrl) { return claims; }
return ACME._getChallenges(me, options, authUrl).then(function (results) { return ACME._getChallenges(me, options, authUrl).then(function (claim) {
// var domain = options.domains[i]; // results.identifier.value // var domain = options.domains[i]; // claim.identifier.value
challenges.push(results); claims.push(claim);
return getNext(); return getNext();
}); });
} }
return getNext().then(function () { return getNext().then(function () {
order.challenges = challenges; order.claims = claims;
options.order = order; options.order = order;
console.log('DEBUG 2 order (too much info for challenges?):', order);
return order; return order;
}); });
}); });
@ -2684,10 +2680,12 @@ ACME.create = function create(me) {
} }
}; };
me.orders = { me.orders = {
create: function (options) { // create + get challlenges
request: function (options) {
return ACME._createOrder(me, 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); return ACME._finalizeOrder(me, options);
} }
}; };

330
app/js/greenlock.js

@ -2,6 +2,7 @@
'use strict'; 'use strict';
/*global URLSearchParams,Headers*/ /*global URLSearchParams,Headers*/
var PromiseA = window.Promise;
var VERSION = '2'; var VERSION = '2';
// ACME recommends ECDSA P-256, but RSA 2048 is still required by some old servers (like what replicated.io uses ) // 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) // 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 $qs = function (s) { return window.document.querySelector(s); };
var $qsa = function (s) { return window.document.querySelectorAll(s); }; var $qsa = function (s) { return window.document.querySelectorAll(s); };
var acme; var acme;
var accountStuff;
var info = {}; var info = {};
var steps = {}; var steps = {};
var i = 1; var i = 1;
var apiUrl = 'https://acme-{{env}}.api.letsencrypt.org/directory'; 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() { function updateApiType() {
console.log("type updated"); console.log("type updated");
@ -59,8 +81,29 @@
i += 1; i += 1;
return PromiseA.resolve(steps[j].submit(ev)).catch(function (err) { return PromiseA.resolve(steps[j].submit(ev)).catch(function (err) {
var ourfault = true;
console.error(err); 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; $qs('.js-acme-form-domains').hidden = false;
}; };
steps[1].submit = function () { 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() }; 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. }).slice(0,1); //Disable multiple values for now. We'll just take the first and work with it.
info.identifiers.sort(function (a, b) { info.identifiers.sort(function (a, b) {
@ -204,12 +248,14 @@
steps[2].submit = function () { steps[2].submit = function () {
var email = $qs('.js-acme-account-email').value.toLowerCase().trim(); var email = $qs('.js-acme-account-email').value.toLowerCase().trim();
info.email = email;
info.contact = [ 'mailto:' + email ]; info.contact = [ 'mailto:' + email ];
info.agree = $qs('.js-acme-account-tos').checked; info.agree = $qs('.js-acme-account-tos').checked;
//info.greenlockAgree = $qs('.js-gl-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 // 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) { function checkTos(tos) {
if (info.agree) { if (info.agree) {
@ -227,10 +273,10 @@
, accountKeypair: { privateKeyJwk: jwk } , accountKeypair: { privateKeyJwk: jwk }
}).then(function (account) { }).then(function (account) {
console.log("account created result:", account); console.log("account created result:", account);
accountStuff.account = account; info.account = account;
accountStuff.privateJwk = jwk; info.privateJwk = jwk;
accountStuff.email = email; info.email = email;
accountStuff.acme = acme; // TODO XXX remove info.acme = acme; // TODO XXX remove
}).catch(function (err) { }).catch(function (err) {
console.error("A bad thing happened:"); console.error("A bad thing happened:");
console.error(err); console.error(err);
@ -241,116 +287,104 @@
}); });
}); });
}).then(function () { }).then(function () {
var jwk = accountStuff.privateJwk; var jwk = info.privateJwk;
var account = accountStuff.account; var account = info.account;
return acme.orders.create({ return acme.orders.request({
account: account account: account
, accountKeypair: { privateKeyJwk: jwk } , accountKeypair: { privateKeyJwk: jwk }
, identifiers: info.identifiers , domains: info.domains
, challenges: challenges
}).then(function (order) { }).then(function (order) {
return acme.orders.create({ info.order = order;
signedOrder: signedOrder
}).then(function (order) { var claims = order.claims;
accountStuff.order = order; console.log('claims:');
var claims = order.challenges; console.log(claims);
console.log('claims:');
console.log(claims); var obj = { 'dns-01': [], 'http-01': [], 'wildcard': [] };
info.challenges = obj;
var obj = { 'dns-01': [], 'http-01': [], 'wildcard': [] }; /*
info.challenges = obj; var map = {
var map = { 'http-01': '.js-acme-verification-http-01'
'http-01': '.js-acme-verification-http-01' , 'dns-01': '.js-acme-verification-dns-01'
, 'dns-01': '.js-acme-verification-dns-01' , 'wildcard': '.js-acme-verification-wildcard'
, 'wildcard': '.js-acme-verification-wildcard' };
}; */
options.challengePriority = [ 'http-01', 'dns-01' ];
claims.forEach(function (claim) {
// TODO make Promise-friendly console.log("Challenge (claim):");
return PromiseA.all(claims.map(function (claim) { console.log(claim);
var hostname = claim.identifier.value; var hostname = claim.identifier.value;
return PromiseA.all(claim.challenges.map(function (c) { claim.challenges.forEach(function (c) {
var keyAuth = BACME.challenges['http-01']({ var auth = c;
token: c.token var data = {
, thumbprint: thumbprint type: c.type
, challengeDomain: hostname , hostname: hostname
}); , url: c.url
return BACME.challenges['dns-01']({ , token: c.token
keyAuth: keyAuth.value , httpPath: auth.challengeUrl
, challengeDomain: hostname , httpAuth: auth.keyAuthorization
}).then(function (dnsAuth) { , dnsType: 'TXT'
var data = { , dnsHost: auth.dnsHost
type: c.type , dnsAnswer: auth.keyAuthorizationDigest
, hostname: hostname };
, url: c.url
, token: c.token console.log('');
, keyAuthorization: keyAuth console.log('CHALLENGE');
, httpPath: keyAuth.path console.log(claim);
, httpAuth: keyAuth.value console.log(c);
, dnsType: dnsAuth.type console.log(data);
, dnsHost: dnsAuth.host console.log('');
, dnsAnswer: dnsAuth.answer
}; var verification;
if (claim.wildcard) {
console.log(''); obj.wildcard.push(data);
console.log('CHALLENGE'); verification = $qs(".js-acme-verification-wildcard");
console.log(claim); verification.querySelector(".js-acme-ver-hostname").innerHTML = data.hostname;
console.log(c); verification.querySelector(".js-acme-ver-txt-host").innerHTML = data.dnsHost;
console.log(data); verification.querySelector(".js-acme-ver-txt-value").innerHTML = data.dnsAnswer;
console.log('');
} else if(obj[data.type]) {
if (claim.wildcard) {
obj.wildcard.push(data); obj[data.type].push(data);
let verification = $qs(".js-acme-verification-wildcard");
verification.querySelector(".js-acme-ver-hostname").innerHTML = data.hostname; if ('dns-01' === data.type) {
verification.querySelector(".js-acme-ver-txt-host").innerHTML = data.dnsHost; verification = $qs(".js-acme-verification-dns-01");
verification.querySelector(".js-acme-ver-txt-value").innerHTML = data.dnsAnswer; verification.querySelector(".js-acme-ver-hostname").innerHTML = data.hostname;
verification.querySelector(".js-acme-ver-txt-host").innerHTML = data.dnsHost;
} else if(obj[data.type]) { verification.querySelector(".js-acme-ver-txt-value").innerHTML = data.dnsAnswer;
} else if ('http-01' === data.type) {
obj[data.type].push(data); $qs(".js-acme-ver-file-location").innerHTML = data.httpPath.split("/").slice(-1);
$qs(".js-acme-ver-content").innerHTML = data.httpAuth;
if ('dns-01' === data.type) { $qs(".js-acme-ver-uri").innerHTML = data.httpPath;
let verification = $qs(".js-acme-verification-dns-01"); $qs(".js-download-verify-link").href =
verification.querySelector(".js-acme-ver-hostname").innerHTML = data.hostname; "data:text/octet-stream;base64," + window.btoa(data.httpAuth);
verification.querySelector(".js-acme-ver-txt-host").innerHTML = data.dnsHost; $qs(".js-download-verify-link").download = data.httpPath.split("/").slice(-1);
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);
}
}
});
}));
})).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 (!obj['http-01'].length) {
$qs('.js-acme-challenges').hidden = true;
} }
});
});
updateChallengeType(); // 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;
}
console.log("MAGIC STEP NUMBER in 2 is:", i); updateChallengeType();
steps[i]();
});
}); console.log("MAGIC STEP NUMBER in 2 is:", i);
steps[i]();
}); });
}).catch(function (err) { }).catch(function (err) {
console.error('Step \'\' Error:'); console.error('Step \'\' Error:');
console.error(err, err.stack); 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; $qs('.js-acme-form-challenges').hidden = false;
}; };
steps[3].submit = function () { steps[3].submit = function () {
options.challengeTypes = [ 'dns-01' ]; var challengePriority = [ 'dns-01' ];
if ('http-01' === $qs('.js-acme-challenge-type:checked').value) { 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) // for now just show the next page immediately (its a spinner)
// TODO put a test challenge in the list // TODO put a test challenge in the list
// TODO warn about wait-time if DNS // TODO warn about wait-time if DNS
steps[i](); return getServerKeypair().then(function (serverJwk) {
return getServerKeypair().then(function () { return acme.orders.complete({
return acme.orders.finalize({ account: info.account
account: accountStuff.account
, accountKeypair: { privateKeyJwk: jwk } , accountKeypair: { privateKeyJwk: jwk }
, order: accountStuff.order , order: info.order
, domainKeypair: 'TODO' , domains: info.domains
}); , domainKeypair: { privateKeyJwk: serverJwk }
}).then(function (certs) { , challengePriority: challengePriority
console.log('WINNING!'); , challenges: challenges
console.log(certs); }).then(function (certs) {
$qs('#js-fullchain').innerHTML = certs; return Keypairs.export({ jwk: serverJwk }).then(function (keyPem) {
$qs("#js-download-fullchain-link").href = console.log('WINNING!');
"data:text/octet-stream;base64," + window.btoa(certs); console.log(certs);
$qs('#js-fullchain').innerHTML = [
var wcOpts; certs.cert.trim() + "\n"
var pemName; , certs.chain + "\n"
if (/^R/.test(info.serverJwk.kty)) { ].join("\n");
pemName = 'RSA'; $qs("#js-download-fullchain-link").href =
wcOpts = { name: "RSASSA-PKCS1-v1_5", hash: { name: "SHA-256" } }; "data:text/octet-stream;base64," + window.btoa(certs);
} else {
pemName = 'EC'; $qs('#js-privkey').innerHTML = keyPem;
wcOpts = { name: "ECDSA", namedCurve: "P-256" }; $qs("#js-download-privkey-link").href =
} "data:text/octet-stream;base64," + window.btoa(keyPem);
return crypto.subtle.importKey( submitForm();
"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]();
}); });
}); });
}).then(function () {
return submitForm();
}); });
}; };
@ -424,11 +444,7 @@
steps[4].submit = function () { steps[4].submit = function () {
console.log('Congrats! Auto advancing...'); console.log('Congrats! Auto advancing...');
window.alert("An error happened in the final step, but it's not your fault. Email aj@rootprojects.org and let him know.");
}).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.");
});
}; };
steps[5] = function () { steps[5] = function () {
@ -436,6 +452,8 @@
hideForms(); hideForms();
$qs('.js-acme-form-download').hidden = false; $qs('.js-acme-form-download').hidden = false;
}; };
// The kickoff
steps[1](); steps[1]();
var params = new URLSearchParams(window.location.search); var params = new URLSearchParams(window.location.search);
@ -481,8 +499,8 @@
return true; return true;
} }
return testRsaSupport().then(function () { return testEcdsaSupport().then(function () {
console.info('[crypto] RSA is supported'); console.info('[crypto] ECDSA is supported');
}).catch(function (err) { }).catch(function (err) {
console.error('[crypto] could not use either RSA nor EC.'); console.error('[crypto] could not use either RSA nor EC.');
console.error(err); console.error(err);

14
index.html

@ -23,15 +23,10 @@
</style> </style>
<link rel="preload" href="./app/fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdu.woff2" as="font" crossorigin="anonymous"> <link rel="preload" href="./app/fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdu.woff2" as="font" crossorigin="anonymous">
<link rel="preload" href="./app/fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7l.woff2" as="font" crossorigin="anonymous"> <link rel="preload" href="./app/fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7l.woff2" as="font" crossorigin="anonymous">
<link rel="prefetch" href="./app/fonts/HI_SiYsKILxRpg3hIP6sJ7fM7PqlPevW.woff2" as="font" crossorigin="anonymous"> <link rel="prefetch" href="./app/fonts/HI_SiYsKILxRpg3hIP6sJ7fM7PqlPevW.woff2" as="font" crossorigin="anonymous">
<link rel="prefetch" href="./app/js/app.js"> <link rel="prefetch" href="./app/js/bluecrypt-acme.js" as="script">
<link rel="prefetch" href="./app/js/bacme.js"> <link rel="prefetch" href="./app/js/greenlock.js" as="script">
<link rel="prefetch" href="./app/js/pkijs.org/v1.3.33/common.js"> <link rel="prefetch" href="./js/app.js" as="script">
<link rel="prefetch" href="./app/js/pkijs.org/v1.3.33/asn1.js">
<link rel="prefetch" href="./app/js/pkijs.org/v1.3.33/x509_schema.js">
<link rel="prefetch" href="./app/js/pkijs.org/v1.3.33/x509_simpl.js">
<link rel="prefetch" href="./app/js/browser-csr/v1.0.0-alpha/csr.js">
</head> </head>
<body class="js-app-ready"> <body class="js-app-ready">
<script> <script>
@ -47,7 +42,8 @@
</div> </div>
<div class="column-row"> <div class="column-row">
<div class="js-javascript-warning"> <div class="js-javascript-warning">
Greenlock will process the CSR in the browser and request the certificates directly from letsencrypt.org. Please enable Javascript before continuing. Greenlock will process the CSR in the browser and request the certificates directly from letsencrypt.org.
Please enable Javascript before continuing.
</div> </div>
<form id="js-acme-form" action="./app/" method=> <form id="js-acme-form" action="./app/" method=>
<div class="domain-psuedo-input"> <div class="domain-psuedo-input">

Loading…
Cancel
Save