WIP get challenges

This commit is contained in:
AJ ONeal 2019-04-29 00:56:40 -06:00
parent 488067ec20
commit 7385dd8580
3 changed files with 188 additions and 122 deletions

52
app.js
View File

@ -1,3 +1,4 @@
/*global Promise*/
(function () { (function () {
'use strict'; 'use strict';
@ -112,15 +113,56 @@
}); });
acme.init('https://acme-staging-v02.api.letsencrypt.org/directory').then(function (result) { acme.init('https://acme-staging-v02.api.letsencrypt.org/directory').then(function (result) {
console.log('acme result', result); console.log('acme result', result);
return acme.accounts.create({ var privJwk = JSON.parse($('.js-jwk').innerText).private;
email: $('.js-email').innerText var email = $('.js-email').innerText;
, agreeToTerms: function (tos) { function checkTos(tos) {
console.log("TODO checkbox for agree to terms"); console.log("TODO checkbox for agree to terms");
return tos; return tos;
} }
, accountKeypair: { return acme.accounts.create({
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);
}); });
}); });
}); });

View File

@ -63,6 +63,10 @@
<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">
<br>
<label for="-acmeDomains">Domains:</label>
<input class="js-domains" type="text" id="-acmeDomains">
<br>
<button class="js-create-account" hidden>Create Account</button> <button class="js-create-account" hidden>Create Account</button>
</form> </form>

View File

