forked from coolaj86/bluecrypt-keypairs.js
WIP get challenges
This commit is contained in:
parent
488067ec20
commit
7385dd8580
62
app.js
62
app.js
|
@ -1,3 +1,4 @@
|
|||
/*global Promise*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
|
@ -47,8 +48,8 @@
|
|||
$$('button').map(function ($el) { $el.disabled = true; });
|
||||
var opts = {
|
||||
kty: $('input[name="kty"]:checked').value
|
||||
, namedCurve: $('input[name="ec-crv"]:checked').value
|
||||
, modulusLength: $('input[name="rsa-len"]:checked').value
|
||||
, namedCurve: $('input[name="ec-crv"]:checked').value
|
||||
, modulusLength: $('input[name="rsa-len"]:checked').value
|
||||
};
|
||||
console.log('opts', opts);
|
||||
Keypairs.generate(opts).then(function (results) {
|
||||
|
@ -112,15 +113,56 @@
|
|||
});
|
||||
acme.init('https://acme-staging-v02.api.letsencrypt.org/directory').then(function (result) {
|
||||
console.log('acme result', result);
|
||||
var privJwk = JSON.parse($('.js-jwk').innerText).private;
|
||||
var email = $('.js-email').innerText;
|
||||
function checkTos(tos) {
|
||||
console.log("TODO checkbox for agree to terms");
|
||||
return tos;
|
||||
}
|
||||
return acme.accounts.create({
|
||||
email: $('.js-email').innerText
|
||||
, agreeToTerms: function (tos) {
|
||||
console.log("TODO checkbox for agree to terms");
|
||||
return tos;
|
||||
}
|
||||
, accountKeypair: {
|
||||
privateKeyJwk: JSON.parse($('.js-jwk').innerText).private
|
||||
}
|
||||
email: email
|
||||
, agreeToTerms: checkTos
|
||||
, accountKeypair: { privateKeyJwk: privJwk }
|
||||
}).then(function (account) {
|
||||
console.log("account created result:", account);
|
||||
return Keypairs.generate({
|
||||
kty: 'RSA'
|
||||
, modulusLength: 2048
|
||||
}).then(function (pair) {
|
||||
console.log('domain keypair:', pair);
|
||||
var domains = ($('.js-domains').innerText||'example.com').split(/[, ]+/g);
|
||||
return acme.certificates.create({
|
||||
accountKeypair: { privateKeyJwk: privJwk }
|
||||
, account: account
|
||||
, domainKeypair: { privateKeyJwk: pair.private }
|
||||
, email: email
|
||||
, domains: domains
|
||||
, agreeToTerms: checkTos
|
||||
, challenges: {
|
||||
'dns-01': {
|
||||
set: function (opts) {
|
||||
console.log('dns-01 set challenge:');
|
||||
console.log(JSON.stringify(opts, null, 2));
|
||||
return new Promise(function (resolve) {
|
||||
while (!window.confirm("Did you set the challenge?")) {}
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
, remove: function (opts) {
|
||||
console.log('dns-01 remove challenge:');
|
||||
console.log(JSON.stringify(opts, null, 2));
|
||||
return new Promise(function (resolve) {
|
||||
while (!window.confirm("Did you delete the challenge?")) {}
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}).catch(function (err) {
|
||||
console.error("A bad thing happened:");
|
||||
console.error(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -63,6 +63,10 @@
|
|||
<form class="js-acme-account">
|
||||
<label for="-acmeEmail">Email:</label>
|
||||
<input class="js-email" type="email" id="-acmeEmail">
|
||||
<br>
|
||||
<label for="-acmeDomains">Domains:</label>
|
||||
<input class="js-domains" type="text" id="-acmeDomains">
|
||||
<br>
|
||||
<button class="js-create-account" hidden>Create Account</button>
|
||||
</form>
|
||||
|
||||
|
|
244
lib/acme.js
244
lib/acme.js
|
@ -7,7 +7,7 @@
|
|||
/* globals Promise */
|
||||
|
||||
var ACME = exports.ACME = {};
|
||||
var Keypairs = exports.Keypairs || {};
|
||||
//var Keypairs = exports.Keypairs || {};
|
||||
var Enc = exports.Enc || {};
|
||||
var Crypto = exports.Crypto || {};
|
||||
|
||||
|
@ -90,7 +90,7 @@ ACME._getNonce = function (me) {
|
|||
break;
|
||||
}
|
||||
}
|
||||
if (nonce) { return Promise.resolve(nonce); }
|
||||
if (nonce) { return Promise.resolve(nonce.nonce); }
|
||||
return me.request({ method: 'HEAD', url: me._directoryUrls.newNonce }).then(function (resp) {
|
||||
return resp.headers['replay-nonce'];
|
||||
});
|
||||
|
@ -132,26 +132,7 @@ ACME._registerAccount = function (me, options) {
|
|||
return;
|
||||
}
|
||||
|
||||
var jwk = options.accountKeypair.privateKeyJwk;
|
||||
var p;
|
||||
if (jwk) {
|
||||
// nix the browser jwk extras
|
||||
jwk.key_ops = undefined;
|
||||
jwk.ext = undefined;
|
||||
p = Promise.resolve({ private: jwk, public: Keypairs.neuter({ jwk: jwk }) });
|
||||
} else {
|
||||
p = Keypairs.import({ pem: options.accountKeypair.privateKeyPem });
|
||||
}
|
||||
return p.then(function (pair) {
|
||||
options.accountKeypair.privateKeyJwk = pair.private;
|
||||
options.accountKeypair.publicKeyJwk = pair.public;
|
||||
if (pair.public.kid) {
|
||||
pair = JSON.parse(JSON.stringify(pair));
|
||||
delete pair.public.kid;
|
||||
delete pair.private.kid;
|
||||
}
|
||||
return pair;
|
||||
}).then(function (pair) {
|
||||
return ACME._importKeypair(me, options.accountKeypair).then(function (pair) {
|
||||
var contact;
|
||||
if (options.contact) {
|
||||
contact = options.contact.slice(0);
|
||||
|
@ -209,7 +190,7 @@ ACME._registerAccount = function (me, options) {
|
|||
status: 'valid'
|
||||
}
|
||||
*/
|
||||
if (!account) { account = { _emptyResponse: true, key: {} }; }
|
||||
if (!account) { account = { _emptyResponse: true }; }
|
||||
// https://git.coolaj86.com/coolaj86/acme-v2.js/issues/8
|
||||
if (!account.key) { account.key = {}; }
|
||||
account.key.kid = options._kid;
|
||||
|
@ -346,9 +327,10 @@ ACME._testChallenges = function (me, options) {
|
|||
, wildcard: identifierValue.includes('*.') || undefined
|
||||
};
|
||||
var dryrun = true;
|
||||
var auth = ACME._challengeToAuth(me, options, results, challenge, dryrun);
|
||||
return ACME._setChallenge(me, options, auth).then(function () {
|
||||
return auth;
|
||||
return ACME._challengeToAuth(me, options, results, challenge, dryrun).then(function (auth) {
|
||||
return ACME._setChallenge(me, options, auth).then(function () {
|
||||
return auth;
|
||||
});
|
||||
});
|
||||
});
|
||||
})).then(function (auths) {
|
||||
|
@ -402,17 +384,19 @@ ACME._challengeToAuth = function (me, options, request, challenge, dryrun) {
|
|||
auth.hostname = auth.identifier.value;
|
||||
// because I'm not 100% clear if the wildcard identifier does or doesn't have the leading *. in all cases
|
||||
auth.altname = ACME._untame(auth.identifier.value, auth.wildcard);
|
||||
return me.Keypairs.thumbprint({ jwk: options.accountKeypair.publicKeyJwk }).then(function (thumb) {
|
||||
auth.thumbprint = thumb;
|
||||
// keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey))
|
||||
auth.keyAuthorization = challenge.token + '.' + auth.thumbprint;
|
||||
// conflicts with ACME challenge id url is already in use, so we call this challengeUrl instead
|
||||
auth.challengeUrl = 'http://' + auth.identifier.value + ACME.challengePrefixes['http-01'] + '/' + auth.token;
|
||||
auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', '');
|
||||
return ACME._importKeypair(me, options.accountKeypair).then(function (pair) {
|
||||
return me.Keypairs.thumbprint({ jwk: pair.public }).then(function (thumb) {
|
||||
auth.thumbprint = thumb;
|
||||
// keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey))
|
||||
auth.keyAuthorization = challenge.token + '.' + auth.thumbprint;
|
||||
// conflicts with ACME challenge id url is already in use, so we call this challengeUrl instead
|
||||
auth.challengeUrl = 'http://' + auth.identifier.value + ACME.challengePrefixes['http-01'] + '/' + auth.token;
|
||||
auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', '');
|
||||
|
||||
return Crypto._sha('sha256', auth.keyAuthorization).then(function (hash) {
|
||||
auth.dnsAuthorization = hash;
|
||||
return auth;
|
||||
return Crypto._sha('sha256', auth.keyAuthorization).then(function (hash) {
|
||||
auth.dnsAuthorization = hash;
|
||||
return auth;
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@ -542,15 +526,20 @@ ACME._postChallenge = function (me, options, auth) {
|
|||
return respondToChallenge();
|
||||
};
|
||||
ACME._setChallenge = function (me, options, auth) {
|
||||
console.log('challenge auth:', auth);
|
||||
console.log('challenges:', options.challenges);
|
||||
return new Promise(function (resolve, reject) {
|
||||
var challengers = options.challenges || {};
|
||||
var challenger = (challengers[auth.type] && challengers[auth.type].set) || options.setChallenge;
|
||||
try {
|
||||
if (1 === options.setChallenge.length) {
|
||||
options.setChallenge(auth).then(resolve).catch(reject);
|
||||
} else if (2 === options.setChallenge.length) {
|
||||
options.setChallenge(auth, function (err) {
|
||||
if (1 === challenger.length) {
|
||||
challenger(auth).then(resolve).catch(reject);
|
||||
} else if (2 === challenger.length) {
|
||||
challenger(auth, function (err) {
|
||||
if(err) { reject(err); } else { resolve(); }
|
||||
});
|
||||
} else {
|
||||
// TODO remove this old backwards-compat
|
||||
var challengeCb = function(err) {
|
||||
if(err) { reject(err); } else { resolve(); }
|
||||
};
|
||||
|
@ -563,7 +552,7 @@ ACME._setChallenge = function (me, options, auth) {
|
|||
console.warn("The API has been changed for compatibility with all ACME / Let's Encrypt challenge types.");
|
||||
ACME._setChallengeWarn = true;
|
||||
}
|
||||
options.setChallenge(auth.identifier.value, auth.token, auth.keyAuthorization, challengeCb);
|
||||
challenger(auth.identifier.value, auth.token, auth.keyAuthorization, challengeCb);
|
||||
}
|
||||
} catch(e) {
|
||||
reject(e);
|
||||
|
@ -577,81 +566,82 @@ ACME._setChallenge = function (me, options, auth) {
|
|||
};
|
||||
ACME._finalizeOrder = function (me, options, validatedDomains) {
|
||||
if (me.debug) { console.debug('finalizeOrder:'); }
|
||||
var csr = me.Keypairs.generateCsrWeb64(options.domainKeypair, validatedDomains);
|
||||
var body = { csr: csr };
|
||||
var payload = JSON.stringify(body);
|
||||
return ACME._generateCsrWeb64(me, options, validatedDomains).then(function (csr) {
|
||||
var body = { csr: csr };
|
||||
var payload = JSON.stringify(body);
|
||||
|
||||
function pollCert() {
|
||||
if (me.debug) { console.debug('[acme-v2.js] pollCert:'); }
|
||||
return ACME._jwsRequest({
|
||||
options: options
|
||||
, url: options._finalize
|
||||
, protected: { kid: options._kid }
|
||||
, payload: Enc.strToBuf(payload)
|
||||
}).then(function (resp) {
|
||||
if (me.debug) { console.debug('order finalized: resp.body:'); }
|
||||
if (me.debug) { console.debug(resp.body); }
|
||||
function pollCert() {
|
||||
if (me.debug) { console.debug('[acme-v2.js] pollCert:'); }
|
||||
return ACME._jwsRequest({
|
||||
options: options
|
||||
, url: options._finalize
|
||||
, protected: { kid: options._kid }
|
||||
, payload: Enc.strToBuf(payload)
|
||||
}).then(function (resp) {
|
||||
if (me.debug) { console.debug('order finalized: resp.body:'); }
|
||||
if (me.debug) { console.debug(resp.body); }
|
||||
|
||||
// https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.1.3
|
||||
// Possible values are: "pending" => ("invalid" || "ready") => "processing" => "valid"
|
||||
if ('valid' === resp.body.status) {
|
||||
options._expires = resp.body.expires;
|
||||
options._certificate = resp.body.certificate;
|
||||
// https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.1.3
|
||||
// Possible values are: "pending" => ("invalid" || "ready") => "processing" => "valid"
|
||||
if ('valid' === resp.body.status) {
|
||||
options._expires = resp.body.expires;
|
||||
options._certificate = resp.body.certificate;
|
||||
|
||||
return resp.body; // return order
|
||||
}
|
||||
return resp.body; // return order
|
||||
}
|
||||
|
||||
if ('processing' === resp.body.status) {
|
||||
return ACME._wait().then(pollCert);
|
||||
}
|
||||
if ('processing' === resp.body.status) {
|
||||
return ACME._wait().then(pollCert);
|
||||
}
|
||||
|
||||
if (me.debug) { console.debug("Error: bad status:\n" + JSON.stringify(resp.body, null, 2)); }
|
||||
if (me.debug) { console.debug("Error: bad status:\n" + JSON.stringify(resp.body, null, 2)); }
|
||||
|
||||
if ('pending' === resp.body.status) {
|
||||
return Promise.reject(new Error(
|
||||
"Did not finalize order: status 'pending'."
|
||||
+ " Best guess: You have not accepted at least one challenge for each domain:\n"
|
||||
+ "Requested: '" + options.domains.join(', ') + "'\n"
|
||||
+ "Validated: '" + validatedDomains.join(', ') + "'\n"
|
||||
+ JSON.stringify(resp.body, null, 2)
|
||||
));
|
||||
}
|
||||
|
||||
if ('invalid' === resp.body.status) {
|
||||
return Promise.reject(new Error(
|
||||
"Did not finalize order: status 'invalid'."
|
||||
+ " Best guess: One or more of the domain challenges could not be verified"
|
||||
+ " (or the order was canceled).\n"
|
||||
+ "Requested: '" + options.domains.join(', ') + "'\n"
|
||||
+ "Validated: '" + validatedDomains.join(', ') + "'\n"
|
||||
+ JSON.stringify(resp.body, null, 2)
|
||||
));
|
||||
}
|
||||
|
||||
if ('ready' === resp.body.status) {
|
||||
return Promise.reject(new Error(
|
||||
"Did not finalize order: status 'ready'."
|
||||
+ " Hmmm... this state shouldn't be possible here. That was the last state."
|
||||
+ " This one should at least be 'processing'.\n"
|
||||
+ "Requested: '" + options.domains.join(', ') + "'\n"
|
||||
+ "Validated: '" + validatedDomains.join(', ') + "'\n"
|
||||
+ JSON.stringify(resp.body, null, 2) + "\n\n"
|
||||
+ "Please open an issue at https://git.coolaj86.com/coolaj86/acme-v2.js"
|
||||
));
|
||||
}
|
||||
|
||||
if ('pending' === resp.body.status) {
|
||||
return Promise.reject(new Error(
|
||||
"Did not finalize order: status 'pending'."
|
||||
+ " Best guess: You have not accepted at least one challenge for each domain:\n"
|
||||
+ "Requested: '" + options.domains.join(', ') + "'\n"
|
||||
+ "Validated: '" + validatedDomains.join(', ') + "'\n"
|
||||
+ JSON.stringify(resp.body, null, 2)
|
||||
));
|
||||
}
|
||||
|
||||
if ('invalid' === resp.body.status) {
|
||||
return Promise.reject(new Error(
|
||||
"Did not finalize order: status 'invalid'."
|
||||
+ " Best guess: One or more of the domain challenges could not be verified"
|
||||
+ " (or the order was canceled).\n"
|
||||
+ "Requested: '" + options.domains.join(', ') + "'\n"
|
||||
+ "Validated: '" + validatedDomains.join(', ') + "'\n"
|
||||
+ JSON.stringify(resp.body, null, 2)
|
||||
));
|
||||
}
|
||||
|
||||
if ('ready' === resp.body.status) {
|
||||
return Promise.reject(new Error(
|
||||
"Did not finalize order: status 'ready'."
|
||||
+ " Hmmm... this state shouldn't be possible here. That was the last state."
|
||||
+ " This one should at least be 'processing'.\n"
|
||||
"Didn't finalize order: Unhandled status '" + resp.body.status + "'."
|
||||
+ " This is not one of the known statuses...\n"
|
||||
+ "Requested: '" + options.domains.join(', ') + "'\n"
|
||||
+ "Validated: '" + validatedDomains.join(', ') + "'\n"
|
||||
+ JSON.stringify(resp.body, null, 2) + "\n\n"
|
||||
+ "Please open an issue at https://git.coolaj86.com/coolaj86/acme-v2.js"
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(
|
||||
"Didn't finalize order: Unhandled status '" + resp.body.status + "'."
|
||||
+ " This is not one of the known statuses...\n"
|
||||
+ "Requested: '" + options.domains.join(', ') + "'\n"
|
||||
+ "Validated: '" + validatedDomains.join(', ') + "'\n"
|
||||
+ JSON.stringify(resp.body, null, 2) + "\n\n"
|
||||
+ "Please open an issue at https://git.coolaj86.com/coolaj86/acme-v2.js"
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
return pollCert();
|
||||
return pollCert();
|
||||
});
|
||||
};
|
||||
// _kid
|
||||
// registerAccount
|
||||
|
@ -686,16 +676,18 @@ ACME._getCertificate = function (me, options) {
|
|||
}
|
||||
if (!(options.domains && options.domains.length)) {
|
||||
return Promise.reject(new Error("options.domains must be a list of string domain names,"
|
||||
+ " with the first being the subject of the domain (or options.subject must specified)."));
|
||||
+ " with the first being the subject of the certificate (or options.subject must specified)."));
|
||||
}
|
||||
|
||||
// It's just fine if there's no account, we'll go get the key id we need via the public key
|
||||
if (options.accountKid || options.account && options.account.kid) {
|
||||
options._kid = options.accountKid || options.account.kid;
|
||||
} else {
|
||||
// It's just fine if there's no account, we'll go get the key id we need via the existing key
|
||||
options._kid = options._kid || options.accountKid
|
||||
|| (options.account && (options.account.kid
|
||||
|| (options.account.key && options.account.key.kid)));
|
||||
if (!options._kid) {
|
||||
//return Promise.reject(new Error("must include KeyID"));
|
||||
// This is an idempotent request. It'll return the same account for the same public key.
|
||||
return ACME._registerAccount(me, options).then(function () {
|
||||
return ACME._registerAccount(me, options).then(function (account) {
|
||||
options._kid = account.key.kid;
|
||||
// start back from the top
|
||||
return ACME._getCertificate(me, options);
|
||||
});
|
||||
|
@ -720,9 +712,6 @@ ACME._getCertificate = function (me, options) {
|
|||
};
|
||||
|
||||
var payload = JSON.stringify(body);
|
||||
// determine the signing algorithm to use in protected header // TODO isn't that handled by the signer?
|
||||
options._kty = (options.accountKeypair.privateKeyJwk && options.accountKeypair.privateKeyJwk.kty || 'RSA');
|
||||
options._alg = ('EC' === options._kty) ? 'ES256' : 'RS256'; // TODO vary with bitwidth of key (if not handled)
|
||||
if (me.debug) { console.debug('\n[DEBUG] newOrder\n'); }
|
||||
return ACME._jwsRequest({
|
||||
options: options
|
||||
|
@ -815,6 +804,13 @@ ACME._getCertificate = function (me, options) {
|
|||
});
|
||||
});
|
||||
};
|
||||
ACME._generateCsrWeb64 = function (me, options, validatedDomains) {
|
||||
return ACME._importKeypair(me, options.domainKeypair).then(function (/*pair*/) {
|
||||
return me.Keypairs.generateCsr(options.domainKeypair, validatedDomains).then(function (der) {
|
||||
return Enc.bufToUrlBase64(der);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
ACME.create = function create(me) {
|
||||
if (!me) { me = {}; }
|
||||
|
@ -942,6 +938,30 @@ ACME._defaultRequest = function (opts) {
|
|||
});
|
||||
});
|
||||
};
|
||||
|
||||
ACME._importKeypair = function (me, kp) {
|
||||
var jwk = kp.privateKeyJwk;
|
||||
var p;
|
||||
if (jwk) {
|
||||
// nix the browser jwk extras
|
||||
jwk.key_ops = undefined;
|
||||
jwk.ext = undefined;
|
||||
p = Promise.resolve({ private: jwk, public: me.Keypairs.neuter({ jwk: jwk }) });
|
||||
} else {
|
||||
p = me.Keypairs.import({ pem: kp.privateKeyPem });
|
||||
}
|
||||
return p.then(function (pair) {
|
||||
kp.privateKeyJwk = pair.private;
|
||||
kp.publicKeyJwk = pair.public;
|
||||
if (pair.public.kid) {
|
||||
pair = JSON.parse(JSON.stringify(pair));
|
||||
delete pair.public.kid;
|
||||
delete pair.private.kid;
|
||||
}
|
||||
return pair;
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
TODO
|
||||
Per-Order State Params
|
||||
|
|
Loading…
Reference in New Issue