working RSA CSR!
This commit is contained in:
		
							parent
							
								
									48507da7f4
								
							
						
					
					
						commit
						07f75f8e43
					
				
							
								
								
									
										158
									
								
								app.js
									
									
									
									
									
								
							
							
						
						
									
										158
									
								
								app.js
									
									
									
									
									
								
							@ -6,7 +6,9 @@
 | 
			
		||||
  var Rasha = window.Rasha;
 | 
			
		||||
  var Eckles = window.Eckles;
 | 
			
		||||
  var x509 = window.x509;
 | 
			
		||||
  var CSR = window.CSR;
 | 
			
		||||
  var ACME = window.ACME;
 | 
			
		||||
  var accountStuff = {};
 | 
			
		||||
 | 
			
		||||
  function $(sel) {
 | 
			
		||||
    return document.querySelector(sel);
 | 
			
		||||
@ -15,6 +17,11 @@
 | 
			
		||||
    return Array.prototype.slice.call(document.querySelectorAll(sel));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function checkTos(tos) {
 | 
			
		||||
    console.log("TODO checkbox for agree to terms");
 | 
			
		||||
    return tos;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function run() {
 | 
			
		||||
    console.log('hello');
 | 
			
		||||
 | 
			
		||||
@ -101,6 +108,9 @@
 | 
			
		||||
        $$('input').map(function ($el) { $el.disabled = false; });
 | 
			
		||||
        $$('button').map(function ($el) { $el.disabled = false; });
 | 
			
		||||
        $('.js-toc-jwk').hidden = false;
 | 
			
		||||
 | 
			
		||||
        $('.js-create-account').hidden = false;
 | 
			
		||||
        $('.js-create-csr').hidden = false;
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@ -115,74 +125,17 @@
 | 
			
		||||
        console.log('acme result', result);
 | 
			
		||||
        var privJwk = JSON.parse($('.js-jwk').innerText).private;
 | 
			
		||||
        var email = $('.js-email').value;
 | 
			
		||||
        function checkTos(tos) {
 | 
			
		||||
          console.log("TODO checkbox for agree to terms");
 | 
			
		||||
          return tos;
 | 
			
		||||
        }
 | 
			
		||||
        return acme.accounts.create({
 | 
			
		||||
          email: email
 | 
			
		||||
        , agreeToTerms: checkTos
 | 
			
		||||
        , accountKeypair: { privateKeyJwk: privJwk }
 | 
			
		||||
        }).then(function (account) {
 | 
			
		||||
          console.log("account created result:", account);
 | 
			
		||||
          return Keypairs.generate({
 | 
			
		||||
            kty: 'RSA'
 | 
			
		||||
          , modulusLength: 2048
 | 
			
		||||
          }).then(function (pair) {
 | 
			
		||||
            console.log('domain keypair:', pair);
 | 
			
		||||
            var domains = ($('.js-domains').value||'example.com').split(/[, ]+/g);
 | 
			
		||||
            return acme.certificates.create({
 | 
			
		||||
              accountKeypair: { privateKeyJwk: privJwk }
 | 
			
		||||
            , account: account
 | 
			
		||||
            , domainKeypair: { privateKeyJwk: pair.private }
 | 
			
		||||
            , email: email
 | 
			
		||||
            , domains: domains
 | 
			
		||||
            , agreeToTerms: checkTos
 | 
			
		||||
            , challenges: {
 | 
			
		||||
                'dns-01': {
 | 
			
		||||
                  set: function (opts) {
 | 
			
		||||
                    console.info('dns-01 set challenge:');
 | 
			
		||||
                    console.info('TXT', opts.dnsHost);
 | 
			
		||||
                    console.info(opts.dnsAuthorization);
 | 
			
		||||
                    return new Promise(function (resolve) {
 | 
			
		||||
                      while (!window.confirm("Did you set the challenge?")) {}
 | 
			
		||||
                      resolve();
 | 
			
		||||
                    });
 | 
			
		||||
                  }
 | 
			
		||||
                , remove: function (opts) {
 | 
			
		||||
                    console.log('dns-01 remove challenge:');
 | 
			
		||||
                    console.info('TXT', opts.dnsHost);
 | 
			
		||||
                    console.info(opts.dnsAuthorization);
 | 
			
		||||
                    return new Promise(function (resolve) {
 | 
			
		||||
                      while (!window.confirm("Did you delete the challenge?")) {}
 | 
			
		||||
                      resolve();
 | 
			
		||||
                    });
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
              , 'http-01': {
 | 
			
		||||
                  set: function (opts) {
 | 
			
		||||
                    console.info('http-01 set challenge:');
 | 
			
		||||
                    console.info(opts.challengeUrl);
 | 
			
		||||
                    console.info(opts.keyAuthorization);
 | 
			
		||||
                    return new Promise(function (resolve) {
 | 
			
		||||
                      while (!window.confirm("Did you set the challenge?")) {}
 | 
			
		||||
                      resolve();
 | 
			
		||||
                    });
 | 
			
		||||
                  }
 | 
			
		||||
                , remove: function (opts) {
 | 
			
		||||
                    console.log('http-01 remove challenge:');
 | 
			
		||||
                    console.info(opts.challengeUrl);
 | 
			
		||||
                    console.info(opts.keyAuthorization);
 | 
			
		||||
                    return new Promise(function (resolve) {
 | 
			
		||||
                      while (!window.confirm("Did you delete the challenge?")) {}
 | 
			
		||||
                      resolve();
 | 
			
		||||
                    });
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            , challengeTypes: [$('input[name="acme-challenge-type"]:checked').value]
 | 
			
		||||
            });
 | 
			
		||||
          });
 | 
			
		||||
          accountStuff.account = account;
 | 
			
		||||
          accountStuff.privateJwk = privJwk;
 | 
			
		||||
          accountStuff.email = email;
 | 
			
		||||
          accountStuff.acme = acme;
 | 
			
		||||
          $('.js-create-order').hidden = false;
 | 
			
		||||
        }).catch(function (err) {
 | 
			
		||||
          console.error("A bad thing happened:");
 | 
			
		||||
          console.error(err);
 | 
			
		||||
@ -191,8 +144,87 @@
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $('form.js-csr').addEventListener('submit', function (ev) {
 | 
			
		||||
      ev.preventDefault();
 | 
			
		||||
      ev.stopPropagation();
 | 
			
		||||
      var domains = ($('.js-domains').value||'example.com').split(/[, ]+/g);
 | 
			
		||||
      var privJwk = JSON.parse($('.js-jwk').innerText).private;
 | 
			
		||||
      return CSR({ jwk: privJwk, domains: domains }).then(function (web64) {
 | 
			
		||||
        // Verify with https://www.sslshopper.com/csr-decoder.html
 | 
			
		||||
        console.log('urlBase64 CSR:');
 | 
			
		||||
        console.log(web64);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $('form.js-acme-order').addEventListener('submit', function (ev) {
 | 
			
		||||
      ev.preventDefault();
 | 
			
		||||
      ev.stopPropagation();
 | 
			
		||||
      var account = accountStuff.account;
 | 
			
		||||
      var privJwk = accountStuff.privateJwk;
 | 
			
		||||
      var email = accountStuff.email;
 | 
			
		||||
      var acme = accountStuff.acme;
 | 
			
		||||
 | 
			
		||||
      return Keypairs.generate({
 | 
			
		||||
        kty: 'RSA'
 | 
			
		||||
      , modulusLength: 2048
 | 
			
		||||
      }).then(function (pair) {
 | 
			
		||||
        console.log('domain keypair:', pair);
 | 
			
		||||
        var domains = ($('.js-domains').value||'example.com').split(/[, ]+/g);
 | 
			
		||||
        return acme.certificates.create({
 | 
			
		||||
          accountKeypair: { privateKeyJwk: privJwk }
 | 
			
		||||
        , account: account
 | 
			
		||||
        , domainKeypair: { privateKeyJwk: pair.private }
 | 
			
		||||
        , email: email
 | 
			
		||||
        , domains: domains
 | 
			
		||||
        , agreeToTerms: checkTos
 | 
			
		||||
        , challenges: {
 | 
			
		||||
            'dns-01': {
 | 
			
		||||
              set: function (opts) {
 | 
			
		||||
                console.info('dns-01 set challenge:');
 | 
			
		||||
                console.info('TXT', opts.dnsHost);
 | 
			
		||||
                console.info(opts.dnsAuthorization);
 | 
			
		||||
                return new Promise(function (resolve) {
 | 
			
		||||
                  while (!window.confirm("Did you set the challenge?")) {}
 | 
			
		||||
                  resolve();
 | 
			
		||||
                });
 | 
			
		||||
              }
 | 
			
		||||
            , remove: function (opts) {
 | 
			
		||||
                console.log('dns-01 remove challenge:');
 | 
			
		||||
                console.info('TXT', opts.dnsHost);
 | 
			
		||||
                console.info(opts.dnsAuthorization);
 | 
			
		||||
                return new Promise(function (resolve) {
 | 
			
		||||
                  while (!window.confirm("Did you delete the challenge?")) {}
 | 
			
		||||
                  resolve();
 | 
			
		||||
                });
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          , 'http-01': {
 | 
			
		||||
              set: function (opts) {
 | 
			
		||||
                console.info('http-01 set challenge:');
 | 
			
		||||
                console.info(opts.challengeUrl);
 | 
			
		||||
                console.info(opts.keyAuthorization);
 | 
			
		||||
                return new Promise(function (resolve) {
 | 
			
		||||
                  while (!window.confirm("Did you set the challenge?")) {}
 | 
			
		||||
                  resolve();
 | 
			
		||||
                });
 | 
			
		||||
              }
 | 
			
		||||
            , remove: function (opts) {
 | 
			
		||||
                console.log('http-01 remove challenge:');
 | 
			
		||||
                console.info(opts.challengeUrl);
 | 
			
		||||
                console.info(opts.keyAuthorization);
 | 
			
		||||
                return new Promise(function (resolve) {
 | 
			
		||||
                  while (!window.confirm("Did you delete the challenge?")) {}
 | 
			
		||||
                  resolve();
 | 
			
		||||
                });
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        , challengeTypes: [$('input[name="acme-challenge-type"]:checked').value]
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $('.js-generate').hidden = false;
 | 
			
		||||
    $('.js-create-account').hidden = false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  window.addEventListener('load', run);
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										14
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								index.html
									
									
									
									
									
								
							@ -58,15 +58,26 @@
 | 
			
		||||
      <label for="-acmeEmail">Email:</label>
 | 
			
		||||
      <input class="js-email" type="email" id="-acmeEmail">
 | 
			
		||||
      <br>
 | 
			
		||||
      <button class="js-create-account" hidden>Create Account</button>
 | 
			
		||||
    </form>
 | 
			
		||||
 | 
			
		||||
    <h2>Certificate Signing Request</h2>
 | 
			
		||||
    <form class="js-csr">
 | 
			
		||||
      <label for="-acmeDomains">Domains:</label>
 | 
			
		||||
      <input class="js-domains" type="text" id="-acmeDomains">
 | 
			
		||||
      <br>
 | 
			
		||||
      <button class="js-create-csr" hidden>Create CSR</button>
 | 
			
		||||
    </form>
 | 
			
		||||
 | 
			
		||||
    <h2>ACME Certificate Order</h2>
 | 
			
		||||
    <form class="js-acme-order">
 | 
			
		||||
      Challenge type:
 | 
			
		||||
      <label for="-http01"><input type="radio" id="-http01"
 | 
			
		||||
       name="acme-challenge-type" value="http-01" checked>http-01</label>
 | 
			
		||||
      <label for="-dns01"><input type="radio" id="-dns01"
 | 
			
		||||
       name="acme-challenge-type" value="dns-01">dns-01</label>
 | 
			
		||||
      <br>
 | 
			
		||||
      <button class="js-create-account" hidden>Create Account</button>
 | 
			
		||||
      <button class="js-create-order" hidden>Create Order</button>
 | 
			
		||||
    </form>
 | 
			
		||||
 | 
			
		||||
    <div class="js-loading" hidden>Loading</div>
 | 
			
		||||
@ -117,6 +128,7 @@
 | 
			
		||||
    <script src="./lib/ecdsa.js"></script>
 | 
			
		||||
    <script src="./lib/rsa.js"></script>
 | 
			
		||||
    <script src="./lib/keypairs.js"></script>
 | 
			
		||||
    <script src="./lib/csr.js"></script>
 | 
			
		||||
    <script src="./lib/acme.js"></script>
 | 
			
		||||
    <script src="./app.js"></script>
 | 
			
		||||
  </body>
 | 
			
		||||
 | 
			
		||||
@ -110,6 +110,8 @@ Enc.binToHex = function (bin) {
 | 
			
		||||
    return h;
 | 
			
		||||
  }).join('');
 | 
			
		||||
};
 | 
			
		||||
// TODO are there any nuance differences here?
 | 
			
		||||
Enc.utf8ToHex = Enc.binToHex;
 | 
			
		||||
 | 
			
		||||
Enc.hexToBase64 = function (hex) {
 | 
			
		||||
  return btoa(Enc.hexToBin(hex));
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										78
									
								
								lib/csr.js
									
									
									
									
									
								
							
							
						
						
									
										78
									
								
								lib/csr.js
									
									
									
									
									
								
							@ -1,14 +1,18 @@
 | 
			
		||||
// Copyright 2018-present AJ ONeal. All rights reserved
 | 
			
		||||
/* This Source Code Form is subject to the terms of the Mozilla Public
 | 
			
		||||
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 | 
			
		||||
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 | 
			
		||||
(function (exports) {
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
var crypto = require('crypto');
 | 
			
		||||
var ASN1 = require('./asn1.js');
 | 
			
		||||
var Enc = require('./encoding.js');
 | 
			
		||||
var PEM = require('./pem.js');
 | 
			
		||||
var X509 = require('./x509.js');
 | 
			
		||||
var RSA = {};
 | 
			
		||||
var ASN1 = exports.ASN1;
 | 
			
		||||
var Enc = exports.Enc;
 | 
			
		||||
var PEM = exports.PEM;
 | 
			
		||||
var X509 = exports.x509;
 | 
			
		||||
var Keypairs = exports.Keypairs;
 | 
			
		||||
 | 
			
		||||
/*global Promise*/
 | 
			
		||||
var CSR = module.exports = function rsacsr(opts) {
 | 
			
		||||
// TODO find a way that the prior node-ish way of `module.exports = function () {}` isn't broken
 | 
			
		||||
var CSR = exports.CSR = function (opts) {
 | 
			
		||||
  // We're using a Promise here to be compatible with the browser version
 | 
			
		||||
  // which will probably use the webcrypto API for some of the conversions
 | 
			
		||||
  opts = CSR._prepare(opts);
 | 
			
		||||
@ -69,11 +73,7 @@ CSR._prepare = function (opts) {
 | 
			
		||||
  opts.jwk = jwk;
 | 
			
		||||
  return opts;
 | 
			
		||||
};
 | 
			
		||||
CSR.sync = function (opts) {
 | 
			
		||||
  opts = CSR._prepare(opts);
 | 
			
		||||
  var bytes = CSR.createSync(opts);
 | 
			
		||||
  return CSR._encode(opts, bytes);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
CSR._encode = function (opts, bytes) {
 | 
			
		||||
  if ('der' === (opts.encoding||'').toLowerCase()) {
 | 
			
		||||
    return bytes;
 | 
			
		||||
@ -84,11 +84,6 @@ CSR._encode = function (opts, bytes) {
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
CSR.createSync = function createCsr(opts) {
 | 
			
		||||
  var hex = CSR.request(opts.jwk, opts.domains);
 | 
			
		||||
  var csr = CSR.signSync(opts.jwk, hex);
 | 
			
		||||
  return Enc.hexToBuf(csr);
 | 
			
		||||
};
 | 
			
		||||
CSR.create = function createCsr(opts) {
 | 
			
		||||
  var hex = CSR.request(opts.jwk, opts.domains);
 | 
			
		||||
  return CSR.sign(opts.jwk, hex).then(function (csr) {
 | 
			
		||||
@ -96,22 +91,25 @@ CSR.create = function createCsr(opts) {
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
//
 | 
			
		||||
// RSA
 | 
			
		||||
//
 | 
			
		||||
//
 | 
			
		||||
CSR.request = function createCsrBodyEc(jwk, domains) {
 | 
			
		||||
  var asn1pub = X509.packCsrPublicKey(jwk);
 | 
			
		||||
  return X509.packCsr(asn1pub, domains);
 | 
			
		||||
  var asn1pub = X509.packCsrRsaPublicKey(jwk);
 | 
			
		||||
  return X509.packCsrRsa(asn1pub, domains);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
CSR.signSync = function csrEcSig(jwk, request) {
 | 
			
		||||
  var keypem = PEM.packBlock({ type: "RSA PRIVATE KEY", bytes: X509.packPkcs1(jwk) });
 | 
			
		||||
  var sig = RSA.signSync(keypem, Enc.hexToBuf(request));
 | 
			
		||||
  return CSR.toDer({ request: request, signature: sig });
 | 
			
		||||
};
 | 
			
		||||
CSR.sign = function csrEcSig(jwk, request) {
 | 
			
		||||
  var keypem = PEM.packBlock({ type: "RSA PRIVATE KEY", bytes: X509.packPkcs1(jwk) });
 | 
			
		||||
  return RSA.sign(keypem, Enc.hexToBuf(request)).then(function (sig) {
 | 
			
		||||
  // Took some tips from https://gist.github.com/codermapuche/da4f96cdb6d5ff53b7ebc156ec46a10a
 | 
			
		||||
  // TODO will have to convert web ECDSA signatures to PEM ECDSA signatures (but RSA should be the same)
 | 
			
		||||
  // TODO have a consistent non-private way to sign
 | 
			
		||||
  return Keypairs._sign({ jwk: jwk }, Enc.hexToBuf(request)).then(function (sig) {
 | 
			
		||||
    return CSR.toDer({ request: request, signature: sig });
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CSR.toDer = function encode(opts) {
 | 
			
		||||
  var sty = ASN1('30'
 | 
			
		||||
    // 1.2.840.113549.1.1.11 sha256WithRSAEncryption (PKCS #1)
 | 
			
		||||
@ -128,30 +126,6 @@ CSR.toDer = function encode(opts) {
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
//
 | 
			
		||||
// RSA
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
// Took some tips from https://gist.github.com/codermapuche/da4f96cdb6d5ff53b7ebc156ec46a10a
 | 
			
		||||
RSA.signSync = function signRsaSync(keypem, ab) {
 | 
			
		||||
  // Signer is a stream
 | 
			
		||||
  var sign = crypto.createSign('SHA256');
 | 
			
		||||
  sign.write(new Uint8Array(ab));
 | 
			
		||||
  sign.end();
 | 
			
		||||
 | 
			
		||||
  // The signature is ASN1 encoded, as it turns out
 | 
			
		||||
  var sig = sign.sign(keypem);
 | 
			
		||||
 | 
			
		||||
  // Convert to a JavaScript ArrayBuffer just because
 | 
			
		||||
  return new Uint8Array(sig.buffer.slice(sig.byteOffset, sig.byteOffset + sig.byteLength));
 | 
			
		||||
};
 | 
			
		||||
RSA.sign = function signRsa(keypem, ab) {
 | 
			
		||||
  return Promise.resolve().then(function () {
 | 
			
		||||
    return RSA.signSync(keypem, ab);
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
X509.packCsrRsa = function (asn1pubkey, domains) {
 | 
			
		||||
  return ASN1('30'
 | 
			
		||||
    // Version (0)
 | 
			
		||||
@ -211,3 +185,5 @@ X509.packCsrRsaPublicKey = function (jwk) {
 | 
			
		||||
  // Add the CSR pub key header
 | 
			
		||||
  return ASN1('30', ASN1('30', ASN1('06', '2a864886f70d010101'), ASN1('05')), ASN1.BitStr(asn1pub));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}('undefined' === typeof window ? module.exports : window));
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user