Browse Source

WIP get challenges

browser-v2
AJ ONeal 5 years ago
parent
commit
7385dd8580
  1. 62
      app.js
  2. 4
      index.html
  3. 244
      lib/acme.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);
});
});
});

4
index.html

@ -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

@ -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 Crypto._sha('sha256', auth.keyAuthorization).then(function (hash) {
auth.dnsAuthorization = hash;
return auth;
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;
});
});
});
};
@ -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);
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); }
return ACME._generateCsrWeb64(me, options, validatedDomains).then(function (csr) {
var body = { csr: csr };
var payload = JSON.stringify(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;
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;
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 ('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 ('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 ('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…
Cancel
Save