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
|
, namedCurve: $('input[name="ec-crv"]:checked').value
|
||||||
, modulusLength: $('input[name="rsa-len"]:checked').value
|
, modulusLength: $('input[name="rsa-len"]:checked').value
|
||||||
};
|
};
|
||||||
console.log(opts);
|
console.log('opts', opts);
|
||||||
Keypairs.generate(opts).then(function (results) {
|
Keypairs.generate(opts).then(function (results) {
|
||||||
$('.js-jwk').innerText = JSON.stringify(results, null, 2);
|
$('.js-jwk').innerText = JSON.stringify(results, null, 2);
|
||||||
//
|
//
|
||||||
|
|
|
@ -48,6 +48,8 @@
|
||||||
<div class="js-loading" hidden>Loading</div>
|
<div class="js-loading" hidden>Loading</div>
|
||||||
<pre><code class="js-jwk"> </code></pre>
|
<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="./lib/keypairs.js"></script>
|
||||||
<script src="./app.js"></script>
|
<script src="./app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -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));
|
|
@ -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));
|
155
lib/keypairs.js
155
lib/keypairs.js
|
@ -3,84 +3,107 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var Keypairs = exports.Keypairs = {};
|
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"
|
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.";
|
+ " 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._universal = "Bluecrypt only supports crypto with standard cross-browser and cross-platform support.";
|
||||||
Keypairs.generate = function (opts) {
|
Keypairs.generate = function (opts) {
|
||||||
var wcOpts = {};
|
opts = opts || {};
|
||||||
if (!opts) {
|
var p;
|
||||||
opts = {};
|
if (!opts.kty) { opts.kty = opts.type; }
|
||||||
}
|
if (!opts.kty) { opts.kty = 'EC'; }
|
||||||
if (!opts.kty) {
|
|
||||||
opts.kty = 'EC';
|
|
||||||
}
|
|
||||||
|
|
||||||
// ECDSA has only the P curves and an associated bitlength
|
|
||||||
if (/^EC/i.test(opts.kty)) {
|
if (/^EC/i.test(opts.kty)) {
|
||||||
wcOpts.name = 'ECDSA';
|
p = Eckles.generate(opts);
|
||||||
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)) {
|
} else if (/^RSA$/i.test(opts.kty)) {
|
||||||
// Support PSS? I don't think it's used for Let's Encrypt
|
p = Rasha.generate(opts);
|
||||||
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 {
|
} else {
|
||||||
return Promise.Reject(new Error("'" + opts.kty + "' is not a well-supported key type."
|
return Promise.Reject(new Error("'" + opts.kty + "' is not a well-supported key type."
|
||||||
+ Keypairs._universal
|
+ 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) {
|
||||||
var extractable = true;
|
return Keypairs.thumbprint({ jwk: pair.public }).then(function (thumb) {
|
||||||
return window.crypto.subtle.generateKey(
|
pair.private.kid = thumb; // maybe not the same id on the private key?
|
||||||
wcOpts
|
pair.public.kid = thumb;
|
||||||
, extractable
|
return pair;
|
||||||
, [ '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));
|
|
||||||
|
// 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 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return now + (mult * num);
|
||||||
|
}
|
||||||
|
|
||||||
|
}('undefined' !== typeof module ? module.exports : window));
|
||||||
|
|
|
@ -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));
|
|
@ -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…
Reference in New Issue