Compare commits

...

18 Commits

9 changed files with 300 additions and 106 deletions

32
.gitignore vendored Normal file
View File

@ -0,0 +1,32 @@
*.pem
letsencrypt.work
letsencrypt.logs
letsencrypt.config
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules

18
.jshintrc Normal file
View File

@ -0,0 +1,18 @@
{ "node": true
, "browser": true
, "jquery": true
, "globals": { "angular": true, "Promise": true }
, "indent": 2
, "onevar": true
, "laxcomma": true
, "laxbreak": true
, "curly": true
, "nonbsp": true
, "eqeqeq": true
, "immed": true
, "undef": true
, "unused": true
, "latedef": true
}

41
LICENSE Normal file
View File

@ -0,0 +1,41 @@
Copyright 2018 AJ ONeal
This is open source software; you can redistribute it and/or modify it under the
terms of either:
a) the "MIT License"
b) the "Apache-2.0 License"
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Apache-2.0 License Summary
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,4 +1,5 @@
'use strict'; 'use strict';
/* global Promise */
var ACME2 = require('./').ACME; var ACME2 = require('./').ACME;
@ -31,13 +32,10 @@ function create(deps) {
options.agreeToTerms = options.agreeToTerms || function (tos) { options.agreeToTerms = options.agreeToTerms || function (tos) {
return Promise.resolve(tos); return Promise.resolve(tos);
}; };
acme2.certificates.create(options).then(function (chainPem) { acme2.certificates.create(options).then(function (certs) {
var privkeyPem = acme2.RSA.exportPrivatePem(options.domainKeypair); var privkeyPem = acme2.RSA.exportPrivatePem(options.domainKeypair);
resolveFn(cb)({ certs.privkey = privkeyPem;
cert: chainPem.split(/[\r\n]{2,}/g)[0] + '\r\n' resolveFn(cb)(certs);
, privkey: privkeyPem
, chain: chainPem.split(/[\r\n]{2,}/g)[1] + '\r\n'
});
}, rejectFn(cb)); }, rejectFn(cb));
}; };
acme2.getAcmeUrls = function (options, cb) { acme2.getAcmeUrls = function (options, cb) {

165
node.js
View File

@ -8,6 +8,15 @@
var ACME = module.exports.ACME = {}; var ACME = module.exports.ACME = {};
ACME.formatPemChain = function formatPemChain(str) {
return str.trim().replace(/[\r\n]+/g, '\n').replace(/\-\n\-/g, '-\n\n-') + '\n';
};
ACME.splitPemChain = function splitPemChain(str) {
return str.trim().split(/[\r\n]{2,}/g).map(function (str) {
return str + '\n';
});
};
ACME.challengePrefixes = { ACME.challengePrefixes = {
'http-01': '/.well-known/acme-challenge' 'http-01': '/.well-known/acme-challenge'
, 'dns-01': '_acme-challenge' , 'dns-01': '_acme-challenge'
@ -106,7 +115,7 @@ ACME._getNonce = function (me) {
} }
*/ */
ACME._registerAccount = function (me, options) { ACME._registerAccount = function (me, options) {
if (me.debug) console.debug('[acme-v2] accounts.create'); if (me.debug) { console.debug('[acme-v2] accounts.create'); }
return ACME._getNonce(me).then(function () { return ACME._getNonce(me).then(function () {
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
@ -125,7 +134,7 @@ ACME._registerAccount = function (me, options) {
if (options.contact) { if (options.contact) {
contact = options.contact.slice(0); contact = options.contact.slice(0);
} else if (options.email) { } else if (options.email) {
contact = [ 'mailto:' + options.email ] contact = [ 'mailto:' + options.email ];
} }
var body = { var body = {
termsOfServiceAgreed: tosUrl === me._tos termsOfServiceAgreed: tosUrl === me._tos
@ -140,7 +149,7 @@ ACME._registerAccount = function (me, options) {
, kid: options.externalAccount.id , kid: options.externalAccount.id
, url: me._directoryUrls.newAccount , url: me._directoryUrls.newAccount
} }
, new Buffer(JSON.stringify(jwk)) , Buffer.from(JSON.stringify(jwk))
); );
} }
var payload = JSON.stringify(body); var payload = JSON.stringify(body);
@ -152,12 +161,12 @@ ACME._registerAccount = function (me, options) {
, url: me._directoryUrls.newAccount , url: me._directoryUrls.newAccount
, jwk: jwk , jwk: jwk
} }
, new Buffer(payload) , Buffer.from(payload)
); );
delete jws.header; delete jws.header;
if (me.debug) console.debug('[acme-v2] accounts.create JSON body:'); if (me.debug) { console.debug('[acme-v2] accounts.create JSON body:'); }
if (me.debug) console.debug(jws); if (me.debug) { console.debug(jws); }
me._nonce = null; me._nonce = null;
return me._request({ return me._request({
method: 'POST' method: 'POST'
@ -167,34 +176,34 @@ ACME._registerAccount = function (me, options) {
}).then(function (resp) { }).then(function (resp) {
var account = resp.body; var account = resp.body;
if (2 !== Math.floor(resp.statusCode / 100)) {
throw new Error('account error: ' + JSON.stringify(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
me._kid = location; me._kid = location;
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()); }
/* /*
{ {
id: 5925245, contact: ["mailto:jon@example.com"],
key: orders: "https://some-url",
{ 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' status: 'valid'
} }
*/ */
if (!account) { account = { _emptyResponse: true, key: {} }; } if (!account) { account = { _emptyResponse: true, key: {} }; }
// https://git.coolaj86.com/coolaj86/acme-v2.js/issues/8
if (!account.key) { account.key = {}; }
account.key.kid = me._kid; account.key.kid = me._kid;
return account; 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 // newer promise API
return options.agreeToTerms(me._tos).then(agree, reject); return options.agreeToTerms(me._tos).then(agree, reject);
@ -234,7 +243,7 @@ ACME._registerAccount = function (me, options) {
} }
*/ */
ACME._getChallenges = function (me, options, auth) { ACME._getChallenges = function (me, options, auth) {
if (me.debug) console.debug('\n[DEBUG] getChallenges\n'); if (me.debug) { console.debug('\n[DEBUG] getChallenges\n'); }
return me._request({ method: 'GET', url: auth, json: true }).then(function (resp) { return me._request({ method: 'GET', url: auth, json: true }).then(function (resp) {
return resp.body; return resp.body;
}); });
@ -288,7 +297,7 @@ ACME._postChallenge = function (me, options, identifier, ch) {
options.accountKeypair options.accountKeypair
, undefined , undefined
, { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } , { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid }
, new Buffer(JSON.stringify({ "status": "deactivated" })) , Buffer.from(JSON.stringify({ "status": "deactivated" }))
); );
me._nonce = null; me._nonce = null;
return me._request({ return me._request({
@ -297,14 +306,14 @@ ACME._postChallenge = function (me, options, identifier, ch) {
, headers: { 'Content-Type': 'application/jose+json' } , headers: { 'Content-Type': 'application/jose+json' }
, json: jws , json: jws
}).then(function (resp) { }).then(function (resp) {
if (me.debug) console.debug('[acme-v2.js] deactivate:'); if (me.debug) { console.debug('[acme-v2.js] deactivate:'); }
if (me.debug) console.debug(resp.headers); if (me.debug) { console.debug(resp.headers); }
if (me.debug) console.debug(resp.body); if (me.debug) { console.debug(resp.body); }
if (me.debug) console.debug(); if (me.debug) { console.debug(); }
me._nonce = resp.toJSON().headers['replay-nonce']; me._nonce = resp.toJSON().headers['replay-nonce'];
if (me.debug) console.debug('deactivate challenge: resp.body:'); if (me.debug) { console.debug('deactivate challenge: resp.body:'); }
if (me.debug) console.debug(resp.body); if (me.debug) { console.debug(resp.body); }
return ACME._wait(10 * 1000); return ACME._wait(10 * 1000);
}); });
} }
@ -316,11 +325,11 @@ ACME._postChallenge = function (me, options, identifier, ch) {
count += 1; count += 1;
if (me.debug) console.debug('\n[DEBUG] statusChallenge\n'); if (me.debug) { console.debug('\n[DEBUG] statusChallenge\n'); }
return me._request({ method: 'GET', url: ch.url, json: true }).then(function (resp) { return me._request({ method: 'GET', url: ch.url, json: true }).then(function (resp) {
if ('processing' === resp.body.status) { if ('processing' === resp.body.status) {
if (me.debug) console.debug('poll: again'); if (me.debug) { console.debug('poll: again'); }
return ACME._wait(1 * 1000).then(pollStatus); return ACME._wait(1 * 1000).then(pollStatus);
} }
@ -329,12 +338,12 @@ ACME._postChallenge = function (me, options, identifier, ch) {
if (count >= 4) { if (count >= 4) {
return ACME._wait(1 * 1000).then(deactivate).then(testChallenge); return ACME._wait(1 * 1000).then(deactivate).then(testChallenge);
} }
if (me.debug) console.debug('poll: again'); if (me.debug) { console.debug('poll: again'); }
return ACME._wait(1 * 1000).then(testChallenge); return ACME._wait(1 * 1000).then(testChallenge);
} }
if ('valid' === resp.body.status) { if ('valid' === resp.body.status) {
if (me.debug) console.debug('poll: valid'); if (me.debug) { console.debug('poll: valid'); }
try { try {
if (1 === options.removeChallenge.length) { if (1 === options.removeChallenge.length) {
@ -367,7 +376,7 @@ ACME._postChallenge = function (me, options, identifier, ch) {
options.accountKeypair options.accountKeypair
, undefined , undefined
, { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } , { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid }
, new Buffer(JSON.stringify({ })) , Buffer.from(JSON.stringify({ }))
); );
me._nonce = null; me._nonce = null;
return me._request({ return me._request({
@ -376,23 +385,18 @@ ACME._postChallenge = function (me, options, identifier, ch) {
, headers: { 'Content-Type': 'application/jose+json' } , headers: { 'Content-Type': 'application/jose+json' }
, json: jws , json: jws
}).then(function (resp) { }).then(function (resp) {
if (me.debug) console.debug('[acme-v2.js] challenge accepted!'); if (me.debug) { console.debug('[acme-v2.js] challenge accepted!'); }
if (me.debug) console.debug(resp.headers); if (me.debug) { console.debug(resp.headers); }
if (me.debug) console.debug(resp.body); if (me.debug) { console.debug(resp.body); }
if (me.debug) console.debug(); if (me.debug) { console.debug(); }
me._nonce = resp.toJSON().headers['replay-nonce']; me._nonce = resp.toJSON().headers['replay-nonce'];
if (me.debug) console.debug('respond to challenge: resp.body:'); if (me.debug) { console.debug('respond to challenge: resp.body:'); }
if (me.debug) console.debug(resp.body); if (me.debug) { console.debug(resp.body); }
return ACME._wait(1 * 1000).then(pollStatus).then(resolve, reject); return ACME._wait(1 * 1000).then(pollStatus);
}); });
} }
function failChallenge(err) {
if (err) { reject(err); return; }
return testChallenge();
}
function testChallenge() { function testChallenge() {
// TODO put check dns / http checks here? // TODO put check dns / http checks here?
// http-01: GET https://example.org/.well-known/acme-challenge/{{token}} => {{keyAuth}} // http-01: GET https://example.org/.well-known/acme-challenge/{{token}} => {{keyAuth}}
@ -410,11 +414,27 @@ ACME._postChallenge = function (me, options, identifier, ch) {
try { try {
if (1 === options.setChallenge.length) { if (1 === options.setChallenge.length) {
options.setChallenge(auth).then(testChallenge, reject); options.setChallenge(auth).then(testChallenge).then(resolve, reject);
} else if (2 === options.setChallenge.length) { } else if (2 === options.setChallenge.length) {
options.setChallenge(auth, failChallenge); options.setChallenge(auth, function (err) {
if(err) {
reject(err);
} else { } else {
options.setChallenge(identifier.value, ch.token, keyAuthorization, failChallenge); testChallenge().then(resolve, reject);
}
});
} else {
var challengeCb = function(err) {
if(err) {
reject(err);
} else {
testChallenge().then(resolve, reject);
}
};
Object.keys(auth).forEach(function (key) {
challengeCb[key] = auth[key];
});
options.setChallenge(identifier.value, ch.token, keyAuthorization, challengeCb);
} }
} catch(e) { } catch(e) {
reject(e); reject(e);
@ -422,7 +442,7 @@ ACME._postChallenge = function (me, options, identifier, ch) {
}); });
}; };
ACME._finalizeOrder = function (me, options, validatedDomains) { ACME._finalizeOrder = function (me, options, validatedDomains) {
if (me.debug) console.debug('finalizeOrder:'); if (me.debug) { console.debug('finalizeOrder:'); }
var csr = me.RSA.generateCsrWeb64(options.domainKeypair, validatedDomains); var csr = me.RSA.generateCsrWeb64(options.domainKeypair, validatedDomains);
var body = { csr: csr }; var body = { csr: csr };
var payload = JSON.stringify(body); var payload = JSON.stringify(body);
@ -432,10 +452,10 @@ ACME._finalizeOrder = function (me, options, validatedDomains) {
options.accountKeypair options.accountKeypair
, undefined , undefined
, { nonce: me._nonce, alg: 'RS256', url: me._finalize, kid: me._kid } , { nonce: me._nonce, alg: 'RS256', url: me._finalize, kid: me._kid }
, new Buffer(payload) , Buffer.from(payload)
); );
if (me.debug) console.debug('finalize:', me._finalize); if (me.debug) { console.debug('finalize:', me._finalize); }
me._nonce = null; me._nonce = null;
return me._request({ return me._request({
method: 'POST' method: 'POST'
@ -447,21 +467,21 @@ ACME._finalizeOrder = function (me, options, validatedDomains) {
// Possible values are: "pending" => ("invalid" || "ready") => "processing" => "valid" // Possible values are: "pending" => ("invalid" || "ready") => "processing" => "valid"
me._nonce = resp.toJSON().headers['replay-nonce']; me._nonce = resp.toJSON().headers['replay-nonce'];
if (me.debug) console.debug('order finalized: resp.body:'); if (me.debug) { console.debug('order finalized: resp.body:'); }
if (me.debug) console.debug(resp.body); if (me.debug) { console.debug(resp.body); }
if ('valid' === resp.body.status) { if ('valid' === resp.body.status) {
me._expires = resp.body.expires; me._expires = resp.body.expires;
me._certificate = resp.body.certificate; me._certificate = resp.body.certificate;
return resp.body; return resp.body; // return order
} }
if ('processing' === resp.body.status) { if ('processing' === resp.body.status) {
return ACME._wait().then(pollCert); return ACME._wait().then(pollCert);
} }
if (me.debug) console.debug("Error: bad status:\n" + JSON.stringify(resp.body, null, 2)); if (me.debug) { console.debug("Error: bad status:\n" + JSON.stringify(resp.body, null, 2)); }
if ('pending' === resp.body.status) { if ('pending' === resp.body.status) {
return Promise.reject(new Error( return Promise.reject(new Error(
@ -502,7 +522,7 @@ ACME._finalizeOrder = function (me, options, validatedDomains) {
return pollCert(); return pollCert();
}; };
ACME._getCertificate = function (me, options) { ACME._getCertificate = function (me, options) {
if (me.debug) console.debug('[acme-v2] DEBUG get cert 1'); if (me.debug) { console.debug('[acme-v2] DEBUG get cert 1'); }
if (!options.challengeTypes) { if (!options.challengeTypes) {
if (!options.challengeType) { if (!options.challengeType) {
@ -522,7 +542,7 @@ ACME._getCertificate = function (me, options) {
} }
} }
if (me.debug) console.debug('[acme-v2] certificates.create'); if (me.debug) { console.debug('[acme-v2] certificates.create'); }
return ACME._getNonce(me).then(function () { return ACME._getNonce(me).then(function () {
var body = { var body = {
identifiers: options.domains.map(function (hostname) { identifiers: options.domains.map(function (hostname) {
@ -537,10 +557,10 @@ ACME._getCertificate = function (me, options) {
options.accountKeypair options.accountKeypair
, undefined , undefined
, { nonce: me._nonce, alg: 'RS256', url: me._directoryUrls.newOrder, kid: me._kid } , { nonce: me._nonce, alg: 'RS256', url: me._directoryUrls.newOrder, kid: me._kid }
, new Buffer(payload) , Buffer.from(payload)
); );
if (me.debug) console.debug('\n[DEBUG] newOrder\n'); if (me.debug) { console.debug('\n[DEBUG] newOrder\n'); }
me._nonce = null; me._nonce = null;
return me._request({ return me._request({
method: 'POST' method: 'POST'
@ -551,8 +571,8 @@ ACME._getCertificate = function (me, options) {
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;
var auths; var auths;
if (me.debug) console.debug(location); // the account id url if (me.debug) { console.debug(location); } // the account id url
if (me.debug) console.debug(resp.toJSON()); if (me.debug) { console.debug(resp.toJSON()); }
me._authorizations = resp.body.authorizations; me._authorizations = resp.body.authorizations;
me._order = location; me._order = location;
me._finalize = resp.body.finalize; me._finalize = resp.body.finalize;
@ -563,7 +583,7 @@ ACME._getCertificate = function (me, options) {
console.error(resp.body); console.error(resp.body);
return Promise.reject(new Error("authorizations were not fetched")); return Promise.reject(new Error("authorizations were not fetched"));
} }
if (me.debug) console.debug("47 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); if (me.debug) { console.debug("[acme-v2] POST newOrder has authorizations"); }
//return resp.body; //return resp.body;
auths = me._authorizations.slice(0); auths = me._authorizations.slice(0);
@ -597,18 +617,29 @@ ACME._getCertificate = function (me, options) {
} }
return next().then(function () { return next().then(function () {
if (me.debug) console.debug("37 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); if (me.debug) { console.debug("[getCertificate] next.then"); }
var validatedDomains = body.identifiers.map(function (ident) { var validatedDomains = body.identifiers.map(function (ident) {
return ident.value; return ident.value;
}); });
return ACME._finalizeOrder(me, options, validatedDomains); return ACME._finalizeOrder(me, options, validatedDomains);
}).then(function () { }).then(function (order) {
if (me.debug) console.debug('acme-v2: order was finalized'); if (me.debug) { console.debug('acme-v2: order was finalized'); }
return me._request({ method: 'GET', url: me._certificate, json: true }).then(function (resp) { return me._request({ method: 'GET', url: me._certificate, json: true }).then(function (resp) {
if (me.debug) console.debug('acme-v2: csr submitted and cert received:'); if (me.debug) { console.debug('acme-v2: csr submitted and cert received:'); }
if (me.debug) console.debug(resp.body); // https://github.com/certbot/certbot/issues/5721
return resp.body; var certsarr = ACME.splitPemChain(ACME.formatPemChain((resp.body||'')));
// cert, chain, fullchain, privkey, /*TODO, subject, altnames, issuedAt, expiresAt */
var certs = {
expires: order.expires
, identifiers: order.identifiers
//, authorizations: order.authorizations
, cert: certsarr.shift()
//, privkey: privkeyPem
, chain: certsarr.join('\n')
};
if (me.debug) { console.debug(certs); }
return certs;
}); });
}); });
}); });
@ -620,7 +651,7 @@ ACME.create = function create(me) {
// me.debug = true; // me.debug = true;
me.challengePrefixes = ACME.challengePrefixes; me.challengePrefixes = ACME.challengePrefixes;
me.RSA = me.RSA || require('rsa-compat').RSA; me.RSA = me.RSA || require('rsa-compat').RSA;
me.request = me.request || require('request'); me.request = me.request || require('@coolaj86/urequest');
me._dig = function (query) { me._dig = function (query) {
// TODO use digd.js // TODO use digd.js
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {

View File

@ -1,6 +1,6 @@
{ {
"name": "acme-v2", "name": "acme-v2",
"version": "1.0.7", "version": "1.2.0",
"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",
@ -12,37 +12,21 @@
"url": "ssh://gitea@git.coolaj86.com:22042/coolaj86/acme-v2.js.git" "url": "ssh://gitea@git.coolaj86.com:22042/coolaj86/acme-v2.js.git"
}, },
"keywords": [ "keywords": [
"acmev2",
"acmev02",
"acme-v2",
"acme-v02",
"acme",
"acme2",
"acme11",
"acme-draft11",
"acme-draft-11",
"draft",
"11",
"ssl",
"tls",
"https",
"Let's Encrypt", "Let's Encrypt",
"letsencrypt", "ACME",
"letsencrypt-v2", "v02",
"letsencrypt-v02", "v2",
"letsencryptv2", "draft-11",
"letsencryptv02", "draft-12",
"letsencrypt2", "free ssl",
"greenlock", "tls",
"greenlock2" "automated https",
"letsencrypt"
], ],
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)", "author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
"license": "(MIT OR Apache-2.0)", "license": "(MIT OR Apache-2.0)",
"dependencies": { "dependencies": {
"request": "^2.85.0", "@coolaj86/urequest": "^1.1.1",
"rsa-compat": "^1.3.0" "rsa-compat": "^1.3.0"
},
"optionalDependencies": {
"bluebird": "^3.5.1"
} }
} }

View File

@ -12,10 +12,23 @@ module.exports.run = function (directoryUrl, RSA, web, chType, email, accountKey
agree(null, tosUrl); agree(null, tosUrl);
} }
, setChallenge: function (hostname, token, val, cb) { , setChallenge: function (hostname, token, val, cb) {
var pathname = hostname + acme2.acmeChallengePrefix + token; var pathname;
console.log("Put the string '" + val + "' into a file at '" + pathname + "'");
console.log("echo '" + val + "' > '" + pathname + "'"); if ('http-01' === cb.type) {
pathname = hostname + acme2.acmeChallengePrefix + token;
console.log("Put the string '" + val /*keyAuthorization*/ + "' into a file at '" + pathname + "'");
console.log("echo '" + val /*keyAuthorization*/ + "' > '" + pathname + "'");
console.log("\nThen hit the 'any' key to continue..."); console.log("\nThen hit the 'any' key to continue...");
} else if ('dns-01' === cb.type) {
// forwards-backwards compat
pathname = acme2.challengePrefixes['dns-01'] + "." + hostname.replace(/^\*\./, '');
console.log("Put the string '" + cb.dnsAuthorization + "' into the TXT record '" + pathname + "'");
console.log("dig TXT " + pathname + " '" + cb.dnsAuthorization + "'");
console.log("\nThen hit the 'any' key to continue...");
} else {
cb(new Error("[acme-v2] unrecognized challenge type: " + cb.type));
return;
}
function onAny() { function onAny() {
console.log("'any' key was hit"); console.log("'any' key was hit");

View File

@ -0,0 +1,77 @@
'use strict';
/*
-----BEGIN CERTIFICATE-----LF
xxxLF
yyyLF
-----END CERTIFICATE-----LF
LF
-----BEGIN CERTIFICATE-----LF
xxxLF
yyyLF
-----END CERTIFICATE-----LF
Rules
* Only Unix LF (\n) Line endings
* Each PEM's lines are separated with \n
* Each PEM ends with \n
* Each PEM is separated with a \n (just like commas separating an array)
*/
// https://github.com/certbot/certbot/issues/5721#issuecomment-402362709
var expected = "----\nxxxx\nyyyy\n----\n\n----\nxxxx\nyyyy\n----\n";
var tests = [
"----\r\nxxxx\r\nyyyy\r\n----\r\n\r\n----\r\nxxxx\r\nyyyy\r\n----\r\n"
, "----\r\nxxxx\r\nyyyy\r\n----\r\n----\r\nxxxx\r\nyyyy\r\n----\r\n"
, "----\nxxxx\nyyyy\n----\n\n----\r\nxxxx\r\nyyyy\r\n----"
, "----\nxxxx\nyyyy\n----\n----\r\nxxxx\r\nyyyy\r\n----"
, "----\nxxxx\nyyyy\n----\n----\nxxxx\nyyyy\n----"
, "----\nxxxx\nyyyy\n----\n----\nxxxx\nyyyy\n----\n"
, "----\nxxxx\nyyyy\n----\n\n----\nxxxx\nyyyy\n----\n"
, "----\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';
});
}
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");
}
});
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");
}
});
console.info('PASS');

View File

@ -35,9 +35,9 @@ module.exports.run = function run(directoryUrl, RSA, web, chType, email, account
console.log("Put the string '" + opts.keyAuthorization + "' into a file at '" + pathname + "'"); console.log("Put the string '" + opts.keyAuthorization + "' into a file at '" + pathname + "'");
console.log("echo '" + opts.keyAuthorization + "' > '" + pathname + "'"); console.log("echo '" + opts.keyAuthorization + "' > '" + pathname + "'");
} else if ('dns-01' === opts.type) { } else if ('dns-01' === opts.type) {
pathname = acme2.challengePrefixes['dns-01'] + "." + opts.hostname.replace(/^\*\./, '');; pathname = acme2.challengePrefixes['dns-01'] + "." + opts.hostname.replace(/^\*\./, '');
console.log("Put the string '" + opts.dnsAuthorization + "' into the TXT record '" + pathname + "'"); console.log("Put the string '" + opts.dnsAuthorization + "' into the TXT record '" + pathname + "'");
console.log("ddig TXT " + pathname + " '" + opts.dnsAuthorization + "'"); console.log("dig TXT " + pathname + " '" + opts.dnsAuthorization + "'");
} else { } else {
reject(new Error("[acme-v2] unrecognized challenge type")); reject(new Error("[acme-v2] unrecognized challenge type"));
return; return;