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/app.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">
<link rel="preload" href="./js/bluecrypt-acme.js" as="script">
<link rel="preload" href="./js/greenlock.js" as="script">
</head> </head>
<body hidden> <body hidden>
@ -342,14 +337,8 @@
<br> <br>
<script src="./js/bacme.js"></script>
<script src="./js/app.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>
<script src="./js/bluecrypt-acme.js"></script>
<script src="./js/greenlock.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 });
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: { 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"
// (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;
});
// 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) { })).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
// { identifier, status, expires, challenges, wildcard }
Object.keys(request).forEach(function (key) {
auth[key] = request[key];
});
var auth = {};
// 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];
});
// straight copy from the new order response
// { identifier, status, expires, challenges, wildcard }
Object.keys(request).forEach(function (key) {
auth[key] = request[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 Crypto._sha('sha256', auth.keyAuthorization).then(function (hash) {
auth.dnsAuthorization = hash;
return auth;
// 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; 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) { 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);
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 // 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);
return ACME._setChallenge(me, options, auth).then(setNext);
});
auths.push(auth);
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) {
// 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();
}); });
} }
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;
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) { }).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 account = accountStuff.account;
var jwk = info.privateJwk;
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({
signedOrder: signedOrder
}).then(function (order) {
accountStuff.order = order;
var claims = order.challenges;
console.log('claims:');
console.log(claims);
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' ];
// 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
};
console.log('');
console.log('CHALLENGE');
console.log(claim);
console.log(c);
console.log(data);
console.log('');
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;
} else if(obj[data.type]) {
obj[data.type].push(data);
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);
}
}
});
}));
})).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;
info.order = order;
var claims = order.claims;
console.log('claims:');
console.log(claims);
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'
};
*/
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
};
console.log('');
console.log('CHALLENGE');
console.log(claim);
console.log(c);
console.log(data);
console.log('');
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;
} else if(obj[data.type]) {
obj[data.type].push(data);
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);
}
} }
});
});
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);
steps[i]();
});
updateChallengeType();
});
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 () {
return acme.orders.finalize({
account: accountStuff.account
return getServerKeypair().then(function (serverJwk) {
return acme.orders.complete({
account: info.account
, accountKeypair: { privateKeyJwk: jwk } , 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);
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]();
, 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);
$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 () { steps[4].submit = function () {
console.log('Congrats! Auto advancing...'); 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 () { 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 () {
console.info('[crypto] RSA is supported');
return testEcdsaSupport().then(function () {
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/bacme.js">
<link rel="prefetch" href="./app/js/pkijs.org/v1.3.33/common.js">
<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">
<link rel="prefetch" href="./app/js/bluecrypt-acme.js" as="script">
<link rel="prefetch" href="./app/js/greenlock.js" as="script">
<link rel="prefetch" href="./js/app.js" as="script">
</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