using new acme
This commit is contained in:
parent
49d5346615
commit
0405a7db90
|
@ -153,9 +153,10 @@
|
||||||
-->
|
-->
|
||||||
<button class="button-next" type="submit">Next</button>
|
<button class="button-next" type="submit">Next</button>
|
||||||
<div class="email-usage">
|
<div class="email-usage">
|
||||||
Why do we need your email? We link your SSL certificates to the
|
Why do we need your email?
|
||||||
email you use so you can manage your certificates in the future,
|
We link your SSL certificates to the email you use so that you'll
|
||||||
and get important email updates about them.
|
be notified before the certificate expires and so you can manage
|
||||||
|
your certificates in the future.
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
670
app/js/app.js
670
app/js/app.js
|
@ -1,670 +0,0 @@
|
||||||
(function () {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
/*global URLSearchParams,Headers*/
|
|
||||||
var BROWSER_SUPPORTS_ECDSA;
|
|
||||||
var $qs = function (s) { return window.document.querySelector(s); };
|
|
||||||
var $qsa = function (s) { return window.document.querySelectorAll(s); };
|
|
||||||
var info = {};
|
|
||||||
var steps = {};
|
|
||||||
var nonce;
|
|
||||||
var kid;
|
|
||||||
var i = 1;
|
|
||||||
var BACME = window.BACME;
|
|
||||||
var PromiseA = window.Promise;
|
|
||||||
var crypto = window.crypto;
|
|
||||||
|
|
||||||
function testEcdsaSupport() {
|
|
||||||
var opts = {
|
|
||||||
type: 'ECDSA'
|
|
||||||
, bitlength: '256'
|
|
||||||
};
|
|
||||||
return BACME.accounts.generateKeypair(opts).then(function (jwk) {
|
|
||||||
return crypto.subtle.importKey(
|
|
||||||
"jwk"
|
|
||||||
, jwk
|
|
||||||
, { name: "ECDSA", namedCurve: "P-256" }
|
|
||||||
, true
|
|
||||||
, ["sign"]
|
|
||||||
).then(function (privateKey) {
|
|
||||||
return window.crypto.subtle.exportKey("pkcs8", privateKey);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function testRsaSupport() {
|
|
||||||
var opts = {
|
|
||||||
type: 'RSA'
|
|
||||||
, bitlength: '2048'
|
|
||||||
};
|
|
||||||
return BACME.accounts.generateKeypair(opts).then(function (jwk) {
|
|
||||||
return crypto.subtle.importKey(
|
|
||||||
"jwk"
|
|
||||||
, jwk
|
|
||||||
, { name: "RSASSA-PKCS1-v1_5", hash: { name: "SHA-256" } }
|
|
||||||
, true
|
|
||||||
, ["sign"]
|
|
||||||
).then(function (privateKey) {
|
|
||||||
return window.crypto.subtle.exportKey("pkcs8", privateKey);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function testKeypairSupport() {
|
|
||||||
return testEcdsaSupport().then(function () {
|
|
||||||
console.info("[crypto] ECDSA is supported");
|
|
||||||
BROWSER_SUPPORTS_ECDSA = true;
|
|
||||||
localStorage.setItem('version', '1');
|
|
||||||
return true;
|
|
||||||
}).catch(function () {
|
|
||||||
console.warn("[crypto] ECDSA is NOT fully supported");
|
|
||||||
BROWSER_SUPPORTS_ECDSA = false;
|
|
||||||
|
|
||||||
// fix previous firefox browsers
|
|
||||||
if (!localStorage.getItem('version')) {
|
|
||||||
localStorage.clear();
|
|
||||||
localStorage.setItem('version', '1');
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
testKeypairSupport().then(function (ecdsaSupport) {
|
|
||||||
if (ecdsaSupport) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return testRsaSupport().then(function () {
|
|
||||||
console.info('[crypto] RSA is supported');
|
|
||||||
}).catch(function (err) {
|
|
||||||
console.error('[crypto] could not use either EC nor RSA.');
|
|
||||||
console.error(err);
|
|
||||||
window.alert("Your browser is cryptography support (neither RSA or EC is usable). Please use Chrome, Firefox, or Safari.");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
var apiUrl = 'https://acme-{{env}}.api.letsencrypt.org/directory';
|
|
||||||
function updateApiType() {
|
|
||||||
console.log("type updated");
|
|
||||||
/*jshint validthis: true */
|
|
||||||
var input = this || Array.prototype.filter.call(
|
|
||||||
$qsa('.js-acme-api-type'), function ($el) { return $el.checked; }
|
|
||||||
)[0];
|
|
||||||
console.log('ACME api type radio:', input.value);
|
|
||||||
$qs('.js-acme-directory-url').value = apiUrl.replace(/{{env}}/g, input.value);
|
|
||||||
}
|
|
||||||
$qsa('.js-acme-api-type').forEach(function ($el) {
|
|
||||||
$el.addEventListener('change', updateApiType);
|
|
||||||
});
|
|
||||||
updateApiType();
|
|
||||||
|
|
||||||
function hideForms() {
|
|
||||||
$qsa('.js-acme-form').forEach(function (el) {
|
|
||||||
el.hidden = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateProgress(currentStep) {
|
|
||||||
var progressSteps = $qs("#js-progress-bar").children;
|
|
||||||
for(var j = 0; j < progressSteps.length; j++) {
|
|
||||||
if(j < currentStep) {
|
|
||||||
progressSteps[j].classList.add("js-progress-step-complete");
|
|
||||||
progressSteps[j].classList.remove("js-progress-step-started");
|
|
||||||
} else if(j === currentStep) {
|
|
||||||
progressSteps[j].classList.remove("js-progress-step-complete");
|
|
||||||
progressSteps[j].classList.add("js-progress-step-started");
|
|
||||||
} else {
|
|
||||||
progressSteps[j].classList.remove("js-progress-step-complete");
|
|
||||||
progressSteps[j].classList.remove("js-progress-step-started");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function submitForm(ev) {
|
|
||||||
var j = i;
|
|
||||||
i += 1;
|
|
||||||
|
|
||||||
return PromiseA.resolve(steps[j].submit(ev)).catch(function (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.");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$qsa('.js-acme-form').forEach(function ($el) {
|
|
||||||
$el.addEventListener('submit', function (ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
submitForm(ev);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function updateChallengeType() {
|
|
||||||
/*jshint validthis: true*/
|
|
||||||
var input = this || Array.prototype.filter.call(
|
|
||||||
$qsa('.js-acme-challenge-type'), function ($el) { return $el.checked; }
|
|
||||||
)[0];
|
|
||||||
console.log('ch type radio:', input.value);
|
|
||||||
$qs('.js-acme-verification-wildcard').hidden = true;
|
|
||||||
$qs('.js-acme-verification-http-01').hidden = true;
|
|
||||||
$qs('.js-acme-verification-dns-01').hidden = true;
|
|
||||||
if (info.challenges.wildcard) {
|
|
||||||
$qs('.js-acme-verification-wildcard').hidden = false;
|
|
||||||
}
|
|
||||||
if (info.challenges[input.value]) {
|
|
||||||
$qs('.js-acme-verification-' + input.value).hidden = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$qsa('.js-acme-challenge-type').forEach(function ($el) {
|
|
||||||
$el.addEventListener('change', updateChallengeType);
|
|
||||||
});
|
|
||||||
|
|
||||||
function saveContact(email, domains) {
|
|
||||||
// to be used for good, not evil
|
|
||||||
return window.fetch('https://api.rootprojects.org/api/rootprojects.org/public/community', {
|
|
||||||
method: 'POST'
|
|
||||||
, cors: true
|
|
||||||
, headers: new Headers({ 'Content-Type': 'application/json' })
|
|
||||||
, body: JSON.stringify({
|
|
||||||
address: email
|
|
||||||
, project: 'greenlock-domains@rootprojects.org'
|
|
||||||
, domain: domains.join(',')
|
|
||||||
})
|
|
||||||
}).catch(function (err) {
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
steps[1] = function () {
|
|
||||||
updateProgress(0);
|
|
||||||
hideForms();
|
|
||||||
$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) {
|
|
||||||
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) {
|
|
||||||
if (a === b) { return 0; }
|
|
||||||
if (a < b) { return 1; }
|
|
||||||
if (a > b) { return -1; }
|
|
||||||
});
|
|
||||||
|
|
||||||
return BACME.directory({ directoryUrl: $qs('.js-acme-directory-url').value }).then(function (directory) {
|
|
||||||
$qs('.js-acme-tos-url').href = directory.meta.termsOfService;
|
|
||||||
return BACME.nonce().then(function (_nonce) {
|
|
||||||
nonce = _nonce;
|
|
||||||
|
|
||||||
console.log("MAGIC STEP NUMBER in 1 is:", i);
|
|
||||||
steps[i]();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
steps[2] = function () {
|
|
||||||
updateProgress(0);
|
|
||||||
hideForms();
|
|
||||||
$qs('.js-acme-form-account').hidden = false;
|
|
||||||
};
|
|
||||||
steps[2].submit = function () {
|
|
||||||
var email = $qs('.js-acme-account-email').value.toLowerCase().trim();
|
|
||||||
|
|
||||||
info.contact = [ 'mailto:' + email ];
|
|
||||||
info.agree = $qs('.js-acme-account-tos').checked;
|
|
||||||
info.greenlockAgree = $qs('.js-gl-tos').checked;
|
|
||||||
// TODO
|
|
||||||
// options for
|
|
||||||
// * regenerate key
|
|
||||||
// * ECDSA / RSA / bitlength
|
|
||||||
|
|
||||||
// TODO ping with version and account creation
|
|
||||||
setTimeout(saveContact, 100, email, info.identifiers.map(function (ident) { return ident.value; }));
|
|
||||||
|
|
||||||
var jwk = JSON.parse(localStorage.getItem('account:' + email) || 'null');
|
|
||||||
var p;
|
|
||||||
|
|
||||||
function createKeypair() {
|
|
||||||
var opts;
|
|
||||||
|
|
||||||
if(BROWSER_SUPPORTS_ECDSA) {
|
|
||||||
opts = {
|
|
||||||
type: 'ECDSA'
|
|
||||||
, bitlength: '256'
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
opts = {
|
|
||||||
type: 'RSA'
|
|
||||||
, bitlength: '2048'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return BACME.accounts.generateKeypair(opts).then(function (jwk) {
|
|
||||||
localStorage.setItem('account:' + email, JSON.stringify(jwk));
|
|
||||||
return jwk;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jwk) {
|
|
||||||
p = PromiseA.resolve(jwk);
|
|
||||||
} else {
|
|
||||||
p = testKeypairSupport().then(createKeypair);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createAccount(jwk) {
|
|
||||||
console.log('account jwk:');
|
|
||||||
console.log(jwk);
|
|
||||||
delete jwk.key_ops;
|
|
||||||
info.jwk = jwk;
|
|
||||||
return BACME.accounts.sign({
|
|
||||||
jwk: jwk
|
|
||||||
, contacts: [ 'mailto:' + email ]
|
|
||||||
, agree: info.agree
|
|
||||||
, nonce: nonce
|
|
||||||
, kid: kid
|
|
||||||
}).then(function (signedAccount) {
|
|
||||||
return BACME.accounts.set({
|
|
||||||
signedAccount: signedAccount
|
|
||||||
}).then(function (account) {
|
|
||||||
console.log('account:');
|
|
||||||
console.log(account);
|
|
||||||
kid = account.kid;
|
|
||||||
return kid;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.then(function (_jwk) {
|
|
||||||
jwk = _jwk;
|
|
||||||
kid = JSON.parse(localStorage.getItem('account-kid:' + email) || 'null');
|
|
||||||
var p2;
|
|
||||||
|
|
||||||
// TODO save account id rather than always retrieving it
|
|
||||||
if (kid) {
|
|
||||||
p2 = PromiseA.resolve(kid);
|
|
||||||
} else {
|
|
||||||
p2 = createAccount(jwk);
|
|
||||||
}
|
|
||||||
|
|
||||||
return p2.then(function (_kid) {
|
|
||||||
kid = _kid;
|
|
||||||
info.kid = kid;
|
|
||||||
return BACME.orders.sign({
|
|
||||||
jwk: jwk
|
|
||||||
, identifiers: info.identifiers
|
|
||||||
, kid: kid
|
|
||||||
}).then(function (signedOrder) {
|
|
||||||
return BACME.orders.create({
|
|
||||||
signedOrder: signedOrder
|
|
||||||
}).then(function (order) {
|
|
||||||
info.finalizeUrl = order.finalize;
|
|
||||||
info.orderUrl = order.url; // from header Location ???
|
|
||||||
return BACME.thumbprint({ jwk: jwk }).then(function (thumbprint) {
|
|
||||||
return BACME.challenges.all().then(function (claims) {
|
|
||||||
console.log('claims:');
|
|
||||||
console.log(claims);
|
|
||||||
var obj = { 'dns-01': [], 'http-01': [], 'wildcard': [] };
|
|
||||||
info.challenges = obj;
|
|
||||||
var map = {
|
|
||||||
'http-01': '.js-acme-verification-http-01'
|
|
||||||
, 'dns-01': '.js-acme-verification-dns-01'
|
|
||||||
, 'wildcard': '.js-acme-verification-wildcard'
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
var tpls = {};
|
|
||||||
Object.keys(map).forEach(function (k) {
|
|
||||||
var sel = map[k] + ' tbody';
|
|
||||||
console.log(sel);
|
|
||||||
tpls[k] = $qs(sel).innerHTML;
|
|
||||||
$qs(map[k] + ' tbody').innerHTML = '';
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
|
|
||||||
// TODO make Promise-friendly
|
|
||||||
return 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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.");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
steps[3] = function () {
|
|
||||||
updateProgress(1);
|
|
||||||
hideForms();
|
|
||||||
$qs('.js-acme-form-challenges').hidden = false;
|
|
||||||
};
|
|
||||||
steps[3].submit = function () {
|
|
||||||
var chType;
|
|
||||||
Array.prototype.some.call($qsa('.js-acme-challenge-type'), function ($el) {
|
|
||||||
if ($el.checked) {
|
|
||||||
chType = $el.value;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
console.log('chType is:', chType);
|
|
||||||
var chs = [];
|
|
||||||
|
|
||||||
// do each wildcard, if any
|
|
||||||
// do each challenge, by selected type only
|
|
||||||
[ 'wildcard', chType].forEach(function (typ) {
|
|
||||||
info.challenges[typ].forEach(function (ch) {
|
|
||||||
// { jwk, challengeUrl, accountId (kid) }
|
|
||||||
chs.push({
|
|
||||||
jwk: info.jwk
|
|
||||||
, challengeUrl: ch.url
|
|
||||||
, accountId: info.kid
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
console.log("INFO.challenges !!!!!", info.challenges);
|
|
||||||
|
|
||||||
var results = [];
|
|
||||||
function nextChallenge() {
|
|
||||||
var ch = chs.pop();
|
|
||||||
if (!ch) { return results; }
|
|
||||||
return BACME.challenges.accept(ch).then(function (result) {
|
|
||||||
results.push(result);
|
|
||||||
return nextChallenge();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// for now just show the next page immediately (its a spinner)
|
|
||||||
steps[i]();
|
|
||||||
return nextChallenge().then(function (results) {
|
|
||||||
console.log('challenge status:', results);
|
|
||||||
var polls = results.slice(0);
|
|
||||||
var allsWell = true;
|
|
||||||
|
|
||||||
function checkPolls() {
|
|
||||||
return new PromiseA(function (resolve) {
|
|
||||||
setTimeout(resolve, 1000);
|
|
||||||
}).then(function () {
|
|
||||||
return PromiseA.all(polls.map(function (poll) {
|
|
||||||
return BACME.challenges.check({ challengePollUrl: poll.url });
|
|
||||||
})).then(function (polls) {
|
|
||||||
console.log(polls);
|
|
||||||
|
|
||||||
polls = polls.filter(function (poll) {
|
|
||||||
//return 'valid' !== poll.status && 'invalid' !== poll.status;
|
|
||||||
if ('pending' === poll.status) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('invalid' === poll.status) {
|
|
||||||
allsWell = false;
|
|
||||||
window.alert("verification failed:" + poll.error.detail);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (poll.error) {
|
|
||||||
window.alert("verification failed:" + poll.error.detail);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('valid' !== poll.status) {
|
|
||||||
allsWell = false;
|
|
||||||
console.warn('BAD POLL STATUS', poll);
|
|
||||||
window.alert("unknown error: " + JSON.stringify(poll, null, 2));
|
|
||||||
}
|
|
||||||
// TODO show status in HTML
|
|
||||||
});
|
|
||||||
|
|
||||||
if (polls.length) {
|
|
||||||
return checkPolls();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return checkPolls().then(function () {
|
|
||||||
if (allsWell) {
|
|
||||||
return submitForm();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// https://stackoverflow.com/questions/40314257/export-webcrypto-key-to-pem-format
|
|
||||||
function spkiToPEM(keydata, pemName){
|
|
||||||
var keydataS = arrayBufferToString(keydata);
|
|
||||||
var keydataB64 = window.btoa(keydataS);
|
|
||||||
var keydataB64Pem = formatAsPem(keydataB64, pemName);
|
|
||||||
return keydataB64Pem;
|
|
||||||
}
|
|
||||||
|
|
||||||
function arrayBufferToString( buffer ) {
|
|
||||||
var binary = '';
|
|
||||||
var bytes = new Uint8Array( buffer );
|
|
||||||
var len = bytes.byteLength;
|
|
||||||
for (var i = 0; i < len; i++) {
|
|
||||||
binary += String.fromCharCode( bytes[ i ] );
|
|
||||||
}
|
|
||||||
return binary;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function formatAsPem(str, pemName) {
|
|
||||||
var finalString = '-----BEGIN ' + pemName + ' PRIVATE KEY-----\n';
|
|
||||||
|
|
||||||
while(str.length > 0) {
|
|
||||||
finalString += str.substring(0, 64) + '\n';
|
|
||||||
str = str.substring(64);
|
|
||||||
}
|
|
||||||
|
|
||||||
finalString = finalString + '-----END ' + pemName + ' PRIVATE KEY-----';
|
|
||||||
|
|
||||||
return finalString;
|
|
||||||
}
|
|
||||||
|
|
||||||
// spinner
|
|
||||||
steps[4] = function () {
|
|
||||||
updateProgress(1);
|
|
||||||
hideForms();
|
|
||||||
$qs('.js-acme-form-poll').hidden = false;
|
|
||||||
};
|
|
||||||
steps[4].submit = function () {
|
|
||||||
console.log('Congrats! Auto advancing...');
|
|
||||||
|
|
||||||
var key = info.identifiers.map(function (ident) { return ident.value; }).join(',');
|
|
||||||
var serverJwk = JSON.parse(localStorage.getItem('server:' + key) || 'null');
|
|
||||||
var p;
|
|
||||||
|
|
||||||
function createKeypair() {
|
|
||||||
var opts;
|
|
||||||
|
|
||||||
if (BROWSER_SUPPORTS_ECDSA) {
|
|
||||||
opts = { type: 'ECDSA', bitlength: '256' };
|
|
||||||
} else {
|
|
||||||
opts = { type: 'RSA', bitlength: '2048' };
|
|
||||||
}
|
|
||||||
|
|
||||||
return BACME.domains.generateKeypair(opts).then(function (serverJwk) {
|
|
||||||
localStorage.setItem('server:' + key, JSON.stringify(serverJwk));
|
|
||||||
return serverJwk;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (serverJwk) {
|
|
||||||
p = PromiseA.resolve(serverJwk);
|
|
||||||
} else {
|
|
||||||
p = createKeypair();
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.then(function (_serverJwk) {
|
|
||||||
serverJwk = _serverJwk;
|
|
||||||
info.serverJwk = serverJwk;
|
|
||||||
// { serverJwk, domains }
|
|
||||||
return BACME.orders.generateCsr({
|
|
||||||
serverJwk: serverJwk
|
|
||||||
, domains: info.identifiers.map(function (ident) {
|
|
||||||
return ident.value;
|
|
||||||
})
|
|
||||||
}).then(function (csrweb64) {
|
|
||||||
return BACME.orders.finalize({
|
|
||||||
csr: csrweb64
|
|
||||||
, jwk: info.jwk
|
|
||||||
, finalizeUrl: info.finalizeUrl
|
|
||||||
, accountId: info.kid
|
|
||||||
});
|
|
||||||
}).then(function () {
|
|
||||||
function checkCert() {
|
|
||||||
return new PromiseA(function (resolve) {
|
|
||||||
setTimeout(resolve, 1000);
|
|
||||||
}).then(function () {
|
|
||||||
return BACME.orders.check({ orderUrl: info.orderUrl });
|
|
||||||
}).then(function (reply) {
|
|
||||||
if ('processing' === reply) {
|
|
||||||
return checkCert();
|
|
||||||
}
|
|
||||||
return reply;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return checkCert();
|
|
||||||
}).then(function (reply) {
|
|
||||||
return BACME.orders.receive({ certificateUrl: reply.certificate });
|
|
||||||
}).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]();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}).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 () {
|
|
||||||
updateProgress(2);
|
|
||||||
hideForms();
|
|
||||||
$qs('.js-acme-form-download').hidden = false;
|
|
||||||
};
|
|
||||||
steps[1]();
|
|
||||||
|
|
||||||
var params = new URLSearchParams(window.location.search);
|
|
||||||
var apiType = params.get('acme-api-type') || "staging-v02";
|
|
||||||
|
|
||||||
if(params.has('acme-domains')) {
|
|
||||||
console.log("acme-domains param: ", params.get('acme-domains'));
|
|
||||||
$qs('.js-acme-domains').value = params.get('acme-domains');
|
|
||||||
|
|
||||||
$qsa('.js-acme-api-type').forEach(function(ele) {
|
|
||||||
if(ele.value === apiType) {
|
|
||||||
ele.checked = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
updateApiType();
|
|
||||||
steps[2]();
|
|
||||||
submitForm();
|
|
||||||
}
|
|
||||||
|
|
||||||
$qs('body').hidden = false;
|
|
||||||
}());
|
|
699
app/js/bacme.js
699
app/js/bacme.js
|
@ -1,699 +0,0 @@
|
||||||
/*global CSR*/
|
|
||||||
// CSR takes a while to load after the page load
|
|
||||||
(function (exports) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var BACME = exports.BACME = {};
|
|
||||||
var webFetch = exports.fetch;
|
|
||||||
var webCrypto = exports.crypto;
|
|
||||||
var Promise = exports.Promise;
|
|
||||||
|
|
||||||
var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory';
|
|
||||||
var directory;
|
|
||||||
|
|
||||||
var nonceUrl;
|
|
||||||
var nonce;
|
|
||||||
|
|
||||||
var accountKeypair;
|
|
||||||
var accountJwk;
|
|
||||||
|
|
||||||
var accountUrl;
|
|
||||||
|
|
||||||
BACME.challengePrefixes = {
|
|
||||||
'http-01': '/.well-known/acme-challenge'
|
|
||||||
, 'dns-01': '_acme-challenge'
|
|
||||||
};
|
|
||||||
|
|
||||||
BACME._logHeaders = function (resp) {
|
|
||||||
console.log('Headers:');
|
|
||||||
Array.from(resp.headers.entries()).forEach(function (h) { console.log(h[0] + ': ' + h[1]); });
|
|
||||||
};
|
|
||||||
|
|
||||||
BACME._logBody = function (body) {
|
|
||||||
console.log('Body:');
|
|
||||||
console.log(JSON.stringify(body, null, 2));
|
|
||||||
console.log('');
|
|
||||||
};
|
|
||||||
|
|
||||||
BACME.directory = function (opts) {
|
|
||||||
return webFetch(opts.directoryUrl || directoryUrl, { mode: 'cors' }).then(function (resp) {
|
|
||||||
BACME._logHeaders(resp);
|
|
||||||
return resp.json().then(function (reply) {
|
|
||||||
if (/error/.test(reply.type)) {
|
|
||||||
return Promise.reject(new Error(reply.detail || reply.type));
|
|
||||||
}
|
|
||||||
directory = reply;
|
|
||||||
nonceUrl = directory.newNonce || 'https://acme-staging-v02.api.letsencrypt.org/acme/new-nonce';
|
|
||||||
accountUrl = directory.newAccount || 'https://acme-staging-v02.api.letsencrypt.org/acme/new-account';
|
|
||||||
orderUrl = directory.newOrder || "https://acme-staging-v02.api.letsencrypt.org/acme/new-order";
|
|
||||||
BACME._logBody(reply);
|
|
||||||
return reply;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
BACME.nonce = function () {
|
|
||||||
return webFetch(nonceUrl, { mode: 'cors' }).then(function (resp) {
|
|
||||||
BACME._logHeaders(resp);
|
|
||||||
nonce = resp.headers.get('replay-nonce');
|
|
||||||
console.log('Nonce:', nonce);
|
|
||||||
// resp.body is empty
|
|
||||||
return resp.headers.get('replay-nonce');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
BACME.accounts = {};
|
|
||||||
|
|
||||||
// type = ECDSA
|
|
||||||
// bitlength = 256
|
|
||||||
BACME.accounts.generateKeypair = function (opts) {
|
|
||||||
return BACME.generateKeypair(opts).then(function (result) {
|
|
||||||
accountKeypair = result;
|
|
||||||
|
|
||||||
return webCrypto.subtle.exportKey(
|
|
||||||
"jwk"
|
|
||||||
, result.privateKey
|
|
||||||
).then(function (privJwk) {
|
|
||||||
|
|
||||||
accountJwk = privJwk;
|
|
||||||
console.log('private jwk:');
|
|
||||||
console.log(JSON.stringify(privJwk, null, 2));
|
|
||||||
|
|
||||||
return privJwk;
|
|
||||||
/*
|
|
||||||
return webCrypto.subtle.exportKey(
|
|
||||||
"pkcs8"
|
|
||||||
, result.privateKey
|
|
||||||
).then(function (keydata) {
|
|
||||||
console.log('pkcs8:');
|
|
||||||
console.log(Array.from(new Uint8Array(keydata)));
|
|
||||||
|
|
||||||
return privJwk;
|
|
||||||
//return accountKeypair;
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// json to url-safe base64
|
|
||||||
BACME._jsto64 = function (json) {
|
|
||||||
return btoa(JSON.stringify(json)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
|
||||||
};
|
|
||||||
|
|
||||||
var textEncoder = new TextEncoder();
|
|
||||||
|
|
||||||
BACME._importKey = function (jwk) {
|
|
||||||
var alg; // I think the 256 refers to the hash
|
|
||||||
var wcOpts = {};
|
|
||||||
var extractable = true; // TODO make optionally false?
|
|
||||||
var priv = jwk;
|
|
||||||
var pub;
|
|
||||||
|
|
||||||
// ECDSA
|
|
||||||
if (/^EC/i.test(jwk.kty)) {
|
|
||||||
wcOpts.name = 'ECDSA';
|
|
||||||
wcOpts.namedCurve = jwk.crv;
|
|
||||||
alg = 'ES256';
|
|
||||||
pub = {
|
|
||||||
crv: priv.crv
|
|
||||||
, kty: priv.kty
|
|
||||||
, x: priv.x
|
|
||||||
, y: priv.y
|
|
||||||
};
|
|
||||||
if (!priv.d) {
|
|
||||||
priv = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RSA
|
|
||||||
if (/^RS/i.test(jwk.kty)) {
|
|
||||||
wcOpts.name = 'RSASSA-PKCS1-v1_5';
|
|
||||||
wcOpts.hash = { name: "SHA-256" };
|
|
||||||
alg = 'RS256';
|
|
||||||
pub = {
|
|
||||||
e: priv.e
|
|
||||||
, kty: priv.kty
|
|
||||||
, n: priv.n
|
|
||||||
};
|
|
||||||
if (!priv.p) {
|
|
||||||
priv = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return window.crypto.subtle.importKey(
|
|
||||||
"jwk"
|
|
||||||
, pub
|
|
||||||
, wcOpts
|
|
||||||
, extractable
|
|
||||||
, [ "verify" ]
|
|
||||||
).then(function (publicKey) {
|
|
||||||
function give(privateKey) {
|
|
||||||
return {
|
|
||||||
wcPub: publicKey
|
|
||||||
, wcKey: privateKey
|
|
||||||
, wcKeypair: { publicKey: publicKey, privateKey: privateKey }
|
|
||||||
, meta: {
|
|
||||||
alg: alg
|
|
||||||
, name: wcOpts.name
|
|
||||||
, hash: wcOpts.hash
|
|
||||||
}
|
|
||||||
, jwk: jwk
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (!priv) {
|
|
||||||
return give();
|
|
||||||
}
|
|
||||||
return window.crypto.subtle.importKey(
|
|
||||||
"jwk"
|
|
||||||
, priv
|
|
||||||
, wcOpts
|
|
||||||
, extractable
|
|
||||||
, [ "sign"/*, "verify"*/ ]
|
|
||||||
).then(give);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
BACME._sign = function (opts) {
|
|
||||||
var wcPrivKey = opts.abstractKey.wcKeypair.privateKey;
|
|
||||||
var wcOpts = opts.abstractKey.meta;
|
|
||||||
var alg = opts.abstractKey.meta.alg; // I think the 256 refers to the hash
|
|
||||||
var signHash;
|
|
||||||
|
|
||||||
console.log('kty', opts.abstractKey.jwk.kty);
|
|
||||||
signHash = { name: "SHA-" + alg.replace(/[a-z]+/ig, '') };
|
|
||||||
|
|
||||||
var msg = textEncoder.encode(opts.protected64 + '.' + opts.payload64);
|
|
||||||
console.log('msg:', msg);
|
|
||||||
return window.crypto.subtle.sign(
|
|
||||||
{ name: wcOpts.name, hash: signHash }
|
|
||||||
, wcPrivKey
|
|
||||||
, msg
|
|
||||||
).then(function (signature) {
|
|
||||||
//console.log('sig1:', signature);
|
|
||||||
//console.log('sig2:', new Uint8Array(signature));
|
|
||||||
//console.log('sig3:', Array.prototype.slice.call(new Uint8Array(signature)));
|
|
||||||
// convert buffer to urlsafe base64
|
|
||||||
var sig64 = btoa(Array.prototype.map.call(new Uint8Array(signature), function (ch) {
|
|
||||||
return String.fromCharCode(ch);
|
|
||||||
}).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
|
||||||
|
|
||||||
console.log('[1] URL-safe Base64 Signature:');
|
|
||||||
console.log(sig64);
|
|
||||||
|
|
||||||
var signedMsg = {
|
|
||||||
protected: opts.protected64
|
|
||||||
, payload: opts.payload64
|
|
||||||
, signature: sig64
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('Signed Base64 Msg:');
|
|
||||||
console.log(JSON.stringify(signedMsg, null, 2));
|
|
||||||
|
|
||||||
return signedMsg;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
// email = john.doe@gmail.com
|
|
||||||
// jwk = { ... }
|
|
||||||
// agree = true
|
|
||||||
BACME.accounts.sign = function (opts) {
|
|
||||||
|
|
||||||
return BACME._importKey(opts.jwk).then(function (abstractKey) {
|
|
||||||
|
|
||||||
var payloadJson =
|
|
||||||
{ termsOfServiceAgreed: opts.agree
|
|
||||||
, onlyReturnExisting: false
|
|
||||||
, contact: opts.contacts || [ 'mailto:' + opts.email ]
|
|
||||||
};
|
|
||||||
console.log('payload:');
|
|
||||||
console.log(payloadJson);
|
|
||||||
var payload64 = BACME._jsto64(
|
|
||||||
payloadJson
|
|
||||||
);
|
|
||||||
|
|
||||||
var protectedJson =
|
|
||||||
{ nonce: opts.nonce
|
|
||||||
, url: accountUrl
|
|
||||||
, alg: abstractKey.meta.alg
|
|
||||||
, jwk: null
|
|
||||||
};
|
|
||||||
|
|
||||||
if (/EC/i.test(opts.jwk.kty)) {
|
|
||||||
protectedJson.jwk = {
|
|
||||||
crv: opts.jwk.crv
|
|
||||||
, kty: opts.jwk.kty
|
|
||||||
, x: opts.jwk.x
|
|
||||||
, y: opts.jwk.y
|
|
||||||
};
|
|
||||||
} else if (/RS/i.test(opts.jwk.kty)) {
|
|
||||||
protectedJson.jwk = {
|
|
||||||
e: opts.jwk.e
|
|
||||||
, kty: opts.jwk.kty
|
|
||||||
, n: opts.jwk.n
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return Promise.reject(new Error("[acme.accounts.sign] unsupported key type '" + opts.jwk.kty + "'"));
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('protected:');
|
|
||||||
console.log(protectedJson);
|
|
||||||
var protected64 = BACME._jsto64(
|
|
||||||
protectedJson
|
|
||||||
);
|
|
||||||
|
|
||||||
// Note: this function hashes before signing so send data, not the hash
|
|
||||||
return BACME._sign({
|
|
||||||
abstractKey: abstractKey
|
|
||||||
, payload64: payload64
|
|
||||||
, protected64: protected64
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
var accountId;
|
|
||||||
|
|
||||||
BACME.accounts.set = function (opts) {
|
|
||||||
nonce = null;
|
|
||||||
return window.fetch(accountUrl, {
|
|
||||||
mode: 'cors'
|
|
||||||
, method: 'POST'
|
|
||||||
, headers: { 'Content-Type': 'application/jose+json' }
|
|
||||||
, body: JSON.stringify(opts.signedAccount)
|
|
||||||
}).then(function (resp) {
|
|
||||||
BACME._logHeaders(resp);
|
|
||||||
nonce = resp.headers.get('replay-nonce');
|
|
||||||
accountId = resp.headers.get('location');
|
|
||||||
console.log('Next nonce:', nonce);
|
|
||||||
console.log('Location/kid:', accountId);
|
|
||||||
|
|
||||||
if (!resp.headers.get('content-type')) {
|
|
||||||
console.log('Body: <none>');
|
|
||||||
|
|
||||||
return { kid: accountId };
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp.json().then(function (result) {
|
|
||||||
if (/^Error/i.test(result.detail)) {
|
|
||||||
return Promise.reject(new Error(result.detail));
|
|
||||||
}
|
|
||||||
result.kid = accountId;
|
|
||||||
BACME._logBody(result);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
var orderUrl;
|
|
||||||
|
|
||||||
BACME.orders = {};
|
|
||||||
|
|
||||||
// identifiers = [ { type: 'dns', value: 'example.com' }, { type: 'dns', value: '*.example.com' } ]
|
|
||||||
// signedAccount
|
|
||||||
BACME.orders.sign = function (opts) {
|
|
||||||
var payload64 = BACME._jsto64({ identifiers: opts.identifiers });
|
|
||||||
|
|
||||||
return BACME._importKey(opts.jwk).then(function (abstractKey) {
|
|
||||||
var protected64 = BACME._jsto64(
|
|
||||||
{ nonce: nonce, alg: abstractKey.meta.alg/*'ES256'*/, url: orderUrl, kid: opts.kid }
|
|
||||||
);
|
|
||||||
console.log('abstractKey:');
|
|
||||||
console.log(abstractKey);
|
|
||||||
return BACME._sign({
|
|
||||||
abstractKey: abstractKey
|
|
||||||
, payload64: payload64
|
|
||||||
, protected64: protected64
|
|
||||||
}).then(function (sig) {
|
|
||||||
if (!sig) {
|
|
||||||
throw new Error('sig is undefined... nonsense!');
|
|
||||||
}
|
|
||||||
console.log('newsig', sig);
|
|
||||||
return sig;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
var currentOrderUrl;
|
|
||||||
var authorizationUrls;
|
|
||||||
var finalizeUrl;
|
|
||||||
|
|
||||||
BACME.orders.create = function (opts) {
|
|
||||||
nonce = null;
|
|
||||||
return window.fetch(orderUrl, {
|
|
||||||
mode: 'cors'
|
|
||||||
, method: 'POST'
|
|
||||||
, headers: { 'Content-Type': 'application/jose+json' }
|
|
||||||
, body: JSON.stringify(opts.signedOrder)
|
|
||||||
}).then(function (resp) {
|
|
||||||
BACME._logHeaders(resp);
|
|
||||||
currentOrderUrl = resp.headers.get('location');
|
|
||||||
nonce = resp.headers.get('replay-nonce');
|
|
||||||
console.log('Next nonce:', nonce);
|
|
||||||
|
|
||||||
return resp.json().then(function (result) {
|
|
||||||
if (/^Error/i.test(result.detail)) {
|
|
||||||
return Promise.reject(new Error(result.detail));
|
|
||||||
}
|
|
||||||
authorizationUrls = result.authorizations;
|
|
||||||
finalizeUrl = result.finalize;
|
|
||||||
BACME._logBody(result);
|
|
||||||
|
|
||||||
result.url = currentOrderUrl;
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
BACME.challenges = {};
|
|
||||||
BACME.challenges.all = function () {
|
|
||||||
var challenges = [];
|
|
||||||
|
|
||||||
function next() {
|
|
||||||
if (!authorizationUrls.length) {
|
|
||||||
return challenges;
|
|
||||||
}
|
|
||||||
|
|
||||||
return BACME.challenges.view().then(function (challenge) {
|
|
||||||
challenges.push(challenge);
|
|
||||||
return next();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return next();
|
|
||||||
};
|
|
||||||
BACME.challenges.view = function () {
|
|
||||||
var authzUrl = authorizationUrls.pop();
|
|
||||||
var token;
|
|
||||||
var challengeDomain;
|
|
||||||
var challengeUrl;
|
|
||||||
|
|
||||||
return window.fetch(authzUrl, {
|
|
||||||
mode: 'cors'
|
|
||||||
}).then(function (resp) {
|
|
||||||
BACME._logHeaders(resp);
|
|
||||||
|
|
||||||
return resp.json().then(function (result) {
|
|
||||||
// Note: select the challenge you wish to use
|
|
||||||
var challenge = result.challenges.slice(0).pop();
|
|
||||||
token = challenge.token;
|
|
||||||
challengeUrl = challenge.url;
|
|
||||||
challengeDomain = result.identifier.value;
|
|
||||||
|
|
||||||
BACME._logBody(result);
|
|
||||||
|
|
||||||
return {
|
|
||||||
challenges: result.challenges
|
|
||||||
, expires: result.expires
|
|
||||||
, identifier: result.identifier
|
|
||||||
, status: result.status
|
|
||||||
, wildcard: result.wildcard
|
|
||||||
//, token: challenge.token
|
|
||||||
//, url: challenge.url
|
|
||||||
//, domain: result.identifier.value,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
var thumbprint;
|
|
||||||
var keyAuth;
|
|
||||||
var httpPath;
|
|
||||||
var dnsAuth;
|
|
||||||
var dnsRecord;
|
|
||||||
|
|
||||||
BACME.thumbprint = function (opts) {
|
|
||||||
// https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk
|
|
||||||
|
|
||||||
var accountJwk = opts.jwk;
|
|
||||||
var keys;
|
|
||||||
|
|
||||||
if (/^EC/i.test(opts.jwk.kty)) {
|
|
||||||
keys = [ 'crv', 'kty', 'x', 'y' ];
|
|
||||||
} else if (/^RS/i.test(opts.jwk.kty)) {
|
|
||||||
keys = [ 'e', 'kty', 'n' ];
|
|
||||||
}
|
|
||||||
|
|
||||||
var accountPublicStr = '{' + keys.map(function (key) {
|
|
||||||
return '"' + key + '":"' + accountJwk[key] + '"';
|
|
||||||
}).join(',') + '}';
|
|
||||||
|
|
||||||
return window.crypto.subtle.digest(
|
|
||||||
{ name: "SHA-256" } // SHA-256 is spec'd, non-optional
|
|
||||||
, textEncoder.encode(accountPublicStr)
|
|
||||||
).then(function (hash) {
|
|
||||||
thumbprint = btoa(Array.prototype.map.call(new Uint8Array(hash), function (ch) {
|
|
||||||
return String.fromCharCode(ch);
|
|
||||||
}).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
|
||||||
|
|
||||||
console.log('Thumbprint:');
|
|
||||||
console.log(opts);
|
|
||||||
console.log(accountPublicStr);
|
|
||||||
console.log(thumbprint);
|
|
||||||
|
|
||||||
return thumbprint;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// { token, thumbprint, challengeDomain }
|
|
||||||
BACME.challenges['http-01'] = function (opts) {
|
|
||||||
// The contents of the key authorization file
|
|
||||||
keyAuth = opts.token + '.' + opts.thumbprint;
|
|
||||||
|
|
||||||
// Where the key authorization file goes
|
|
||||||
httpPath = 'http://' + opts.challengeDomain + '/.well-known/acme-challenge/' + opts.token;
|
|
||||||
|
|
||||||
console.log("echo '" + keyAuth + "' > '" + httpPath + "'");
|
|
||||||
|
|
||||||
return {
|
|
||||||
path: httpPath
|
|
||||||
, value: keyAuth
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// { keyAuth }
|
|
||||||
BACME.challenges['dns-01'] = function (opts) {
|
|
||||||
console.log('opts.keyAuth for DNS:');
|
|
||||||
console.log(opts.keyAuth);
|
|
||||||
return window.crypto.subtle.digest(
|
|
||||||
{ name: "SHA-256", }
|
|
||||||
, textEncoder.encode(opts.keyAuth)
|
|
||||||
).then(function (hash) {
|
|
||||||
dnsAuth = btoa(Array.prototype.map.call(new Uint8Array(hash), function (ch) {
|
|
||||||
return String.fromCharCode(ch);
|
|
||||||
}).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
|
||||||
|
|
||||||
dnsRecord = '_acme-challenge.' + opts.challengeDomain;
|
|
||||||
|
|
||||||
console.log('DNS TXT Auth:');
|
|
||||||
// The name of the record
|
|
||||||
console.log(dnsRecord);
|
|
||||||
// The TXT record value
|
|
||||||
console.log(dnsAuth);
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: 'TXT'
|
|
||||||
, host: dnsRecord
|
|
||||||
, answer: dnsAuth
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
var challengePollUrl;
|
|
||||||
|
|
||||||
// { jwk, challengeUrl, accountId (kid) }
|
|
||||||
BACME.challenges.accept = function (opts) {
|
|
||||||
var payload64 = BACME._jsto64({});
|
|
||||||
|
|
||||||
return BACME._importKey(opts.jwk).then(function (abstractKey) {
|
|
||||||
var protected64 = BACME._jsto64(
|
|
||||||
{ nonce: nonce, alg: abstractKey.meta.alg/*'ES256'*/, url: opts.challengeUrl, kid: opts.accountId }
|
|
||||||
);
|
|
||||||
return BACME._sign({
|
|
||||||
abstractKey: abstractKey
|
|
||||||
, payload64: payload64
|
|
||||||
, protected64: protected64
|
|
||||||
});
|
|
||||||
}).then(function (signedAccept) {
|
|
||||||
|
|
||||||
nonce = null;
|
|
||||||
return window.fetch(
|
|
||||||
opts.challengeUrl
|
|
||||||
, { mode: 'cors'
|
|
||||||
, method: 'POST'
|
|
||||||
, headers: { 'Content-Type': 'application/jose+json' }
|
|
||||||
, body: JSON.stringify(signedAccept)
|
|
||||||
}
|
|
||||||
).then(function (resp) {
|
|
||||||
BACME._logHeaders(resp);
|
|
||||||
nonce = resp.headers.get('replay-nonce');
|
|
||||||
console.log("ACCEPT NONCE:", nonce);
|
|
||||||
|
|
||||||
return resp.json().then(function (reply) {
|
|
||||||
challengePollUrl = reply.url;
|
|
||||||
|
|
||||||
console.log('Challenge ACK:');
|
|
||||||
console.log(JSON.stringify(reply));
|
|
||||||
return reply;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
BACME.challenges.check = function (opts) {
|
|
||||||
return window.fetch(opts.challengePollUrl, { mode: 'cors' }).then(function (resp) {
|
|
||||||
BACME._logHeaders(resp);
|
|
||||||
|
|
||||||
return resp.json().then(function (reply) {
|
|
||||||
if (/error/.test(reply.type)) {
|
|
||||||
return Promise.reject(new Error(reply.detail || reply.type));
|
|
||||||
}
|
|
||||||
challengePollUrl = reply.url;
|
|
||||||
|
|
||||||
BACME._logBody(reply);
|
|
||||||
|
|
||||||
return reply;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
var domainKeypair;
|
|
||||||
var domainJwk;
|
|
||||||
|
|
||||||
BACME.generateKeypair = function (opts) {
|
|
||||||
var wcOpts = {};
|
|
||||||
|
|
||||||
// ECDSA has only the P curves and an associated bitlength
|
|
||||||
if (/^EC/i.test(opts.type)) {
|
|
||||||
wcOpts.name = 'ECDSA';
|
|
||||||
if (/256/.test(opts.bitlength)) {
|
|
||||||
wcOpts.namedCurve = 'P-256';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RSA-PSS is another option, but I don't think it's used for Let's Encrypt
|
|
||||||
// I think the hash is only necessary for signing, not generation or import
|
|
||||||
if (/^RS/i.test(opts.type)) {
|
|
||||||
wcOpts.name = 'RSASSA-PKCS1-v1_5';
|
|
||||||
wcOpts.modulusLength = opts.bitlength;
|
|
||||||
if (opts.bitlength < 2048) {
|
|
||||||
wcOpts.modulusLength = opts.bitlength * 8;
|
|
||||||
}
|
|
||||||
wcOpts.publicExponent = new Uint8Array([0x01, 0x00, 0x01]);
|
|
||||||
wcOpts.hash = { name: "SHA-256" };
|
|
||||||
}
|
|
||||||
var extractable = true;
|
|
||||||
return window.crypto.subtle.generateKey(
|
|
||||||
wcOpts
|
|
||||||
, extractable
|
|
||||||
, [ 'sign', 'verify' ]
|
|
||||||
);
|
|
||||||
};
|
|
||||||
BACME.domains = {};
|
|
||||||
// TODO factor out from BACME.accounts.generateKeypair even more
|
|
||||||
BACME.domains.generateKeypair = function (opts) {
|
|
||||||
return BACME.generateKeypair(opts).then(function (result) {
|
|
||||||
domainKeypair = result;
|
|
||||||
|
|
||||||
return window.crypto.subtle.exportKey(
|
|
||||||
"jwk"
|
|
||||||
, result.privateKey
|
|
||||||
).then(function (privJwk) {
|
|
||||||
|
|
||||||
domainJwk = privJwk;
|
|
||||||
console.log('private jwk:');
|
|
||||||
console.log(JSON.stringify(privJwk, null, 2));
|
|
||||||
|
|
||||||
return privJwk;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// { serverJwk, domains }
|
|
||||||
BACME.orders.generateCsr = function (opts) {
|
|
||||||
return BACME._importKey(opts.serverJwk).then(function (abstractKey) {
|
|
||||||
return Promise.resolve(CSR.generate({ keypair: abstractKey.wcKeypair, domains: opts.domains }));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
var certificateUrl;
|
|
||||||
|
|
||||||
// { csr, jwk, finalizeUrl, accountId }
|
|
||||||
BACME.orders.finalize = function (opts) {
|
|
||||||
var payload64 = BACME._jsto64(
|
|
||||||
{ csr: opts.csr }
|
|
||||||
);
|
|
||||||
|
|
||||||
return BACME._importKey(opts.jwk).then(function (abstractKey) {
|
|
||||||
var protected64 = BACME._jsto64(
|
|
||||||
{ nonce: nonce, alg: abstractKey.meta.alg/*'ES256'*/, url: opts.finalizeUrl, kid: opts.accountId }
|
|
||||||
);
|
|
||||||
return BACME._sign({
|
|
||||||
abstractKey: abstractKey
|
|
||||||
, payload64: payload64
|
|
||||||
, protected64: protected64
|
|
||||||
});
|
|
||||||
}).then(function (signedFinal) {
|
|
||||||
|
|
||||||
nonce = null;
|
|
||||||
return window.fetch(
|
|
||||||
opts.finalizeUrl
|
|
||||||
, { mode: 'cors'
|
|
||||||
, method: 'POST'
|
|
||||||
, headers: { 'Content-Type': 'application/jose+json' }
|
|
||||||
, body: JSON.stringify(signedFinal)
|
|
||||||
}
|
|
||||||
).then(function (resp) {
|
|
||||||
BACME._logHeaders(resp);
|
|
||||||
nonce = resp.headers.get('replay-nonce');
|
|
||||||
|
|
||||||
return resp.json().then(function (reply) {
|
|
||||||
if (/error/.test(reply.type)) {
|
|
||||||
return Promise.reject(new Error(reply.detail || reply.type));
|
|
||||||
}
|
|
||||||
certificateUrl = reply.certificate;
|
|
||||||
BACME._logBody(reply);
|
|
||||||
|
|
||||||
return reply;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
BACME.orders.receive = function (opts) {
|
|
||||||
return window.fetch(
|
|
||||||
opts.certificateUrl
|
|
||||||
, { mode: 'cors'
|
|
||||||
, method: 'GET'
|
|
||||||
}
|
|
||||||
).then(function (resp) {
|
|
||||||
BACME._logHeaders(resp);
|
|
||||||
nonce = resp.headers.get('replay-nonce');
|
|
||||||
|
|
||||||
return resp.text().then(function (reply) {
|
|
||||||
BACME._logBody(reply);
|
|
||||||
|
|
||||||
return reply;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
BACME.orders.check = function (opts) {
|
|
||||||
return window.fetch(
|
|
||||||
opts.orderUrl
|
|
||||||
, { mode: 'cors'
|
|
||||||
, method: 'GET'
|
|
||||||
}
|
|
||||||
).then(function (resp) {
|
|
||||||
BACME._logHeaders(resp);
|
|
||||||
|
|
||||||
return resp.json().then(function (reply) {
|
|
||||||
if (/error/.test(reply.type)) {
|
|
||||||
return Promise.reject(new Error(reply.detail || reply.type));
|
|
||||||
}
|
|
||||||
BACME._logBody(reply);
|
|
||||||
|
|
||||||
return reply;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
}(window));
|
|
|
@ -20,6 +20,15 @@
|
||||||
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';
|
||||||
|
|
||||||
|
// fix previous browsers
|
||||||
|
var isCurrent = (localStorage.getItem('version') === VERSION);
|
||||||
|
if (!isCurrent) {
|
||||||
|
localStorage.clear();
|
||||||
|
localStorage.setItem('version', VERSION);
|
||||||
|
}
|
||||||
|
localStorage.setItem('version', VERSION);
|
||||||
|
|
||||||
var challenges = {
|
var challenges = {
|
||||||
'http-01': {
|
'http-01': {
|
||||||
set: function (auth) {
|
set: function (auth) {
|
||||||
|
@ -120,13 +129,6 @@
|
||||||
return Keypairs.generate(RSA_OPTS);
|
return Keypairs.generate(RSA_OPTS);
|
||||||
}
|
}
|
||||||
function testKeypairSupport() {
|
function testKeypairSupport() {
|
||||||
// fix previous browsers
|
|
||||||
var isCurrent = (localStorage.getItem('version') === VERSION);
|
|
||||||
if (!isCurrent) {
|
|
||||||
localStorage.clear();
|
|
||||||
localStorage.setItem('version', VERSION);
|
|
||||||
}
|
|
||||||
localStorage.setItem('version', VERSION);
|
|
||||||
|
|
||||||
return testRsaSupport().then(function () {
|
return testRsaSupport().then(function () {
|
||||||
console.info("[crypto] RSA is supported");
|
console.info("[crypto] RSA is supported");
|
||||||
|
@ -221,7 +223,7 @@
|
||||||
$qs('.js-acme-form-domains').hidden = false;
|
$qs('.js-acme-form-domains').hidden = false;
|
||||||
};
|
};
|
||||||
steps[1].submit = function () {
|
steps[1].submit = function () {
|
||||||
info.domains = $qs('.js-acme-domains').value.replace(/https?:\/\//g, ' ').replace(/,/g, ' ').trim().split(/\s+/g);
|
info.domains = $qs('.js-acme-domains').value.replace(/https?:\/\//g, ' ').replace(/[,+]/g, ' ').trim().split(/\s+/g);
|
||||||
info.identifiers = info.domains.map(function (hostname) {
|
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.
|
||||||
|
|
|
@ -45,7 +45,7 @@
|
||||||
Greenlock will process the CSR in the browser and request the certificates directly from letsencrypt.org.
|
Greenlock will process the CSR in the browser and request the certificates directly from letsencrypt.org.
|
||||||
Please enable Javascript before continuing.
|
Please enable Javascript before continuing.
|
||||||
</div>
|
</div>
|
||||||
<form id="js-acme-form" action="./app/" method=>
|
<form id="js-acme-form" action="./app/" method="GET">
|
||||||
<div class="domain-psuedo-input">
|
<div class="domain-psuedo-input">
|
||||||
<span class="secure-green">Secure</span> | <span class="secure-green">https:</span>//<input aria-label="domains to secure" id="acme-domains" type="text" name="acme-domains" placeholder="Your domain name" required>
|
<span class="secure-green">Secure</span> | <span class="secure-green">https:</span>//<input aria-label="domains to secure" id="acme-domains" type="text" name="acme-domains" placeholder="Your domain name" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
mkdir -p app/js/
|
||||||
|
pushd app/js/
|
||||||
|
wget -c https://rootprojects.org/acme/bluecrypt-acme.js
|
||||||
|
wget -c https://rootprojects.org/acme/bluecrypt-acme.min.js
|
||||||
|
popd
|
||||||
|
|
||||||
mkdir -p app/js/pkijs.org/v1.3.33/
|
mkdir -p app/js/pkijs.org/v1.3.33/
|
||||||
pushd app/js/pkijs.org/v1.3.33/
|
pushd app/js/pkijs.org/v1.3.33/
|
||||||
wget -c https://raw.githubusercontent.com/PeculiarVentures/PKI.js/41b63af760cacb565dd850fb3466ada4ca163eff/org/pkijs/common.js
|
wget -c https://raw.githubusercontent.com/PeculiarVentures/PKI.js/41b63af760cacb565dd850fb3466ada4ca163eff/org/pkijs/common.js
|
||||||
|
|
|
@ -2,11 +2,11 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
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); };
|
|
||||||
|
|
||||||
$qs('.js-javascript-warning').hidden = true;
|
$qs('.js-javascript-warning').hidden = true;
|
||||||
|
|
||||||
var apiUrl = 'https://acme-{{env}}.api.letsencrypt.org/directory';
|
var apiUrl = 'https://acme-{{env}}.api.letsencrypt.org/directory';
|
||||||
|
|
||||||
function updateApiType() {
|
function updateApiType() {
|
||||||
var formData = new FormData($qs("#js-acme-form"));
|
var formData = new FormData($qs("#js-acme-form"));
|
||||||
|
|
||||||
|
@ -15,13 +15,15 @@
|
||||||
var value = formData.get("acme-api-type");
|
var value = formData.get("acme-api-type");
|
||||||
$qs('#js-acme-api-url').value = apiUrl.replace(/{{env}}/g, value);
|
$qs('#js-acme-api-url').value = apiUrl.replace(/{{env}}/g, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
$qs('#js-acme-form').addEventListener('change', updateApiType);
|
$qs('#js-acme-form').addEventListener('change', updateApiType);
|
||||||
|
//$qs('#js-acme-form').addEventListener('submit', prettyRedirect);
|
||||||
|
|
||||||
updateApiType();
|
updateApiType();
|
||||||
try {
|
try {
|
||||||
document.fonts.load().then(function() {
|
document.fonts.load().then(function() {
|
||||||
$qs('body').classList.add("js-app-ready");
|
$qs('body').classList.add("js-app-ready");
|
||||||
}).catch(function(error) {
|
}).catch(function(e) {
|
||||||
$qs('body').classList.add("js-app-ready");
|
$qs('body').classList.add("js-app-ready");
|
||||||
});
|
});
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
|
|
Loading…
Reference in New Issue