forked from coolaj86/bluecrypt-keypairs.js
WIP get challenges
This commit is contained in:
parent
488067ec20
commit
7385dd8580
52
app.js
52
app.js
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
100
lib/acme.js
100
lib/acme.js
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue