forked from root/acme.js
update API and tests
This commit is contained in:
parent
f05e9db38e
commit
0efa94eeb0
263
acme.js
263
acme.js
|
@ -179,17 +179,17 @@ ACME._testChallengeOptions = function() {
|
|||
|
||||
ACME._thumber = function(me, options, thumb) {
|
||||
var thumbPromise;
|
||||
return function() {
|
||||
return function(key) {
|
||||
if (thumb) {
|
||||
return Promise.resolve(thumb);
|
||||
}
|
||||
if (thumbPromise) {
|
||||
return thumbPromise;
|
||||
}
|
||||
thumbPromise = U._importKeypair(
|
||||
me,
|
||||
options.accountKey || options.accountKeypair
|
||||
).then(function(pair) {
|
||||
if (!key) {
|
||||
key = options.accountKey || options.accountKeypair;
|
||||
}
|
||||
thumbPromise = U._importKeypair(null, key).then(function(pair) {
|
||||
return Keypairs.thumbprint({
|
||||
jwk: pair.public
|
||||
});
|
||||
|
@ -266,7 +266,14 @@ ACME._dryRun = function(me, realOptions) {
|
|||
type: ch.type
|
||||
//challenge: ch
|
||||
});
|
||||
noopts.challenges[ch.type].remove({ challenge: ch });
|
||||
noopts.challenges[ch.type]
|
||||
.remove({ challenge: ch })
|
||||
.catch(function(err) {
|
||||
err.action = 'challenge_remove';
|
||||
err.altname = ch.altname;
|
||||
err.type = ch.type;
|
||||
ACME._notify(me, noopts, 'error', err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -310,95 +317,117 @@ ACME._computeAuths = function(me, options, thumb, request, dryrun) {
|
|||
);
|
||||
}
|
||||
|
||||
var getThumbprint = ACME._thumber(me, options, thumb);
|
||||
var getThumbprint = ACME._thumber(null, options, thumb);
|
||||
|
||||
return getThumbprint().then(function(thumb) {
|
||||
return Promise.all(
|
||||
request.challenges.map(function(challenge) {
|
||||
// Don't do extra work for challenges that we can't satisfy
|
||||
if (!options._presenterTypes.includes(challenge.type)) {
|
||||
return null;
|
||||
}
|
||||
return Promise.all(
|
||||
request.challenges.map(function(challenge) {
|
||||
// Don't do extra work for challenges that we can't satisfy
|
||||
if (!options._presenterTypes.includes(challenge.type)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var auth = {};
|
||||
var auth = {};
|
||||
|
||||
// straight copy from the new order response
|
||||
// { identifier, status, expires, challenges, wildcard }
|
||||
Object.keys(request).forEach(function(key) {
|
||||
auth[key] = request[key];
|
||||
// straight copy from the new order response
|
||||
// { identifier, status, expires, challenges, wildcard }
|
||||
Object.keys(request).forEach(function(key) {
|
||||
auth[key] = request[key];
|
||||
});
|
||||
|
||||
// copy from the challenge we've chosen
|
||||
// { type, status, url, token }
|
||||
// (note the duplicate status overwrites the one above, but they should be the same)
|
||||
Object.keys(challenge).forEach(function(key) {
|
||||
// don't confused devs with the id url
|
||||
auth[key] = challenge[key];
|
||||
});
|
||||
|
||||
// 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);
|
||||
|
||||
var zone = pluckZone(
|
||||
options.zonenames || [],
|
||||
auth.identifier.value
|
||||
);
|
||||
|
||||
return ACME.computeChallenge({
|
||||
accountKey: options.accountKey,
|
||||
_getThumbprint: getThumbprint,
|
||||
challenge: auth,
|
||||
zone: zone,
|
||||
dnsPrefix: dnsPrefix
|
||||
}).then(function(resp) {
|
||||
Object.keys(resp).forEach(function(k) {
|
||||
auth[k] = resp[k];
|
||||
});
|
||||
return auth;
|
||||
});
|
||||
})
|
||||
).then(function(auths) {
|
||||
return auths.filter(Boolean);
|
||||
});
|
||||
};
|
||||
|
||||
// copy from the challenge we've chosen
|
||||
// { type, status, url, token }
|
||||
// (note the duplicate status overwrites the one above, but they should be the same)
|
||||
Object.keys(challenge).forEach(function(key) {
|
||||
// don't confused devs with the id url
|
||||
auth[key] = challenge[key];
|
||||
});
|
||||
ACME.computeChallenge = function(opts) {
|
||||
var auth = opts.challenge;
|
||||
var hostname = auth.hostname || opts.hostname;
|
||||
var zone = opts.zone;
|
||||
var thumb = opts.thumbprint || '';
|
||||
var accountKey = opts.accountKey;
|
||||
var getThumbprint = opts._getThumbprint || ACME._thumber(null, opts, thumb);
|
||||
var dnsPrefix = opts.dnsPrefix || ACME.challengePrefixes['dns-01'];
|
||||
|
||||
// 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 getThumbprint(accountKey).then(function(thumb) {
|
||||
var resp = {};
|
||||
resp.thumbprint = thumb;
|
||||
// keyAuthorization = token + '.' + base64url(JWK_Thumbprint(accountKey))
|
||||
resp.keyAuthorization = auth.token + '.' + thumb;
|
||||
|
||||
auth.thumbprint = thumb;
|
||||
// keyAuthorization = token + '.' + base64url(JWK_Thumbprint(accountKey))
|
||||
auth.keyAuthorization = challenge.token + '.' + auth.thumbprint;
|
||||
if ('http-01' === auth.type) {
|
||||
// conflicts with ACME challenge id url is already in use,
|
||||
// so we call this challengeUrl instead
|
||||
// TODO auth.http01Url ?
|
||||
resp.challengeUrl =
|
||||
'http://' +
|
||||
// `hostname` is an alias of `auth.indentifier.value`
|
||||
hostname +
|
||||
ACME.challengePrefixes['http-01'] +
|
||||
'/' +
|
||||
auth.token;
|
||||
}
|
||||
|
||||
if ('http-01' === auth.type) {
|
||||
// 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;
|
||||
return auth;
|
||||
}
|
||||
if ('dns-01' !== auth.type) {
|
||||
return resp;
|
||||
}
|
||||
|
||||
if ('dns-01' !== auth.type) {
|
||||
return auth;
|
||||
}
|
||||
|
||||
var zone = pluckZone(
|
||||
options.zonenames || [],
|
||||
auth.identifier.value
|
||||
);
|
||||
|
||||
// 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.dnsHost =
|
||||
dnsPrefix + '.' + auth.hostname.replace('*.', '');
|
||||
|
||||
auth.dnsAuthorization = hash64;
|
||||
auth.keyAuthorizationDigest = hash64;
|
||||
|
||||
if (zone) {
|
||||
auth.dnsZone = zone;
|
||||
auth.dnsPrefix = auth.dnsHost
|
||||
.replace(newZoneRegExp(zone), '')
|
||||
.replace(/\.$/, '');
|
||||
}
|
||||
|
||||
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, resp.keyAuthorization)
|
||||
.then(function(hash) {
|
||||
return Enc.bufToUrlBase64(Uint8Array.from(hash));
|
||||
})
|
||||
).then(function(auths) {
|
||||
return auths.filter(Boolean);
|
||||
});
|
||||
.then(function(hash64) {
|
||||
resp.dnsHost = dnsPrefix + '.' + hostname; // .replace('*.', '');
|
||||
|
||||
// deprecated
|
||||
resp.dnsAuthorization = hash64;
|
||||
// should use this instead
|
||||
resp.keyAuthorizationDigest = hash64;
|
||||
|
||||
if (zone) {
|
||||
resp.dnsZone = zone;
|
||||
resp.dnsPrefix = resp.dnsHost
|
||||
.replace(newZoneRegExp(zone), '')
|
||||
.replace(/\.$/, '');
|
||||
}
|
||||
|
||||
return resp;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -583,6 +612,7 @@ ACME._setChallenges = function(me, options, order) {
|
|||
var claims = order._claims.slice(0);
|
||||
var valids = [];
|
||||
var auths = [];
|
||||
var placed = [];
|
||||
var USE_DNS = false;
|
||||
var DNS_DELAY = 0;
|
||||
|
||||
|
@ -618,6 +648,7 @@ ACME._setChallenges = function(me, options, order) {
|
|||
);
|
||||
}
|
||||
auths.push(selected);
|
||||
placed.push(selected);
|
||||
ACME._notify(me, options, 'challenge_select', {
|
||||
// API-locked
|
||||
altname: ACME._untame(
|
||||
|
@ -651,10 +682,13 @@ ACME._setChallenges = function(me, options, order) {
|
|||
function waitAll() {
|
||||
//#console.debug('\n[DEBUG] waitChallengeDelay %s\n', DELAY);
|
||||
if (!DNS_DELAY || DNS_DELAY <= 0) {
|
||||
console.warn(
|
||||
'the given dns-01 challenge did not specify `propagationDelay`'
|
||||
);
|
||||
console.warn('the default of 5000ms will be used');
|
||||
if (!ACME._propagationDelayWarning) {
|
||||
console.warn(
|
||||
'warn: the given dns-01 challenge did not specify `propagationDelay`'
|
||||
);
|
||||
console.warn('warn: the default of 5000ms will be used');
|
||||
ACME._propagationDelayWarning = true;
|
||||
}
|
||||
DNS_DELAY = 5000;
|
||||
}
|
||||
return ACME._wait(DNS_DELAY);
|
||||
|
@ -683,7 +717,22 @@ ACME._setChallenges = function(me, options, order) {
|
|||
// is so that we don't poison our own DNS cache with misses.
|
||||
return setNext()
|
||||
.then(waitAll)
|
||||
.then(checkNext);
|
||||
.then(checkNext)
|
||||
.catch(function(err) {
|
||||
if (!options.debug) {
|
||||
placed.forEach(function(ch) {
|
||||
options.challenges[ch.type]
|
||||
.remove({ challenge: ch })
|
||||
.catch(function(err) {
|
||||
err.action = 'challenge_remove';
|
||||
err.altname = ch.altname;
|
||||
err.type = ch.type;
|
||||
ACME._notify(me, options, 'error', err);
|
||||
});
|
||||
});
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
};
|
||||
|
||||
ACME._normalizePresenters = function(me, options, presenters) {
|
||||
|
@ -1283,40 +1332,6 @@ ACME._prnd = function(n) {
|
|||
ACME._toHex = function(pair) {
|
||||
return parseInt(pair, 10).toString(16);
|
||||
};
|
||||
ACME._removeChallenge = function(me, options, auth) {
|
||||
var challengers = options.challenges || {};
|
||||
var ch = auth.challenge;
|
||||
var removeChallenge = challengers[ch.type] && challengers[ch.type].remove;
|
||||
if (!removeChallenge) {
|
||||
throw new Error('challenge plugin is missing remove()');
|
||||
}
|
||||
|
||||
// TODO normalize, warn, and just use promises
|
||||
if (1 === removeChallenge.length) {
|
||||
return Promise.resolve(removeChallenge(auth)).then(
|
||||
function() {},
|
||||
function(e) {
|
||||
console.error('Error during remove challenge:');
|
||||
console.error(e);
|
||||
}
|
||||
);
|
||||
} else if (2 === removeChallenge.length) {
|
||||
return new Promise(function(resolve) {
|
||||
removeChallenge(auth, function(err) {
|
||||
resolve();
|
||||
if (err) {
|
||||
console.error('Error during remove challenge:');
|
||||
console.error(err);
|
||||
}
|
||||
return err;
|
||||
});
|
||||
});
|
||||
} else {
|
||||
throw new Error(
|
||||
"Bad function signature for '" + auth.type + "' challenge.remove()"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
ACME._depInit = function(me, presenter) {
|
||||
if ('function' !== typeof presenter.init) {
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
'use strict';
|
||||
|
||||
var ACME = require('../');
|
||||
var accountKey = require('../fixtures/account.jwk.json').private;
|
||||
|
||||
var authorization = {
|
||||
identifier: {
|
||||
type: 'dns',
|
||||
value: 'example.com'
|
||||
},
|
||||
status: 'pending',
|
||||
expires: '2018-04-25T00:23:57Z',
|
||||
challenges: [
|
||||
{
|
||||
type: 'dns-01',
|
||||
status: 'pending',
|
||||
url:
|
||||
'https://acme-staging-v02.api.letsencrypt.org/acme/challenge/cMkwXI8pIeKN04Ynfem8ErHK3GeqAPdSt2x6q7PvVGU/118755342',
|
||||
token: 'LZdlUiZ-kWPs6q5WTmQFYQHZKpz9szn2vxEUu0XhyyM'
|
||||
},
|
||||
{
|
||||
type: 'http-01',
|
||||
status: 'pending',
|
||||
url:
|
||||
'https://acme-staging-v02.api.letsencrypt.org/acme/challenge/cMkwXI8pIeKN04Ynfem8ErHK3GeqAPdSt2x6q7PvVGU/118755343',
|
||||
token: '1S4zBG5YVhwSBaIY4ksI_KNMRrSmH0DZfNM9v7PYjDU'
|
||||
}
|
||||
]
|
||||
};
|
||||
var expectedChallengeUrl =
|
||||
'http://example.com/.well-known/acme-challenge/1S4zBG5YVhwSBaIY4ksI_KNMRrSmH0DZfNM9v7PYjDU';
|
||||
var expectedKeyAuth =
|
||||
'1S4zBG5YVhwSBaIY4ksI_KNMRrSmH0DZfNM9v7PYjDU.UuuZa_56jCM2douUq1riGyRphPtRvCPkxtkg0bP-pNs';
|
||||
var expectedKeyAuthDigest = 'iQiMcQUDiAeD0TJV1RHJuGnI5D2-PuSpxKz9JqUaZ2M';
|
||||
var expectedDnsHost = '_test-challenge.example.com';
|
||||
|
||||
async function main() {
|
||||
console.info('\n[Test] computing challenge authorizatin responses');
|
||||
var challenges = authorization.challenges.slice(0);
|
||||
|
||||
function next() {
|
||||
var ch = challenges.shift();
|
||||
if (!ch) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var hostname = authorization.identifier.value;
|
||||
return ACME.computeChallenge({
|
||||
accountKey: accountKey,
|
||||
hostname: hostname,
|
||||
challenge: ch,
|
||||
dnsPrefix: '_test-challenge'
|
||||
})
|
||||
.then(function(auth) {
|
||||
if ('dns-01' === ch.type) {
|
||||
if (auth.keyAuthorizationDigest !== expectedKeyAuthDigest) {
|
||||
console.error('[keyAuthorizationDigest]');
|
||||
console.error(auth.keyAuthorizationDigest);
|
||||
console.error(expectedKeyAuthDigest);
|
||||
throw new Error('bad keyAuthDigest');
|
||||
}
|
||||
if (auth.dnsHost !== expectedDnsHost) {
|
||||
console.error('[dnsHost]');
|
||||
console.error(auth.dnsHost);
|
||||
console.error(expectedDnsHost);
|
||||
throw new Error('bad dnsHost');
|
||||
}
|
||||
} else if ('http-01' === ch.type) {
|
||||
if (auth.challengeUrl !== expectedChallengeUrl) {
|
||||
console.error('[challengeUrl]');
|
||||
console.error(auth.challengeUrl);
|
||||
console.error(expectedChallengeUrl);
|
||||
throw new Error('bad challengeUrl');
|
||||
}
|
||||
if (auth.challengeUrl !== expectedChallengeUrl) {
|
||||
console.error('[keyAuthorization]');
|
||||
console.error(auth.keyAuthorization);
|
||||
console.error(expectedKeyAuth);
|
||||
throw new Error('bad keyAuth');
|
||||
}
|
||||
} else {
|
||||
throw new Error('bad authorization inputs');
|
||||
}
|
||||
console.info('PASS', hostname, ch.type);
|
||||
return next();
|
||||
})
|
||||
.catch(function(err) {
|
||||
err.message =
|
||||
'Error computing ' +
|
||||
ch.type +
|
||||
' for ' +
|
||||
hostname +
|
||||
':' +
|
||||
err.message;
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
||||
|
||||
module.exports = function() {
|
||||
return main(authorization)
|
||||
.then(function() {
|
||||
console.info('PASS');
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.error(err.stack);
|
||||
process.exit(1);
|
||||
});
|
||||
};
|
|
@ -35,55 +35,46 @@ var tests = [
|
|||
'----\nxxxx\nyyyy\n----\r\n----\nxxxx\ryyyy\n----\n'
|
||||
];
|
||||
|
||||
function formatPemChain(str) {
|
||||
return (
|
||||
str
|
||||
.trim()
|
||||
.replace(/[\r\n]+/g, '\n')
|
||||
.replace(/\-\n\-/g, '-\n\n-') + '\n'
|
||||
);
|
||||
}
|
||||
function splitPemChain(str) {
|
||||
return str
|
||||
.trim()
|
||||
.split(/[\r\n]{2,}/g)
|
||||
.map(function(str) {
|
||||
return str + '\n';
|
||||
});
|
||||
}
|
||||
var ACME = require('../');
|
||||
|
||||
tests.forEach(function(str) {
|
||||
var actual = formatPemChain(str);
|
||||
if (expected !== actual) {
|
||||
console.error('input: ', JSON.stringify(str));
|
||||
console.error('expected:', JSON.stringify(expected));
|
||||
console.error('actual: ', JSON.stringify(actual));
|
||||
throw new Error('did not pass');
|
||||
module.exports = function() {
|
||||
console.info('\n[Test] can split and format PEM chain properly');
|
||||
|
||||
tests.forEach(function(str) {
|
||||
var actual = ACME.formatPemChain(str);
|
||||
if (expected !== actual) {
|
||||
console.error('input: ', JSON.stringify(str));
|
||||
console.error('expected:', JSON.stringify(expected));
|
||||
console.error('actual: ', JSON.stringify(actual));
|
||||
throw new Error('did not pass');
|
||||
}
|
||||
});
|
||||
|
||||
if (
|
||||
'----\nxxxx\nyyyy\n----\n' !==
|
||||
ACME.formatPemChain('\n\n----\r\nxxxx\r\nyyyy\r\n----\n\n')
|
||||
) {
|
||||
throw new Error('Not proper for single cert in chain');
|
||||
}
|
||||
});
|
||||
|
||||
if (
|
||||
'----\nxxxx\nyyyy\n----\n' !==
|
||||
formatPemChain('\n\n----\r\nxxxx\r\nyyyy\r\n----\n\n')
|
||||
) {
|
||||
throw new Error('Not proper for single cert in chain');
|
||||
}
|
||||
|
||||
if (
|
||||
'--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n' !==
|
||||
formatPemChain(
|
||||
'\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n'
|
||||
)
|
||||
) {
|
||||
throw new Error('Not proper for three certs in chain');
|
||||
}
|
||||
|
||||
splitPemChain(
|
||||
'--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n'
|
||||
).forEach(function(str) {
|
||||
if ('--B--\nxxxx\nyyyy\n--E--\n' !== str) {
|
||||
throw new Error('bad thingy');
|
||||
if (
|
||||
'--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n' !==
|
||||
ACME.formatPemChain(
|
||||
'\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n'
|
||||
)
|
||||
) {
|
||||
throw new Error('Not proper for three certs in chain');
|
||||
}
|
||||
});
|
||||
|
||||
console.info('PASS');
|
||||
ACME.splitPemChain(
|
||||
'--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n'
|
||||
).forEach(function(str) {
|
||||
if ('--B--\nxxxx\nyyyy\n--E--\n' !== str) {
|
||||
throw new Error('bad thingy');
|
||||
}
|
||||
});
|
||||
|
||||
console.info('PASS');
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
|
@ -1,15 +1,27 @@
|
|||
'use strict';
|
||||
|
||||
async function run() {
|
||||
module.exports = async function() {
|
||||
console.log('[Test] can generate, export, and import key');
|
||||
var Keypairs = require('@root/keypairs');
|
||||
|
||||
var certKeypair = await Keypairs.generate({ kty: 'RSA' });
|
||||
console.log(certKeypair);
|
||||
//console.log(certKeypair);
|
||||
var pem = await Keypairs.export({
|
||||
jwk: certKeypair.private,
|
||||
encoding: 'pem'
|
||||
});
|
||||
console.log(pem);
|
||||
}
|
||||
var jwk = await Keypairs.import({
|
||||
pem: pem
|
||||
});
|
||||
['kty', 'd', 'n', 'e'].forEach(function(k) {
|
||||
if (!jwk[k] || jwk[k] !== certKeypair.private[k]) {
|
||||
throw new Error('bad export/import');
|
||||
}
|
||||
});
|
||||
//console.log(pem);
|
||||
console.log('PASS');
|
||||
};
|
||||
|
||||
run();
|
||||
if (require.main === module) {
|
||||
module.exports();
|
||||
}
|
||||
|
|
245
tests/index.js
245
tests/index.js
|
@ -1,245 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
var CSR = require('@root/csr');
|
||||
var Enc = require('@root/encoding/base64');
|
||||
var PEM = require('@root/pem');
|
||||
var punycode = require('punycode');
|
||||
var ACME = require('../acme.js');
|
||||
var Keypairs = require('@root/keypairs');
|
||||
|
||||
// TODO exec npm install --save-dev CHALLENGE_MODULE
|
||||
if (!process.env.CHALLENGE_OPTIONS) {
|
||||
console.error(
|
||||
'Please create a .env in the format of examples/example.env to run the tests'
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var config = {
|
||||
env: process.env.ENV,
|
||||
email: process.env.SUBSCRIBER_EMAIL,
|
||||
domain: process.env.BASE_DOMAIN,
|
||||
challengeType: process.env.CHALLENGE_TYPE,
|
||||
challengeModule: process.env.CHALLENGE_PLUGIN,
|
||||
challengeOptions: JSON.parse(process.env.CHALLENGE_OPTIONS)
|
||||
};
|
||||
config.debug = !/^PROD/i.test(config.env);
|
||||
var pluginPrefix = 'acme-' + config.challengeType + '-';
|
||||
var pluginName = config.challengeModule;
|
||||
var plugin;
|
||||
|
||||
var acme = ACME.create({
|
||||
// debug: true
|
||||
maintainerEmail: config.email,
|
||||
notify: function(ev, params) {
|
||||
console.info(
|
||||
ev,
|
||||
params.subject || params.altname || params.domain,
|
||||
params.status
|
||||
);
|
||||
if ('error' === ev) {
|
||||
console.error(params);
|
||||
console.error(params.error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function badPlugin(err) {
|
||||
if ('MODULE_NOT_FOUND' !== err.code) {
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
console.error("Couldn't find '" + pluginName + "'. Is it installed?");
|
||||
console.error("\tnpm install --save-dev '" + pluginName + "'");
|
||||
}
|
||||
try {
|
||||
plugin = require(pluginName);
|
||||
} catch (err) {
|
||||
if (
|
||||
'MODULE_NOT_FOUND' !== err.code ||
|
||||
0 === pluginName.indexOf(pluginPrefix)
|
||||
) {
|
||||
badPlugin(err);
|
||||
process.exit(1);
|
||||
}
|
||||
try {
|
||||
pluginName = pluginPrefix + pluginName;
|
||||
plugin = require(pluginName);
|
||||
} catch (e) {
|
||||
badPlugin(e);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
config.challenger = plugin.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(accKty, srvKty, rnd) {
|
||||
var agreed = false;
|
||||
var metadata = await acme.init(
|
||||
'https://acme-staging-v02.api.letsencrypt.org/directory'
|
||||
);
|
||||
|
||||
// Ready to use, show page
|
||||
if (config.debug) {
|
||||
console.info('ACME.js initialized');
|
||||
console.info(metadata);
|
||||
console.info();
|
||||
console.info();
|
||||
}
|
||||
|
||||
var accountKeypair = await Keypairs.generate({ kty: accKty });
|
||||
var accountKey = accountKeypair.private;
|
||||
if (config.debug) {
|
||||
console.info('Account Key Created');
|
||||
console.info(JSON.stringify(accountKey, null, 2));
|
||||
console.info();
|
||||
console.info();
|
||||
}
|
||||
|
||||
var account = await acme.accounts.create({
|
||||
agreeToTerms: agree,
|
||||
// TODO detect jwk/pem/der?
|
||||
accountKey: accountKey,
|
||||
subscriberEmail: config.email
|
||||
});
|
||||
|
||||
// TODO top-level agree
|
||||
function agree(tos) {
|
||||
if (config.debug) {
|
||||
console.info('Agreeing to Terms of Service:');
|
||||
console.info(tos);
|
||||
console.info();
|
||||
console.info();
|
||||
}
|
||||
agreed = true;
|
||||
return Promise.resolve(tos);
|
||||
}
|
||||
if (config.debug) {
|
||||
console.info('New Subscriber Account');
|
||||
console.info(JSON.stringify(account, null, 2));
|
||||
console.info();
|
||||
console.info();
|
||||
}
|
||||
if (!agreed) {
|
||||
throw new Error('Failed to ask the user to agree to terms');
|
||||
}
|
||||
|
||||
var certKeypair = await Keypairs.generate({ kty: srvKty });
|
||||
var pem = await Keypairs.export({
|
||||
jwk: certKeypair.private,
|
||||
encoding: 'pem'
|
||||
});
|
||||
if (config.debug) {
|
||||
console.info('Server Key Created');
|
||||
console.info('privkey.jwk.json');
|
||||
console.info(JSON.stringify(certKeypair, null, 2));
|
||||
// This should be saved as `privkey.pem`
|
||||
console.info();
|
||||
console.info('privkey.' + srvKty.toLowerCase() + '.pem:');
|
||||
console.info(pem);
|
||||
console.info();
|
||||
}
|
||||
|
||||
// 'subject' should be first in list
|
||||
var domains = randomDomains(rnd);
|
||||
if (config.debug) {
|
||||
console.info('Get certificates for random domains:');
|
||||
console.info(
|
||||
domains
|
||||
.map(function(puny) {
|
||||
var uni = punycode.toUnicode(puny);
|
||||
if (puny !== uni) {
|
||||
return puny + ' (' + uni + ')';
|
||||
}
|
||||
return puny;
|
||||
})
|
||||
.join('\n')
|
||||
);
|
||||
console.info();
|
||||
}
|
||||
|
||||
// Create CSR
|
||||
var csrDer = await CSR.csr({
|
||||
jwk: certKeypair.private,
|
||||
domains: domains,
|
||||
encoding: 'der'
|
||||
});
|
||||
var csr = Enc.bufToUrlBase64(csrDer);
|
||||
var csrPem = PEM.packBlock({
|
||||
type: 'CERTIFICATE REQUEST',
|
||||
bytes: csrDer /* { jwk: jwk, domains: opts.domains } */
|
||||
});
|
||||
if (config.debug) {
|
||||
console.info('Certificate Signing Request');
|
||||
console.info(csrPem);
|
||||
console.info();
|
||||
}
|
||||
|
||||
var results = await acme.certificates.create({
|
||||
account: account,
|
||||
accountKey: accountKey,
|
||||
csr: csr,
|
||||
domains: domains,
|
||||
challenges: challenges, // must be implemented
|
||||
customerEmail: null
|
||||
});
|
||||
|
||||
if (config.debug) {
|
||||
console.info('Got SSL Certificate:');
|
||||
console.info(Object.keys(results));
|
||||
console.info(results.expires);
|
||||
console.info(results.cert);
|
||||
console.info(results.chain);
|
||||
console.info();
|
||||
console.info();
|
||||
}
|
||||
}
|
||||
|
||||
// Try EC + RSA
|
||||
var rnd = random();
|
||||
happyPath('EC', 'RSA', rnd)
|
||||
.then(function() {
|
||||
// Now try RSA + EC
|
||||
rnd = random();
|
||||
return happyPath('RSA', 'EC', rnd).then(function() {
|
||||
console.info('success');
|
||||
});
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.error('Error:');
|
||||
console.error(err.stack);
|
||||
});
|
||||
|
||||
function randomDomains(rnd) {
|
||||
return ['foo-acmejs', 'bar-acmejs', '*.baz-acmejs', 'baz-acmejs'].map(
|
||||
function(pre) {
|
||||
return punycode.toASCII(pre + '-' + rnd + '.' + config.domain);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function random() {
|
||||
return (
|
||||
parseInt(
|
||||
Math.random()
|
||||
.toString()
|
||||
.slice(2, 99),
|
||||
10
|
||||
)
|
||||
.toString(16)
|
||||
.slice(0, 4) + '例'
|
||||
);
|
||||
}
|
|
@ -0,0 +1,253 @@
|
|||
'use strict';
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
var CSR = require('@root/csr');
|
||||
var Enc = require('@root/encoding/base64');
|
||||
var PEM = require('@root/pem');
|
||||
var punycode = require('punycode');
|
||||
var ACME = require('../acme.js');
|
||||
var Keypairs = require('@root/keypairs');
|
||||
|
||||
// TODO exec npm install --save-dev CHALLENGE_MODULE
|
||||
if (!process.env.CHALLENGE_OPTIONS) {
|
||||
console.error(
|
||||
'Please create a .env in the format of examples/example.env to run the tests'
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var config = {
|
||||
env: process.env.ENV,
|
||||
email: process.env.SUBSCRIBER_EMAIL,
|
||||
domain: process.env.BASE_DOMAIN,
|
||||
challengeType: process.env.CHALLENGE_TYPE,
|
||||
challengeModule: process.env.CHALLENGE_PLUGIN,
|
||||
challengeOptions: JSON.parse(process.env.CHALLENGE_OPTIONS)
|
||||
};
|
||||
//config.debug = !/^PROD/i.test(config.env);
|
||||
var pluginPrefix = 'acme-' + config.challengeType + '-';
|
||||
var pluginName = config.challengeModule;
|
||||
var plugin;
|
||||
|
||||
module.exports = function() {
|
||||
console.info('\n[Test] end-to-end issue certificates');
|
||||
|
||||
var acme = ACME.create({
|
||||
// debug: true
|
||||
maintainerEmail: config.email,
|
||||
notify: function(ev, params) {
|
||||
console.info(
|
||||
'\t' + ev,
|
||||
params.subject || params.altname || params.domain || '',
|
||||
params.status || ''
|
||||
);
|
||||
if ('error' === ev) {
|
||||
console.error(params.action || params.type || '');
|
||||
console.error(params);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function badPlugin(err) {
|
||||
if ('MODULE_NOT_FOUND' !== err.code) {
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
console.error("Couldn't find '" + pluginName + "'. Is it installed?");
|
||||
console.error("\tnpm install --save-dev '" + pluginName + "'");
|
||||
}
|
||||
try {
|
||||
plugin = require(pluginName);
|
||||
} catch (err) {
|
||||
if (
|
||||
'MODULE_NOT_FOUND' !== err.code ||
|
||||
0 === pluginName.indexOf(pluginPrefix)
|
||||
) {
|
||||
badPlugin(err);
|
||||
process.exit(1);
|
||||
}
|
||||
try {
|
||||
pluginName = pluginPrefix + pluginName;
|
||||
plugin = require(pluginName);
|
||||
} catch (e) {
|
||||
badPlugin(e);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
config.challenger = plugin.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(accKty, srvKty, rnd) {
|
||||
var agreed = false;
|
||||
var metadata = await acme.init(
|
||||
'https://acme-staging-v02.api.letsencrypt.org/directory'
|
||||
);
|
||||
|
||||
// Ready to use, show page
|
||||
if (config.debug) {
|
||||
console.info('ACME.js initialized');
|
||||
console.info(metadata);
|
||||
console.info();
|
||||
console.info();
|
||||
}
|
||||
|
||||
var accountKeypair = await Keypairs.generate({ kty: accKty });
|
||||
var accountKey = accountKeypair.private;
|
||||
if (config.debug) {
|
||||
console.info('Account Key Created');
|
||||
console.info(JSON.stringify(accountKey, null, 2));
|
||||
console.info();
|
||||
console.info();
|
||||
}
|
||||
|
||||
var account = await acme.accounts.create({
|
||||
agreeToTerms: agree,
|
||||
// TODO detect jwk/pem/der?
|
||||
accountKey: accountKey,
|
||||
subscriberEmail: config.email
|
||||
});
|
||||
|
||||
// TODO top-level agree
|
||||
function agree(tos) {
|
||||
if (config.debug) {
|
||||
console.info('Agreeing to Terms of Service:');
|
||||
console.info(tos);
|
||||
console.info();
|
||||
console.info();
|
||||
}
|
||||
agreed = true;
|
||||
return Promise.resolve(tos);
|
||||
}
|
||||
if (config.debug) {
|
||||
console.info('New Subscriber Account');
|
||||
console.info(JSON.stringify(account, null, 2));
|
||||
console.info();
|
||||
console.info();
|
||||
}
|
||||
if (!agreed) {
|
||||
throw new Error('Failed to ask the user to agree to terms');
|
||||
}
|
||||
|
||||
var certKeypair = await Keypairs.generate({ kty: srvKty });
|
||||
var pem = await Keypairs.export({
|
||||
jwk: certKeypair.private,
|
||||
encoding: 'pem'
|
||||
});
|
||||
if (config.debug) {
|
||||
console.info('Server Key Created');
|
||||
console.info('privkey.jwk.json');
|
||||
console.info(JSON.stringify(certKeypair, null, 2));
|
||||
// This should be saved as `privkey.pem`
|
||||
console.info();
|
||||
console.info('privkey.' + srvKty.toLowerCase() + '.pem:');
|
||||
console.info(pem);
|
||||
console.info();
|
||||
}
|
||||
|
||||
// 'subject' should be first in list
|
||||
var domains = randomDomains(rnd);
|
||||
if (config.debug) {
|
||||
console.info('Get certificates for random domains:');
|
||||
console.info(
|
||||
domains
|
||||
.map(function(puny) {
|
||||
var uni = punycode.toUnicode(puny);
|
||||
if (puny !== uni) {
|
||||
return puny + ' (' + uni + ')';
|
||||
}
|
||||
return puny;
|
||||
})
|
||||
.join('\n')
|
||||
);
|
||||
console.info();
|
||||
}
|
||||
|
||||
// Create CSR
|
||||
var csrDer = await CSR.csr({
|
||||
jwk: certKeypair.private,
|
||||
domains: domains,
|
||||
encoding: 'der'
|
||||
});
|
||||
var csr = Enc.bufToUrlBase64(csrDer);
|
||||
var csrPem = PEM.packBlock({
|
||||
type: 'CERTIFICATE REQUEST',
|
||||
bytes: csrDer /* { jwk: jwk, domains: opts.domains } */
|
||||
});
|
||||
if (config.debug) {
|
||||
console.info('Certificate Signing Request');
|
||||
console.info(csrPem);
|
||||
console.info();
|
||||
}
|
||||
|
||||
var results = await acme.certificates.create({
|
||||
account: account,
|
||||
accountKey: accountKey,
|
||||
csr: csr,
|
||||
domains: domains,
|
||||
challenges: challenges, // must be implemented
|
||||
customerEmail: null
|
||||
});
|
||||
|
||||
if (config.debug) {
|
||||
console.info('Got SSL Certificate:');
|
||||
console.info(Object.keys(results));
|
||||
console.info(results.expires);
|
||||
console.info(results.cert);
|
||||
console.info(results.chain);
|
||||
console.info();
|
||||
console.info();
|
||||
}
|
||||
}
|
||||
|
||||
// Try EC + RSA
|
||||
var rnd = random();
|
||||
happyPath('EC', 'RSA', rnd)
|
||||
.then(function() {
|
||||
console.info('PASS: ECDSA account key with RSA server key');
|
||||
// Now try RSA + EC
|
||||
rnd = random();
|
||||
return happyPath('RSA', 'EC', rnd).then(function() {
|
||||
console.info('PASS: RSA account key with ECDSA server key');
|
||||
});
|
||||
})
|
||||
.then(function() {
|
||||
console.info('PASS');
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.error('Error:');
|
||||
console.error(err.stack);
|
||||
});
|
||||
|
||||
function randomDomains(rnd) {
|
||||
return ['foo-acmejs', 'bar-acmejs', '*.baz-acmejs', 'baz-acmejs'].map(
|
||||
function(pre) {
|
||||
return punycode.toASCII(pre + '-' + rnd + '.' + config.domain);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function random() {
|
||||
return (
|
||||
parseInt(
|
||||
Math.random()
|
||||
.toString()
|
||||
.slice(2, 99),
|
||||
10
|
||||
)
|
||||
.toString(16)
|
||||
.slice(0, 4) + '例'
|
||||
);
|
||||
}
|
||||
};
|
27
utils.js
27
utils.js
|
@ -122,29 +122,26 @@ U._setNonce = function(me, nonce) {
|
|||
me._nonces.unshift({ nonce: nonce, createdAt: Date.now() });
|
||||
};
|
||||
|
||||
U._importKeypair = function(me, kp) {
|
||||
var jwk = kp.privateKeyJwk;
|
||||
if (kp.kty) {
|
||||
jwk = kp;
|
||||
kp = {};
|
||||
}
|
||||
var pub;
|
||||
U._importKeypair = function(me, key) {
|
||||
var p;
|
||||
if (jwk) {
|
||||
var pub;
|
||||
|
||||
if (key && key.kty) {
|
||||
// nix the browser jwk extras
|
||||
jwk.key_ops = undefined;
|
||||
jwk.ext = undefined;
|
||||
pub = Keypairs.neuter({ jwk: jwk });
|
||||
key.key_ops = undefined;
|
||||
key.ext = undefined;
|
||||
pub = Keypairs.neuter({ jwk: key });
|
||||
p = Promise.resolve({
|
||||
private: jwk,
|
||||
private: key,
|
||||
public: pub
|
||||
});
|
||||
} else if ('string' === typeof key) {
|
||||
p = Keypairs.import({ pem: key });
|
||||
} else {
|
||||
p = Keypairs.import({ pem: kp.privateKeyPem });
|
||||
throw new Error('no private key given');
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
Loading…
Reference in New Issue