ACME fully tested and working

This commit is contained in:
AJ ONeal 2019-05-06 23:05:25 -06:00
parent 009e0dc1fb
commit 4ae3b19ed7
5 changed files with 34 additions and 797 deletions

25
app.js
View File

@ -18,8 +18,11 @@
} }
function checkTos(tos) { function checkTos(tos) {
console.log("TODO checkbox for agree to terms"); if ($('input[name="tos"]:checked')) {
return tos; return tos;
} else {
return '';
}
} }
function run() { function run() {
@ -139,6 +142,8 @@
accountStuff.email = email; accountStuff.email = email;
accountStuff.acme = acme; accountStuff.acme = acme;
$('.js-create-order').hidden = false; $('.js-create-order').hidden = false;
$('.js-toc-acme-account-response').hidden = false;
$('.js-acme-account-response').innerText = JSON.stringify(account, null, 2);
}).catch(function (err) { }).catch(function (err) {
console.error("A bad thing happened:"); console.error("A bad thing happened:");
console.error(err); console.error(err);
@ -163,14 +168,17 @@
var domains = ($('.js-domains').value||'example.com').split(/[, ]+/g); var domains = ($('.js-domains').value||'example.com').split(/[, ]+/g);
return getDomainPrivkey().then(function () { return getDomainPrivkey().then(function (domainPrivJwk) {
console.log('Has CSR already?');
console.log(accountStuff.csr);
return acme.certificates.create({ return acme.certificates.create({
accountKeypair: { privateKeyJwk: privJwk } accountKeypair: { privateKeyJwk: privJwk }
, account: account , account: account
//, domainKeypair: { privateKeyJwk: accountStuff.domainPrivateJwk } , domainKeypair: { privateKeyJwk: domainPrivJwk }
, csr: accountStuff.csr , csr: accountStuff.csr
, email: email , email: email
, domains: domains , domains: domains
, skipDryRun: $('input[name="skip-dryrun"]:checked') && true
, agreeToTerms: checkTos , agreeToTerms: checkTos
, challenges: { , challenges: {
'dns-01': { 'dns-01': {
@ -215,7 +223,14 @@
} }
} }
, challengeTypes: [$('input[name="acme-challenge-type"]:checked').value] , challengeTypes: [$('input[name="acme-challenge-type"]:checked').value]
}).then(function (results) {
console.log('Got Certificates:');
console.log(results);
$('.js-toc-acme-order-response').hidden = false;
$('.js-acme-order-response').innerText = JSON.stringify(results, null, 2);
}).catch(function (err) { }).catch(function (err) {
console.error("challenge failed:");
console.error(err);
window.alert("failed! " + err.message || JSON.stringify(err)); window.alert("failed! " + err.message || JSON.stringify(err));
}); });
}); });
@ -245,7 +260,7 @@
return CSR({ jwk: privJwk, domains: domains }).then(function (pem) { return CSR({ jwk: privJwk, domains: domains }).then(function (pem) {
// Verify with https://www.sslshopper.com/csr-decoder.html // Verify with https://www.sslshopper.com/csr-decoder.html
accountStuff.csr = pem; accountStuff.csr = pem;
console.log('CSR:'); console.log('Created CSR:');
console.log(pem); console.log(pem);
console.log('CSR info:'); console.log('CSR info:');

View File

@ -56,7 +56,10 @@
<h2>ACME Account</h2> <h2>ACME Account</h2>
<form class="js-acme-account"> <form class="js-acme-account">
<label for="-acmeEmail">Email:</label> <label for="-acmeEmail">Email:</label>
<input class="js-email" type="email" id="-acmeEmail"> <input class="js-email" type="email" id="-acmeEmail" value="john.doe@gmail.com">
<br>
<label for="-acmeTos"><input class="js-tos" name="tos" type="checkbox" id="-acmeTos" checked>
Agree to Let's Encrypt Terms of Service</label>
<br> <br>
<button class="js-create-account" hidden>Create Account</button> <button class="js-create-account" hidden>Create Account</button>
</form> </form>
@ -64,7 +67,7 @@
<h2>Certificate Signing Request</h2> <h2>Certificate Signing Request</h2>
<form class="js-csr"> <form class="js-csr">
<label for="-acmeDomains">Domains:</label> <label for="-acmeDomains">Domains:</label>
<input class="js-domains" type="text" id="-acmeDomains"> <input class="js-domains" type="text" id="-acmeDomains" value="example.com www.example.com">
<br> <br>
<button class="js-create-csr" hidden>Create CSR</button> <button class="js-create-csr" hidden>Create CSR</button>
</form> </form>
@ -77,6 +80,9 @@
<label for="-dns01"><input type="radio" id="-dns01" <label for="-dns01"><input type="radio" id="-dns01"
name="acme-challenge-type" value="dns-01">dns-01</label> name="acme-challenge-type" value="dns-01">dns-01</label>
<br> <br>
<label for="-skipDryrun"><input class="js-skip-dryrun" name="skip-dryrun"
type="checkbox" id="-skipDryrun" checked> Skip dry-run challenge</label>
<br>
<button class="js-create-order" hidden>Create Order</button> <button class="js-create-order" hidden>Create Order</button>
</form> </form>
@ -114,14 +120,14 @@
<summary>PEM Public (base64-encoded SPKI/PKIX DER)</summary> <summary>PEM Public (base64-encoded SPKI/PKIX DER)</summary>
<pre><code class="js-input-pem-spki-public" ></code></pre> <pre><code class="js-input-pem-spki-public" ></code></pre>
</details> </details>
<details class="js-toc-acme-account-request" hidden>
<summary>ACME Account Request</summary>
<pre><code class="js-acme-account-request">&nbsp;</code></pre>
</details>
<details class="js-toc-acme-account-response" hidden> <details class="js-toc-acme-account-response" hidden>
<summary>ACME Account Response</summary> <summary>ACME Account Request</summary>
<pre><code class="js-acme-account-response">&nbsp;</code></pre> <pre><code class="js-acme-account-response">&nbsp;</code></pre>
</details> </details>
<details class="js-toc-acme-order-response" hidden>
<summary>ACME Order Response</summary>
<pre><code class="js-acme-order-response">&nbsp;</code></pre>
</details>
<script src="./lib/bluecrypt-encoding.js"></script> <script src="./lib/bluecrypt-encoding.js"></script>
<script src="./lib/asn1-packer.js"></script> <script src="./lib/asn1-packer.js"></script>
<script src="./lib/x509.js"></script> <script src="./lib/x509.js"></script>

View File

@ -837,6 +837,7 @@ ACME._generateCsrWeb64 = function (me, options, validatedDomains) {
csr = Enc.base64ToUrlBase64(csr.trim().replace(/\s+/g, '')); csr = Enc.base64ToUrlBase64(csr.trim().replace(/\s+/g, ''));
return Promise.resolve(csr); return Promise.resolve(csr);
} }
return ACME._importKeypair(me, options.domainKeypair).then(function (pair) { return ACME._importKeypair(me, options.domainKeypair).then(function (pair) {
return me.CSR({ jwk: pair.private, domains: validatedDomains, encoding: 'der' }).then(function (der) { return me.CSR({ jwk: pair.private, domains: validatedDomains, encoding: 'der' }).then(function (der) {
return Enc.bufToUrlBase64(der); return Enc.bufToUrlBase64(der);

View File

@ -1,699 +0,0 @@
/*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));

View File

@ -1,86 +0,0 @@
/*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));