WIP gets a cert... nice!
This commit is contained in:
parent
e75c503356
commit
24c3633d75
393
lib/acme.js
393
lib/acme.js
|
@ -165,7 +165,7 @@ ACME._registerAccount = function(me, options) {
|
||||||
} else if (options.email) {
|
} else if (options.email) {
|
||||||
contact = ['mailto:' + options.email];
|
contact = ['mailto:' + options.email];
|
||||||
}
|
}
|
||||||
var body = {
|
var accountRequest = {
|
||||||
termsOfServiceAgreed: tosUrl === me._tos,
|
termsOfServiceAgreed: tosUrl === me._tos,
|
||||||
onlyReturnExisting: false,
|
onlyReturnExisting: false,
|
||||||
contact: contact
|
contact: contact
|
||||||
|
@ -182,14 +182,14 @@ ACME._registerAccount = function(me, options) {
|
||||||
},
|
},
|
||||||
payload: Enc.strToBuf(JSON.stringify(pair.public))
|
payload: Enc.strToBuf(JSON.stringify(pair.public))
|
||||||
}).then(function(jws) {
|
}).then(function(jws) {
|
||||||
body.externalAccountBinding = jws;
|
accountRequest.externalAccountBinding = jws;
|
||||||
return body;
|
return accountRequest;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
pExt = Promise.resolve(body);
|
pExt = Promise.resolve(accountRequest);
|
||||||
}
|
}
|
||||||
return pExt.then(function(body) {
|
return pExt.then(function(accountRequest) {
|
||||||
var payload = JSON.stringify(body);
|
var payload = JSON.stringify(accountRequest);
|
||||||
return ACME._jwsRequest(me, {
|
return ACME._jwsRequest(me, {
|
||||||
options: options,
|
options: options,
|
||||||
url: me._directoryUrls.newAccount,
|
url: me._directoryUrls.newAccount,
|
||||||
|
@ -199,10 +199,20 @@ ACME._registerAccount = function(me, options) {
|
||||||
.then(function(resp) {
|
.then(function(resp) {
|
||||||
var account = resp.body;
|
var account = resp.body;
|
||||||
|
|
||||||
if (2 !== Math.floor(resp.statusCode / 100)) {
|
if (
|
||||||
|
resp.statusCode < 200 ||
|
||||||
|
resp.statusCode >= 300
|
||||||
|
) {
|
||||||
|
if ('string' !== typeof account) {
|
||||||
|
account = JSON.stringify(account);
|
||||||
|
}
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'account error: ' +
|
'account error: ' +
|
||||||
JSON.stringify(resp.body)
|
resp.statusCode +
|
||||||
|
' ' +
|
||||||
|
account +
|
||||||
|
'\n' +
|
||||||
|
JSON.stringify(accountRequest)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -344,7 +354,24 @@ ACME._testChallengeOptions = function() {
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
ACME._testChallenges = function(me, options) {
|
ACME._testChallenges = function(me, options) {
|
||||||
|
console.log('[debug] testChallenges');
|
||||||
var CHECK_DELAY = 0;
|
var CHECK_DELAY = 0;
|
||||||
|
|
||||||
|
// memoized so that it doesn't run until it's first called
|
||||||
|
var getThumbnail = function() {
|
||||||
|
var thumbPromise = ACME._importKeypair(me, options.accountKeypair).then(
|
||||||
|
function(pair) {
|
||||||
|
return me.Keypairs.thumbprint({
|
||||||
|
jwk: pair.public
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
getThumbnail = function() {
|
||||||
|
return thumbPromise;
|
||||||
|
};
|
||||||
|
return thumbPromise;
|
||||||
|
};
|
||||||
|
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
options.domains.map(function(identifierValue) {
|
options.domains.map(function(identifierValue) {
|
||||||
// TODO we really only need one to pass, not all to pass
|
// TODO we really only need one to pass, not all to pass
|
||||||
|
@ -389,10 +416,11 @@ ACME._testChallenges = function(me, options) {
|
||||||
|
|
||||||
if ('dns-01' === challenge.type) {
|
if ('dns-01' === challenge.type) {
|
||||||
// Give the nameservers a moment to propagate
|
// Give the nameservers a moment to propagate
|
||||||
CHECK_DELAY = 1.5 * 1000;
|
// TODO get this value from the plugin
|
||||||
|
CHECK_DELAY = 7 * 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve().then(function() {
|
return getThumbnail().then(function(accountKeyThumb) {
|
||||||
var results = {
|
var results = {
|
||||||
identifier: {
|
identifier: {
|
||||||
type: 'dns',
|
type: 'dns',
|
||||||
|
@ -409,6 +437,7 @@ ACME._testChallenges = function(me, options) {
|
||||||
return ACME._challengeToAuth(
|
return ACME._challengeToAuth(
|
||||||
me,
|
me,
|
||||||
options,
|
options,
|
||||||
|
accountKeyThumb,
|
||||||
results,
|
results,
|
||||||
challenge,
|
challenge,
|
||||||
dryrun
|
dryrun
|
||||||
|
@ -460,7 +489,14 @@ ACME._chooseChallenge = function(options, results) {
|
||||||
|
|
||||||
return challenge;
|
return challenge;
|
||||||
};
|
};
|
||||||
ACME._challengeToAuth = function(me, options, request, challenge, dryrun) {
|
ACME._challengeToAuth = function(
|
||||||
|
me,
|
||||||
|
options,
|
||||||
|
accountKeyThumb,
|
||||||
|
request,
|
||||||
|
challenge,
|
||||||
|
dryrun
|
||||||
|
) {
|
||||||
// we don't poison the dns cache with our dummy request
|
// we don't poison the dns cache with our dummy request
|
||||||
var dnsPrefix = ACME.challengePrefixes['dns-01'];
|
var dnsPrefix = ACME.challengePrefixes['dns-01'];
|
||||||
if (dryrun) {
|
if (dryrun) {
|
||||||
|
@ -486,15 +522,15 @@ ACME._challengeToAuth = function(me, options, request, challenge, dryrun) {
|
||||||
auth[key] = challenge[key];
|
auth[key] = challenge[key];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var zone = pluckZone(options.zonenames || [], auth.identifier.value);
|
||||||
// batteries-included helpers
|
// batteries-included helpers
|
||||||
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 ACME._importKeypair(me, options.accountKeypair).then(function(pair) {
|
// we must accept JWKs that we didn't generate and we can't guarantee
|
||||||
return me.Keypairs.thumbprint({ jwk: pair.public }).then(function(
|
// that they properly set kid to thumbnail (especially since ACME doesn't do this)
|
||||||
thumb
|
// so we have to regenerate it every time we need it, which is quite often
|
||||||
) {
|
auth.thumbprint = accountKeyThumb;
|
||||||
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;
|
||||||
// conflicts with ACME challenge id url is already in use, so we call this challengeUrl instead
|
// conflicts with ACME challenge id url is already in use, so we call this challengeUrl instead
|
||||||
|
@ -507,6 +543,9 @@ ACME._challengeToAuth = function(me, options, request, challenge, dryrun) {
|
||||||
auth.token;
|
auth.token;
|
||||||
auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', '');
|
auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', '');
|
||||||
|
|
||||||
|
// Always calculate dnsAuthorization because we
|
||||||
|
// may need to present to the user for confirmation / instruction
|
||||||
|
// _as part of_ the decision making process
|
||||||
return sha2
|
return sha2
|
||||||
.sum(256, auth.keyAuthorization)
|
.sum(256, auth.keyAuthorization)
|
||||||
.then(function(hash) {
|
.then(function(hash) {
|
||||||
|
@ -514,10 +553,27 @@ ACME._challengeToAuth = function(me, options, request, challenge, dryrun) {
|
||||||
})
|
})
|
||||||
.then(function(hash64) {
|
.then(function(hash64) {
|
||||||
auth.dnsAuthorization = hash64;
|
auth.dnsAuthorization = hash64;
|
||||||
|
if (zone) {
|
||||||
|
auth.dnsZone = zone;
|
||||||
|
auth.dnsPrefix = auth.dnsHost
|
||||||
|
.replace(newZoneRegExp(zone), '')
|
||||||
|
.replace(/\.$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// For backwards compat with the v2.7 plugins
|
||||||
|
auth.challenge = auth;
|
||||||
|
// TODO can we use just { challenge: auth }?
|
||||||
|
auth.request = function() {
|
||||||
|
// TODO see https://git.rootprojects.org/root/acme.js/issues/###
|
||||||
|
console.warn(
|
||||||
|
"[warn] deprecated use of request on '" +
|
||||||
|
auth.type +
|
||||||
|
"' challenge object. Receive from challenger.init() instead."
|
||||||
|
);
|
||||||
|
me.request.apply(null, arguments);
|
||||||
|
};
|
||||||
return auth;
|
return auth;
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
ACME._untame = function(name, wild) {
|
ACME._untame = function(name, wild) {
|
||||||
|
@ -597,7 +653,7 @@ ACME._postChallenge = function(me, options, auth) {
|
||||||
.then(function(resp) {
|
.then(function(resp) {
|
||||||
if ('processing' === resp.body.status) {
|
if ('processing' === resp.body.status) {
|
||||||
if (me.debug) {
|
if (me.debug) {
|
||||||
console.debug('poll: again');
|
console.debug('poll: again', auth.url);
|
||||||
}
|
}
|
||||||
return ACME._wait(RETRY_INTERVAL).then(pollStatus);
|
return ACME._wait(RETRY_INTERVAL).then(pollStatus);
|
||||||
}
|
}
|
||||||
|
@ -610,14 +666,14 @@ ACME._postChallenge = function(me, options, auth) {
|
||||||
.then(respondToChallenge);
|
.then(respondToChallenge);
|
||||||
}
|
}
|
||||||
if (me.debug) {
|
if (me.debug) {
|
||||||
console.debug('poll: again');
|
console.debug('poll: again', auth.url);
|
||||||
}
|
}
|
||||||
return ACME._wait(RETRY_INTERVAL).then(respondToChallenge);
|
return ACME._wait(RETRY_INTERVAL).then(respondToChallenge);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('valid' === resp.body.status) {
|
if ('valid' === resp.body.status) {
|
||||||
if (me.debug) {
|
if (me.debug) {
|
||||||
console.debug('poll: valid');
|
console.debug('VALID !!!!!!!!!!!!!!!! poll: valid');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -637,7 +693,8 @@ ACME._postChallenge = function(me, options, auth) {
|
||||||
"[acme-v2] (E_STATE_INVALID) challenge state for '" +
|
"[acme-v2] (E_STATE_INVALID) challenge state for '" +
|
||||||
altname +
|
altname +
|
||||||
"': '" +
|
"': '" +
|
||||||
resp.body.status +
|
//resp.body.status +
|
||||||
|
JSON.stringify(resp.body) +
|
||||||
"'";
|
"'";
|
||||||
} else {
|
} else {
|
||||||
errmsg =
|
errmsg =
|
||||||
|
@ -675,17 +732,20 @@ ACME._postChallenge = function(me, options, auth) {
|
||||||
return respondToChallenge();
|
return respondToChallenge();
|
||||||
};
|
};
|
||||||
ACME._setChallenge = function(me, options, auth) {
|
ACME._setChallenge = function(me, options, auth) {
|
||||||
return new Promise(function(resolve, reject) {
|
return Promise.resolve().then(function() {
|
||||||
var challengers = options.challenges || {};
|
var challengers = options.challenges || {};
|
||||||
var challenger =
|
var challenger = challengers[auth.type] && challengers[auth.type].set;
|
||||||
(challengers[auth.type] && challengers[auth.type].set) ||
|
if (!challenger) {
|
||||||
options.setChallenge;
|
throw new Error(
|
||||||
try {
|
"options.challenges did not have a valid entry for '" +
|
||||||
|
auth.type +
|
||||||
|
"'"
|
||||||
|
);
|
||||||
|
}
|
||||||
if (1 === challenger.length) {
|
if (1 === challenger.length) {
|
||||||
challenger(auth)
|
return Promise.resolve(challenger(auth));
|
||||||
.then(resolve)
|
|
||||||
.catch(reject);
|
|
||||||
} else if (2 === challenger.length) {
|
} else if (2 === challenger.length) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
challenger(auth, function(err) {
|
challenger(auth, function(err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
|
@ -693,45 +753,12 @@ ACME._setChallenge = function(me, options, auth) {
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
// TODO remove this old backwards-compat
|
|
||||||
var challengeCb = function(err) {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// for backwards compat adding extra keys without changing params length
|
|
||||||
Object.keys(auth).forEach(function(key) {
|
|
||||||
challengeCb[key] = auth[key];
|
|
||||||
});
|
});
|
||||||
if (!ACME._setChallengeWarn) {
|
} else {
|
||||||
console.warn(
|
throw new Error(
|
||||||
'Please update to acme-v2 setChallenge(options) <Promise> or setChallenge(options, cb).'
|
"Bad function signature for '" + auth.type + "' challenge.set()"
|
||||||
);
|
|
||||||
console.warn(
|
|
||||||
"The API has been changed for compatibility with all ACME / Let's Encrypt challenge types."
|
|
||||||
);
|
|
||||||
ACME._setChallengeWarn = true;
|
|
||||||
}
|
|
||||||
challenger(
|
|
||||||
auth.identifier.value,
|
|
||||||
auth.token,
|
|
||||||
auth.keyAuthorization,
|
|
||||||
challengeCb
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
reject(e);
|
|
||||||
}
|
|
||||||
}).then(function() {
|
|
||||||
// TODO: Do we still need this delay? Or shall we leave it to plugins to account for themselves?
|
|
||||||
var DELAY = me.setChallengeWait || 500;
|
|
||||||
if (me.debug) {
|
|
||||||
console.debug('\n[DEBUG] waitChallengeDelay %s\n', DELAY);
|
|
||||||
}
|
|
||||||
return ACME._wait(DELAY);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
ACME._finalizeOrder = function(me, options, validatedDomains) {
|
ACME._finalizeOrder = function(me, options, validatedDomains) {
|
||||||
|
@ -943,12 +970,45 @@ ACME._getCertificate = function(me, options) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO Promise.all()?
|
||||||
|
Object.keys(options.challenges).forEach(function(key) {
|
||||||
|
var presenter = options.challenges[key];
|
||||||
|
if ('function' === typeof presenter.init && !presenter._initialized) {
|
||||||
|
presenter._initialized = true;
|
||||||
|
return ACME._depInit(me, presenter);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var promiseZones;
|
||||||
|
if (options.challenges['dns-01']) {
|
||||||
|
// a little bit of random to ensure that getZones()
|
||||||
|
// actually returns the zones and not the hosts as zones
|
||||||
|
var dnsHosts = options.domains.map(function(d) {
|
||||||
|
var rnd = require('crypto')
|
||||||
|
.randomBytes(2)
|
||||||
|
.toString('hex');
|
||||||
|
return rnd + '.' + d;
|
||||||
|
});
|
||||||
|
promiseZones = ACME._getZones(
|
||||||
|
me,
|
||||||
|
options.challenges['dns-01'],
|
||||||
|
dnsHosts
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
promiseZones = Promise.resolve([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return promiseZones
|
||||||
|
.then(function(zonenames) {
|
||||||
|
options.zonenames = zonenames;
|
||||||
// Do a little dry-run / self-test
|
// Do a little dry-run / self-test
|
||||||
return ACME._testChallenges(me, options).then(function() {
|
return ACME._testChallenges(me, options);
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
if (me.debug) {
|
if (me.debug) {
|
||||||
console.debug('[acme-v2] certificates.create');
|
console.debug('[acme-v2] certificates.create');
|
||||||
}
|
}
|
||||||
var body = {
|
var certOrder = {
|
||||||
// raw wildcard syntax MUST be used here
|
// raw wildcard syntax MUST be used here
|
||||||
identifiers: options.domains
|
identifiers: options.domains
|
||||||
.sort(function(a, b) {
|
.sort(function(a, b) {
|
||||||
|
@ -971,7 +1031,7 @@ ACME._getCertificate = function(me, options) {
|
||||||
//, "notAfter": "2016-01-08T00:00:00Z"
|
//, "notAfter": "2016-01-08T00:00:00Z"
|
||||||
};
|
};
|
||||||
|
|
||||||
var payload = JSON.stringify(body);
|
var payload = JSON.stringify(certOrder);
|
||||||
if (me.debug) {
|
if (me.debug) {
|
||||||
console.debug('\n[DEBUG] newOrder\n');
|
console.debug('\n[DEBUG] newOrder\n');
|
||||||
}
|
}
|
||||||
|
@ -1011,15 +1071,27 @@ ACME._getCertificate = function(me, options) {
|
||||||
}
|
}
|
||||||
setAuths = options._authorizations.slice(0);
|
setAuths = options._authorizations.slice(0);
|
||||||
|
|
||||||
|
var accountKeyThumb;
|
||||||
|
function setThumbnail() {
|
||||||
|
return ACME._importKeypair(me, options.accountKeypair).then(
|
||||||
|
function(pair) {
|
||||||
|
return me.Keypairs.thumbprint({
|
||||||
|
jwk: pair.public
|
||||||
|
}).then(function(_thumb) {
|
||||||
|
accountKeyThumb = _thumb;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function setNext() {
|
function setNext() {
|
||||||
var authUrl = setAuths.shift();
|
var authUrl = setAuths.shift();
|
||||||
if (!authUrl) {
|
if (!authUrl) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ACME._getChallenges(me, options, authUrl).then(function(
|
return ACME._getChallenges(me, options, authUrl).then(
|
||||||
results
|
function(results) {
|
||||||
) {
|
|
||||||
// var domain = options.domains[i]; // results.identifier.value
|
// var domain = options.domains[i]; // results.identifier.value
|
||||||
|
|
||||||
// If it's already valid, we're golden it regardless
|
// If it's already valid, we're golden it regardless
|
||||||
|
@ -1031,7 +1103,10 @@ ACME._getCertificate = function(me, options) {
|
||||||
return setNext();
|
return setNext();
|
||||||
}
|
}
|
||||||
|
|
||||||
var challenge = ACME._chooseChallenge(options, results);
|
var challenge = ACME._chooseChallenge(
|
||||||
|
options,
|
||||||
|
results
|
||||||
|
);
|
||||||
if (!challenge) {
|
if (!challenge) {
|
||||||
// For example, wildcards require dns-01 and, if we don't have that, we have to bail
|
// For example, wildcards require dns-01 and, if we don't have that, we have to bail
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
|
@ -1046,19 +1121,37 @@ ACME._getCertificate = function(me, options) {
|
||||||
return ACME._challengeToAuth(
|
return ACME._challengeToAuth(
|
||||||
me,
|
me,
|
||||||
options,
|
options,
|
||||||
|
accountKeyThumb,
|
||||||
results,
|
results,
|
||||||
challenge,
|
challenge,
|
||||||
false
|
false
|
||||||
).then(function(auth) {
|
).then(function(auth) {
|
||||||
|
console.log('ADD DUBIOUS AUTH');
|
||||||
auths.push(auth);
|
auths.push(auth);
|
||||||
return ACME._setChallenge(me, options, auth).then(
|
return ACME._setChallenge(
|
||||||
setNext
|
me,
|
||||||
|
options,
|
||||||
|
auth
|
||||||
|
).then(setNext);
|
||||||
|
});
|
||||||
|
}
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
});
|
|
||||||
|
function waitAll() {
|
||||||
|
// TODO take the max wait of all challenge plugins and wait that long, or 1000ms
|
||||||
|
var DELAY = me.setChallengeWait || 7000;
|
||||||
|
if (true || me.debug) {
|
||||||
|
console.debug(
|
||||||
|
'\n[DEBUG] waitChallengeDelay %s\n',
|
||||||
|
DELAY
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return ACME._wait(DELAY);
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkNext() {
|
function checkNext() {
|
||||||
|
console.log('CONSUME DUBIOUS AUTH', auths.length);
|
||||||
var auth = auths.shift();
|
var auth = auths.shift();
|
||||||
if (!auth) {
|
if (!auth) {
|
||||||
return;
|
return;
|
||||||
|
@ -1068,45 +1161,43 @@ ACME._getCertificate = function(me, options) {
|
||||||
// not so much "valid" as "not invalid"
|
// not so much "valid" as "not invalid"
|
||||||
// but in this case we can't confirm either way
|
// but in this case we can't confirm either way
|
||||||
validAuths.push(auth);
|
validAuths.push(auth);
|
||||||
return Promise.resolve();
|
console.log('ADD VALID AUTH (skip)', validAuths.length);
|
||||||
|
return checkNext();
|
||||||
}
|
}
|
||||||
|
|
||||||
return ACME.challengeTests[auth.type](me, auth)
|
return ACME.challengeTests[auth.type](me, auth)
|
||||||
.then(function() {
|
.then(function() {
|
||||||
|
console.log('ADD VALID AUTH');
|
||||||
validAuths.push(auth);
|
validAuths.push(auth);
|
||||||
})
|
})
|
||||||
.then(checkNext);
|
.then(checkNext);
|
||||||
}
|
}
|
||||||
|
|
||||||
function challengeNext() {
|
function presentNext() {
|
||||||
|
console.log('CONSUME VALID AUTH', validAuths.length);
|
||||||
var auth = validAuths.shift();
|
var auth = validAuths.shift();
|
||||||
if (!auth) {
|
if (!auth) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return ACME._postChallenge(me, options, auth).then(
|
return ACME._postChallenge(me, options, auth).then(
|
||||||
challengeNext
|
presentNext
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// First we set every challenge
|
function finalizeOrder() {
|
||||||
// Then we ask for each challenge to be checked
|
|
||||||
// Doing otherwise would potentially cause us to poison our own DNS cache with misses
|
|
||||||
return setNext()
|
|
||||||
.then(checkNext)
|
|
||||||
.then(challengeNext)
|
|
||||||
.then(function() {
|
|
||||||
if (me.debug) {
|
if (me.debug) {
|
||||||
console.debug('[getCertificate] next.then');
|
console.debug('[getCertificate] next.then');
|
||||||
}
|
}
|
||||||
var validatedDomains = body.identifiers.map(function(
|
var validatedDomains = certOrder.identifiers.map(function(
|
||||||
ident
|
ident
|
||||||
) {
|
) {
|
||||||
return ident.value;
|
return ident.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
return ACME._finalizeOrder(me, options, validatedDomains);
|
return ACME._finalizeOrder(me, options, validatedDomains);
|
||||||
})
|
}
|
||||||
.then(function(order) {
|
|
||||||
|
function retrieveCerts(order) {
|
||||||
if (me.debug) {
|
if (me.debug) {
|
||||||
console.debug('acme-v2: order was finalized');
|
console.debug('acme-v2: order was finalized');
|
||||||
}
|
}
|
||||||
|
@ -1141,10 +1232,22 @@ ACME._getCertificate = function(me, options) {
|
||||||
}
|
}
|
||||||
return certs;
|
return certs;
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
|
// First we set each and every challenge
|
||||||
|
// Then we ask for each challenge to be checked
|
||||||
|
// Doing otherwise would potentially cause us to poison our own DNS cache with misses
|
||||||
|
return setThumbnail()
|
||||||
|
.then(setNext)
|
||||||
|
.then(waitAll)
|
||||||
|
.then(checkNext)
|
||||||
|
.then(presentNext)
|
||||||
|
.then(finalizeOrder)
|
||||||
|
.then(retrieveCerts);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
ACME._generateCsrWeb64 = function(me, options, validatedDomains) {
|
ACME._generateCsrWeb64 = function(me, options, validatedDomains) {
|
||||||
var csr;
|
var csr;
|
||||||
if (options.csr) {
|
if (options.csr) {
|
||||||
|
@ -1153,6 +1256,7 @@ ACME._generateCsrWeb64 = function(me, options, validatedDomains) {
|
||||||
if ('string' !== typeof csr) {
|
if ('string' !== typeof csr) {
|
||||||
csr = Enc.bufToUrlBase64(csr);
|
csr = Enc.bufToUrlBase64(csr);
|
||||||
}
|
}
|
||||||
|
// TODO PEM.parseBlock()
|
||||||
// nix PEM headers, if any
|
// nix PEM headers, if any
|
||||||
if ('-' === csr[0]) {
|
if ('-' === csr[0]) {
|
||||||
csr = csr
|
csr = csr
|
||||||
|
@ -1168,13 +1272,11 @@ ACME._generateCsrWeb64 = function(me, options, validatedDomains) {
|
||||||
me,
|
me,
|
||||||
options.serverKeypair || options.domainKeypair
|
options.serverKeypair || options.domainKeypair
|
||||||
).then(function(pair) {
|
).then(function(pair) {
|
||||||
return me
|
return me.CSR.csr({
|
||||||
.CSR({
|
|
||||||
jwk: pair.private,
|
jwk: pair.private,
|
||||||
domains: validatedDomains,
|
domains: validatedDomains,
|
||||||
encoding: 'der'
|
encoding: 'der'
|
||||||
})
|
}).then(function(der) {
|
||||||
.then(function(der) {
|
|
||||||
return Enc.bufToUrlBase64(der);
|
return Enc.bufToUrlBase64(der);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1276,6 +1378,7 @@ ACME._jwsRequest = function(me, bigopts) {
|
||||||
bigopts.protected.kid = bigopts.options._kid;
|
bigopts.protected.kid = bigopts.options._kid;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// this will shasum the thumbnail the 2nd time
|
||||||
return me.Keypairs.signJws({
|
return me.Keypairs.signJws({
|
||||||
jwk: bigopts.options.accountKeypair.privateKeyJwk,
|
jwk: bigopts.options.accountKeypair.privateKeyJwk,
|
||||||
protected: bigopts.protected,
|
protected: bigopts.protected,
|
||||||
|
@ -1291,6 +1394,7 @@ ACME._jwsRequest = function(me, bigopts) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle some ACME-specific defaults
|
// Handle some ACME-specific defaults
|
||||||
ACME._request = function(me, opts) {
|
ACME._request = function(me, opts) {
|
||||||
if (!opts.headers) {
|
if (!opts.headers) {
|
||||||
|
@ -1430,24 +1534,99 @@ ACME._http01 = function(me, auth) {
|
||||||
ACME._removeChallenge = function(me, options, auth) {
|
ACME._removeChallenge = function(me, options, auth) {
|
||||||
var challengers = options.challenges || {};
|
var challengers = options.challenges || {};
|
||||||
var removeChallenge =
|
var removeChallenge =
|
||||||
(challengers[auth.type] && challengers[auth.type].remove) ||
|
challengers[auth.type] && challengers[auth.type].remove;
|
||||||
options.removeChallenge;
|
|
||||||
if (1 === removeChallenge.length) {
|
if (1 === removeChallenge.length) {
|
||||||
removeChallenge(auth).then(function() {}, function() {});
|
return Promise.resolve(removeChallenge(auth)).then(
|
||||||
|
function() {},
|
||||||
|
function() {}
|
||||||
|
);
|
||||||
} else if (2 === removeChallenge.length) {
|
} else if (2 === removeChallenge.length) {
|
||||||
removeChallenge(auth, function(err) {
|
removeChallenge(auth, function(err) {
|
||||||
return err;
|
return err;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (!ACME._removeChallengeWarn) {
|
throw new Error(
|
||||||
console.warn(
|
"Bad function signature for '" + auth.type + "' challenge.remove()"
|
||||||
'Please update to acme-v2 removeChallenge(options) <Promise> or removeChallenge(options, cb).'
|
|
||||||
);
|
);
|
||||||
console.warn(
|
|
||||||
"The API has been changed for compatibility with all ACME / Let's Encrypt challenge types."
|
|
||||||
);
|
|
||||||
ACME._removeChallengeWarn = true;
|
|
||||||
}
|
|
||||||
removeChallenge(auth.request.identifier, auth.token, function() {});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ACME._depInit = function(me, presenter) {
|
||||||
|
if ('function' !== typeof presenter.init) {
|
||||||
|
return Promise.resolve(null);
|
||||||
|
}
|
||||||
|
return ACME._wrapCb(
|
||||||
|
me,
|
||||||
|
presenter,
|
||||||
|
'init',
|
||||||
|
{ type: '*', request: me.request },
|
||||||
|
'null'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ACME._getZones = function(me, presenter, dnsHosts) {
|
||||||
|
if ('function' !== typeof presenter.zones) {
|
||||||
|
presenter.zones = function() {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
var challenge = {
|
||||||
|
type: 'dns-01',
|
||||||
|
dnsHosts: dnsHosts,
|
||||||
|
request: me.request
|
||||||
|
};
|
||||||
|
// back/forwards-compat
|
||||||
|
challenge.challenge = challenge;
|
||||||
|
return ACME._wrapCb(
|
||||||
|
me,
|
||||||
|
presenter,
|
||||||
|
'zones',
|
||||||
|
challenge,
|
||||||
|
'an array of zone names'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ACME._wrapCb = function(me, options, _name, args, _desc) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
if (options[_name].length <= 1) {
|
||||||
|
return Promise.resolve(options[_name](args))
|
||||||
|
.then(resolve)
|
||||||
|
.catch(reject);
|
||||||
|
} else if (2 === options[_name].length) {
|
||||||
|
options[_name](args, function(err, results) {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve(results);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
'options.' + _name + ' should accept opts and Promise ' + _desc
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function newZoneRegExp(zonename) {
|
||||||
|
// (^|\.)example\.com$
|
||||||
|
// which matches:
|
||||||
|
// foo.example.com
|
||||||
|
// example.com
|
||||||
|
// but not:
|
||||||
|
// fooexample.com
|
||||||
|
return new RegExp('(^|\\.)' + zonename.replace(/\./g, '\\.') + '$');
|
||||||
|
}
|
||||||
|
|
||||||
|
function pluckZone(zonenames, dnsHost) {
|
||||||
|
return zonenames
|
||||||
|
.filter(function(zonename) {
|
||||||
|
// the only character that needs to be escaped for regex
|
||||||
|
// and is allowed in a domain name is '.'
|
||||||
|
return newZoneRegExp(zonename).test(dnsHost);
|
||||||
|
})
|
||||||
|
.sort(function(a, b) {
|
||||||
|
// longest match first
|
||||||
|
return b.length - a.length;
|
||||||
|
})[0];
|
||||||
|
}
|
||||||
|
|
22
lib/csr.js
22
lib/csr.js
|
@ -5,18 +5,19 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
/*global Promise*/
|
/*global Promise*/
|
||||||
|
|
||||||
var ASN1 = require('./asn1/parser.js'); // DER, actually
|
var ASN1 = require('./asn1/packer.js'); // DER, actually
|
||||||
var Asn1 = ASN1.Any;
|
var Asn1 = ASN1.Any;
|
||||||
var BitStr = ASN1.BitStr;
|
var BitStr = ASN1.BitStr;
|
||||||
var UInt = ASN1.UInt;
|
var UInt = ASN1.UInt;
|
||||||
var Asn1Parser = require('./asn1/packer.js'); // DER, actually
|
var Asn1Parser = require('./asn1/parser.js');
|
||||||
var Enc = require('omnibuffer');
|
var Enc = require('omnibuffer');
|
||||||
var PEM = require('./pem.js');
|
var PEM = require('./pem.js');
|
||||||
var X509 = require('./x509.js');
|
var X509 = require('./x509.js');
|
||||||
var Keypairs = require('./keypairs');
|
var Keypairs = require('./keypairs');
|
||||||
|
|
||||||
// TODO find a way that the prior node-ish way of `module.exports = function () {}` isn't broken
|
// TODO find a way that the prior node-ish way of `module.exports = function () {}` isn't broken
|
||||||
var CSR = (exports.CSR = function(opts) {
|
var CSR = module.exports;
|
||||||
|
CSR.csr = function(opts) {
|
||||||
// We're using a Promise here to be compatible with the browser version
|
// We're using a Promise here to be compatible with the browser version
|
||||||
// which will probably use the webcrypto API for some of the conversions
|
// which will probably use the webcrypto API for some of the conversions
|
||||||
return CSR._prepare(opts).then(function(opts) {
|
return CSR._prepare(opts).then(function(opts) {
|
||||||
|
@ -24,11 +25,10 @@ var CSR = (exports.CSR = function(opts) {
|
||||||
return CSR._encode(opts, bytes);
|
return CSR._encode(opts, bytes);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
|
|
||||||
CSR._prepare = function(opts) {
|
CSR._prepare = function(opts) {
|
||||||
return Promise.resolve().then(function() {
|
return Promise.resolve().then(function() {
|
||||||
var Keypairs;
|
|
||||||
opts = JSON.parse(JSON.stringify(opts));
|
opts = JSON.parse(JSON.stringify(opts));
|
||||||
|
|
||||||
// We do a bit of extra error checking for user convenience
|
// We do a bit of extra error checking for user convenience
|
||||||
|
@ -66,16 +66,6 @@ CSR._prepare = function(opts) {
|
||||||
throw new Error('You must pass options.key as a JSON web key');
|
throw new Error('You must pass options.key as a JSON web key');
|
||||||
}
|
}
|
||||||
|
|
||||||
Keypairs = exports.Keypairs;
|
|
||||||
if (!exports.Keypairs) {
|
|
||||||
throw new Error(
|
|
||||||
'Keypairs.js is an optional dependency for PEM-to-JWK.\n' +
|
|
||||||
"Install it if you'd like to use it:\n" +
|
|
||||||
'\tnpm install --save rasha\n' +
|
|
||||||
'Otherwise supply a jwk as the private key.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Keypairs.import({ pem: opts.pem || opts.key }).then(function(
|
return Keypairs.import({ pem: opts.pem || opts.key }).then(function(
|
||||||
pair
|
pair
|
||||||
) {
|
) {
|
||||||
|
@ -119,7 +109,7 @@ CSR._sign = function csrEcSig(jwk, request) {
|
||||||
// Took some tips from https://gist.github.com/codermapuche/da4f96cdb6d5ff53b7ebc156ec46a10a
|
// Took some tips from https://gist.github.com/codermapuche/da4f96cdb6d5ff53b7ebc156ec46a10a
|
||||||
// TODO will have to convert web ECDSA signatures to PEM ECDSA signatures (but RSA should be the same)
|
// TODO will have to convert web ECDSA signatures to PEM ECDSA signatures (but RSA should be the same)
|
||||||
// TODO have a consistent non-private way to sign
|
// TODO have a consistent non-private way to sign
|
||||||
return Keypairs._sign(
|
return Keypairs.sign(
|
||||||
{ jwk: jwk, format: 'x509' },
|
{ jwk: jwk, format: 'x509' },
|
||||||
Enc.hexToBuf(request)
|
Enc.hexToBuf(request)
|
||||||
).then(function(sig) {
|
).then(function(sig) {
|
||||||
|
|
|
@ -76,6 +76,7 @@ Keypairs.neuter = function(opts) {
|
||||||
};
|
};
|
||||||
|
|
||||||
Keypairs.thumbprint = function(opts) {
|
Keypairs.thumbprint = function(opts) {
|
||||||
|
//console.log('[debug]', new Error('NOT_ERROR').stack);
|
||||||
return Promise.resolve().then(function() {
|
return Promise.resolve().then(function() {
|
||||||
if (/EC/i.test(opts.jwk.kty)) {
|
if (/EC/i.test(opts.jwk.kty)) {
|
||||||
console.log('[debug] EC thumbprint');
|
console.log('[debug] EC thumbprint');
|
||||||
|
@ -121,6 +122,7 @@ Keypairs.publish = function(opts) {
|
||||||
|
|
||||||
// JWT a.k.a. JWS with Claims using Compact Serialization
|
// JWT a.k.a. JWS with Claims using Compact Serialization
|
||||||
Keypairs.signJwt = function(opts) {
|
Keypairs.signJwt = function(opts) {
|
||||||
|
console.log('[debug] signJwt');
|
||||||
return Keypairs.thumbprint({ jwk: opts.jwk }).then(function(thumb) {
|
return Keypairs.thumbprint({ jwk: opts.jwk }).then(function(thumb) {
|
||||||
var header = opts.header || {};
|
var header = opts.header || {};
|
||||||
var claims = JSON.parse(JSON.stringify(opts.claims || {}));
|
var claims = JSON.parse(JSON.stringify(opts.claims || {}));
|
||||||
|
@ -255,6 +257,9 @@ Keypairs.signJws = function(opts) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO expose consistently
|
||||||
|
Keypairs.sign = native._sign;
|
||||||
|
|
||||||
Keypairs._getBits = function(opts) {
|
Keypairs._getBits = function(opts) {
|
||||||
if (opts.alg) {
|
if (opts.alg) {
|
||||||
return opts.alg.replace(/[a-z\-]/gi, '');
|
return opts.alg.replace(/[a-z\-]/gi, '');
|
||||||
|
|
|
@ -15,7 +15,7 @@ Keypairs._sign = function(opts, payload) {
|
||||||
.update(payload)
|
.update(payload)
|
||||||
.sign(pem);
|
.sign(pem);
|
||||||
|
|
||||||
if ('EC' === opts.jwk.kty) {
|
if ('EC' === opts.jwk.kty && !/x509|asn1/i.test(opts.format)) {
|
||||||
// ECDSA JWT signatures differ from "normal" ECDSA signatures
|
// ECDSA JWT signatures differ from "normal" ECDSA signatures
|
||||||
// https://tools.ietf.org/html/rfc7518#section-3.4
|
// https://tools.ietf.org/html/rfc7518#section-3.4
|
||||||
binsig = Keypairs._ecdsaAsn1SigToJoseSig(binsig);
|
binsig = Keypairs._ecdsaAsn1SigToJoseSig(binsig);
|
||||||
|
|
|
@ -1,18 +1,39 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
var ACME = require('../');
|
var ACME = require('../');
|
||||||
var Keypairs = require('../lib/keypairs.js');
|
var Keypairs = require('../lib/keypairs.js');
|
||||||
var acme = ACME.create({});
|
var acme = ACME.create({ debug: true });
|
||||||
|
|
||||||
|
// TODO exec npm install --save-dev CHALLENGE_MODULE
|
||||||
|
|
||||||
var config = {
|
var config = {
|
||||||
env: process.env.ENV,
|
env: process.env.ENV,
|
||||||
email: process.env.SUBSCRIBER_EMAIL,
|
email: process.env.SUBSCRIBER_EMAIL,
|
||||||
domain: process.env.BASE_DOMAIN
|
domain: process.env.BASE_DOMAIN,
|
||||||
|
challengeType: process.env.CHALLENGE_TYPE,
|
||||||
|
challengeModule: process.env.CHALLENGE_MODULE,
|
||||||
|
challengeOptions: JSON.parse(process.env.CHALLENGE_OPTIONS)
|
||||||
};
|
};
|
||||||
config.debug = !/^PROD/i.test(config.env);
|
config.debug = !/^PROD/i.test(config.env);
|
||||||
|
config.challenger = require('acme-' +
|
||||||
|
config.challengeType +
|
||||||
|
'-' +
|
||||||
|
config.challengeModule).create(config.challengeOptions);
|
||||||
|
if (!config.challengeType || !config.domain) {
|
||||||
|
console.error(
|
||||||
|
new Error('Missing config variables. Check you .env and the docs')
|
||||||
|
.message
|
||||||
|
);
|
||||||
|
console.error(config);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
var challenges = {};
|
||||||
|
challenges[config.challengeType] = config.challenger;
|
||||||
|
|
||||||
async function happyPath() {
|
async function happyPath() {
|
||||||
var domains = randomDomains();
|
|
||||||
var agreed = false;
|
var agreed = false;
|
||||||
var metadata = await acme.init(
|
var metadata = await acme.init(
|
||||||
'https://acme-staging-v02.api.letsencrypt.org/directory'
|
'https://acme-staging-v02.api.letsencrypt.org/directory'
|
||||||
|
@ -66,8 +87,31 @@ async function happyPath() {
|
||||||
if (config.debug) {
|
if (config.debug) {
|
||||||
console.info('Server Key Created');
|
console.info('Server Key Created');
|
||||||
console.info(JSON.stringify(serverKeypair, null, 2));
|
console.info(JSON.stringify(serverKeypair, null, 2));
|
||||||
console.info('');
|
|
||||||
console.info();
|
console.info();
|
||||||
|
console.info();
|
||||||
|
}
|
||||||
|
|
||||||
|
var domains = randomDomains();
|
||||||
|
if (config.debug) {
|
||||||
|
console.info('Get certificates for random domains:');
|
||||||
|
console.info(domains);
|
||||||
|
}
|
||||||
|
var results = await acme.certificates.create({
|
||||||
|
account: account,
|
||||||
|
accountKeypair: { privateKeyJwk: accountKeypair.private },
|
||||||
|
serverKeypair: { privateKeyJwk: serverKeypair.private },
|
||||||
|
domains: domains,
|
||||||
|
challenges: challenges, // must be implemented
|
||||||
|
skipDryRun: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (config.debug) {
|
||||||
|
console.info('Got SSL Certificate:');
|
||||||
|
console.info(results.expires);
|
||||||
|
console.info(results.cert);
|
||||||
|
console.info(results.chain);
|
||||||
|
console.info('');
|
||||||
|
console.info('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue