forked from root/acme.js
WIP gets a cert... nice!
This commit is contained in:
parent
e75c503356
commit
24c3633d75
703
lib/acme.js
703
lib/acme.js
|
@ -165,7 +165,7 @@ ACME._registerAccount = function(me, options) {
|
|||
} else if (options.email) {
|
||||
contact = ['mailto:' + options.email];
|
||||
}
|
||||
var body = {
|
||||
var accountRequest = {
|
||||
termsOfServiceAgreed: tosUrl === me._tos,
|
||||
onlyReturnExisting: false,
|
||||
contact: contact
|
||||
|
@ -182,14 +182,14 @@ ACME._registerAccount = function(me, options) {
|
|||
},
|
||||
payload: Enc.strToBuf(JSON.stringify(pair.public))
|
||||
}).then(function(jws) {
|
||||
body.externalAccountBinding = jws;
|
||||
return body;
|
||||
accountRequest.externalAccountBinding = jws;
|
||||
return accountRequest;
|
||||
});
|
||||
} else {
|
||||
pExt = Promise.resolve(body);
|
||||
pExt = Promise.resolve(accountRequest);
|
||||
}
|
||||
return pExt.then(function(body) {
|
||||
var payload = JSON.stringify(body);
|
||||
return pExt.then(function(accountRequest) {
|
||||
var payload = JSON.stringify(accountRequest);
|
||||
return ACME._jwsRequest(me, {
|
||||
options: options,
|
||||
url: me._directoryUrls.newAccount,
|
||||
|
@ -199,10 +199,20 @@ ACME._registerAccount = function(me, options) {
|
|||
.then(function(resp) {
|
||||
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(
|
||||
'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) {
|
||||
console.log('[debug] testChallenges');
|
||||
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(
|
||||
options.domains.map(function(identifierValue) {
|
||||
// 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) {
|
||||
// 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 = {
|
||||
identifier: {
|
||||
type: 'dns',
|
||||
|
@ -409,6 +437,7 @@ ACME._testChallenges = function(me, options) {
|
|||
return ACME._challengeToAuth(
|
||||
me,
|
||||
options,
|
||||
accountKeyThumb,
|
||||
results,
|
||||
challenge,
|
||||
dryrun
|
||||
|
@ -460,7 +489,14 @@ ACME._chooseChallenge = function(options, results) {
|
|||
|
||||
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
|
||||
var dnsPrefix = ACME.challengePrefixes['dns-01'];
|
||||
if (dryrun) {
|
||||
|
@ -486,38 +522,58 @@ ACME._challengeToAuth = function(me, options, request, challenge, dryrun) {
|
|||
auth[key] = challenge[key];
|
||||
});
|
||||
|
||||
var zone = pluckZone(options.zonenames || [], auth.identifier.value);
|
||||
// batteries-included helpers
|
||||
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 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
|
||||
// TODO auth.http01Url ?
|
||||
auth.challengeUrl =
|
||||
'http://' +
|
||||
auth.identifier.value +
|
||||
ACME.challengePrefixes['http-01'] +
|
||||
'/' +
|
||||
auth.token;
|
||||
auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', '');
|
||||
// we must accept JWKs that we didn't generate and we can't guarantee
|
||||
// that they properly set kid to thumbnail (especially since ACME doesn't do this)
|
||||
// so we have to regenerate it every time we need it, which is quite often
|
||||
auth.thumbprint = accountKeyThumb;
|
||||
// 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
|
||||
// TODO auth.http01Url ?
|
||||
auth.challengeUrl =
|
||||
'http://' +
|
||||
auth.identifier.value +
|
||||
ACME.challengePrefixes['http-01'] +
|
||||
'/' +
|
||||
auth.token;
|
||||
auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', '');
|
||||
|
||||
return sha2
|
||||
.sum(256, auth.keyAuthorization)
|
||||
.then(function(hash) {
|
||||
return Enc.bufToUrlBase64(new Uint8Array(hash));
|
||||
})
|
||||
.then(function(hash64) {
|
||||
auth.dnsAuthorization = hash64;
|
||||
return auth;
|
||||
});
|
||||
// Always calculate dnsAuthorization because we
|
||||
// may need to present to the user for confirmation / instruction
|
||||
// _as part of_ the decision making process
|
||||
return sha2
|
||||
.sum(256, auth.keyAuthorization)
|
||||
.then(function(hash) {
|
||||
return Enc.bufToUrlBase64(new Uint8Array(hash));
|
||||
})
|
||||
.then(function(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;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
ACME._untame = function(name, wild) {
|
||||
|
@ -597,7 +653,7 @@ ACME._postChallenge = function(me, options, auth) {
|
|||
.then(function(resp) {
|
||||
if ('processing' === resp.body.status) {
|
||||
if (me.debug) {
|
||||
console.debug('poll: again');
|
||||
console.debug('poll: again', auth.url);
|
||||
}
|
||||
return ACME._wait(RETRY_INTERVAL).then(pollStatus);
|
||||
}
|
||||
|
@ -610,14 +666,14 @@ ACME._postChallenge = function(me, options, auth) {
|
|||
.then(respondToChallenge);
|
||||
}
|
||||
if (me.debug) {
|
||||
console.debug('poll: again');
|
||||
console.debug('poll: again', auth.url);
|
||||
}
|
||||
return ACME._wait(RETRY_INTERVAL).then(respondToChallenge);
|
||||
}
|
||||
|
||||
if ('valid' === resp.body.status) {
|
||||
if (me.debug) {
|
||||
console.debug('poll: valid');
|
||||
console.debug('VALID !!!!!!!!!!!!!!!! poll: valid');
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -637,7 +693,8 @@ ACME._postChallenge = function(me, options, auth) {
|
|||
"[acme-v2] (E_STATE_INVALID) challenge state for '" +
|
||||
altname +
|
||||
"': '" +
|
||||
resp.body.status +
|
||||
//resp.body.status +
|
||||
JSON.stringify(resp.body) +
|
||||
"'";
|
||||
} else {
|
||||
errmsg =
|
||||
|
@ -675,17 +732,20 @@ ACME._postChallenge = function(me, options, auth) {
|
|||
return respondToChallenge();
|
||||
};
|
||||
ACME._setChallenge = function(me, options, auth) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
return Promise.resolve().then(function() {
|
||||
var challengers = options.challenges || {};
|
||||
var challenger =
|
||||
(challengers[auth.type] && challengers[auth.type].set) ||
|
||||
options.setChallenge;
|
||||
try {
|
||||
if (1 === challenger.length) {
|
||||
challenger(auth)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
} else if (2 === challenger.length) {
|
||||
var challenger = challengers[auth.type] && challengers[auth.type].set;
|
||||
if (!challenger) {
|
||||
throw new Error(
|
||||
"options.challenges did not have a valid entry for '" +
|
||||
auth.type +
|
||||
"'"
|
||||
);
|
||||
}
|
||||
if (1 === challenger.length) {
|
||||
return Promise.resolve(challenger(auth));
|
||||
} else if (2 === challenger.length) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
challenger(auth, function(err) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
|
@ -693,45 +753,12 @@ ACME._setChallenge = function(me, options, auth) {
|
|||
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) {
|
||||
console.warn(
|
||||
'Please update to acme-v2 setChallenge(options) <Promise> or setChallenge(options, cb).'
|
||||
);
|
||||
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);
|
||||
});
|
||||
} else {
|
||||
throw new Error(
|
||||
"Bad function signature for '" + auth.type + "' challenge.set()"
|
||||
);
|
||||
}
|
||||
}).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) {
|
||||
|
@ -943,170 +970,234 @@ ACME._getCertificate = function(me, options) {
|
|||
});
|
||||
}
|
||||
|
||||
// Do a little dry-run / self-test
|
||||
return ACME._testChallenges(me, options).then(function() {
|
||||
if (me.debug) {
|
||||
console.debug('[acme-v2] certificates.create');
|
||||
// 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 body = {
|
||||
// raw wildcard syntax MUST be used here
|
||||
identifiers: options.domains
|
||||
.sort(function(a, b) {
|
||||
// the first in the list will be the subject of the certificate, I believe (and hope)
|
||||
if (!options.subject) {
|
||||
});
|
||||
|
||||
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
|
||||
return ACME._testChallenges(me, options);
|
||||
})
|
||||
.then(function() {
|
||||
if (me.debug) {
|
||||
console.debug('[acme-v2] certificates.create');
|
||||
}
|
||||
var certOrder = {
|
||||
// raw wildcard syntax MUST be used here
|
||||
identifiers: options.domains
|
||||
.sort(function(a, b) {
|
||||
// the first in the list will be the subject of the certificate, I believe (and hope)
|
||||
if (!options.subject) {
|
||||
return 0;
|
||||
}
|
||||
if (options.subject === a) {
|
||||
return -1;
|
||||
}
|
||||
if (options.subject === b) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (options.subject === a) {
|
||||
return -1;
|
||||
}
|
||||
if (options.subject === b) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
.map(function(hostname) {
|
||||
return { type: 'dns', value: hostname };
|
||||
})
|
||||
//, "notBefore": "2016-01-01T00:00:00Z"
|
||||
//, "notAfter": "2016-01-08T00:00:00Z"
|
||||
};
|
||||
|
||||
var payload = JSON.stringify(body);
|
||||
if (me.debug) {
|
||||
console.debug('\n[DEBUG] newOrder\n');
|
||||
}
|
||||
return ACME._jwsRequest(me, {
|
||||
options: options,
|
||||
url: me._directoryUrls.newOrder,
|
||||
protected: { kid: options._kid },
|
||||
payload: Enc.strToBuf(payload)
|
||||
}).then(function(resp) {
|
||||
var location = resp.headers.location;
|
||||
var setAuths;
|
||||
var validAuths = [];
|
||||
var auths = [];
|
||||
if (me.debug) {
|
||||
console.debug('[ordered]', location);
|
||||
} // the account id url
|
||||
if (me.debug) {
|
||||
console.debug(resp);
|
||||
}
|
||||
options._authorizations = resp.body.authorizations;
|
||||
options._order = location;
|
||||
options._finalize = resp.body.finalize;
|
||||
//if (me.debug) console.debug('[DEBUG] finalize:', options._finalize); return;
|
||||
|
||||
if (!options._authorizations) {
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
"[acme-v2.js] authorizations were not fetched for '" +
|
||||
options.domains.join() +
|
||||
"':\n" +
|
||||
JSON.stringify(resp.body)
|
||||
)
|
||||
);
|
||||
}
|
||||
if (me.debug) {
|
||||
console.debug('[acme-v2] POST newOrder has authorizations');
|
||||
}
|
||||
setAuths = options._authorizations.slice(0);
|
||||
|
||||
function setNext() {
|
||||
var authUrl = setAuths.shift();
|
||||
if (!authUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
return ACME._getChallenges(me, options, authUrl).then(function(
|
||||
results
|
||||
) {
|
||||
// var domain = options.domains[i]; // results.identifier.value
|
||||
|
||||
// If it's already valid, we're golden it regardless
|
||||
if (
|
||||
results.challenges.some(function(ch) {
|
||||
return 'valid' === ch.status;
|
||||
})
|
||||
) {
|
||||
return setNext();
|
||||
}
|
||||
|
||||
var challenge = ACME._chooseChallenge(options, results);
|
||||
if (!challenge) {
|
||||
// For example, wildcards require dns-01 and, if we don't have that, we have to bail
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
"Server didn't offer any challenge we can handle for '" +
|
||||
options.domains.join() +
|
||||
"'."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return ACME._challengeToAuth(
|
||||
me,
|
||||
options,
|
||||
results,
|
||||
challenge,
|
||||
false
|
||||
).then(function(auth) {
|
||||
auths.push(auth);
|
||||
return ACME._setChallenge(me, options, auth).then(
|
||||
setNext
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function checkNext() {
|
||||
var auth = auths.shift();
|
||||
if (!auth) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!me._canUse[auth.type] || me.skipChallengeTest) {
|
||||
// not so much "valid" as "not invalid"
|
||||
// but in this case we can't confirm either way
|
||||
validAuths.push(auth);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return ACME.challengeTests[auth.type](me, auth)
|
||||
.then(function() {
|
||||
validAuths.push(auth);
|
||||
})
|
||||
.then(checkNext);
|
||||
}
|
||||
.map(function(hostname) {
|
||||
return { type: 'dns', value: hostname };
|
||||
})
|
||||
//, "notBefore": "2016-01-01T00:00:00Z"
|
||||
//, "notAfter": "2016-01-08T00:00:00Z"
|
||||
};
|
||||
|
||||
function challengeNext() {
|
||||
var auth = validAuths.shift();
|
||||
if (!auth) {
|
||||
return;
|
||||
var payload = JSON.stringify(certOrder);
|
||||
if (me.debug) {
|
||||
console.debug('\n[DEBUG] newOrder\n');
|
||||
}
|
||||
return ACME._jwsRequest(me, {
|
||||
options: options,
|
||||
url: me._directoryUrls.newOrder,
|
||||
protected: { kid: options._kid },
|
||||
payload: Enc.strToBuf(payload)
|
||||
}).then(function(resp) {
|
||||
var location = resp.headers.location;
|
||||
var setAuths;
|
||||
var validAuths = [];
|
||||
var auths = [];
|
||||
if (me.debug) {
|
||||
console.debug('[ordered]', location);
|
||||
} // the account id url
|
||||
if (me.debug) {
|
||||
console.debug(resp);
|
||||
}
|
||||
return ACME._postChallenge(me, options, auth).then(
|
||||
challengeNext
|
||||
);
|
||||
}
|
||||
options._authorizations = resp.body.authorizations;
|
||||
options._order = location;
|
||||
options._finalize = resp.body.finalize;
|
||||
//if (me.debug) console.debug('[DEBUG] finalize:', options._finalize); return;
|
||||
|
||||
// First we set 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 setNext()
|
||||
.then(checkNext)
|
||||
.then(challengeNext)
|
||||
.then(function() {
|
||||
if (!options._authorizations) {
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
"[acme-v2.js] authorizations were not fetched for '" +
|
||||
options.domains.join() +
|
||||
"':\n" +
|
||||
JSON.stringify(resp.body)
|
||||
)
|
||||
);
|
||||
}
|
||||
if (me.debug) {
|
||||
console.debug('[acme-v2] POST newOrder has authorizations');
|
||||
}
|
||||
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() {
|
||||
var authUrl = setAuths.shift();
|
||||
if (!authUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
return ACME._getChallenges(me, options, authUrl).then(
|
||||
function(results) {
|
||||
// var domain = options.domains[i]; // results.identifier.value
|
||||
|
||||
// If it's already valid, we're golden it regardless
|
||||
if (
|
||||
results.challenges.some(function(ch) {
|
||||
return 'valid' === ch.status;
|
||||
})
|
||||
) {
|
||||
return setNext();
|
||||
}
|
||||
|
||||
var challenge = ACME._chooseChallenge(
|
||||
options,
|
||||
results
|
||||
);
|
||||
if (!challenge) {
|
||||
// For example, wildcards require dns-01 and, if we don't have that, we have to bail
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
"Server didn't offer any challenge we can handle for '" +
|
||||
options.domains.join() +
|
||||
"'."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return ACME._challengeToAuth(
|
||||
me,
|
||||
options,
|
||||
accountKeyThumb,
|
||||
results,
|
||||
challenge,
|
||||
false
|
||||
).then(function(auth) {
|
||||
console.log('ADD DUBIOUS AUTH');
|
||||
auths.push(auth);
|
||||
return ACME._setChallenge(
|
||||
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() {
|
||||
console.log('CONSUME DUBIOUS AUTH', auths.length);
|
||||
var auth = auths.shift();
|
||||
if (!auth) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!me._canUse[auth.type] || me.skipChallengeTest) {
|
||||
// not so much "valid" as "not invalid"
|
||||
// but in this case we can't confirm either way
|
||||
validAuths.push(auth);
|
||||
console.log('ADD VALID AUTH (skip)', validAuths.length);
|
||||
return checkNext();
|
||||
}
|
||||
|
||||
return ACME.challengeTests[auth.type](me, auth)
|
||||
.then(function() {
|
||||
console.log('ADD VALID AUTH');
|
||||
validAuths.push(auth);
|
||||
})
|
||||
.then(checkNext);
|
||||
}
|
||||
|
||||
function presentNext() {
|
||||
console.log('CONSUME VALID AUTH', validAuths.length);
|
||||
var auth = validAuths.shift();
|
||||
if (!auth) {
|
||||
return;
|
||||
}
|
||||
return ACME._postChallenge(me, options, auth).then(
|
||||
presentNext
|
||||
);
|
||||
}
|
||||
|
||||
function finalizeOrder() {
|
||||
if (me.debug) {
|
||||
console.debug('[getCertificate] next.then');
|
||||
}
|
||||
var validatedDomains = body.identifiers.map(function(
|
||||
var validatedDomains = certOrder.identifiers.map(function(
|
||||
ident
|
||||
) {
|
||||
return ident.value;
|
||||
});
|
||||
|
||||
return ACME._finalizeOrder(me, options, validatedDomains);
|
||||
})
|
||||
.then(function(order) {
|
||||
}
|
||||
|
||||
function retrieveCerts(order) {
|
||||
if (me.debug) {
|
||||
console.debug('acme-v2: order was finalized');
|
||||
}
|
||||
|
@ -1141,10 +1232,22 @@ ACME._getCertificate = function(me, options) {
|
|||
}
|
||||
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) {
|
||||
var csr;
|
||||
if (options.csr) {
|
||||
|
@ -1153,6 +1256,7 @@ ACME._generateCsrWeb64 = function(me, options, validatedDomains) {
|
|||
if ('string' !== typeof csr) {
|
||||
csr = Enc.bufToUrlBase64(csr);
|
||||
}
|
||||
// TODO PEM.parseBlock()
|
||||
// nix PEM headers, if any
|
||||
if ('-' === csr[0]) {
|
||||
csr = csr
|
||||
|
@ -1168,15 +1272,13 @@ ACME._generateCsrWeb64 = function(me, options, validatedDomains) {
|
|||
me,
|
||||
options.serverKeypair || options.domainKeypair
|
||||
).then(function(pair) {
|
||||
return me
|
||||
.CSR({
|
||||
jwk: pair.private,
|
||||
domains: validatedDomains,
|
||||
encoding: 'der'
|
||||
})
|
||||
.then(function(der) {
|
||||
return Enc.bufToUrlBase64(der);
|
||||
});
|
||||
return me.CSR.csr({
|
||||
jwk: pair.private,
|
||||
domains: validatedDomains,
|
||||
encoding: 'der'
|
||||
}).then(function(der) {
|
||||
return Enc.bufToUrlBase64(der);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -1276,6 +1378,7 @@ ACME._jwsRequest = function(me, bigopts) {
|
|||
bigopts.protected.kid = bigopts.options._kid;
|
||||
}
|
||||
}
|
||||
// this will shasum the thumbnail the 2nd time
|
||||
return me.Keypairs.signJws({
|
||||
jwk: bigopts.options.accountKeypair.privateKeyJwk,
|
||||
protected: bigopts.protected,
|
||||
|
@ -1291,6 +1394,7 @@ ACME._jwsRequest = function(me, bigopts) {
|
|||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Handle some ACME-specific defaults
|
||||
ACME._request = function(me, opts) {
|
||||
if (!opts.headers) {
|
||||
|
@ -1430,24 +1534,99 @@ ACME._http01 = function(me, auth) {
|
|||
ACME._removeChallenge = function(me, options, auth) {
|
||||
var challengers = options.challenges || {};
|
||||
var removeChallenge =
|
||||
(challengers[auth.type] && challengers[auth.type].remove) ||
|
||||
options.removeChallenge;
|
||||
challengers[auth.type] && challengers[auth.type].remove;
|
||||
if (1 === removeChallenge.length) {
|
||||
removeChallenge(auth).then(function() {}, function() {});
|
||||
return Promise.resolve(removeChallenge(auth)).then(
|
||||
function() {},
|
||||
function() {}
|
||||
);
|
||||
} else if (2 === removeChallenge.length) {
|
||||
removeChallenge(auth, function(err) {
|
||||
return err;
|
||||
});
|
||||
} else {
|
||||
if (!ACME._removeChallengeWarn) {
|
||||
console.warn(
|
||||
'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() {});
|
||||
throw new Error(
|
||||
"Bad function signature for '" + auth.type + "' challenge.remove()"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
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';
|
||||
/*global Promise*/
|
||||
|
||||
var ASN1 = require('./asn1/parser.js'); // DER, actually
|
||||
var ASN1 = require('./asn1/packer.js'); // DER, actually
|
||||
var Asn1 = ASN1.Any;
|
||||
var BitStr = ASN1.BitStr;
|
||||
var UInt = ASN1.UInt;
|
||||
var Asn1Parser = require('./asn1/packer.js'); // DER, actually
|
||||
var Asn1Parser = require('./asn1/parser.js');
|
||||
var Enc = require('omnibuffer');
|
||||
var PEM = require('./pem.js');
|
||||
var X509 = require('./x509.js');
|
||||
var Keypairs = require('./keypairs');
|
||||
|
||||
// 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
|
||||
// which will probably use the webcrypto API for some of the conversions
|
||||
return CSR._prepare(opts).then(function(opts) {
|
||||
|
@ -24,11 +25,10 @@ var CSR = (exports.CSR = function(opts) {
|
|||
return CSR._encode(opts, bytes);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
CSR._prepare = function(opts) {
|
||||
return Promise.resolve().then(function() {
|
||||
var Keypairs;
|
||||
opts = JSON.parse(JSON.stringify(opts));
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
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(
|
||||
pair
|
||||
) {
|
||||
|
@ -119,7 +109,7 @@ CSR._sign = function csrEcSig(jwk, request) {
|
|||
// 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 have a consistent non-private way to sign
|
||||
return Keypairs._sign(
|
||||
return Keypairs.sign(
|
||||
{ jwk: jwk, format: 'x509' },
|
||||
Enc.hexToBuf(request)
|
||||
).then(function(sig) {
|
||||
|
|
|
@ -76,12 +76,13 @@ Keypairs.neuter = function(opts) {
|
|||
};
|
||||
|
||||
Keypairs.thumbprint = function(opts) {
|
||||
//console.log('[debug]', new Error('NOT_ERROR').stack);
|
||||
return Promise.resolve().then(function() {
|
||||
if (/EC/i.test(opts.jwk.kty)) {
|
||||
console.log('[debug] EC thumbprint');
|
||||
console.log('[debug] EC thumbprint');
|
||||
return Eckles.thumbprint(opts);
|
||||
} else {
|
||||
console.log('[debug] RSA thumbprint');
|
||||
console.log('[debug] RSA thumbprint');
|
||||
return Rasha.thumbprint(opts);
|
||||
}
|
||||
});
|
||||
|
@ -121,6 +122,7 @@ Keypairs.publish = function(opts) {
|
|||
|
||||
// JWT a.k.a. JWS with Claims using Compact Serialization
|
||||
Keypairs.signJwt = function(opts) {
|
||||
console.log('[debug] signJwt');
|
||||
return Keypairs.thumbprint({ jwk: opts.jwk }).then(function(thumb) {
|
||||
var header = opts.header || {};
|
||||
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) {
|
||||
if (opts.alg) {
|
||||
return opts.alg.replace(/[a-z\-]/gi, '');
|
||||
|
|
|
@ -15,7 +15,7 @@ Keypairs._sign = function(opts, payload) {
|
|||
.update(payload)
|
||||
.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
|
||||
// https://tools.ietf.org/html/rfc7518#section-3.4
|
||||
binsig = Keypairs._ecdsaAsn1SigToJoseSig(binsig);
|
||||
|
|
|
@ -1,18 +1,39 @@
|
|||
'use strict';
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
var ACME = require('../');
|
||||
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 = {
|
||||
env: process.env.ENV,
|
||||
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.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() {
|
||||
var domains = randomDomains();
|
||||
var agreed = false;
|
||||
var metadata = await acme.init(
|
||||
'https://acme-staging-v02.api.letsencrypt.org/directory'
|
||||
|
@ -66,8 +87,31 @@ async function happyPath() {
|
|||
if (config.debug) {
|
||||
console.info('Server Key Created');
|
||||
console.info(JSON.stringify(serverKeypair, null, 2));
|
||||
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