@ -7,7 +7,7 @@
/* globals Promise */ /* globals Promise */
var ACME = exports.ACME = {}; var ACME = exports.ACME = {};
var Keypairs = exports.Keypairs || {}; //var Keypairs = exports.Keypairs || {};
var Enc = exports.Enc || {}; var Enc = exports.Enc || {};
var Crypto = exports.Crypto || {}; var Crypto = exports.Crypto || {};
@ -90,7 +90,7 @@ ACME._getNonce = function (me) {
break; 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 me.request({ method: 'HEAD', url: me._directoryUrls.newNonce }).then(function (resp) {
return resp.headers['replay-nonce']; return resp.headers['replay-nonce'];
}); });
@ -132,26 +132,7 @@ ACME._registerAccount = function (me, options) {
return; return;
} }
var jwk = options.accountKeypair.privateKeyJwk; return ACME._importKeypair(me, options.accountKeypair).then(function (pair) {
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) {
var contact; var contact;
if (options.contact) { if (options.contact) {
contact = options.contact.slice(0); contact = options.contact.slice(0);
@ -209,7 +190,7 @@ ACME._registerAccount = function (me, options) {
status: 'valid' status: 'valid'
} }
*/ */
if (!account) { account = { _emptyResponse: true, key: {} }; } if (!account) { account = { _emptyResponse: true }; }
// https://git.coolaj86.com/coolaj86/acme-v2.js/issues/8 // https://git.coolaj86.com/coolaj86/acme-v2.js/issues/8
if (!account.key) { account.key = {}; } if (!account.key) { account.key = {}; }
account.key.kid = options._kid; account.key.kid = options._kid;
@ -346,11 +327,12 @@ ACME._testChallenges = function (me, options) {
, wildcard: identifierValue.includes('*.') || undefined , wildcard: identifierValue.includes('*.') || undefined
}; };
var dryrun = true; var dryrun = true;
var auth = ACME._challengeToAuth(me, options, results, challenge, dryrun); return ACME._challengeToAuth(me, options, results, challenge, dryrun).then(function (auth) {
return ACME._setChallenge(me, options, auth).then(function () { return ACME._setChallenge(me, options, auth).then(function () {
return auth; return auth;
}); });
}); });
});
})).then(function (auths) { })).then(function (auths) {
return ACME._wait(CHECK_DELAY).then(function () { return ACME._wait(CHECK_DELAY).then(function () {
return Promise.all(auths.map(function (auth) { return Promise.all(auths.map(function (auth) {
@ -402,7 +384,8 @@ ACME._challengeToAuth = function (me, options, request, challenge, dryrun) {
auth.hostname = auth.identifier.value; 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 // 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); auth.altname = ACME._untame(auth.identifier.value, auth.wildcard);
return me.Keypairs.thumbprint({ jwk: options.accountKeypair.publicKeyJwk }).then(function (thumb) { return ACME._importKeypair(me, options.accountKeypair).then(function (pair) {
return me.Keypairs.thumbprint({ jwk: pair.public }).then(function (thumb) {
auth.thumbprint = thumb; auth.thumbprint = thumb;
// keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey)) // keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey))
auth.keyAuthorization = challenge.token + '.' + auth.thumbprint; auth.keyAuthorization = challenge.token + '.' + auth.thumbprint;
@ -415,6 +398,7 @@ ACME._challengeToAuth = function (me, options, request, challenge, dryrun) {
return auth; return auth;
}); });
}); });
});
}; };
ACME._untame = function (name, wild) { ACME._untame = function (name, wild) {
@ -542,15 +526,20 @@ ACME._postChallenge = function (me, options, auth) {
return respondToChallenge(); return respondToChallenge();
}; };
ACME._setChallenge = function (me, options, auth) { ACME._setChallenge = function (me, options, auth) {
console.log('challenge auth:', auth);
console.log('challenges:', options.challenges);
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
var challengers = options.challenges || {};
var challenger = (challengers[auth.type] && challengers[auth.type].set) || options.setChallenge;
try { try {
if (1 === options.setChallenge.length) { if (1 === challenger.length) {
options.setChallenge(auth).then(resolve).catch(reject); challenger(auth).then(resolve).catch(reject);
} else if (2 === options.setChallenge.length) { } else if (2 === challenger.length) {
options.setChallenge(auth, function (err) { challenger(auth, function (err) {
if(err) { reject(err); } else { resolve(); } if(err) { reject(err); } else { resolve(); }
}); });
} else { } else {
// TODO remove this old backwards-compat
var challengeCb = function(err) { var challengeCb = function(err) {
if(err) { reject(err); } else { resolve(); } 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."); console.warn("The API has been changed for compatibility with all ACME / Let's Encrypt challenge types.");
ACME._setChallengeWarn = true; ACME._setChallengeWarn = true;
} }
options.setChallenge(auth.identifier.value, auth.token, auth.keyAuthorization, challengeCb); challenger(auth.identifier.value, auth.token, auth.keyAuthorization, challengeCb);
} }
} catch(e) { } catch(e) {
reject(e); reject(e);
@ -577,7 +566,7 @@ ACME._setChallenge = function (me, options, auth) {
}; };
ACME._finalizeOrder = function (me, options, validatedDomains) { ACME._finalizeOrder = function (me, options, validatedDomains) {
if (me.debug) { console.debug('finalizeOrder:'); } if (me.debug) { console.debug('finalizeOrder:'); }
var csr = me.Keypairs.generateCsrWeb64(options.domainKeypair, validatedDomains); return ACME._generateCsrWeb64(me, options, validatedDomains).then(function (csr) {
var body = { csr: csr }; var body = { csr: csr };
var payload = JSON.stringify(body); var payload = JSON.stringify(body);
@ -652,6 +641,7 @@ ACME._finalizeOrder = function (me, options, validatedDomains) {
} }
return pollCert(); return pollCert();
});
}; };
// _kid // _kid
// registerAccount // registerAccount
@ -686,16 +676,18 @@ ACME._getCertificate = function (me, options) {
} }
if (!(options.domains && options.domains.length)) { if (!(options.domains && options.domains.length)) {
return Promise.reject(new Error("options.domains must be a list of string domain names," 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 // It's just fine if there's no account, we'll go get the key id we need via the existing key
if (options.accountKid || options.account && options.account.kid) { options._kid = options._kid || options.accountKid
options._kid = options.accountKid || options.account.kid; || (options.account && (options.account.kid
} else { || (options.account.key && options.account.key.kid)));
if (!options._kid) {
//return Promise.reject(new Error("must include KeyID")); //return Promise.reject(new Error("must include KeyID"));
// This is an idempotent request. It'll return the same account for the same public key. // 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 // start back from the top
return ACME._getCertificate(me, options); return ACME._getCertificate(me, options);
}); });
@ -720,9 +712,6 @@ ACME._getCertificate = function (me, options) {
}; };
var payload = JSON.stringify(body); 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'); } if (me.debug) { console.debug('\n[DEBUG] newOrder\n'); }
return ACME._jwsRequest({ return ACME._jwsRequest({
options: options 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) { ACME.create = function create(me) {
if (!me) { 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 TODO
Per-Order State Params Per-Order State Params