AJ ONeal
5 years ago
7 changed files with 1111 additions and 67 deletions
@ -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)); |
@ -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