This commit is contained in:
AJ ONeal 2018-04-16 01:04:06 +00:00
parent 52406344ed
commit 28f5baf3d3
11 changed files with 158 additions and 43 deletions

View File

@ -1,11 +1,59 @@
acme-v2.js (draft 11) acme-v2.js (draft 11)
========== ==========
| [acme-v2.js](https://git.coolaj86.com/coolaj86/acme-v2.js)
| [acme-v2-cli.js](https://git.coolaj86.com/coolaj86/acme-v2-cli.js)
| [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js)
| [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js)
| Sponsored by [ppl](https://ppl.family) | Sponsored by [ppl](https://ppl.family)
A framework for building letsencrypt v2 (IETF ACME draft 11) clients, successor to `le-acme-core.js`. A framework for building Let's Encrypt v2 (ACME draft 11) clients, successor to `le-acme-core.js`.
Built [by request](https://git.coolaj86.com/coolaj86/greenlock.js/issues/5#issuecomment-8).
Summary of spec that I'm working off of here: https://git.coolaj86.com/coolaj86/greenlock.js/issues/5#issuecomment-8 ## Looking for Quick 'n' Easy™?
If you're looking for an *ACME-enabled webserver*, try [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js).
If you're looking to *build a webserver*, try [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js).
* [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js)
* [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js)
## How to build ACME clients
As this is intended to build ACME clients, there is not a simple 2-line example.
I'd recommend first running the example CLI client with a test domain and then investigating the files used for that example:
```bash
node examples/cli.js
```
The example cli has the following prompts:
```
What web address(es) would you like to get certificates for? (ex: example.com,*.example.com)
What challenge will you be testing today? http-01 or dns-01? [http-01]
What email should we use? (optional)
What API style would you like to test? v1-compat or promise? [v1-compat]
Put the string 'mBfh0SqaAV3MOK3B6cAhCbIReAyDuwuxlO1Sl70x6bM.VNAzCR4THe4czVzo9piNn73B1ZXRLaB2CESwJfKkvRM' into a file at 'example.com/.well-known/acme-challenge/mBfh0SqaAV3MOK3B6cAhCbIReAyDuwuxlO1Sl70x6bM'
echo 'mBfh0SqaAV3MOK3B6cAhCbIReAyDuwuxlO1Sl70x6bM.VNAzCR4THe4czVzo9piNn73B1ZXRLaB2CESwJfKkvRM' > 'example.com/.well-known/acme-challenge/mBfh0SqaAV3MOK3B6cAhCbIReAyDuwuxlO1Sl70x6bM'
Then hit the 'any' key to continue...
```
When you've completed the challenge you can hit a key to continue the process.
If you place the certificate you receive back in `tests/fullchain.pem`
you can then test it with `examples/https-server.js`.
```
examples/cli.js
examples/genkeypair.js
tests/compat.js
```
## Let's Encrypt Directory URLs ## Let's Encrypt Directory URLs
@ -136,7 +184,11 @@ Todo
Changelog Changelog
--------- ---------
* v1.0.0 * v1.0.2
* use `options.contact` to provide raw contact array
* made `options.email` optional
* file cleanup
* v1.0.1
* Compat API is ready for use * Compat API is ready for use
* Eliminate debug logging * Eliminate debug logging
* Apr 10, 2018 - tested backwards-compatibility using greenlock.js * Apr 10, 2018 - tested backwards-compatibility using greenlock.js

View File

@ -7,6 +7,8 @@ var rl = readline.createInterface({
output: process.stdout output: process.stdout
}); });
require('./genkeypair.js');
function getWeb() { function getWeb() {
rl.question('What web address(es) would you like to get certificates for? (ex: example.com,*.example.com) ', function (web) { rl.question('What web address(es) would you like to get certificates for? (ex: example.com,*.example.com) ', function (web) {
web = (web||'').trim().split(/,/g); web = (web||'').trim().split(/,/g);
@ -35,12 +37,31 @@ function getEmail(web, chType) {
email = (email||'').trim(); email = (email||'').trim();
if (!email) { email = null; } if (!email) { email = null; }
getApiStyle(web, chType, email);
});
}
function getApiStyle(web, chType, email) {
var defaultStyle = 'compat';
rl.question('What API style would you like to test? v1-compat or promise? [v1-compat] ', function (apiStyle) {
apiStyle = (apiStyle||'').trim();
if (!apiStyle) { apiStyle = 'v1-compat'; }
rl.close(); rl.close();
var accountKeypair = RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/account.privkey.pem') });
var domainKeypair = RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }); var RSA = require('rsa-compat').RSA;
//require('./test.compat.js').run(web, chType, email, accountKeypair, domainKeypair); var accountKeypair = RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/../tests/account.privkey.pem') });
//require('./test.cb.js').run(web, chType, email, accountKeypair, domainKeypair); var domainKeypair = RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/../tests/privkey.pem') });
require('./test.promise.js').run(web, chType, email, accountKeypair, domainKeypair); var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory';
if ('promise' === apiStyle) {
require('../tests/promise.js').run(directoryUrl, RSA, web, chType, email, accountKeypair, domainKeypair);
} else if ('cb' === apiStyle) {
require('../tests/cb.js').run(directoryUrl, RSA, web, chType, email, accountKeypair, domainKeypair);
} else {
if ('v1-compat' !== apiStyle) { console.warn("Didn't understand '" + apiStyle + "', using 'v1-compat' instead..."); }
require('../tests/compat.js').run(directoryUrl, RSA, web, chType, email, accountKeypair, domainKeypair);
}
}); });
} }

22
examples/genkeypair.js Normal file
View File

@ -0,0 +1,22 @@
var RSA = require('rsa-compat').RSA;
var fs = require('fs');
if (!fs.existsSync(__dirname + '/../tests/account.privkey.pem')) {
RSA.generateKeypair(2048, 65537, {}, function (err, keypair) {
console.log(keypair);
var privkeyPem = RSA.exportPrivatePem(keypair)
console.log(privkeyPem);
fs.writeFileSync(__dirname + '/../tests/account.privkey.pem', privkeyPem);
});
}
if (!fs.existsSync(__dirname + '/../tests/privkey.pem')) {
RSA.generateKeypair(2048, 65537, {}, function (err, keypair) {
console.log(keypair);
var privkeyPem = RSA.exportPrivatePem(keypair)
console.log(privkeyPem);
fs.writeFileSync(__dirname + '/../tests/privkey.pem', privkeyPem);
});
}

7
examples/http-server.js Normal file
View File

@ -0,0 +1,7 @@
'use strict';
var http = require('http');
var express = require('express');
var server = http.createServer(express.static('../tests')).listen(80, function () {
console.log('Listening on', this.address());
});

11
examples/https-server.js Normal file
View File

@ -0,0 +1,11 @@
'use strict';
var https = require('https');
var server = https.createServer({
key: require('fs').readFileSync('../tests/privkey.pem')
, cert: require('fs').readFileSync('../tests/fullchain.pem')
}, function (req, res) {
res.end("Hello, World!");
}).listen(443, function () {
console.log('Listening on', this.address());
});

View File

@ -1,18 +0,0 @@
var RSA = require('rsa-compat').RSA;
var fs = require('fs');
RSA.generateKeypair(2048, 65537, {}, function (err, keypair) {
console.log(keypair);
var privkeyPem = RSA.exportPrivatePem(keypair)
console.log(privkeyPem);
fs.writeFileSync(__dirname + '/account.privkey.pem', privkeyPem);
});
RSA.generateKeypair(2048, 65537, {}, function (err, keypair) {
console.log(keypair);
var privkeyPem = RSA.exportPrivatePem(keypair)
console.log(privkeyPem);
fs.writeFileSync(__dirname + '/privkey.pem', privkeyPem);
});

30
node.js
View File

@ -112,10 +112,16 @@ ACME._registerAccount = function (me, options) {
} }
var jwk = me.RSA.exportPublicJwk(options.accountKeypair); var jwk = me.RSA.exportPublicJwk(options.accountKeypair);
var contact;
if (options.contact) {
contact = options.contact.slice(0);
} else if (options.email) {
contact = [ 'mailto:' + options.email ]
}
var body = { var body = {
termsOfServiceAgreed: tosUrl === me._tos termsOfServiceAgreed: tosUrl === me._tos
, onlyReturnExisting: false , onlyReturnExisting: false
, contact: [ 'mailto:' + options.email ] , contact: contact
}; };
if (options.externalAccount) { if (options.externalAccount) {
body.externalAccountBinding = me.RSA.signJws( body.externalAccountBinding = me.RSA.signJws(
@ -150,6 +156,8 @@ ACME._registerAccount = function (me, options) {
, headers: { 'Content-Type': 'application/jose+json' } , headers: { 'Content-Type': 'application/jose+json' }
, json: jws , json: jws
}).then(function (resp) { }).then(function (resp) {
var account = resp.body;
me._nonce = resp.toJSON().headers['replay-nonce']; me._nonce = resp.toJSON().headers['replay-nonce'];
var location = resp.toJSON().headers.location; var location = resp.toJSON().headers.location;
// the account id url // the account id url
@ -157,15 +165,33 @@ ACME._registerAccount = function (me, options) {
if (me.debug) console.debug('[DEBUG] new account location:'); if (me.debug) console.debug('[DEBUG] new account location:');
if (me.debug) console.debug(location); if (me.debug) console.debug(location);
if (me.debug) console.debug(resp.toJSON()); if (me.debug) console.debug(resp.toJSON());
return resp.body;
/*
{
id: 5925245,
key:
{ kty: 'RSA',
n: 'tBr7m1hVaUNQjUeakznGidnrYyegVUQrsQjNrcipljI9Vxvxd0baHc3vvRZWFyFO5BlS7UDl-KHQdbdqb-MQzfP6T2sNXsOHARQ41pCGY5BYzIPRJF0nD48-CY717is-7BKISv8rf9yx5iSjvK1wZ3Ke3YIpxzK2fWRqccVxXQ92VYioxOfGObACgEUSvdoEttWV2B0Uv4Sdi6zZbk5eo2zALvyGb1P4fKVfQycGLXC41AyhHOAuTqzNCyIkiWEkbfh2lZNcYClP2epS0pHRFXYyjJN6-c8InfM3PISo4k6Qew65HZ-oqUow0tTIgNwuen9q5O6Hc73GvU-2npGJVQ',
e: 'AQAB' },
contact: [],
initialIp: '198.199.82.211',
createdAt: '2018-04-16T00:41:00.720584972Z',
status: 'valid'
}
*/
if (!account) { account = { _emptyResponse: true, key: {} }; }
account.key.kid = me._kid;
return account;
}).then(resolve, reject); }).then(resolve, reject);
} }
if (me.debug) console.debug('[acme-v2] agreeToTerms'); if (me.debug) console.debug('[acme-v2] agreeToTerms');
if (1 === options.agreeToTerms.length) { if (1 === options.agreeToTerms.length) {
// newer promise API
return options.agreeToTerms(me._tos).then(agree, reject); return options.agreeToTerms(me._tos).then(agree, reject);
} }
else if (2 === options.agreeToTerms.length) { else if (2 === options.agreeToTerms.length) {
// backwards compat cb API
return options.agreeToTerms(me._tos, function (err, tosUrl) { return options.agreeToTerms(me._tos, function (err, tosUrl) {
if (!err) { agree(tosUrl); return; } if (!err) { agree(tosUrl); return; }
reject(err); reject(err);

View File

@ -1,6 +1,6 @@
{ {
"name": "acme-v2", "name": "acme-v2",
"version": "1.0.1", "version": "1.0.2",
"description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js",
"homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js",
"main": "node.js", "main": "node.js",

View File

@ -1,10 +1,8 @@
'use strict'; 'use strict';
module.exports.run = function run(web, chType, email, accountKeypair, domainKeypair) { module.exports.run = function run(directoryUrl, RSA, web, chType, email, accountKeypair, domainKeypair) {
var RSA = require('rsa-compat').RSA;
var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory';
var acme2 = require('./').ACME.create({ RSA: RSA });
// [ 'test.ppl.family' ] 'coolaj86@gmail.com''http-01' // [ 'test.ppl.family' ] 'coolaj86@gmail.com''http-01'
var acme2 = require('../').ACME.create({ RSA: RSA });
acme2.init(directoryUrl).then(function () { acme2.init(directoryUrl).then(function () {
var options = { var options = {
agreeToTerms: function (tosUrl, agree) { agreeToTerms: function (tosUrl, agree) {

View File

@ -1,11 +1,9 @@
'use strict'; 'use strict';
var RSA = require('rsa-compat').RSA; module.exports.run = function (directoryUrl, RSA, web, chType, email, accountKeypair, domainKeypair) {
module.exports.run = function (web, chType, email, accountKeypair, domainKeypair) {
console.log('[DEBUG] run', web, chType, email); console.log('[DEBUG] run', web, chType, email);
var acme2 = require('./compat.js').ACME.create({ RSA: RSA }); var acme2 = require('../compat.js').ACME.create({ RSA: RSA });
acme2.getAcmeUrls(acme2.stagingServerUrl, function (err/*, directoryUrls*/) { acme2.getAcmeUrls(acme2.stagingServerUrl, function (err/*, directoryUrls*/) {
if (err) { console.log('err 1'); throw err; } if (err) { console.log('err 1'); throw err; }
@ -44,8 +42,8 @@ module.exports.run = function (web, chType, email, accountKeypair, domainKeypair
acme2.registerNewAccount(options, function (err, account) { acme2.registerNewAccount(options, function (err, account) {
if (err) { console.log('err 2'); throw err; } if (err) { console.log('err 2'); throw err; }
console.log('account:'); if (options.debug) console.debug('account:');
console.log(account); if (options.debug) console.log(account);
acme2.getCertificate(options, function (err, fullchainPem) { acme2.getCertificate(options, function (err, fullchainPem) {
if (err) { console.log('err 3'); throw err; } if (err) { console.log('err 3'); throw err; }

View File

@ -1,10 +1,8 @@
'use strict'; 'use strict';
/* global Promise */ /* global Promise */
module.exports.run = function run(web, chType, email, accountKeypair, domainKeypair) { module.exports.run = function run(directoryUrl, RSA, web, chType, email, accountKeypair, domainKeypair) {
var RSA = require('rsa-compat').RSA; var acme2 = require('../').ACME.create({ RSA: RSA });
var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory';
var acme2 = require('./').ACME.create({ RSA: RSA });
// [ 'test.ppl.family' ] 'coolaj86@gmail.com''http-01' // [ 'test.ppl.family' ] 'coolaj86@gmail.com''http-01'
acme2.init(directoryUrl).then(function () { acme2.init(directoryUrl).then(function () {
var options = { var options = {