Compare commits

..

No commits in common. "b1c591b6ed43d5c7e2b165bf1ce7dada21f979b7" and "54cda5a888acf7910533409cac9b39d751e9d034" have entirely different histories.

13 changed files with 63 additions and 317 deletions

View File

@ -1,4 +1,4 @@
# [ACME.js](https://git.rootprojects.org/root/acme.js) (RFC 8555 / November 2019)
# [ACME.js](https://git.rootprojects.org/root/acme.js) v3
| Built by [Root](https://therootcompany.com) for [Greenlock](https://greenlock.domains)
@ -52,31 +52,6 @@ If they don't, please open an issue to let us know why.
We'd much rather improve the app than have a hundred different versions running in the wild.
However, in keeping to our values we've made the source visible for others to inspect, improve, and modify.
# API Overview
```js
ACME.create({ maintainerEmail, packageAgent });
acme.init(directoryUrl);
acme.accounts.create({ subscriberEmail, agreeToTerms, accountKey });
acme.certificates.create({
customerEmail, // do not use
account,
accountKey,
serverKey,
csr,
domains,
challenges
});
```
```js
ACME.computeChallenge({
accountKey: jwk,
hostname: 'example.com',
challenge: { type: 'dns-01', token: 'xxxx' }
});
```
# Install
To make it easy to generate, encode, and decode keys and certificates,
@ -259,6 +234,9 @@ is a required part of the process, which requires `set` and `remove` callbacks/p
```js
var certinfo = await acme.certificates.create({
agreeToTerms: function(tos) {
return tos;
},
account: account,
accountKey: accountPrivateJwk,
csr: csr,

27
acme.js
View File

@ -14,8 +14,7 @@ var sha2 = require('@root/keypairs/lib/node/sha2.js');
var http = require('./lib/node/http.js');
var A = require('./account.js');
var U = require('./utils.js');
var E = require('./errors.js');
var M = require('./maintainers.js');
var E = require('./errors');
var native = require('./lib/native.js');
@ -28,20 +27,6 @@ ACME.create = function create(me) {
me._nonces = [];
me._canCheck = {};
if (!/.+@.+\..+/.test(me.maintainerEmail)) {
throw new Error(
'you should supply `maintainerEmail` as a contact for security and critical bug notices'
);
}
if (!/\w\/v?\d/.test(me.packageAgent) && false !== me.packageAgent) {
console.error(
"\nyou should supply `packageAgent` as an rfc7231-style User-Agent such as Foo/v1.1\n\n\t// your package agent should be this:\n\tvar pkg = require('./package.json');\n\tvar agent = pkg.name + '/' + pkg.version\n"
);
process.exit(1);
return;
}
if (!me.dns01) {
me.dns01 = function(ch) {
return native._dns01(me, ch);
@ -58,17 +43,15 @@ ACME.create = function create(me) {
};
}
if (!me.__request) {
me.__request = http.request;
if (!me.request) {
me.request = http.request;
}
// passed to dependencies
me.request = function(opts) {
me._urequest = function(opts) {
return U._request(me, opts);
};
me.init = function(opts) {
M.init(me);
function fin(dir) {
me._directoryUrls = dir;
me._tos = dir.meta.termsOfService;
@ -1258,7 +1241,7 @@ ACME._prepRequest = function(me, options) {
!presenter._acme_initialized
) {
presenter._acme_initialized = true;
return presenter.init({ type: '*', request: me.request });
return presenter.init({ type: '*', request: me._urequest });
}
});
});

View File

@ -1,6 +1,5 @@
{
"type": "dns-01",
"status": "pending",
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603341/yCthUw",
"token": "DiO9DFHuFTpNsJxIbOxfVCSPVkpe4lJUjozeSyzkMjI"
}
{ type: 'dns-01',
status: 'pending',
url:
'https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603341/yCthUw',
token: 'DiO9DFHuFTpNsJxIbOxfVCSPVkpe4lJUjozeSyzkMjI' }

View File

@ -18,37 +18,33 @@ native._canCheck = function(me) {
};
native._dns01 = function(me, ch) {
return me
.request({
url: me._baseUrl + '/api/dns/' + ch.dnsHost + '?type=TXT'
})
.then(function(resp) {
var err;
if (!resp.body || !Array.isArray(resp.body.answer)) {
err = new Error('failed to get DNS response');
console.error(err);
throw err;
}
if (!resp.body.answer.length) {
err = new Error('failed to get DNS answer record in response');
console.error(err);
throw err;
}
return {
answer: resp.body.answer.map(function(ans) {
return { data: ans.data, ttl: ans.ttl };
})
};
});
return new me.request({
url: me._baseUrl + '/api/dns/' + ch.dnsHost + '?type=TXT'
}).then(function(resp) {
var err;
if (!resp.body || !Array.isArray(resp.body.answer)) {
err = new Error('failed to get DNS response');
console.error(err);
throw err;
}
if (!resp.body.answer.length) {
err = new Error('failed to get DNS answer record in response');
console.error(err);
throw err;
}
return {
answer: resp.body.answer.map(function(ans) {
return { data: ans.data, ttl: ans.ttl };
})
};
});
};
native._http01 = function(me, ch) {
var url = encodeURIComponent(ch.challengeUrl);
return me
.request({
url: me._baseUrl + '/api/http?url=' + url
})
.then(function(resp) {
return resp.body;
});
return new me.request({
url: me._baseUrl + '/api/http?url=' + url
}).then(function(resp) {
return resp.body;
});
};

View File

@ -1,6 +1,6 @@
'use strict';
var UserAgent = module.exports;
UserAgent.get = function() {
return false;
UserAgent.get = function () {
return false;
};

View File

@ -3,7 +3,6 @@
var native = module.exports;
var promisify = require('util').promisify;
var resolveTxt = promisify(require('dns').resolveTxt);
var crypto = require('crypto');
native._canCheck = function(me) {
me._canCheck = {};
@ -32,57 +31,3 @@ native._http01 = function(me, ch) {
return resp.body;
});
};
// the hashcash here is for browser parity only
// basically we ask the client to find a needle in a haystack
// (very similar to CloudFlare's api protection)
native._hashcash = function(ch) {
if (!ch || !ch.nonce) {
ch = { nonce: 'xxx' };
}
return Promise.resolve()
.then(function() {
// only get easy answers
var len = ch.needle.length;
var start = ch.start || 0;
var end = ch.end || Math.ceil(len / 2);
var window = parseInt(end - start, 10) || 0;
var maxLen = 6;
var maxTries = Math.pow(2, maxLen * 8);
if (
len > maxLen ||
window < Math.ceil(len / 2) ||
ch.needle.toLowerCase() !== ch.needle ||
ch.alg !== 'SHA-256'
) {
// bail unless the server is issuing very easy challenges
throw new Error('possible and easy answers only, please');
}
var haystack;
var i;
var answer;
var needle = Buffer.from(ch.needle, 'hex');
for (i = 0; i < maxTries; i += 1) {
answer = i.toString(16);
if (answer.length % 2) {
answer = '0' + answer;
}
haystack = crypto
.createHash('sha256')
.update(Buffer.from(ch.nonce + answer, 'hex'))
.digest()
.slice(ch.start, ch.end);
if (-1 !== haystack.indexOf(needle)) {
return ch.nonce + ':' + answer;
}
}
return ch.nonce + ':xxx';
})
.catch(function() {
//console.log('[debug]', err);
// ignore any error
return ch.nonce + ':xxx';
});
};

View File

@ -1,7 +1,7 @@
'use strict';
var os = require('os');
var ver = require('../../package.json').version;
var ver = require('../../package.json');
var UserAgent = module.exports;
UserAgent.get = function(me) {

View File

@ -5,5 +5,15 @@ var promisify = require('util').promisify;
var request = promisify(require('@root/request'));
http.request = function(opts) {
if (!opts.headers) {
opts.headers = {};
}
if (
!Object.keys(opts.headers).some(function(key) {
return 'user-agent' === key.toLowerCase();
})
) {
// TODO opts.headers['User-Agent'] = 'TODO';
}
return request(opts);
};

View File

@ -1,85 +0,0 @@
'use strict';
var M = module.exports;
var native = require('./lib/native.js');
// Keep track of active maintainers so that we know who to inform if
// something breaks or has a serious bug or flaw.
var oldCollegeTries = {};
M.init = function(me) {
if (oldCollegeTries[me.maintainerEmail]) {
return;
}
var tz = '';
try {
// Use timezone to stagger messages to maintainers
tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
} catch (e) {
// ignore node versions with no or incomplete Intl
}
// Use locale to know what language to use
var env = process.env;
var locale = env.LC_ALL || env.LC_MESSAGES || env.LANG || env.LANGUAGE;
try {
M._init(me, tz, locale);
} catch (e) {
//console.log(e);
// ignore
}
};
M._init = function(me, tz, locale) {
// prevent a stampede from misconfigured clients in an eternal loop
setTimeout(function() {
me.request({
method: 'GET',
url: 'https://api.rootprojects.org/api/nonce',
json: true
})
.then(function(resp) {
// in the browser this will work until solved, but in
// node this will bail unless the challenge is trivial
return native._hashcash(resp.body || {});
})
.then(function(hashcash) {
var req = {
headers: {
'x-root-nonce-v1': hashcash
},
method: 'POST',
url:
'https://api.rootprojects.org/api/projects/ACME.js/dependents',
json: {
maintainer: me.maintainerEmail,
tz: tz,
locale: locale
}
};
return me
.request(req)
.catch(function(err) {
if (true || me.debug) {
console.error(err);
}
})
.then(function(/*resp*/) {
oldCollegeTries[me.maintainerEmail] = true;
//console.log(resp);
});
});
}, me.__timeout || 3000);
};
if (require.main === module) {
var ACME = require('./');
var acme = ACME.create({
maintainerEmail: 'aj+acme-test@rootprojects.org',
packageAgent: 'test/v0',
__timeout: 100
});
M.init(acme);
}

View File

@ -76,5 +76,5 @@ module.exports = function() {
console.info('PASS');
return Promise.resolve();
return Promise.resolve();
};

View File

@ -2,14 +2,12 @@
require('dotenv').config();
var pkg = require('../package.json');
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');
var ecJwk = require('../fixtures/account.jwk.json');
// TODO exec npm install --save-dev CHALLENGE_MODULE
if (!process.env.CHALLENGE_OPTIONS) {
@ -38,7 +36,6 @@ module.exports = function() {
var acme = ACME.create({
// debug: true
maintainerEmail: config.email,
packageAgent: 'test-' + pkg.name + '/' + pkg.version,
notify: function(ev, params) {
console.info(
'\t' + ev,
@ -107,10 +104,6 @@ module.exports = function() {
}
var accountKeypair = await Keypairs.generate({ kty: accKty });
if (/EC/i.test(accKty)) {
// to test that an existing account gets back data
accountKeypair = ecJwk;
}
var accountKey = accountKeypair.private;
if (config.debug) {
console.info('Account Key Created');

View File

@ -1,74 +0,0 @@
'use strict';
var native = require('../lib/native.js');
var crypto = require('crypto');
native
._hashcash({
alg: 'SHA-256',
nonce: '00',
needle: '0000',
start: 0,
end: 2
})
.then(function(hashcash) {
if ('00:76de' !== hashcash) {
throw new Error('hashcash algorthim changed');
}
console.info('PASS: known hash solves correctly');
return native
._hashcash({
alg: 'SHA-256',
nonce: '10',
needle: '',
start: 0,
end: 2
})
.then(function(hashcash) {
if ('10:00' !== hashcash) {
throw new Error('hashcash algorthim changed');
}
console.info('PASS: empty hash solves correctly');
var now = Date.now();
var nonce = '20';
var needle = crypto
.randomBytes(3)
.toString('hex')
.slice(0, 5);
native
._hashcash({
alg: 'SHA-256',
nonce: nonce,
needle: needle,
start: 0,
end: Math.ceil(needle.length / 2)
})
.then(function(hashcash) {
var later = Date.now();
var parts = hashcash.split(':');
var answer = parts[1];
if (parts[0] !== nonce) {
throw new Error('incorrect nonce');
}
var haystack = crypto
.createHash('sha256')
.update(Buffer.from(nonce + answer, 'hex'))
.digest()
.slice(0, Math.ceil(needle.length / 2));
if (
-1 === haystack.indexOf(Buffer.from(needle, 'hex'))
) {
throw new Error('incorrect solution');
}
if (later - now > 2000) {
throw new Error('took too long to solve');
}
console.info(
'PASS: rando hash solves correctly (and in good time - %dms)',
later - now
);
});
});
});

View File

@ -82,14 +82,9 @@ U._request = function(me, opts) {
if (ua && !opts.headers['User-Agent']) {
opts.headers['User-Agent'] = ua;
}
if (opts.json) {
opts.headers.Accept = 'application/json';
if (true !== opts.json) {
opts.body = JSON.stringify(opts.json);
}
if (/*opts.jose ||*/ opts.json.protected) {
opts.headers['Content-Type'] = 'application/jose+json';
}
if (opts.json && true !== opts.json) {
opts.headers['Content-Type'] = 'application/jose+json';
opts.body = JSON.stringify(opts.json);
}
if (!opts.method) {
opts.method = 'GET';
@ -97,10 +92,16 @@ U._request = function(me, opts) {
opts.method = 'POST';
}
}
if (opts.json) {
opts.headers.Accept = 'application/json';
if (true !== opts.json) {
opts.body = JSON.stringify(opts.json);
}
}
//console.log('\n[debug] REQUEST');
//console.log(opts);
return me.__request(opts).then(function(resp) {
return me.request(opts).then(function(resp) {
if (resp.toJSON) {
resp = resp.toJSON();
}