forked from root/acme.js
v1.0.2
This commit is contained in:
parent
52406344ed
commit
28f5baf3d3
58
README.md
58
README.md
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
|
@ -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());
|
||||||
|
});
|
|
@ -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());
|
||||||
|
});
|
|
@ -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
30
node.js
|
@ -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);
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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) {
|
|
@ -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; }
|
|
@ -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 = {
|
Loading…
Reference in New Issue