using new acme
Этот коммит содержится в:
		
							родитель
							
								
									352e334b5d
								
							
						
					
					
						Коммит
						506280f764
					
				@ -153,9 +153,10 @@
 | 
			
		||||
          -->
 | 
			
		||||
          <button class="button-next" type="submit">Next</button>
 | 
			
		||||
          <div class="email-usage">
 | 
			
		||||
            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.
 | 
			
		||||
          </div>
 | 
			
		||||
        </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 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.
 | 
			
		||||
 | 
			
		||||
@ -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.
 | 
			
		||||
        </div>
 | 
			
		||||
        <form id="js-acme-form" action="./app/" method=>
 | 
			
		||||
        <form id="js-acme-form" action="./app/" method="GET">
 | 
			
		||||
          <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>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,11 @@
 | 
			
		||||
#!/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/
 | 
			
		||||
pushd app/js/pkijs.org/v1.3.33/
 | 
			
		||||
  wget -c https://raw.githubusercontent.com/PeculiarVentures/PKI.js/41b63af760cacb565dd850fb3466ada4ca163eff/org/pkijs/common.js
 | 
			
		||||
 | 
			
		||||
@ -2,11 +2,11 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
  var $qs = function (s) { return window.document.querySelector(s); };
 | 
			
		||||
  var $qsa = function (s) { return window.document.querySelectorAll(s); };
 | 
			
		||||
 | 
			
		||||
  $qs('.js-javascript-warning').hidden = true;
 | 
			
		||||
 | 
			
		||||
  var apiUrl = 'https://acme-{{env}}.api.letsencrypt.org/directory';
 | 
			
		||||
 | 
			
		||||
  function updateApiType() {
 | 
			
		||||
    var formData = new FormData($qs("#js-acme-form"));
 | 
			
		||||
 | 
			
		||||
@ -15,13 +15,15 @@
 | 
			
		||||
    var value = formData.get("acme-api-type");
 | 
			
		||||
    $qs('#js-acme-api-url').value = apiUrl.replace(/{{env}}/g, value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $qs('#js-acme-form').addEventListener('change', updateApiType);
 | 
			
		||||
  //$qs('#js-acme-form').addEventListener('submit', prettyRedirect);
 | 
			
		||||
 | 
			
		||||
  updateApiType();
 | 
			
		||||
  try {
 | 
			
		||||
    document.fonts.load().then(function() {
 | 
			
		||||
      $qs('body').classList.add("js-app-ready");
 | 
			
		||||
    }).catch(function(error) {
 | 
			
		||||
    }).catch(function(e) {
 | 
			
		||||
      $qs('body').classList.add("js-app-ready");
 | 
			
		||||
    });
 | 
			
		||||
  } catch(e) {
 | 
			
		||||
 | 
			
		||||
		Загрузка…
	
	
			
			x
			
			
		
	
		Ссылка в новой задаче
	
	Block a user