forked from root/acme.js
		
	making headway
This commit is contained in:
		
							parent
							
								
									bfc4ab6795
								
							
						
					
					
						commit
						692301e37d
					
				
							
								
								
									
										2
									
								
								app.js
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								app.js
									
									
									
									
									
								
							@ -41,7 +41,7 @@ function run() {
 | 
			
		||||
    , namedCurve: $('input[name="ec-crv"]:checked').value
 | 
			
		||||
    , modulusLength: $('input[name="rsa-len"]:checked').value
 | 
			
		||||
    };
 | 
			
		||||
    console.log(opts);
 | 
			
		||||
    console.log('opts', opts);
 | 
			
		||||
    Keypairs.generate(opts).then(function (results) {
 | 
			
		||||
      $('.js-jwk').innerText = JSON.stringify(results, null, 2);
 | 
			
		||||
      //
 | 
			
		||||
 | 
			
		||||
@ -48,6 +48,8 @@
 | 
			
		||||
    <div class="js-loading" hidden>Loading</div>
 | 
			
		||||
    <pre><code class="js-jwk"> </code></pre>
 | 
			
		||||
 | 
			
		||||
    <script src="./lib/ecdsa.js"></script>
 | 
			
		||||
    <script src="./lib/rsa.js"></script>
 | 
			
		||||
    <script src="./lib/keypairs.js"></script>
 | 
			
		||||
    <script src="./app.js"></script>
 | 
			
		||||
  </body>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										699
									
								
								lib/acme.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										699
									
								
								lib/acme.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,699 @@
 | 
			
		||||
/*global CSR*/
 | 
			
		||||
// CSR takes a while to load after the page load
 | 
			
		||||
(function (exports) {
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
var BACME = exports.ACME = {};
 | 
			
		||||
var webFetch = exports.fetch;
 | 
			
		||||
var Keypairs = exports.Keypairs;
 | 
			
		||||
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));
 | 
			
		||||
							
								
								
									
										112
									
								
								lib/ecdsa.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								lib/ecdsa.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,112 @@
 | 
			
		||||
/*global Promise*/
 | 
			
		||||
(function (exports) {
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
var EC = exports.Eckles = {};
 | 
			
		||||
if ('undefined' !== typeof module) { module.exports = EC; }
 | 
			
		||||
var Enc = {};
 | 
			
		||||
var textEncoder = new TextEncoder();
 | 
			
		||||
 | 
			
		||||
EC._stance = "We take the stance that if you're knowledgeable enough to"
 | 
			
		||||
  + " properly and securely use non-standard crypto then you shouldn't need Bluecrypt anyway.";
 | 
			
		||||
EC._universal = "Bluecrypt only supports crypto with standard cross-browser and cross-platform support.";
 | 
			
		||||
EC.generate = function (opts) {
 | 
			
		||||
  var wcOpts = {};
 | 
			
		||||
  if (!opts) { opts = {}; }
 | 
			
		||||
  if (!opts.kty) { opts.kty = 'EC'; }
 | 
			
		||||
 | 
			
		||||
  // ECDSA has only the P curves and an associated bitlength
 | 
			
		||||
  wcOpts.name = 'ECDSA';
 | 
			
		||||
  if (!opts.namedCurve) {
 | 
			
		||||
    opts.namedCurve = 'P-256';
 | 
			
		||||
  }
 | 
			
		||||
  wcOpts.namedCurve = opts.namedCurve; // true for supported curves
 | 
			
		||||
  if (/256/.test(wcOpts.namedCurve)) {
 | 
			
		||||
    wcOpts.namedCurve = 'P-256';
 | 
			
		||||
    wcOpts.hash = { name: "SHA-256" };
 | 
			
		||||
  } else if (/384/.test(wcOpts.namedCurve)) {
 | 
			
		||||
    wcOpts.namedCurve = 'P-384';
 | 
			
		||||
    wcOpts.hash = { name: "SHA-384" };
 | 
			
		||||
  } else {
 | 
			
		||||
    return Promise.Reject(new Error("'" + wcOpts.namedCurve + "' is not an NIST approved ECDSA namedCurve. "
 | 
			
		||||
      + " Please choose either 'P-256' or 'P-384'. "
 | 
			
		||||
      + EC._stance));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  var extractable = true;
 | 
			
		||||
  return window.crypto.subtle.generateKey(
 | 
			
		||||
    wcOpts
 | 
			
		||||
  , extractable
 | 
			
		||||
  , [ 'sign', 'verify' ]
 | 
			
		||||
  ).then(function (result) {
 | 
			
		||||
    return window.crypto.subtle.exportKey(
 | 
			
		||||
      "jwk"
 | 
			
		||||
    , result.privateKey
 | 
			
		||||
    ).then(function (privJwk) {
 | 
			
		||||
      return {
 | 
			
		||||
        private: privJwk
 | 
			
		||||
      , public: EC.neuter({ jwk: privJwk })
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Chopping off the private parts is now part of the public API.
 | 
			
		||||
// I thought it sounded a little too crude at first, but it really is the best name in every possible way.
 | 
			
		||||
EC.neuter = function (opts) {
 | 
			
		||||
  // trying to find the best balance of an immutable copy with custom attributes
 | 
			
		||||
  var jwk = {};
 | 
			
		||||
  Object.keys(opts.jwk).forEach(function (k) {
 | 
			
		||||
    if ('undefined' === typeof opts.jwk[k]) { return; }
 | 
			
		||||
    // ignore EC private parts
 | 
			
		||||
    if ('d' === k) { return; }
 | 
			
		||||
    jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k]));
 | 
			
		||||
  });
 | 
			
		||||
  return jwk;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk
 | 
			
		||||
EC.__thumbprint = function (jwk) {
 | 
			
		||||
  // Use the same entropy for SHA as for key
 | 
			
		||||
  var alg = 'SHA-256';
 | 
			
		||||
  if (/384/.test(jwk.crv)) {
 | 
			
		||||
    alg = 'SHA-384';
 | 
			
		||||
  }
 | 
			
		||||
  return window.crypto.subtle.digest(
 | 
			
		||||
    { name: alg }
 | 
			
		||||
  , textEncoder.encode('{"crv":"' + jwk.crv + '","kty":"EC","x":"' + jwk.x + '","y":"' + jwk.y + '"}')
 | 
			
		||||
  ).then(function (hash) {
 | 
			
		||||
    return Enc.bufToUrlBase64(new Uint8Array(hash));
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
EC.thumbprint = function (opts) {
 | 
			
		||||
  return Promise.resolve().then(function () {
 | 
			
		||||
    var jwk;
 | 
			
		||||
    if ('EC' === opts.kty) {
 | 
			
		||||
      jwk = opts;
 | 
			
		||||
    } else if (opts.jwk) {
 | 
			
		||||
      jwk = opts.jwk;
 | 
			
		||||
    } else {
 | 
			
		||||
      return EC.import(opts).then(function (jwk) {
 | 
			
		||||
        return EC.__thumbprint(jwk);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    return EC.__thumbprint(jwk);
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
Enc.bufToUrlBase64 = function (u8) {
 | 
			
		||||
  return Enc.bufToBase64(u8)
 | 
			
		||||
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
Enc.bufToBase64 = function (u8) {
 | 
			
		||||
  var bin = '';
 | 
			
		||||
  u8.forEach(function (i) {
 | 
			
		||||
    bin += String.fromCharCode(i);
 | 
			
		||||
  });
 | 
			
		||||
  return btoa(bin);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}('undefined' !== typeof module ? module.exports : window));
 | 
			
		||||
							
								
								
									
										159
									
								
								lib/keypairs.js
									
									
									
									
									
								
							
							
						
						
									
										159
									
								
								lib/keypairs.js
									
									
									
									
									
								
							@ -3,84 +3,107 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
var Keypairs = exports.Keypairs = {};
 | 
			
		||||
var Rasha = exports.Rasha || require('rasha');
 | 
			
		||||
var Eckles = exports.Eckles || require('eckles');
 | 
			
		||||
 | 
			
		||||
Keypairs._stance = "We take the stance that if you're knowledgeable enough to"
 | 
			
		||||
  + " properly and securely use non-standard crypto then you shouldn't need Bluecrypt anyway.";
 | 
			
		||||
Keypairs._universal = "Bluecrypt only supports crypto with standard cross-browser and cross-platform support.";
 | 
			
		||||
Keypairs.generate = function (opts) {
 | 
			
		||||
  var wcOpts = {};
 | 
			
		||||
  if (!opts) {
 | 
			
		||||
    opts = {};
 | 
			
		||||
  }
 | 
			
		||||
  if (!opts.kty) {
 | 
			
		||||
    opts.kty = 'EC';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // ECDSA has only the P curves and an associated bitlength
 | 
			
		||||
  opts = opts || {};
 | 
			
		||||
  var p;
 | 
			
		||||
  if (!opts.kty) { opts.kty = opts.type; }
 | 
			
		||||
  if (!opts.kty) { opts.kty = 'EC'; }
 | 
			
		||||
  if (/^EC/i.test(opts.kty)) {
 | 
			
		||||
    wcOpts.name = 'ECDSA';
 | 
			
		||||
    if (!opts.namedCurve) {
 | 
			
		||||
      opts.namedCurve = 'P-256';
 | 
			
		||||
    }
 | 
			
		||||
    wcOpts.namedCurve = opts.namedCurve; // true for supported curves
 | 
			
		||||
    if (/256/.test(wcOpts.namedCurve)) {
 | 
			
		||||
      wcOpts.namedCurve = 'P-256';
 | 
			
		||||
      wcOpts.hash = { name: "SHA-256" };
 | 
			
		||||
    } else if (/384/.test(wcOpts.namedCurve)) {
 | 
			
		||||
      wcOpts.namedCurve = 'P-384';
 | 
			
		||||
      wcOpts.hash = { name: "SHA-384" };
 | 
			
		||||
    } else {
 | 
			
		||||
      return Promise.Reject(new Error("'" + wcOpts.namedCurve + "' is not an NIST approved ECDSA namedCurve. "
 | 
			
		||||
        + " Please choose either 'P-256' or 'P-384'. "
 | 
			
		||||
        + Keypairs._stance));
 | 
			
		||||
    }
 | 
			
		||||
    p = Eckles.generate(opts);
 | 
			
		||||
  } else if (/^RSA$/i.test(opts.kty)) {
 | 
			
		||||
    // Support PSS? I don't think it's used for Let's Encrypt
 | 
			
		||||
    wcOpts.name = 'RSASSA-PKCS1-v1_5';
 | 
			
		||||
    if (!opts.modulusLength) {
 | 
			
		||||
      opts.modulusLength = 2048;
 | 
			
		||||
    }
 | 
			
		||||
    wcOpts.modulusLength = opts.modulusLength;
 | 
			
		||||
    if (wcOpts.modulusLength >= 2048 && wcOpts.modulusLength < 3072) {
 | 
			
		||||
      // erring on the small side... for no good reason
 | 
			
		||||
      wcOpts.hash = { name: "SHA-256" };
 | 
			
		||||
    } else if (wcOpts.modulusLength >= 3072 && wcOpts.modulusLength < 4096) {
 | 
			
		||||
      wcOpts.hash = { name: "SHA-384" };
 | 
			
		||||
    } else if (wcOpts.modulusLength < 4097) {
 | 
			
		||||
      wcOpts.hash = { name: "SHA-512" };
 | 
			
		||||
    } else {
 | 
			
		||||
      // Public key thumbprints should be paired with a hash of similar length,
 | 
			
		||||
      // so anything above SHA-512's keyspace would be left under-represented anyway.
 | 
			
		||||
      return Promise.Reject(new Error("'" + wcOpts.modulusLength + "' is not within the safe and universally"
 | 
			
		||||
        + " acceptable range of 2048-4096. Typically you should pick 2048, 3072, or 4096, though other values"
 | 
			
		||||
        + " divisible by 8 are allowed. " + Keypairs._stance));
 | 
			
		||||
    }
 | 
			
		||||
    // TODO maybe allow this to be set to any of the standard values?
 | 
			
		||||
    wcOpts.publicExponent = new Uint8Array([0x01, 0x00, 0x01]);
 | 
			
		||||
    p = Rasha.generate(opts);
 | 
			
		||||
  } else {
 | 
			
		||||
    return Promise.Reject(new Error("'" + opts.kty + "' is not a well-supported key type."
 | 
			
		||||
      + Keypairs._universal
 | 
			
		||||
      + " Please choose either 'EC' or 'RSA' keys."));
 | 
			
		||||
      + " Please choose 'EC', or 'RSA' if you have good reason to."));
 | 
			
		||||
  }
 | 
			
		||||
  return p.then(function (pair) {
 | 
			
		||||
    return Keypairs.thumbprint({ jwk: pair.public }).then(function (thumb) {
 | 
			
		||||
      pair.private.kid = thumb; // maybe not the same id on the private key?
 | 
			
		||||
      pair.public.kid = thumb;
 | 
			
		||||
      return pair;
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// Chopping off the private parts is now part of the public API.
 | 
			
		||||
// I thought it sounded a little too crude at first, but it really is the best name in every possible way.
 | 
			
		||||
Keypairs.neuter = Keypairs._neuter = function (opts) {
 | 
			
		||||
  // trying to find the best balance of an immutable copy with custom attributes
 | 
			
		||||
  var jwk = {};
 | 
			
		||||
  Object.keys(opts.jwk).forEach(function (k) {
 | 
			
		||||
    if ('undefined' === typeof opts.jwk[k]) { return; }
 | 
			
		||||
    // ignore RSA and EC private parts
 | 
			
		||||
    if (-1 !== ['d', 'p', 'q', 'dp', 'dq', 'qi'].indexOf(k)) { return; }
 | 
			
		||||
    jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k]));
 | 
			
		||||
  });
 | 
			
		||||
  return jwk;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
Keypairs.thumbprint = function (opts) {
 | 
			
		||||
  return Promise.resolve().then(function () {
 | 
			
		||||
    if (/EC/i.test(opts.jwk.kty)) {
 | 
			
		||||
      return Eckles.thumbprint(opts);
 | 
			
		||||
    } else {
 | 
			
		||||
      return Rasha.thumbprint(opts);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
Keypairs.publish = function (opts) {
 | 
			
		||||
  if ('object' !== typeof opts.jwk || !opts.jwk.kty) { throw new Error("invalid jwk: " + JSON.stringify(opts.jwk)); }
 | 
			
		||||
 | 
			
		||||
  // returns a copy
 | 
			
		||||
  var jwk = Keypairs.neuter(opts);
 | 
			
		||||
 | 
			
		||||
  if (jwk.exp) {
 | 
			
		||||
    jwk.exp = setTime(jwk.exp);
 | 
			
		||||
  } else {
 | 
			
		||||
    if (opts.exp) { jwk.exp = setTime(opts.exp); }
 | 
			
		||||
    else if (opts.expiresIn) { jwk.exp = Math.round(Date.now()/1000) + opts.expiresIn; }
 | 
			
		||||
    else if (opts.expiresAt) { jwk.exp = opts.expiresAt; }
 | 
			
		||||
  }
 | 
			
		||||
  if (!jwk.use && false !== jwk.use) { jwk.use = "sig"; }
 | 
			
		||||
 | 
			
		||||
  if (jwk.kid) { return Promise.resolve(jwk); }
 | 
			
		||||
  return Keypairs.thumbprint({ jwk: jwk }).then(function (thumb) { jwk.kid = thumb; return jwk; });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function setTime(time) {
 | 
			
		||||
  if ('number' === typeof time) { return time; }
 | 
			
		||||
 | 
			
		||||
  var t = time.match(/^(\-?\d+)([dhms])$/i);
 | 
			
		||||
  if (!t || !t[0]) {
 | 
			
		||||
    throw new Error("'" + time + "' should be datetime in seconds or human-readable format (i.e. 3d, 1h, 15m, 30s");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  var extractable = true;
 | 
			
		||||
  return window.crypto.subtle.generateKey(
 | 
			
		||||
    wcOpts
 | 
			
		||||
  , extractable
 | 
			
		||||
  , [ 'sign', 'verify' ]
 | 
			
		||||
  ).then(function (result) {
 | 
			
		||||
    return window.crypto.subtle.exportKey(
 | 
			
		||||
      "jwk"
 | 
			
		||||
    , result.privateKey
 | 
			
		||||
    ).then(function (privJwk) {
 | 
			
		||||
      // TODO remove
 | 
			
		||||
      console.log('private jwk:');
 | 
			
		||||
      console.log(JSON.stringify(privJwk, null, 2));
 | 
			
		||||
      return {
 | 
			
		||||
        privateKey: privJwk
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
  var now = Math.round(Date.now()/1000);
 | 
			
		||||
  var num = parseInt(t[1], 10);
 | 
			
		||||
  var unit = t[2];
 | 
			
		||||
  var mult = 1;
 | 
			
		||||
  switch(unit) {
 | 
			
		||||
    // fancy fallthrough, what fun!
 | 
			
		||||
    case 'd':
 | 
			
		||||
      mult *= 24;
 | 
			
		||||
      /*falls through*/
 | 
			
		||||
    case 'h':
 | 
			
		||||
      mult *= 60;
 | 
			
		||||
      /*falls through*/
 | 
			
		||||
    case 'm':
 | 
			
		||||
      mult *= 60;
 | 
			
		||||
      /*falls through*/
 | 
			
		||||
    case 's':
 | 
			
		||||
      mult *= 1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}(window));
 | 
			
		||||
  return now + (mult * num);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}('undefined' !== typeof module ? module.exports : window));
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										86
									
								
								lib/keypairs.js.min2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								lib/keypairs.js.min2
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,86 @@
 | 
			
		||||
/*global Promise*/
 | 
			
		||||
(function (exports) {
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
var Keypairs = exports.Keypairs = {};
 | 
			
		||||
 | 
			
		||||
Keypairs._stance = "We take the stance that if you're knowledgeable enough to"
 | 
			
		||||
  + " properly and securely use non-standard crypto then you shouldn't need Bluecrypt anyway.";
 | 
			
		||||
Keypairs._universal = "Bluecrypt only supports crypto with standard cross-browser and cross-platform support.";
 | 
			
		||||
Keypairs.generate = function (opts) {
 | 
			
		||||
  var wcOpts = {};
 | 
			
		||||
  if (!opts) {
 | 
			
		||||
    opts = {};
 | 
			
		||||
  }
 | 
			
		||||
  if (!opts.kty) {
 | 
			
		||||
    opts.kty = 'EC';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // ECDSA has only the P curves and an associated bitlength
 | 
			
		||||
  if (/^EC/i.test(opts.kty)) {
 | 
			
		||||
    wcOpts.name = 'ECDSA';
 | 
			
		||||
    if (!opts.namedCurve) {
 | 
			
		||||
      opts.namedCurve = 'P-256';
 | 
			
		||||
    }
 | 
			
		||||
    wcOpts.namedCurve = opts.namedCurve; // true for supported curves
 | 
			
		||||
    if (/256/.test(wcOpts.namedCurve)) {
 | 
			
		||||
      wcOpts.namedCurve = 'P-256';
 | 
			
		||||
      wcOpts.hash = { name: "SHA-256" };
 | 
			
		||||
    } else if (/384/.test(wcOpts.namedCurve)) {
 | 
			
		||||
      wcOpts.namedCurve = 'P-384';
 | 
			
		||||
      wcOpts.hash = { name: "SHA-384" };
 | 
			
		||||
    } else {
 | 
			
		||||
      return Promise.Reject(new Error("'" + wcOpts.namedCurve + "' is not an NIST approved ECDSA namedCurve. "
 | 
			
		||||
        + " Please choose either 'P-256' or 'P-384'. "
 | 
			
		||||
        + Keypairs._stance));
 | 
			
		||||
    }
 | 
			
		||||
  } else if (/^RSA$/i.test(opts.kty)) {
 | 
			
		||||
    // Support PSS? I don't think it's used for Let's Encrypt
 | 
			
		||||
    wcOpts.name = 'RSASSA-PKCS1-v1_5';
 | 
			
		||||
    if (!opts.modulusLength) {
 | 
			
		||||
      opts.modulusLength = 2048;
 | 
			
		||||
    }
 | 
			
		||||
    wcOpts.modulusLength = opts.modulusLength;
 | 
			
		||||
    if (wcOpts.modulusLength >= 2048 && wcOpts.modulusLength < 3072) {
 | 
			
		||||
      // erring on the small side... for no good reason
 | 
			
		||||
      wcOpts.hash = { name: "SHA-256" };
 | 
			
		||||
    } else if (wcOpts.modulusLength >= 3072 && wcOpts.modulusLength < 4096) {
 | 
			
		||||
      wcOpts.hash = { name: "SHA-384" };
 | 
			
		||||
    } else if (wcOpts.modulusLength < 4097) {
 | 
			
		||||
      wcOpts.hash = { name: "SHA-512" };
 | 
			
		||||
    } else {
 | 
			
		||||
      // Public key thumbprints should be paired with a hash of similar length,
 | 
			
		||||
      // so anything above SHA-512's keyspace would be left under-represented anyway.
 | 
			
		||||
      return Promise.Reject(new Error("'" + wcOpts.modulusLength + "' is not within the safe and universally"
 | 
			
		||||
        + " acceptable range of 2048-4096. Typically you should pick 2048, 3072, or 4096, though other values"
 | 
			
		||||
        + " divisible by 8 are allowed. " + Keypairs._stance));
 | 
			
		||||
    }
 | 
			
		||||
    // TODO maybe allow this to be set to any of the standard values?
 | 
			
		||||
    wcOpts.publicExponent = new Uint8Array([0x01, 0x00, 0x01]);
 | 
			
		||||
  } else {
 | 
			
		||||
    return Promise.Reject(new Error("'" + opts.kty + "' is not a well-supported key type."
 | 
			
		||||
      + Keypairs._universal
 | 
			
		||||
      + " Please choose either 'EC' or 'RSA' keys."));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  var extractable = true;
 | 
			
		||||
  return window.crypto.subtle.generateKey(
 | 
			
		||||
    wcOpts
 | 
			
		||||
  , extractable
 | 
			
		||||
  , [ 'sign', 'verify' ]
 | 
			
		||||
  ).then(function (result) {
 | 
			
		||||
    return window.crypto.subtle.exportKey(
 | 
			
		||||
      "jwk"
 | 
			
		||||
    , result.privateKey
 | 
			
		||||
    ).then(function (privJwk) {
 | 
			
		||||
      // TODO remove
 | 
			
		||||
      console.log('private jwk:');
 | 
			
		||||
      console.log(JSON.stringify(privJwk, null, 2));
 | 
			
		||||
      return {
 | 
			
		||||
        privateKey: privJwk
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}(window));
 | 
			
		||||
							
								
								
									
										122
									
								
								lib/rsa.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								lib/rsa.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,122 @@
 | 
			
		||||
/*global Promise*/
 | 
			
		||||
(function (exports) {
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
var RSA = exports.Rasha = {};
 | 
			
		||||
if ('undefined' !== typeof module) { module.exports = RSA; }
 | 
			
		||||
var Enc = {};
 | 
			
		||||
var textEncoder = new TextEncoder();
 | 
			
		||||
 | 
			
		||||
RSA._stance = "We take the stance that if you're knowledgeable enough to"
 | 
			
		||||
  + " properly and securely use non-standard crypto then you shouldn't need Bluecrypt anyway.";
 | 
			
		||||
RSA._universal = "Bluecrypt only supports crypto with standard cross-browser and cross-platform support.";
 | 
			
		||||
RSA.generate = function (opts) {
 | 
			
		||||
  var wcOpts = {};
 | 
			
		||||
  if (!opts) { opts = {}; }
 | 
			
		||||
  if (!opts.kty) { opts.kty = 'RSA'; }
 | 
			
		||||
 | 
			
		||||
  // Support PSS? I don't think it's used for Let's Encrypt
 | 
			
		||||
  wcOpts.name = 'RSASSA-PKCS1-v1_5';
 | 
			
		||||
  if (!opts.modulusLength) {
 | 
			
		||||
    opts.modulusLength = 2048;
 | 
			
		||||
  }
 | 
			
		||||
  wcOpts.modulusLength = opts.modulusLength;
 | 
			
		||||
  if (wcOpts.modulusLength >= 2048 && wcOpts.modulusLength < 3072) {
 | 
			
		||||
    // erring on the small side... for no good reason
 | 
			
		||||
    wcOpts.hash = { name: "SHA-256" };
 | 
			
		||||
  } else if (wcOpts.modulusLength >= 3072 && wcOpts.modulusLength < 4096) {
 | 
			
		||||
    wcOpts.hash = { name: "SHA-384" };
 | 
			
		||||
  } else if (wcOpts.modulusLength < 4097) {
 | 
			
		||||
    wcOpts.hash = { name: "SHA-512" };
 | 
			
		||||
  } else {
 | 
			
		||||
    // Public key thumbprints should be paired with a hash of similar length,
 | 
			
		||||
    // so anything above SHA-512's keyspace would be left under-represented anyway.
 | 
			
		||||
    return Promise.Reject(new Error("'" + wcOpts.modulusLength + "' is not within the safe and universally"
 | 
			
		||||
      + " acceptable range of 2048-4096. Typically you should pick 2048, 3072, or 4096, though other values"
 | 
			
		||||
      + " divisible by 8 are allowed. " + RSA._stance));
 | 
			
		||||
  }
 | 
			
		||||
  // TODO maybe allow this to be set to any of the standard values?
 | 
			
		||||
  wcOpts.publicExponent = new Uint8Array([0x01, 0x00, 0x01]);
 | 
			
		||||
 | 
			
		||||
  var extractable = true;
 | 
			
		||||
  return window.crypto.subtle.generateKey(
 | 
			
		||||
    wcOpts
 | 
			
		||||
  , extractable
 | 
			
		||||
  , [ 'sign', 'verify' ]
 | 
			
		||||
  ).then(function (result) {
 | 
			
		||||
    return window.crypto.subtle.exportKey(
 | 
			
		||||
      "jwk"
 | 
			
		||||
    , result.privateKey
 | 
			
		||||
    ).then(function (privJwk) {
 | 
			
		||||
      return {
 | 
			
		||||
        private: privJwk
 | 
			
		||||
      , public: RSA.neuter({ jwk: privJwk })
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Chopping off the private parts is now part of the public API.
 | 
			
		||||
// I thought it sounded a little too crude at first, but it really is the best name in every possible way.
 | 
			
		||||
RSA.neuter = function (opts) {
 | 
			
		||||
  // trying to find the best balance of an immutable copy with custom attributes
 | 
			
		||||
  var jwk = {};
 | 
			
		||||
  Object.keys(opts.jwk).forEach(function (k) {
 | 
			
		||||
    if ('undefined' === typeof opts.jwk[k]) { return; }
 | 
			
		||||
    // ignore RSA private parts
 | 
			
		||||
    if (-1 !== ['d', 'p', 'q', 'dp', 'dq', 'qi'].indexOf(k)) { return; }
 | 
			
		||||
    jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k]));
 | 
			
		||||
  });
 | 
			
		||||
  return jwk;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk
 | 
			
		||||
RSA.__thumbprint = function (jwk) {
 | 
			
		||||
  // Use the same entropy for SHA as for key
 | 
			
		||||
  var len = Math.floor(jwk.n.length * 0.75);
 | 
			
		||||
  var alg = 'SHA-256';
 | 
			
		||||
  // TODO this may be a bug
 | 
			
		||||
  // need to confirm that the padding is no more or less than 1 byte
 | 
			
		||||
  if (len >= 511) {
 | 
			
		||||
    alg = 'SHA-512';
 | 
			
		||||
  } else if (len >= 383) {
 | 
			
		||||
    alg = 'SHA-384';
 | 
			
		||||
  }
 | 
			
		||||
  return window.crypto.subtle.digest(
 | 
			
		||||
    { name: alg }
 | 
			
		||||
  , textEncoder.encode('{"e":"' + jwk.e + '","kty":"RSA","n":"' + jwk.n + '"}')
 | 
			
		||||
  ).then(function (hash) {
 | 
			
		||||
    return Enc.bufToUrlBase64(new Uint8Array(hash));
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
RSA.thumbprint = function (opts) {
 | 
			
		||||
  return Promise.resolve().then(function () {
 | 
			
		||||
    var jwk;
 | 
			
		||||
    if ('EC' === opts.kty) {
 | 
			
		||||
      jwk = opts;
 | 
			
		||||
    } else if (opts.jwk) {
 | 
			
		||||
      jwk = opts.jwk;
 | 
			
		||||
    } else {
 | 
			
		||||
      return RSA.import(opts).then(function (jwk) {
 | 
			
		||||
        return RSA.__thumbprint(jwk);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    return RSA.__thumbprint(jwk);
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
Enc.bufToUrlBase64 = function (u8) {
 | 
			
		||||
  return Enc.bufToBase64(u8)
 | 
			
		||||
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
Enc.bufToBase64 = function (u8) {
 | 
			
		||||
  var bin = '';
 | 
			
		||||
  u8.forEach(function (i) {
 | 
			
		||||
    bin += String.fromCharCode(i);
 | 
			
		||||
  });
 | 
			
		||||
  return btoa(bin);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}('undefined' !== typeof module ? module.exports : window));
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user