ソースを参照

yay for backwards compat tested and working

v1
AJ ONeal 6年前
コミット
b12a1a7ea4
  1. 42
      README.md
  2. 53
      compat.js
  3. 822
      node.js
  4. 75
      test.cb.js
  5. 57
      test.compat.js
  6. 91
      test.js
  7. 84
      test.promise.js

42
README.md

@ -31,27 +31,52 @@ Todo
* export http and dns challenge tests
* support ECDSA keys
## Let's Encrypt Directory URLs
```
# Production URL
https://acme-v02.api.letsencrypt.org/directory
```
```
# Staging URL
https://acme-staging-v02.api.letsencrypt.org/directory
```
## API
```
var ACME = require('acme-v2.js').ACME.create({
var ACME = require('acme-v2').ACME.create({
RSA: require('rsa-compat').RSA
// other overrides
, request: require('request')
, promisify: require('util').promisify
// used for constructing user-agent
, os: require('os')
, process: require('process')
// used for overriding the default user-agent
, userAgent: 'My custom UA String'
, getUserAgentString: function (deps) { return 'My custom UA String'; }
});
```
```javascript
// Accounts
ACME.registerNewAccount(options, cb) // returns "regr" registration data
ACME.accounts.create(options) // returns Promise<regr> registration data
{ email: '<email>' // valid email (server checks MX records)
, accountKeypair: { // privateKeyPem or privateKeyJwt
privateKeyPem: '<ASCII PEM>'
}
, agreeToTerms: fn (tosUrl, cb) {} // must specify agree=tosUrl to continue (or falsey to end)
, agreeToTerms: fn (tosUrl) {} // returns Promise with tosUrl
}
// Registration
ACME.getCertificate(options, cb) // returns (err, pems={ privkey (key), cert, chain (ca) })
ACME.certificates.create(options) // returns Promise<pems={ privkey (key), cert, chain (ca) }>
{ newAuthzUrl: '<url>' // specify acmeUrls.newAuthz
, newCertUrl: '<url>' // specify acmeUrls.newCert
@ -64,20 +89,19 @@ ACME.getCertificate(options, cb) // returns (err, pems={ privkey (key
}
, domains: [ 'example.com' ]
, setChallenge: fn (hostname, key, val, cb)
, removeChallenge: fn (hostname, key, cb)
, setChallenge: fn (hostname, key, val) // return Promise
, removeChallenge: fn (hostname, key) // return Promise
}
// Discovery URLs
ACME.getAcmeUrls(acmeDiscoveryUrl, cb) // returns (err, acmeUrls={newReg,newAuthz,newCert,revokeCert})
ACME.init(acmeDirectoryUrl) // returns Promise<acmeUrls={keyChange,meta,newAccount,newNonce,newOrder,revokeCert}>
```
Helpers & Stuff
```javascript
// Constants
ACME.productionServerUrl // https://acme-v02.api.letsencrypt.org/directory
ACME.stagingServerUrl // https://acme-staging-v02.api.letsencrypt.org/directory
ACME.acmeChallengePrefix // /.well-known/acme-challenge/
```

53
compat.js

@ -0,0 +1,53 @@
'use strict';
var ACME2 = require('./').ACME;
function resolveFn(cb) {
return function (val) {
// nextTick to get out of Promise chain
process.nextTick(function () { cb(null, val); });
};
}
function rejectFn(cb) {
return function (err) {
console.log('reject something or other:');
console.log(err.stack);
// nextTick to get out of Promise chain
process.nextTick(function () { cb(err); });
};
}
function create(deps) {
deps.LeCore = {};
var acme2 = ACME2.create(deps);
acme2.registerNewAccount = function (options, cb) {
acme2.accounts.create(options).then(resolveFn(cb), rejectFn(cb));
};
acme2.getCertificate = function (options, cb) {
acme2.certificates.create(options).then(resolveFn(cb), rejectFn(cb));
};
acme2.getAcmeUrls = function (options, cb) {
acme2.init(options).then(resolveFn(cb), rejectFn(cb));
};
acme2.stagingServerUrl = module.exports.defaults.stagingServerUrl;
acme2.productionServerUrl = module.exports.defaults.productionServerUrl;
return acme2;
}
module.exports.ACME = { };
module.exports.defaults = {
productionServerUrl: 'https://acme-v02.api.letsencrypt.org/directory'
, stagingServerUrl: 'https://acme-staging-v02.api.letsencrypt.org/directory'
, knownEndpoints: [ 'keyChange', 'meta', 'newAccount', 'newNonce', 'newOrder', 'revokeCert' ]
, challengeTypes: [ 'http-01', 'dns-01' ]
, challengeType: 'http-01'
, keyType: 'rsa' // ecdsa
, keySize: 2048 // 256
};
Object.keys(module.exports.defaults).forEach(function (key) {
module.exports.ACME[key] = module.exports.defaults[key];
});
Object.keys(ACME2).forEach(function (key) {
module.exports.ACME[key] = ACME2[key];
module.exports.ACME.create = create;
});

