updates
This commit is contained in:
parent
aec8958ca8
commit
4015e792dd
20
README.md
20
README.md
|
@ -23,19 +23,21 @@ leCore.
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
```
|
```javascript
|
||||||
LeCore.registerNewAccount();
|
LeCore.registerNewAccount(options, cb);
|
||||||
|
|
||||||
LeCore.getCertificate();
|
LeCore.getCertificate(options, cb);
|
||||||
|
|
||||||
LeCore.Acme // Signs requests with JWK
|
LeCore.Acme // Signs requests with JWK
|
||||||
acme = new Acme(lePrivateKey) // privateKey format is abstract
|
acme = new Acme(lePrivateKey) // privateKey format is abstract
|
||||||
acme.post(url, body, cb) // POST with signature
|
acme.post(url, body, cb) // POST with signature
|
||||||
acme.parseLinks(link) // (internal) parses 'link' header
|
acme.parseLinks(link) // (internal) parses 'link' header
|
||||||
acme.getNonce(url, cb) // (internal) HEAD request to get 'replay-nonce' strings
|
acme.getNonce(url, cb) // (internal) HEAD request to get 'replay-nonce' strings
|
||||||
|
|
||||||
LeCore.leCrypto
|
LeCore.leCrypto
|
||||||
generateSignature(lePrivateKey, nodeBufferBody, nonceString)
|
thumbprint(lePubKey) // generates thumbprint
|
||||||
|
generateSignature(lePrivKey, bodyBuf, nonce) // generates a signature
|
||||||
|
importPemPrivateKey(privateKeyPem); // returns abstract private key
|
||||||
```
|
```
|
||||||
|
|
||||||
For testing and development, you can also inject the dependencies you want to use:
|
For testing and development, you can also inject the dependencies you want to use:
|
||||||
|
|
|
@ -4,243 +4,245 @@
|
||||||
* Some code used from https://github.com/letsencrypt/boulder/tree/master/test/js
|
* Some code used from https://github.com/letsencrypt/boulder/tree/master/test/js
|
||||||
* MPL 2.0
|
* MPL 2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var NOOP=function () {}, log=NOOP;
|
module.exports.create = function (deps) {
|
||||||
var request=require('request');
|
var NOOP=function () {}, log=NOOP;
|
||||||
var util=require('./acme-util');
|
var request=require('request');
|
||||||
var cryptoUtil=require('./crypto-util');
|
var util=require('./acme-util');
|
||||||
var Acme = require('./acme-client');
|
var importPemPrivateKey = deps.leCrypto.importPemPrivateKey;
|
||||||
|
var thumbprinter = deps.leCrypto.thumbprint;
|
||||||
|
var generateCsr = deps.leCrypto.generateCsr || deps.leCrypto.generateCSR;
|
||||||
|
var Acme = deps.Acme;
|
||||||
|
|
||||||
function getCert(options, cb) {
|
function getCert(options, cb) {
|
||||||
var state={
|
var state={
|
||||||
validatedDomains:[]
|
validatedDomains:[]
|
||||||
, validAuthorizationUrls:[]
|
, validAuthorizationUrls:[]
|
||||||
, newAuthorizationUrl: options.newAuthorizationUrl || options.newAuthz
|
, newAuthorizationUrl: options.newAuthorizationUrl || options.newAuthz
|
||||||
, newCertificateUrl: options.newCertificateUrl || options.newCert
|
, newCertificateUrl: options.newCertificateUrl || options.newCert
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!options.accountPrivateKeyPem) {
|
if (!options.accountPrivateKeyPem) {
|
||||||
return handleErr(new Error("options.accountPrivateKeyPem must be an ascii private key pem"));
|
return handleErr(new Error("options.accountPrivateKeyPem must be an ascii private key pem"));
|
||||||
}
|
}
|
||||||
if (!options.domainPrivateKeyPem) {
|
if (!options.domainPrivateKeyPem) {
|
||||||
return handleErr(new Error("options.domainPrivateKeyPem must be an ascii private key pem"));
|
return handleErr(new Error("options.domainPrivateKeyPem must be an ascii private key pem"));
|
||||||
}
|
}
|
||||||
if (!options.setChallenge) {
|
if (!options.setChallenge) {
|
||||||
return handleErr(new Error("options.setChallenge must be function(hostname, challengeKey, tokenValue, done) {}"));
|
return handleErr(new Error("options.setChallenge must be function(hostname, challengeKey, tokenValue, done) {}"));
|
||||||
}
|
}
|
||||||
if (!options.removeChallenge) {
|
if (!options.removeChallenge) {
|
||||||
return handleErr(new Error("options.removeChallenge must be function(hostname, challengeKey, done) {}"));
|
return handleErr(new Error("options.removeChallenge must be function(hostname, challengeKey, done) {}"));
|
||||||
}
|
}
|
||||||
if (!(options.domains && options.domains.length)) {
|
if (!(options.domains && options.domains.length)) {
|
||||||
return handleErr(new Error("options.domains must be an array of domains such as ['example.com', 'www.example.com']"));
|
return handleErr(new Error("options.domains must be an array of domains such as ['example.com', 'www.example.com']"));
|
||||||
}
|
|
||||||
|
|
||||||
state.domains = options.domains.slice(0); // copy array
|
|
||||||
try {
|
|
||||||
state.accountKeyPem=options.accountPrivateKeyPem;
|
|
||||||
state.accountKeyPair=cryptoUtil.importPemPrivateKey(state.accountKeyPem);
|
|
||||||
state.acme=new Acme(state.accountKeyPair);
|
|
||||||
state.certPrivateKeyPem=options.domainPrivateKeyPem;
|
|
||||||
state.certPrivateKey=cryptoUtil.importPemPrivateKey(state.certPrivateKeyPem);
|
|
||||||
} catch(err) {
|
|
||||||
return handleErr(err, 'Failed to parse privateKey');
|
|
||||||
}
|
|
||||||
|
|
||||||
nextDomain();
|
|
||||||
|
|
||||||
function nextDomain() {
|
|
||||||
if (state.domains.length > 0) {
|
|
||||||
getChallenges(state.domains.shift());
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
getCertificate();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function getChallenges(domain) {
|
state.domains = options.domains.slice(0); // copy array
|
||||||
state.domain=domain;
|
try {
|
||||||
|
state.accountKeyPem=options.accountPrivateKeyPem;
|
||||||
|
state.accountKeyPair=importPemPrivateKey(state.accountKeyPem);
|
||||||
|
state.acme=new Acme(state.accountKeyPair);
|
||||||
|
state.certPrivateKeyPem=options.domainPrivateKeyPem;
|
||||||
|
state.certPrivateKey=importPemPrivateKey(state.certPrivateKeyPem);
|
||||||
|
} catch(err) {
|
||||||
|
return handleErr(err, 'Failed to parse privateKey');
|
||||||
|
}
|
||||||
|
|
||||||
state.acme.post(state.newAuthorizationUrl, {
|
nextDomain();
|
||||||
resource:'new-authz',
|
|
||||||
identifier:{
|
function nextDomain() {
|
||||||
type:'dns',
|
if (state.domains.length > 0) {
|
||||||
value:state.domain,
|
getChallenges(state.domains.shift());
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
getCertificate();
|
||||||
}
|
}
|
||||||
}, getReadyToValidate);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getReadyToValidate(err, res, body) {
|
|
||||||
var links, authz, httpChallenges, challenge, thumbprint, keyAuthorization, challengePath;
|
|
||||||
|
|
||||||
if (err) {
|
|
||||||
return handleErr(err);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Math.floor(res.statusCode/100)!==2) {
|
function getChallenges(domain) {
|
||||||
return handleErr(null, 'Authorization request failed ('+res.statusCode+')');
|
state.domain=domain;
|
||||||
|
|
||||||
|
state.acme.post(state.newAuthorizationUrl, {
|
||||||
|
resource:'new-authz',
|
||||||
|
identifier:{
|
||||||
|
type:'dns',
|
||||||
|
value:state.domain,
|
||||||
|
}
|
||||||
|
}, getReadyToValidate);
|
||||||
}
|
}
|
||||||
|
|
||||||
links=Acme.parseLink(res.headers.link);
|
function getReadyToValidate(err, res, body) {
|
||||||
if (!links || !('next' in links)) {
|
var links, authz, httpChallenges, challenge, thumbprint, keyAuthorization, challengePath;
|
||||||
return handleErr(err, 'Server didn\'t provide information to proceed (2)');
|
|
||||||
}
|
|
||||||
|
|
||||||
state.authorizationUrl=res.headers.location;
|
if (err) {
|
||||||
state.newCertificateUrl=links.next;
|
return handleErr(err);
|
||||||
|
}
|
||||||
|
|
||||||
authz=JSON.parse(body);
|
if (Math.floor(res.statusCode/100)!==2) {
|
||||||
|
return handleErr(null, 'Authorization request failed ('+res.statusCode+')');
|
||||||
|
}
|
||||||
|
|
||||||
httpChallenges=authz.challenges.filter(function(x) {
|
links=Acme.parseLink(res.headers.link);
|
||||||
return x.type==='http-01';
|
if (!links || !('next' in links)) {
|
||||||
});
|
return handleErr(err, 'Server didn\'t provide information to proceed (2)');
|
||||||
if (httpChallenges.length===0) {
|
}
|
||||||
return handleErr(null, 'Server didn\'t offer any challenge we can handle.');
|
|
||||||
}
|
|
||||||
challenge=httpChallenges[0];
|
|
||||||
|
|
||||||
thumbprint=cryptoUtil.thumbprint(state.accountKeyPair.publicKey);
|
state.authorizationUrl=res.headers.location;
|
||||||
keyAuthorization=challenge.token+'.'+thumbprint;
|
state.newCertificateUrl=links.next;
|
||||||
state.responseUrl=challenge.uri;
|
|
||||||
|
|
||||||
options.setChallenge(state.domain, challenge.token, keyAuthorization, challengeDone);
|
authz=JSON.parse(body);
|
||||||
|
|
||||||
function challengeDone() {
|
httpChallenges=authz.challenges.filter(function(x) {
|
||||||
state.acme.post(state.responseUrl, {
|
return x.type==='http-01';
|
||||||
resource:'challenge',
|
});
|
||||||
keyAuthorization:keyAuthorization
|
if (httpChallenges.length===0) {
|
||||||
}, function(err, res, body) {
|
return handleErr(null, 'Server didn\'t offer any challenge we can handle.');
|
||||||
ensureValidation(err, res, body, function unlink() {
|
}
|
||||||
options.removeChallenge(state.domain, challenge.token, function () {
|
challenge=httpChallenges[0];
|
||||||
// ignore
|
|
||||||
|
thumbprint=thumbprinter(state.accountKeyPair.publicKey);
|
||||||
|
keyAuthorization=challenge.token+'.'+thumbprint;
|
||||||
|
state.responseUrl=challenge.uri;
|
||||||
|
|
||||||
|
options.setChallenge(state.domain, challenge.token, keyAuthorization, challengeDone);
|
||||||
|
|
||||||
|
function challengeDone() {
|
||||||
|
state.acme.post(state.responseUrl, {
|
||||||
|
resource:'challenge',
|
||||||
|
keyAuthorization:keyAuthorization
|
||||||
|
}, function(err, res, body) {
|
||||||
|
ensureValidation(err, res, body, function unlink() {
|
||||||
|
options.removeChallenge(state.domain, challenge.token, function () {
|
||||||
|
// ignore
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureValidation(err, res, body, unlink) {
|
||||||
|
var authz;
|
||||||
|
|
||||||
|
if (err || Math.floor(res.statusCode/100)!==2) {
|
||||||
|
unlink();
|
||||||
|
return handleErr(err, 'Authorization status request failed ('+res.statusCode+')');
|
||||||
|
}
|
||||||
|
|
||||||
|
authz=JSON.parse(body);
|
||||||
|
|
||||||
|
if (authz.status==='pending') {
|
||||||
|
setTimeout(function() {
|
||||||
|
request.get(state.authorizationUrl, {}, function(err, res, body) {
|
||||||
|
ensureValidation(err, res, body, unlink);
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
} else if (authz.status==='valid') {
|
||||||
|
log('Validating domain ... done');
|
||||||
|
state.validatedDomains.push(state.domain);
|
||||||
|
state.validAuthorizationUrls.push(state.authorizationUrl);
|
||||||
|
unlink();
|
||||||
|
nextDomain();
|
||||||
|
} else if (authz.status==='invalid') {
|
||||||
|
unlink();
|
||||||
|
return handleErr(null, 'The CA was unable to validate the file you provisioned', body);
|
||||||
|
} else {
|
||||||
|
unlink();
|
||||||
|
return handleErr(null, 'CA returned an authorization in an unexpected state', authz);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCertificate() {
|
||||||
|
var csr=generateCsr(state.certPrivateKey, state.validatedDomains);
|
||||||
|
log('Requesting certificate...');
|
||||||
|
state.acme.post(state.newCertificateUrl, {
|
||||||
|
resource:'new-cert',
|
||||||
|
csr:csr,
|
||||||
|
authorizations:state.validAuthorizationUrls
|
||||||
|
}, downloadCertificate);
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadCertificate(err, res, body) {
|
||||||
|
var links, certUrl;
|
||||||
|
|
||||||
|
if (err || Math.floor(res.statusCode/100)!==2) {
|
||||||
|
log('Certificate request failed with error ', err);
|
||||||
|
if (body) {
|
||||||
|
log(body.toString());
|
||||||
|
}
|
||||||
|
return handleErr(err, 'Certificate request failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
links=Acme.parseLink(res.headers.link);
|
||||||
|
if (!links || !('up' in links)) {
|
||||||
|
return handleErr(err, 'Failed to fetch issuer certificate');
|
||||||
|
}
|
||||||
|
|
||||||
|
log('Requesting certificate: done');
|
||||||
|
|
||||||
|
state.certificate=body;
|
||||||
|
certUrl=res.headers.location;
|
||||||
|
request.get({
|
||||||
|
url:certUrl,
|
||||||
|
encoding:null
|
||||||
|
}, function(err, res, body) {
|
||||||
|
if (err) {
|
||||||
|
return handleErr(err, 'Failed to fetch cert from '+certUrl);
|
||||||
|
}
|
||||||
|
if (res.statusCode!==200) {
|
||||||
|
return handleErr(err, 'Failed to fetch cert from '+certUrl, res.body.toString());
|
||||||
|
}
|
||||||
|
if (body.toString()!==state.certificate.toString()) {
|
||||||
|
handleErr(null, 'Cert at '+certUrl+' did not match returned cert');
|
||||||
|
} else {
|
||||||
|
log('Successfully verified cert at '+certUrl);
|
||||||
|
log('Requesting issuer certificate...');
|
||||||
|
request.get({
|
||||||
|
url:links.up,
|
||||||
|
encoding:null
|
||||||
|
}, function(err, res, body) {
|
||||||
|
if (err || res.statusCode!==200) {
|
||||||
|
return handleErr(err, 'Failed to fetch issuer certificate');
|
||||||
|
}
|
||||||
|
state.caCert=certBufferToPem(body);
|
||||||
|
log('Requesting issuer certificate: done');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function ensureValidation(err, res, body, unlink) {
|
function done() {
|
||||||
var authz;
|
var cert;
|
||||||
|
|
||||||
if (err || Math.floor(res.statusCode/100)!==2) {
|
try {
|
||||||
unlink();
|
cert=certBufferToPem(state.certificate);
|
||||||
return handleErr(err, 'Authorization status request failed ('+res.statusCode+')');
|
} catch(e) {
|
||||||
}
|
console.error(e.stack);
|
||||||
|
//cb(new Error("Could not write output files. Please check permissions!"));
|
||||||
authz=JSON.parse(body);
|
handleErr(e, 'Could not write output files. Please check permissions!');
|
||||||
|
return;
|
||||||
if (authz.status==='pending') {
|
|
||||||
setTimeout(function() {
|
|
||||||
request.get(state.authorizationUrl, {}, function(err, res, body) {
|
|
||||||
ensureValidation(err, res, body, unlink);
|
|
||||||
});
|
|
||||||
}, 1000);
|
|
||||||
} else if (authz.status==='valid') {
|
|
||||||
log('Validating domain ... done');
|
|
||||||
state.validatedDomains.push(state.domain);
|
|
||||||
state.validAuthorizationUrls.push(state.authorizationUrl);
|
|
||||||
unlink();
|
|
||||||
nextDomain();
|
|
||||||
} else if (authz.status==='invalid') {
|
|
||||||
unlink();
|
|
||||||
return handleErr(null, 'The CA was unable to validate the file you provisioned', body);
|
|
||||||
} else {
|
|
||||||
unlink();
|
|
||||||
return handleErr(null, 'CA returned an authorization in an unexpected state', authz);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCertificate() {
|
|
||||||
var csr=cryptoUtil.generateCSR(state.certPrivateKey, state.validatedDomains);
|
|
||||||
log('Requesting certificate...');
|
|
||||||
state.acme.post(state.newCertificateUrl, {
|
|
||||||
resource:'new-cert',
|
|
||||||
csr:csr,
|
|
||||||
authorizations:state.validAuthorizationUrls
|
|
||||||
}, downloadCertificate);
|
|
||||||
}
|
|
||||||
|
|
||||||
function downloadCertificate(err, res, body) {
|
|
||||||
var links, certUrl;
|
|
||||||
|
|
||||||
if (err || Math.floor(res.statusCode/100)!==2) {
|
|
||||||
log('Certificate request failed with error ', err);
|
|
||||||
if (body) {
|
|
||||||
log(body.toString());
|
|
||||||
}
|
}
|
||||||
return handleErr(err, 'Certificate request failed');
|
|
||||||
|
cb(null, {
|
||||||
|
cert: cert
|
||||||
|
, key: state.certPrivateKeyPem
|
||||||
|
, ca: state.caCert
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
links=Acme.parseLink(res.headers.link);
|
function handleErr(err, text, info) {
|
||||||
if (!links || !('up' in links)) {
|
log(text, err, info);
|
||||||
return handleErr(err, 'Failed to fetch issuer certificate');
|
cb(err || new Error(text));
|
||||||
}
|
}
|
||||||
|
|
||||||
log('Requesting certificate: done');
|
|
||||||
|
|
||||||
state.certificate=body;
|
|
||||||
certUrl=res.headers.location;
|
|
||||||
request.get({
|
|
||||||
url:certUrl,
|
|
||||||
encoding:null
|
|
||||||
}, function(err, res, body) {
|
|
||||||
if (err) {
|
|
||||||
return handleErr(err, 'Failed to fetch cert from '+certUrl);
|
|
||||||
}
|
|
||||||
if (res.statusCode!==200) {
|
|
||||||
return handleErr(err, 'Failed to fetch cert from '+certUrl, res.body.toString());
|
|
||||||
}
|
|
||||||
if (body.toString()!==state.certificate.toString()) {
|
|
||||||
handleErr(null, 'Cert at '+certUrl+' did not match returned cert');
|
|
||||||
} else {
|
|
||||||
log('Successfully verified cert at '+certUrl);
|
|
||||||
log('Requesting issuer certificate...');
|
|
||||||
request.get({
|
|
||||||
url:links.up,
|
|
||||||
encoding:null
|
|
||||||
}, function(err, res, body) {
|
|
||||||
if (err || res.statusCode!==200) {
|
|
||||||
return handleErr(err, 'Failed to fetch issuer certificate');
|
|
||||||
}
|
|
||||||
state.caCert=certBufferToPem(body);
|
|
||||||
log('Requesting issuer certificate: done');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function done() {
|
function certBufferToPem(cert) {
|
||||||
var cert;
|
cert=util.toStandardB64(cert.toString('base64'));
|
||||||
|
cert=cert.match(/.{1,64}/g).join('\n');
|
||||||
try {
|
return '-----BEGIN CERTIFICATE-----\n'+cert+'\n-----END CERTIFICATE-----';
|
||||||
cert=certBufferToPem(state.certificate);
|
|
||||||
} catch(e) {
|
|
||||||
console.error(e.stack);
|
|
||||||
//cb(new Error("Could not write output files. Please check permissions!"));
|
|
||||||
handleErr(e, 'Could not write output files. Please check permissions!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cb(null, {
|
|
||||||
cert: cert
|
|
||||||
, key: state.certPrivateKeyPem
|
|
||||||
, ca: state.caCert
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleErr(err, text, info) {
|
return getCert;
|
||||||
log(text, err, info);
|
};
|
||||||
cb(err || new Error(text));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function certBufferToPem(cert) {
|
|
||||||
cert=util.toStandardB64(cert.toString('base64'));
|
|
||||||
cert=cert.match(/.{1,64}/g).join('\n');
|
|
||||||
return '-----BEGIN CERTIFICATE-----\n'+cert+'\n-----END CERTIFICATE-----';
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = getCert;
|
|
||||||
|
|
2
node.js
2
node.js
|
@ -9,7 +9,7 @@ function create(deps) {
|
||||||
var LeCore = {};
|
var LeCore = {};
|
||||||
|
|
||||||
LeCore.leCrypto = deps.leCrypto;
|
LeCore.leCrypto = deps.leCrypto;
|
||||||
LeCore.Acme = require('./lib/acme-client').create(deps);
|
deps.Acme = LeCore.Acme = require('./lib/acme-client').create(deps);
|
||||||
LeCore.registerNewAccount = require('./lib/register-new-account').create(deps);
|
LeCore.registerNewAccount = require('./lib/register-new-account').create(deps);
|
||||||
LeCore.getCertificate = require('./lib/get-certificate').create(deps);
|
LeCore.getCertificate = require('./lib/get-certificate').create(deps);
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue