diff --git a/app/index.html b/app/index.html
index 72771c3..5bb4e95 100644
--- a/app/index.html
+++ b/app/index.html
@@ -153,9 +153,10 @@
-->
Next
- Why do we need your email? We link your SSL certificates to the
- email you use so you can manage your certificates in the future,
- and get important email updates about them.
+ Why do we need your email?
+ We link your SSL certificates to the email you use so that you'll
+ be notified before the certificate expires and so you can manage
+ your certificates in the future.
diff --git a/app/js/app.js b/app/js/app.js
deleted file mode 100644
index 95ab751..0000000
--- a/app/js/app.js
+++ /dev/null
@@ -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;
-}());
diff --git a/app/js/bacme.js b/app/js/bacme.js
deleted file mode 100644
index fb63042..0000000
--- a/app/js/bacme.js
+++ /dev/null
@@ -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: ');
-
- 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));
diff --git a/app/js/greenlock.js b/app/js/greenlock.js
index dafc748..02442a8 100644
--- a/app/js/greenlock.js
+++ b/app/js/greenlock.js
@@ -20,6 +20,15 @@
var steps = {};
var i = 1;
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 = {
'http-01': {
set: function (auth) {
@@ -120,13 +129,6 @@
return Keypairs.generate(RSA_OPTS);
}
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 () {
console.info("[crypto] RSA is supported");
@@ -221,7 +223,7 @@
$qs('.js-acme-form-domains').hidden = false;
};
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) {
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.
diff --git a/index.html b/index.html
index 6874cc6..4bd744c 100644
--- a/index.html
+++ b/index.html
@@ -45,7 +45,7 @@
Greenlock will process the CSR in the browser and request the certificates directly from letsencrypt.org.
Please enable Javascript before continuing.
-