822
node.js

@ -6,463 +6,487 @@
'use strict';
/* globals Promise */
var defaults = {
productionServerUrl: 'https://acme-v02.api.letsencrypt.org/directory'
, stagingServerUrl: 'https://acme-staging-v02.api.letsencrypt.org/directory'
, acmeChallengePrefix: '/.well-known/acme-challenge/'
, knownEndpoints: [ 'keyChange', 'meta', 'newAccount', 'newNonce', 'newOrder', 'revokeCert' ]
, challengeType: 'http-01' // dns-01
, keyType: 'rsa' // ecdsa
, keySize: 2048 // 256
};
var ACME = module.exports.ACME = {};
function create(deps) {
if (!deps) { deps = {}; }
deps.LeCore = {};
deps.pkg = deps.pkg || require('./package.json');
deps.os = deps.os || require('os');
deps.process = deps.process || require('process');
ACME.acmeChallengePrefix = '/.well-known/acme-challenge/';
ACME._getUserAgentString = function (deps) {
var uaDefaults = {
pkg: "Greenlock/" + deps.pkg.version
, os: " (" + deps.os.type() + "; " + deps.process.arch + " " + deps.os.platform() + " " + deps.os.release() + ")"
, node: " Node.js/" + deps.process.version
, os: "(" + deps.os.type() + "; " + deps.process.arch + " " + deps.os.platform() + " " + deps.os.release() + ")"
, node: "Node.js/" + deps.process.version
, user: ''
};
//var currentUAProps;
function getUaString() {
var userAgent = '';
var userAgent = [];
//Object.keys(currentUAProps)
Object.keys(uaDefaults).forEach(function (key) {
userAgent += uaDefaults[key];
//userAgent += currentUAProps[key];
});
//Object.keys(currentUAProps)
Object.keys(uaDefaults).forEach(function (key) {
if (uaDefaults[key]) {
userAgent.push(uaDefaults[key]);
}
});
return userAgent.trim();
}
return userAgent.join(' ').trim();
};
ACME._directory = function (me) {
return me._request({ url: me.directoryUrl, json: true });
};
ACME._getNonce = function (me) {
if (me._nonce) { return new Promise(function (resolve) { resolve(me._nonce); return; }); }
return me._request({ method: 'HEAD', url: me._directoryUrls.newNonce }).then(function (resp) {
me._nonce = resp.toJSON().headers['replay-nonce'];
return me._nonce;
});
};
// ACME RFC Section 7.3 Account Creation
/*
{
"protected": base64url({
"alg": "ES256",
"jwk": {...},
"nonce": "6S8IqOGY7eL2lsGoTZYifg",
"url": "https://example.com/acme/new-account"
}),
"payload": base64url({
"termsOfServiceAgreed": true,
"onlyReturnExisting": false,
"contact": [
"mailto:cert-admin@example.com",
"mailto:admin@example.com"
]
}),
"signature": "RZPOnYoPs1PhjszF...-nh6X1qtOFPB519I"
}
*/
ACME._registerAccount = function (me, options) {
console.log('[acme-v2] accounts.create');
return ACME._getNonce(me).then(function () {
return new Promise(function (resolve, reject) {
function agree(tosUrl) {
var err;
if (me._tos !== tosUrl) {
err = new Error("You must agree to the ToS at '" + me._tos + "'");
err.code = "E_AGREE_TOS";
reject(err);
return;
}
function getRequest(opts) {
if (!opts) { opts = {}; }
var jwk = me.RSA.exportPublicJwk(options.accountKeypair);
var body = {
termsOfServiceAgreed: tosUrl === me._tos
, onlyReturnExisting: false
, contact: [ 'mailto:' + options.email ]
};
if (options.externalAccount) {
body.externalAccountBinding = me.RSA.signJws(
options.externalAccount.secret
, undefined
, { alg: "HS256"
, kid: options.externalAccount.id
, url: me._directoryUrls.newAccount
}
, new Buffer(JSON.stringify(jwk))
);
}
var payload = JSON.stringify(body);
var jws = me.RSA.signJws(
options.accountKeypair
, undefined
, { nonce: me._nonce
, alg: 'RS256'
, url: me._directoryUrls.newAccount
, jwk: jwk
}
, new Buffer(payload)
);
return deps.request.defaults({
headers: {
'User-Agent': opts.userAgent || getUaString()
console.log('[acme-v2] accounts.create JSON body:');
delete jws.header;
console.log(jws);
me._nonce = null;
return me._request({
method: 'POST'
, url: me._directoryUrls.newAccount
, headers: { 'Content-Type': 'application/jose+json' }
, json: jws
}).then(function (resp) {
me._nonce = resp.toJSON().headers['replay-nonce'];
var location = resp.toJSON().headers.location;
console.log('[DEBUG] new account location:'); // the account id url
console.log(location); // the account id url
console.log(resp.toJSON());
me._kid = location;
return resp.body;
}).then(resolve, reject);
}
});
}
var RSA = deps.RSA || require('rsa-compat').RSA;
deps.request = deps.request || require('request');
deps.promisify = deps.promisify || require('util').promisify;
var directoryUrl = deps.directoryUrl || defaults.stagingServerUrl;
var request = deps.promisify(getRequest({}));
console.log('[acme-v2] agreeToTerms');
if (1 === options.agreeToTerms.length) {
return options.agreeToTerms(me._tos).then(agree, reject);
}
else if (2 === options.agreeToTerms.length) {
return options.agreeToTerms(me._tos, function (err, tosUrl) {
if (!err) { agree(tosUrl); return; }
reject(err);
});
}
else {
reject(new Error('agreeToTerms has incorrect function signature.'
+ ' Should be fn(tos) { return Promise<tos>; }'));
}
});
});
};
/*
POST /acme/new-order HTTP/1.1
Host: example.com
Content-Type: application/jose+json
{
"protected": base64url({
"alg": "ES256",
"kid": "https://example.com/acme/acct/1",
"nonce": "5XJ1L3lEkMG7tR6pA00clA",
"url": "https://example.com/acme/new-order"
}),
"payload": base64url({
"identifiers": [{"type:"dns","value":"example.com"}],
"notBefore": "2016-01-01T00:00:00Z",
"notAfter": "2016-01-08T00:00:00Z"
}),
"signature": "H6ZXtGjTZyUnPeKn...wEA4TklBdh3e454g"
}
*/
ACME._getChallenges = function (me, options, auth) {
console.log('\n[DEBUG] getChallenges\n');
return me._request({ method: 'GET', url: auth, json: true }).then(function (resp) {
return resp.body;
});
};
ACME._wait = function wait(ms) {
return new Promise(function (resolve) {
setTimeout(resolve, (ms || 1100));
});
};
// https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.5.1
ACME._postChallenge = function (me, options, identifier, ch) {
var body = { };
var acme2 = {
getAcmeUrls: function (_directoryUrl) {
var me = this;
return request({ url: _directoryUrl || directoryUrl, json: true }).then(function (resp) {
me._directoryUrls = resp.body;
me._tos = me._directoryUrls.meta.termsOfService;
return me._directoryUrls;
});
}
, getNonce: function () {
var me = this;
if (me._nonce) { return new Promise(function (resolve) { resolve(me._nonce); return; }); }
return request({ method: 'HEAD', url: me._directoryUrls.newNonce }).then(function (resp) {
me._nonce = resp.toJSON().headers['replay-nonce'];
return me._nonce;
});
}
// ACME RFC Section 7.3 Account Creation
/*
{
"protected": base64url({
"alg": "ES256",
"jwk": {...},
"nonce": "6S8IqOGY7eL2lsGoTZYifg",
"url": "https://example.com/acme/new-account"
}),
"payload": base64url({
"termsOfServiceAgreed": true,
"onlyReturnExisting": false,
"contact": [
"mailto:cert-admin@example.com",
"mailto:admin@example.com"
]
}),
"signature": "RZPOnYoPs1PhjszF...-nh6X1qtOFPB519I"
}
*/
, registerNewAccount: function (options) {
var me = this;
console.log('[acme-v2] registerNewAccount');
return me.getNonce().then(function () {
return new Promise(function (resolve, reject) {
function agree(err, tosUrl) {
if (err) { reject(err); return; }
if (me._tos !== tosUrl) {
err = new Error("You must agree to the ToS at '" + me._tos + "'");
err.code = "E_AGREE_TOS";
reject(err);
return;
}
var payload = JSON.stringify(body);
var jwk = RSA.exportPublicJwk(options.accountKeypair);
var body = {
termsOfServiceAgreed: tosUrl === me._tos
, onlyReturnExisting: false
, contact: [ 'mailto:' + options.email ]
};
if (options.externalAccount) {
body.externalAccountBinding = RSA.signJws(
options.externalAccount.secret
, undefined
, { alg: "HS256"
, kid: options.externalAccount.id
, url: me._directoryUrls.newAccount
}
, new Buffer(JSON.stringify(jwk))
);
}
var payload = JSON.stringify(body);
var jws = RSA.signJws(
options.accountKeypair
, undefined
, { nonce: me._nonce
, alg: 'RS256'
, url: me._directoryUrls.newAccount
, jwk: jwk
}
, new Buffer(payload)
);
console.log('[acme-v2] registerNewAccount JSON body:');
delete jws.header;
console.log(jws);
me._nonce = null;
return request({
method: 'POST'
, url: me._directoryUrls.newAccount
, headers: { 'Content-Type': 'application/jose+json' }
, json: jws
}).then(function (resp) {
me._nonce = resp.toJSON().headers['replay-nonce'];
var location = resp.toJSON().headers.location;
console.log('[DEBUG] new account location:'); // the account id url
console.log(location); // the account id url
console.log(resp.toJSON());
me._kid = location;
return resp.body;
}).then(resolve);
}
var thumbprint = me.RSA.thumbprint(options.accountKeypair);
var keyAuthorization = ch.token + '.' + thumbprint;
// keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey))
// /.well-known/acme-challenge/:token
console.log('[acme-v2] agreeToTerms');
options.agreeToTerms(me._tos, agree);
});
});
return new Promise(function (resolve, reject) {
function failChallenge(err) {
if (err) { reject(err); return; }
testChallenge();
}
/*
POST /acme/new-order HTTP/1.1
Host: example.com
Content-Type: application/jose+json
{
"protected": base64url({
"alg": "ES256",
"kid": "https://example.com/acme/acct/1",
"nonce": "5XJ1L3lEkMG7tR6pA00clA",
"url": "https://example.com/acme/new-order"
}),
"payload": base64url({
"identifiers": [{"type:"dns","value":"example.com"}],
"notBefore": "2016-01-01T00:00:00Z",
"notAfter": "2016-01-08T00:00:00Z"
}),
"signature": "H6ZXtGjTZyUnPeKn...wEA4TklBdh3e454g"
}
*/
, _getChallenges: function (options, auth) {
console.log('\n[DEBUG] getChallenges\n');
return request({ method: 'GET', url: auth, json: true }).then(function (resp) {
return resp.body;
});
}
// https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.5.1
, _postChallenge: function (options, identifier, ch) {
var me = this;
var body = { };
var payload = JSON.stringify(body);
var thumbprint = RSA.thumbprint(options.accountKeypair);
var keyAuthorization = ch.token + '.' + thumbprint;
// keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey))
// /.well-known/acme-challenge/:token
return new Promise(function (resolve, reject) {
if (options.setupChallenge) {
options.setupChallenge(
{ identifier: identifier
, hostname: identifier.value
, type: ch.type
, token: ch.token
, thumbprint: thumbprint
, keyAuthorization: keyAuthorization
, dnsAuthorization: RSA.utils.toWebsafeBase64(
require('crypto').createHash('sha256').update(keyAuthorization).digest('base64')
)
}
, testChallenge
);
} else {
options.setChallenge(identifier.value, ch.token, keyAuthorization, testChallenge);
}
function testChallenge(err) {
if (err) { reject(err); return; }
function testChallenge() {
// TODO put check dns / http checks here?
// http-01: GET https://example.org/.well-known/acme-challenge/{{token}} => {{keyAuth}}
// dns-01: TXT _acme-challenge.example.org. => "{{urlSafeBase64(sha256(keyAuth))}}"
// TODO put check dns / http checks here?
// http-01: GET https://example.org/.well-known/acme-challenge/{{token}} => {{keyAuth}}
// dns-01: TXT _acme-challenge.example.org. => "{{urlSafeBase64(sha256(keyAuth))}}"
function pollStatus() {
console.log('\n[DEBUG] statusChallenge\n');
return me._request({ method: 'GET', url: ch.url, json: true }).then(function (resp) {
console.error('poll: resp.body:');
console.error(resp.body);
function wait(ms) {
return new Promise(function (resolve) {
setTimeout(resolve, (ms || 1100));
});
if ('pending' === resp.body.status) {
console.log('poll: again');
return ACME._wait(1 * 1000).then(pollStatus);
}
function pollStatus() {
console.log('\n[DEBUG] statusChallenge\n');
return request({ method: 'GET', url: ch.url, json: true }).then(function (resp) {
console.error('poll: resp.body:');
console.error(resp.body);
if ('pending' === resp.body.status) {
console.log('poll: again');
return wait().then(pollStatus);
}
if ('valid' === resp.body.status) {
console.log('poll: valid');
try {
if (options.teardownChallenge) {
options.teardownChallenge(
{ identifier: identifier
, type: ch.type
, token: ch.token
}
, function () {}
);
} else {
options.removeChallenge(identifier.value, ch.token, function () {});
if ('valid' === resp.body.status) {
console.log('poll: valid');
try {
if (1 === options.removeChallenge.length) {
options.removeChallenge(
{ identifier: identifier
, type: ch.type
, token: ch.token
}
} catch(e) {}
return resp.body;
}
if (!resp.body.status) {
console.error("[acme-v2] (y) bad challenge state:");
}
else if ('invalid' === resp.body.status) {
console.error("[acme-v2] (x) invalid challenge state:");
}
else {
console.error("[acme-v2] (z) bad challenge state:");
).then(function () {}, function () {});
} else if (2 === options.removeChallenge.length) {
options.removeChallenge(
{ identifier: identifier
, type: ch.type
, token: ch.token
}
, function (err) { return err; }
);
} else {
options.removeChallenge(identifier.value, ch.token, function () {});
}
return Promise.reject(new Error("[acme-v2] bad challenge state"));
});
} catch(e) {}
return resp.body;
}
console.log('\n[DEBUG] postChallenge\n');
//console.log('\n[DEBUG] stop to fix things\n'); return;
function post() {
var jws = RSA.signJws(
options.accountKeypair
, undefined
, { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid }
, new Buffer(payload)
);
me._nonce = null;
return request({
method: 'POST'
, url: ch.url
, headers: { 'Content-Type': 'application/jose+json' }
, json: jws
}).then(function (resp) {
me._nonce = resp.toJSON().headers['replay-nonce'];
console.log('respond to challenge: resp.body:');
console.log(resp.body);
return wait().then(pollStatus).then(resolve, reject);
});
if (!resp.body.status) {
console.error("[acme-v2] (y) bad challenge state:");
}
else if ('invalid' === resp.body.status) {
console.error("[acme-v2] (x) invalid challenge state:");
}
else {
console.error("[acme-v2] (z) bad challenge state:");
}
return wait(20 * 1000).then(post);
}
});
}
, _finalizeOrder: function (options, validatedDomains) {
console.log('finalizeOrder:');
var me = this;
var csr = RSA.generateCsrWeb64(options.domainKeypair, validatedDomains);
var body = { csr: csr };
var payload = JSON.stringify(body);
function wait(ms) {
return new Promise(function (resolve) {
setTimeout(resolve, (ms || 1100));
return Promise.reject(new Error("[acme-v2] bad challenge state"));
});
}
function pollCert() {
var jws = RSA.signJws(
console.log('\n[DEBUG] postChallenge\n');
//console.log('\n[DEBUG] stop to fix things\n'); return;
function post() {
var jws = me.RSA.signJws(
options.accountKeypair
, undefined
, { nonce: me._nonce, alg: 'RS256', url: me._finalize, kid: me._kid }
, { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid }
, new Buffer(payload)
);
console.log('finalize:', me._finalize);
me._nonce = null;
return request({
return me._request({
method: 'POST'
, url: me._finalize
, url: ch.url
, headers: { 'Content-Type': 'application/jose+json' }
, json: jws
}).then(function (resp) {
me._nonce = resp.toJSON().headers['replay-nonce'];
console.log('order finalized: resp.body:');
console.log('respond to challenge: resp.body:');
console.log(resp.body);
return ACME._wait(1 * 1000).then(pollStatus).then(resolve, reject);
});
}
if ('processing' === resp.body.status) {
return wait().then(pollCert);
}
if ('valid' === resp.body.status) {
me._expires = resp.body.expires;
me._certificate = resp.body.certificate;
return ACME._wait(1 * 1000).then(post);
}
return resp.body;
try {
if (1 === options.setChallenge.length) {
options.setChallenge(
{ identifier: identifier
, hostname: identifier.value
, type: ch.type
, token: ch.token
, thumbprint: thumbprint
, keyAuthorization: keyAuthorization
, dnsAuthorization: me.RSA.utils.toWebsafeBase64(
require('crypto').createHash('sha256').update(keyAuthorization).digest('base64')
)
}
if ('invalid' === resp.body.status) {
console.error('cannot finalize: badness');
return;
).then(testChallenge, reject);
} else if (2 === options.setChallenge.length) {
options.setChallenge(
{ identifier: identifier
, hostname: identifier.value
, type: ch.type
, token: ch.token
, thumbprint: thumbprint
, keyAuthorization: keyAuthorization
, dnsAuthorization: me.RSA.utils.toWebsafeBase64(
require('crypto').createHash('sha256').update(keyAuthorization).digest('base64')
)
}
console.error('(x) cannot finalize: badness');
return;
});
, failChallenge
);
} else {
options.setChallenge(identifier.value, ch.token, keyAuthorization, failChallenge);
}
return pollCert();
} catch(e) {
reject(e);
}
, _getCertificate: function () {
var me = this;
return request({ method: 'GET', url: me._certificate, json: true }).then(function (resp) {
console.log('Certificate:');
console.log(resp.body);
});
};
ACME._finalizeOrder = function (me, options, validatedDomains) {
console.log('finalizeOrder:');
var csr = me.RSA.generateCsrWeb64(options.domainKeypair, validatedDomains);
var body = { csr: csr };
var payload = JSON.stringify(body);
function pollCert() {
var jws = me.RSA.signJws(
options.accountKeypair
, undefined
, { nonce: me._nonce, alg: 'RS256', url: me._finalize, kid: me._kid }
, new Buffer(payload)
);
console.log('finalize:', me._finalize);
me._nonce = null;
return me._request({
method: 'POST'
, url: me._finalize
, headers: { 'Content-Type': 'application/jose+json' }
, json: jws
}).then(function (resp) {
me._nonce = resp.toJSON().headers['replay-nonce'];
console.log('order finalized: resp.body:');
console.log(resp.body);
if ('processing' === resp.body.status) {
return ACME._wait().then(pollCert);
}
if ('valid' === resp.body.status) {
me._expires = resp.body.expires;
me._certificate = resp.body.certificate;
return resp.body;
});
}
, getCertificate: function (options, cb) {
console.log('[acme-v2] DEBUG get cert 1');
var me = this;
if (!options.challengeTypes) {
if (!options.challengeType) {
cb(new Error("challenge type must be specified"));
return Promise.reject(new Error("challenge type must be specified"));
}
options.challengeTypes = [ options.challengeType ];
}
console.log('[acme-v2] getCertificate');
return me.getNonce().then(function () {
var body = {
identifiers: options.domains.map(function (hostname) {
return { type: "dns" , value: hostname };
})
//, "notBefore": "2016-01-01T00:00:00Z"
//, "notAfter": "2016-01-08T00:00:00Z"
};
if ('invalid' === resp.body.status) {
console.error('cannot finalize: badness');
return;
}
var payload = JSON.stringify(body);
var jws = RSA.signJws(
options.accountKeypair
, undefined
, { nonce: me._nonce, alg: 'RS256', url: me._directoryUrls.newOrder, kid: me._kid }
, new Buffer(payload)
);
console.error('(x) cannot finalize: badness');
return;
});
}
console.log('\n[DEBUG] newOrder\n');
me._nonce = null;
return request({
method: 'POST'
, url: me._directoryUrls.newOrder
, headers: { 'Content-Type': 'application/jose+json' }
, json: jws
}).then(function (resp) {
me._nonce = resp.toJSON().headers['replay-nonce'];
var location = resp.toJSON().headers.location;
console.log(location); // the account id url
console.log(resp.toJSON());
me._authorizations = resp.body.authorizations;
me._order = location;
me._finalize = resp.body.finalize;
//console.log('[DEBUG] finalize:', me._finalize); return;
//return resp.body;
return Promise.all(me._authorizations.map(function (authUrl) {
return me._getChallenges(options, authUrl).then(function (results) {
// var domain = options.domains[i]; // results.identifier.value
var chType = options.challengeTypes.filter(function (chType) {
return results.challenges.some(function (ch) {
return ch.type === chType;
});
})[0];
var challenge = results.challenges.filter(function (ch) {
if (chType === ch.type) {
return ch;
}
})[0];
if (!challenge) {
return Promise.reject(new Error("Server didn't offer any challenge we can handle."));
}
return pollCert();
};
ACME._getCertificate = function (me, options) {
console.log('[acme-v2] DEBUG get cert 1');
return me._postChallenge(options, results.identifier, challenge);
});
})).then(function () {
var validatedDomains = body.identifiers.map(function (ident) {
return ident.value;
if (!options.challengeTypes) {
if (!options.challengeType) {
return Promise.reject(new Error("challenge type must be specified"));
}
options.challengeTypes = [ options.challengeType ];
}
console.log('[acme-v2] certificates.create');
return ACME._getNonce(me).then(function () {
var body = {
identifiers: options.domains.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);
var jws = me.RSA.signJws(
options.accountKeypair
, undefined
, { nonce: me._nonce, alg: 'RS256', url: me._directoryUrls.newOrder, kid: me._kid }
, new Buffer(payload)
);
console.log('\n[DEBUG] newOrder\n');
me._nonce = null;
return me._request({
method: 'POST'
, url: me._directoryUrls.newOrder
, headers: { 'Content-Type': 'application/jose+json' }
, json: jws
}).then(function (resp) {
me._nonce = resp.toJSON().headers['replay-nonce'];
var location = resp.toJSON().headers.location;
console.log(location); // the account id url
console.log(resp.toJSON());
me._authorizations = resp.body.authorizations;
me._order = location;
me._finalize = resp.body.finalize;
//console.log('[DEBUG] finalize:', me._finalize); return;
//return resp.body;
return Promise.all(me._authorizations.map(function (authUrl, i) {
console.log("Authorizations map #" + i);
return ACME._getChallenges(me, options, authUrl).then(function (results) {
// var domain = options.domains[i]; // results.identifier.value
var chType = options.challengeTypes.filter(function (chType) {
return results.challenges.some(function (ch) {
return ch.type === chType;
});
})[0];
return me._finalizeOrder(options, validatedDomains);
}).then(function () {
return me._getCertificate().then(function (result) { cb(null, result); return result; }, cb);
});
var challenge = results.challenges.filter(function (ch) {
if (chType === ch.type) {
return ch;
}
})[0];
if (!challenge) {
return Promise.reject(new Error("Server didn't offer any challenge we can handle."));
}
return ACME._postChallenge(me, options, results.identifier, challenge);
});
})).then(function () {
var validatedDomains = body.identifiers.map(function (ident) {
return ident.value;
});
return ACME._finalizeOrder(me, options, validatedDomains);
}).then(function () {
return me._request({ method: 'GET', url: me._certificate, json: true }).then(function (resp) {
console.log('Certificate:');
console.log(resp.body);
return resp.body;
});
});
});
});
};
ACME.create = function create(me) {
if (!me) { me = {}; }
me.acmeChallengePrefix = ACME.acmeChallengePrefix;
me.RSA = me.RSA || require('rsa-compat').RSA;
me.request = me.request || require('request');
me.promisify = me.promisify || require('util').promisify;
if ('function' !== typeof me.getUserAgentString) {
me.pkg = me.pkg || require('./package.json');
me.os = me.os || require('os');
me.process = me.process || require('process');
me.userAgent = ACME._getUserAgentString(me);
}
function getRequest(opts) {
if (!opts) { opts = {}; }
return me.request.defaults({
headers: {
'User-Agent': opts.userAgent || me.userAgent || me.getUserAgentString(me)
}
});
}
if ('function' !== typeof me._request) {
me._request = me.promisify(getRequest({}));
}
me.init = function (_directoryUrl) {
me.directoryUrl = me.directoryUrl || _directoryUrl;
return ACME._directory(me).then(function (resp) {
me._directoryUrls = resp.body;
me._tos = me._directoryUrls.meta.termsOfService;
return me._directoryUrls;
});
};
me.accounts = {
create: function (options) {
return ACME._registerAccount(me, options);
}
};
return acme2;
}
module.exports.ACME = {
create: create
me.certificates = {
create: function (options) {
return ACME._getCertificate(me, options);
}
};
return me;
};
Object.keys(defaults).forEach(function (key) {
module.exports.ACME[key] = defaults[key];
});

75
test.cb.js

@ -0,0 +1,75 @@
'use strict';
module.exports.run = function run(web, chType, email) {
var RSA = require('rsa-compat').RSA;
var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory';
var acme2 = require('./compat').ACME.create({ RSA: RSA });
// [ 'test.ppl.family' ] 'coolaj86@gmail.com''http-01'
console.log(web, chType, email);
return;
acme2.init(directoryUrl).then(function (body) {
console.log(body);
return;
var options = {
agreeToTerms: function (tosUrl, agree) {
agree(null, tosUrl);
}
, setChallenge: function (opts, cb) {
console.log("");
console.log('identifier:');
console.log(opts.identifier);
console.log('hostname:');
console.log(opts.hostname);
console.log('type:');
console.log(opts.type);
console.log('token:');
console.log(opts.token);
console.log('thumbprint:');
console.log(opts.thumbprint);
console.log('keyAuthorization:');
console.log(opts.keyAuthorization);
console.log('dnsAuthorization:');
console.log(opts.dnsAuthorization);
console.log("");
console.log("Put the string '" + opts.keyAuthorization + "' into a file at '" + opts.hostname + "/" + opts.token + "'");
console.log("\nThen hit the 'any' key to continue (must be specifically the 'any' key)...");
function onAny() {
process.stdin.pause();
process.stdin.removeEventListener('data', onAny);
process.stdin.setRawMode(false);
cb();
}
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.on('data', onAny);
}
, removeChallenge: function (opts, cb) {
// hostname, key
console.log('[DEBUG] remove challenge', hostname, key);
setTimeout(cb, 1 * 1000);
}
, challengeType: chType
, email: email
, accountKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/account.privkey.pem') })
, domainKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') })
, domains: web
};
acme2.registerNewAccount(options).then(function (account) {
console.log('account:');
console.log(account);
acme2.getCertificate(options, function (fullchainPem) {
console.log('[acme-v2] A fullchain.pem:');
console.log(fullchainPem);
}).then(function (fullchainPem) {
console.log('[acme-v2] B fullchain.pem:');
console.log(fullchainPem);
});
});
});
};

57
test.compat.js

@ -0,0 +1,57 @@
'use strict';
var RSA = require('rsa-compat').RSA;
module.exports.run = function (web, chType, email) {
console.log('[DEBUG] run', web, chType, email);
var acme2 = require('./compat.js').ACME.create({ RSA: RSA });
acme2.getAcmeUrls(acme2.stagingServerUrl, function (err, body) {
if (err) { console.log('err 1'); throw err; }
console.log(body);
var options = {
agreeToTerms: function (tosUrl, agree) {
agree(null, tosUrl);
}
, setChallenge: function (hostname, token, val, cb) {
console.log("Put the string '" + val + "' into a file at '" + hostname + "/" + acme2.acmeChallengePrefix + "/" + token + "'");
console.log("echo '" + val + "' > '" + hostname + "/" + acme2.acmeChallengePrefix + "/" + token + "'");
console.log("\nThen hit the 'any' key to continue (must be specifically the 'any' key)...");
function onAny() {
console.log("'any' key was hit");
process.stdin.pause();
process.stdin.removeListener('data', onAny);
process.stdin.setRawMode(false);
cb();
}
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.on('data', onAny);
}
, removeChallenge: function (hostname, key, cb) {
console.log('[DEBUG] remove challenge', hostname, key);
setTimeout(cb, 1 * 1000);
}
, challengeType: chType
, email: email
, accountKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/account.privkey.pem') })
, domainKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') })
, domains: web
};
acme2.registerNewAccount(options, function (err, account) {
if (err) { console.log('err 2'); throw err; }
console.log('account:');
console.log(account);
acme2.getCertificate(options, function (err, fullchainPem) {
if (err) { console.log('err 3'); throw err; }
console.log('[acme-v2] A fullchain.pem:');
console.log(fullchainPem);
});
});
});
};

91
test.js

@ -1,56 +1,45 @@
'use strict';
var RSA = require('rsa-compat').RSA;
var acme2 = require('./').ACME.create({ RSA: RSA });
var readline = require('readline');
var rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
acme2.getAcmeUrls(acme2.stagingServerUrl).then(function (body) {
console.log(body);
function getWeb() {
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);
if (!web[0]) { getWeb(); return; }
var options = {
agreeToTerms: function (tosUrl, agree) {
agree(null, tosUrl);
}
/*
, setupChallenge: function (opts) {
console.log('type:');
console.log(ch.type);
console.log('ch.token:');
console.log(ch.token);
console.log('thumbprint:');
console.log(thumbprint);
console.log('keyAuthorization:');
console.log(keyAuthorization);
console.log('dnsAuthorization:');
console.log(dnsAuthorization);
}
*/
// teardownChallenge
, setChallenge: function (hostname, key, val, cb) {
console.log('[DEBUG] set challenge', hostname, key, val);
console.log("You have 20 seconds to put the string '" + val + "' into a file at '" + hostname + "/" + key + "'");
setTimeout(cb, 20 * 1000);
if (web.some(function (w) { return '*' === w[0]; })) {
console.log('Wildcard domains must use dns-01');
getEmail(web, 'dns-01');
} else {
getChallengeType(web);
}
, removeChallenge: function (hostname, key, cb) {
console.log('[DEBUG] remove challenge', hostname, key);
setTimeout(cb, 1 * 1000);
}
, challengeType: 'http-01'
, email: 'coolaj86@gmail.com'
, accountKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/account.privkey.pem') })
, domainKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') })
, domains: [ 'test.ppl.family' ]
};
acme2.registerNewAccount(options).then(function (account) {
console.log('account:');
console.log(account);
acme2.getCertificate(options, function (fullchainPem) {
console.log('[acme-v2] A fullchain.pem:');
console.log(fullchainPem);
}).then(function (fullchainPem) {
console.log('[acme-v2] B fullchain.pem:');
console.log(fullchainPem);
});
});
});
});
}
function getChallengeType(web) {
rl.question('What challenge will you be testing today? http-01 or dns-01? [http-01] ', function (chType) {
chType = (chType||'').trim();
if (!chType) { chType = 'http-01'; }
getEmail(web, chType);
});
}
function getEmail(web, chType) {
rl.question('What email should we use? (optional) ', function (email) {
email = (email||'').trim();
if (!email) { email = null; }
rl.close();
console.log("[DEBUG] rl blah blah");
require('./test.compat.js').run(web, chType, email);
//require('./test.cb.js').run(web, chType, email);
//require('./test.promise.js').run(web, chType, email);
});
}
getWeb();

84
test.promise.js

@ -0,0 +1,84 @@
'use strict';
/* global Promise */
module.exports.run = function run(web, chType, email) {
var RSA = require('rsa-compat').RSA;
var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory';
var acme2 = require('./compat').ACME.create({ RSA: RSA });
// [ 'test.ppl.family' ] 'coolaj86@gmail.com''http-01'
console.log(web, chType, email);
return;
acme2.init(directoryUrl).then(function (body) {
console.log(body);
return;
var options = {
agreeToTerms: function (tosUrl, agree) {
agree(null, tosUrl);
}
, setChallenge: function (opts) {
console.log("");
console.log('identifier:');
console.log(opts.identifier);
console.log('hostname:');
console.log(opts.hostname);
console.log('type:');
console.log(opts.type);
console.log('token:');
console.log(opts.token);
console.log('thumbprint:');
console.log(opts.thumbprint);
console.log('keyAuthorization:');
console.log(opts.keyAuthorization);
console.log('dnsAuthorization:');
console.log(opts.dnsAuthorization);
console.log("");
console.log("Put the string '" + opts.keyAuthorization + "' into a file at '" + opts.hostname + "/" + opts.token + "'");
console.log("\nThen hit the 'any' key to continue (must be specifically the 'any' key)...");
return new Promise(function (resolve) {
function onAny() {
process.stdin.pause();
process.stdin.removeEventListener('data', onAny);
process.stdin.setRawMode(false);
resolve();
}
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.on('data', onAny);
});
}
, removeChallenge: function (opts) {
// hostname, key
console.log('[DEBUG] remove challenge', opts.hostname, opts.keyAuthorization);
console.log("Remove the file '" + opts.hostname + "/" + opts.token + "'");
return new Promise(function (resolve) {
setTimeout(resolve, 1 * 1000);
});
}
, challengeType: chType
, email: email
, accountKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/account.privkey.pem') })
, domainKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') })
, domains: web
};
acme2.registerNewAccount(options).then(function (account) {
console.log('account:');
console.log(account);
acme2.getCertificate(options, function (fullchainPem) {
console.log('[acme-v2] A fullchain.pem:');
console.log(fullchainPem);
}).then(function (fullchainPem) {
console.log('[acme-v2] B fullchain.pem:');
console.log(fullchainPem);
});
});
});
};
読み込み中…
キャンセル
保存