525 lines
15 KiB
Markdown
525 lines
15 KiB
Markdown
IMPORTANT: Try this first
|
|
=========
|
|
|
|
2015-Jul-13: I just discovered that the most common reason you would have the kind of problems this module solves is actually due to **failing to properly bundle the Intermediate CAs** with the server certificate.
|
|
|
|
**Incorrect Example**
|
|
|
|
```js
|
|
// INCORRECT (but might still work)
|
|
var server https.createServer({
|
|
key: fs.readFileSync('privkey.pem', 'ascii')
|
|
, cert: fs.readFileSync('cert.pem', 'ascii') // a PEM containing ONLY the SERVER certificate
|
|
});
|
|
```
|
|
|
|
**Correct Example**
|
|
|
|
```js
|
|
// CORRECT (should always work)
|
|
var server https.createServer({
|
|
key: fs.readFileSync('privkey.pem', 'ascii')
|
|
, cert: fs.readFileSync('fullchain.pem', 'ascii') // a PEM containing the SERVER and ALL INTERMEDIATES
|
|
});
|
|
```
|
|
|
|
```bash
|
|
# Test your HTTPS effortlessly
|
|
npm -g install serve-https
|
|
|
|
serve-https --servername example.com --cert ./fullchain.pem --key ./privkey.pem
|
|
```
|
|
|
|
You can debug the certificate chain with `openssl`:
|
|
|
|
```bash
|
|
openssl s_client -showcerts \
|
|
-connect example.com:443 \
|
|
-servername example.com
|
|
```
|
|
|
|
|
|
**Example `fullchain.pem`**
|
|
|
|
```
|
|
cat \
|
|
cert.pem \
|
|
intermediate-twice-removed.pem \
|
|
interemediate-once-removed.pem \
|
|
> fullchain.pem
|
|
```
|
|
|
|
Note that you **should not** include the `root.pem` in the bundle and that the bundle should be constructed with the least authoritative certificate first - your server's certificate, followed by the furthest removed intermediate, and then the next closest to the root, etc.
|
|
|
|
Also note that in the case of cross-signed certificates (typically only issued from new root certificate authorities) there may be more than one intermediate at equal distances, in which case either in that tier may come first.
|
|
|
|
IMPORTANT: Try this next
|
|
========================
|
|
|
|
As of node.js v7.3 the `NODE_EXTRA_CA_CERTS` environment variable can accomplish what most people intend to do with this package. See nodejs/node#9139
|
|
|
|
```bash
|
|
NODE_EXTRA_CA_CERTS='./path/to/root-cas.pem' node example.js
|
|
```
|
|
|
|
SSL Root CAs
|
|
=================
|
|
|
|
The module you need to solve node's SSL woes when including a custom certificate. Particularly, if you need to add a **non-standard Root CA**, then this is the right module for you.
|
|
|
|
Let's say you're trying to connect to a site with a cheap-o SSL cert -
|
|
such as RapidSSL certificate from [name.com](http://name.com) (the **best** place to get your domains, btw) -
|
|
you'll probably get an error like `UNABLE_TO_VERIFY_LEAF_SIGNATURE` and after you google around and figure that
|
|
out you'll be able to connect to that site just fine, but now when you try to connect to other sites you get
|
|
`CERT_UNTRUSTED` or possibly other errors.
|
|
|
|
**Common Errors**
|
|
|
|
* `CERT_UNTRUSTED` - the common root CAs are missing, this module fixes that.
|
|
* `UNABLE_TO_VERIFY_LEAF_SIGNATURE` could be either the same as the above, or the below
|
|
* `unable to verify the first certificate` - the intermediate certificate wasn't bundled along with the server certificate, you'll need to fix that
|
|
|
|
This module is the solution to your woes!
|
|
|
|
FYI, I'm merely the publisher, not the author of this module.
|
|
See here: https://groups.google.com/d/msg/nodejs/AjkHSYmiGYs/1LfNHbMhd48J
|
|
|
|
The script downloads the same root CAs that are included with
|
|
[Mozilla Firefox](http://www.mozilla.org/en-US/about/governance/policies/security-group/certs/included/),
|
|
[Google Chrome](http://www.chromium.org/Home/chromium-security/root-ca-policy),
|
|
[`libnss`](https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS#CA_certificates_pre-loaded_into_NSS),
|
|
and [OpenSSL](https://www.openssl.org/support/faq.html#USER16)\*:
|
|
<https://mxr.mozilla.org/nss/source/lib/ckfw/builtins/certdata.txt?raw=1>
|
|
|
|
\* OpenSSL doesn't actually bundle these CAs, but they suggest using them
|
|
|
|
**Other Implementations**
|
|
|
|
* Golang <https://github.com/agl/extract-nss-root-certs>
|
|
* Perl <https://github.com/bagder/curl/blob/master/lib/mk-ca-bundle.pl>
|
|
|
|
**Usage Examples**
|
|
|
|
* https://github.com/coolaj86/nodejs-self-signed-certificate-example
|
|
* https://github.com/coolaj86/nodejs-ssl-trusted-peer-example
|
|
|
|
Install
|
|
=====
|
|
|
|
```javascript
|
|
npm install ssl-root-cas --save
|
|
```
|
|
|
|
Usage
|
|
=====
|
|
|
|
General usage:
|
|
|
|
```js
|
|
'use strict';
|
|
var rootCas = require('ssl-root-cas/latest').create();
|
|
|
|
// default for all https requests
|
|
// (whether using https directly, request, or another module)
|
|
require('https').globalAgent.options.ca = rootCas;
|
|
```
|
|
|
|
### CERT_UNTRUSTED
|
|
|
|
`CERT_UNTRUSTED`
|
|
|
|
**Old Versions of node.js**:
|
|
|
|
If you have to run an old version of node, but need the latest CAs
|
|
(i.e. you get `CERT_UNTRUSTED` on well-known and properly configured websites)
|
|
then this alone should solve your problems:
|
|
|
|
```javascript
|
|
var rootCas = require('ssl-root-cas/latest').create();
|
|
|
|
// fixes ALL https requests (whether using https directly or the request module)
|
|
require('https').globalAgent.options.ca = rootCas;
|
|
|
|
var secureContext = require('tls').createSecureContext({
|
|
ca: rootCas
|
|
// ...
|
|
});
|
|
```
|
|
|
|
**missing Root CA** (such as a company ca)
|
|
|
|
If you have a newer version of node and still get `CERT_UNTRUSTED`, it's probably
|
|
because you're testing against a self-signed or company-issued certificate.
|
|
|
|
Follow the instructions above, but also use `addFile`, like this:
|
|
|
|
```
|
|
var rootCas = require('ssl-root-cas/latest').create();
|
|
|
|
rootCas.addFile(__dirname + '/ssl/00-company-root-ca.pem');
|
|
```
|
|
|
|
### unable to verify the first certificate
|
|
|
|
`unable to verify the first certificate`
|
|
|
|
When you get this error it means that the webserver you are connecting to
|
|
is misconfigured and did not include the intermediate certificates in the certificate
|
|
it sent to you.
|
|
|
|
You can work around this by adding the missing certificate:
|
|
|
|
```javascript
|
|
'use strict';
|
|
|
|
var rootCas = require('ssl-root-cas/latest').create();
|
|
|
|
rootCas
|
|
.addFile(__dirname + '/ssl/01-cheap-ssl-intermediary-a.pem')
|
|
.addFile(__dirname + '/ssl/02-cheap-ssl-intermediary-b.pem')
|
|
;
|
|
|
|
// will work with all https requests will all libraries (i.e. request.js)
|
|
require('https').globalAgent.options.ca = rootCas;
|
|
```
|
|
|
|
### using the latest certificates
|
|
|
|
For the sake of version consistency this package ships with the CA certs that were
|
|
available at the time it was published,
|
|
but for the sake of security I recommend you use the latest ones.
|
|
|
|
If you want the latest certificates (downloaded as part of the postinstall process),
|
|
you can require those like so:
|
|
|
|
```
|
|
var rootCas = require('ssl-root-cas/latest').create();
|
|
|
|
require('https').globalAgent.options.ca = rootCas;
|
|
```
|
|
|
|
You can use the ones that shippped with package like so:
|
|
|
|
```
|
|
var rootCas = require('ssl-root-cas').create();
|
|
|
|
require('https').globalAgent.options.ca = rootCas;
|
|
```
|
|
|
|
API
|
|
---
|
|
|
|
### addFile(filepath)
|
|
|
|
This is just a convenience method so that you don't
|
|
have to require `fs` and `path` if you don't need them.
|
|
|
|
```javascript
|
|
require('ssl-root-cas/latest')
|
|
.addFile(__dirname + '/ssl/03-cheap-ssl-site.pem')
|
|
;
|
|
```
|
|
|
|
is the same as
|
|
|
|
```javascript
|
|
var https = require('https');
|
|
var cas;
|
|
|
|
cas = https.globalAgent.options.ca || [];
|
|
cas.push(fs.readFileSync(path.join(__dirname, 'ssl', '03-cheap-ssl-site.pem')));
|
|
https.globalAgent.options.ca = cas;
|
|
```
|
|
|
|
### rootCas
|
|
|
|
If for some reason you just want to look at the array of Root CAs without actually injecting
|
|
them, or you just prefer to
|
|
`https.globalAgent.options.ca = require('ssl-root-cas').rootCas;`
|
|
yourself, well, you can.
|
|
|
|
### inject()
|
|
|
|
(deprecated)
|
|
|
|
I thought it might be rude to modify `https.globalAgent.options.ca` on `require`,
|
|
so I afford you the opportunity to `inject()` the certs at your leisure.
|
|
|
|
`inject()` keeps track of whether or not it's been run, so no worries about calling it twice.
|
|
|
|
|
|
Kinda Bad Ideas
|
|
=====
|
|
|
|
```javascript
|
|
'use strict';
|
|
|
|
var request = require('request');
|
|
var agentOptions;
|
|
var agent;
|
|
|
|
agentOptions = {
|
|
host: 'www.example.com'
|
|
, port: '443'
|
|
, path: '/'
|
|
, rejectUnauthorized: false
|
|
};
|
|
|
|
agent = new https.Agent(agentOptions);
|
|
|
|
request({
|
|
url: "https://www.example.com/api/endpoint"
|
|
, method: 'GET'
|
|
, agent: agent
|
|
}, function (err, resp, body) {
|
|
// ...
|
|
});
|
|
```
|
|
|
|
By using an `agent` with `rejectUnauthorized` you at limit the security vulnerability to the requests that deal with that one site instead of making your entire node process completely, utterly insecure.
|
|
|
|
### Other Options
|
|
|
|
If you were using a self-signed cert you would add this option:
|
|
|
|
```javascript
|
|
agentOptions.ca = [ selfSignedRootCaPemCrtBuffer ];
|
|
```
|
|
|
|
For trusted-peer connections you would also add these 2 options:
|
|
|
|
```javascript
|
|
agentOptions.key = clientPemKeyBuffer;
|
|
agentOptions.cert = clientPemCrtSignedBySelfSignedRootCaBuffer;
|
|
```
|
|
|
|
|
|
|
|
REALLY Bad Ideas
|
|
===
|
|
|
|
Don't use dissolutions such as these. :-)
|
|
|
|
This will turn off SSL validation checking. This is not a good idea. Please do not do it.
|
|
(really I'm only providing it as a reference for search engine seo so that people who are trying
|
|
to figure out how to avoid doing that will end up here)
|
|
|
|
```javascript
|
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"
|
|
```
|
|
|
|
The same dissolution from the terminal would be
|
|
|
|
```bash
|
|
export NODE_TLS_REJECT_UNAUTHORIZED="0"
|
|
node my-service.js
|
|
```
|
|
|
|
It's unfortunate that `process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';` is even documented. It should only be used for debugging and should never make it into in sort of code that runs in the wild. Almost every library that runs atop `https` has a way of passing agent options through. Those that don't should be fixed.
|
|
|
|
|
|
# Appendix
|
|
|
|
Other information you might want to know while you're here.
|
|
|
|
## Generating an SSL Cert
|
|
|
|
Just in case you didn't know, here's how you do it:
|
|
|
|
```
|
|
openssl req -new -sha256 -newkey rsa:2048 -nodes -keyout server.key -out server.csr
|
|
```
|
|
|
|
**DO NOT FILL OUT** email address, challenge password, or optional company name
|
|
|
|
However, you *should* fill out country name, FULL state name, locality name, organization name.
|
|
|
|
*organizational unit* is optional.
|
|
|
|
```
|
|
cat server.csr
|
|
```
|
|
|
|
That created a signing request with a sha-256 hash.
|
|
|
|
When you submit that to the likes of RapidSSL you'll get back an X.509 that you should call `server.crt.pem` (at least for the purposes of this mini-tutorial).
|
|
|
|
You **must** use a bundled certificate for your server (the server and intermediates, **not** root) and pass that as the `cert` option, **not** as the `ca` (which is used for peer-certificate checking).
|
|
|
|
### A single HTTPS server
|
|
|
|
Here's a complete working example:
|
|
|
|
```javascript
|
|
'use strict';
|
|
|
|
var https = require('https');
|
|
var fs = require('fs');
|
|
var express = require('express');
|
|
var app = express();
|
|
var sslOptions;
|
|
var server;
|
|
var port = 4080;
|
|
|
|
require('ssl-root-cas/latest')
|
|
.inject()
|
|
.addFile(__dirname + '/ssl/Geotrust Cross Root CA.txt')
|
|
// NOTE: intermediate certificates should be bundled with
|
|
// the site's certificate, which is issued by the server
|
|
// when you connect. You only need to add them here if the
|
|
// server is misconfigured and you can't change it
|
|
//.addFile(__dirname + '/ssl/Rapid SSL CA.txt')
|
|
;
|
|
|
|
sslOptions = {
|
|
key: fs.readFileSync('./ssl/privkey.pem')
|
|
, cert: fs.readFileSync('./ssl/fullchain.pem')
|
|
};
|
|
|
|
app.use('/', function (req, res) {
|
|
res.end('<html><body><h1>Hello World</h1></body></html>');
|
|
});
|
|
|
|
server = https.createServer(sslOptions);
|
|
server.on('request', app);
|
|
server.listen(port, function(){
|
|
console.log('Listening on https://' + server.address().address + ':' + server.address().port);
|
|
});
|
|
```
|
|
|
|
### Multiple HTTPS servers using SNI
|
|
|
|
I know this works - because I just bought two SSL certs from RapidSSL (through name.com),
|
|
a Digital Ocean VPS,
|
|
and played around for an hour until it did.
|
|
|
|
:-)
|
|
|
|
File hierarchy:
|
|
|
|
```
|
|
/etc/letsencrypt
|
|
└── live
|
|
├── aj.the.dj
|
|
│ ├── cert.pem // contains my server certificate
|
|
│ ├── chain.pem // contains RapidSSL intermediate
|
|
│ ├── cert+chain.pem // contains both
|
|
│ └── privkey.pem // my private key
|
|
├── ballprovo.com
|
|
│ ├── cert.pem
|
|
│ ├── chain.pem
|
|
│ ├── cert+chain.pem
|
|
│ └── privkey.pem
|
|
├── server.js
|
|
└── ssl
|
|
├── Geotrust Cross Root CA.txt // the Root Authority
|
|
└── Rapid SSL CA.txt // the Intermediate Authority
|
|
```
|
|
|
|
|
|
#### `server.js`
|
|
|
|
```javascript
|
|
'use strict';
|
|
|
|
var https = require('https');
|
|
var http = require('http');
|
|
var fs = require('fs');
|
|
var crypto = require('crypto');
|
|
var express = require('express');
|
|
var vhost = require('vhost');
|
|
|
|
// connect / express app
|
|
var app = express();
|
|
|
|
// SSL Server
|
|
var secureContexts = {};
|
|
var secureOpts;
|
|
var secureServer;
|
|
var securePort = 4443;
|
|
|
|
// force SSL upgrade server
|
|
var server;
|
|
var port = 4080;
|
|
|
|
// the ssl domains I have
|
|
var domains = ['aj.the.dj', 'ballprovo.com'];
|
|
|
|
require('ssl-root-cas/latest')
|
|
.inject()
|
|
.addFile(__dirname + '/ssl/Geotrust Cross Root CA.txt')
|
|
//.addFile(__dirname + '/ssl/Rapid SSL CA.txt')
|
|
;
|
|
|
|
function getAppContext(domain) {
|
|
// Really you'd want to do this:
|
|
// return require(__dirname + '/' + domain + '/app.js');
|
|
|
|
// But for this demo we'll do this:
|
|
return connect().use('/', function (req, res) {
|
|
console.log('req.vhost', JSON.stringify(req.vhost));
|
|
res.end('<html><body><h1>Welcome to ' + domain + '!</h1></body></html>');
|
|
});
|
|
}
|
|
|
|
domains.forEach(function (domain) {
|
|
secureContexts[domain] = crypto.createCredentials({
|
|
key: fs.readFileSync(__dirname + '/' + domain + '/privkey.pem')
|
|
, cert: fs.readFileSync(__dirname + '/' + domain + '/cert+chain.pem')
|
|
}).context;
|
|
|
|
app.use(vhost('*.' + domain, getAppContext(domain)));
|
|
app.use(vhost(domain, getAppContext(domain)));
|
|
});
|
|
|
|
// fallback / default domain
|
|
app.use('/', function (req, res) {
|
|
res.end('<html><body><h1>Hello World</h1></body></html>');
|
|
});
|
|
|
|
//provide a SNICallback when you create the options for the https server
|
|
secureOpts = {
|
|
//SNICallback is passed the domain name, see NodeJS docs on TLS
|
|
SNICallback: function (domain) {
|
|
console.log('SNI:', domain);
|
|
return secureContexts[domain];
|
|
}
|
|
// fallback / default domain
|
|
, key: fs.readFileSync(__dirname + '/aj.the.dj/privkey.pem')
|
|
, cert: fs.readFileSync(__dirname + '/aj.the.dj/cert+chain.pem')
|
|
};
|
|
|
|
secureServer = https.createServer(secureOpts, app).listen(securePort, function(){
|
|
console.log("Listening on https://localhost:" + secureServer.address().port);
|
|
});
|
|
|
|
server = http.createServer(function (req, res) {
|
|
res.setHeader(
|
|
'Location'
|
|
, 'https://' + req.headers.host.replace(/:\d+/, ':' + securePort)
|
|
);
|
|
res.statusCode = 302;
|
|
res.end();
|
|
}).listen(port, function(){
|
|
console.log("Listening on http://localhost:" + server.address().port);
|
|
});
|
|
```
|
|
|
|
Other SSL Resources
|
|
=========
|
|
|
|
Zero-Config clone 'n' run (tm) Repos:
|
|
|
|
|
|
* [io.js / node.js HTTPS SSL Example](https://github.com/coolaj86/nodejs-ssl-example)
|
|
* [io.js / node.js HTTPS SSL Self-Signed Certificate Example](https://github.com/coolaj86/nodejs-self-signed-certificate-example)
|
|
* [io.js / node.js HTTPS SSL Trusted Peer Client Certificate Example](https://github.com/coolaj86/nodejs-ssl-trusted-peer-example)
|
|
* [SSL Root CAs](https://github.com/coolaj86/node-ssl-root-cas)
|
|
|
|
Articles
|
|
|
|
* [Creating an SSL Certificate for node.js](http://greengeckodesign.com/blog/2013/06/15/creating-an-ssl-certificate-for-node-dot-js/)
|
|
* [HTTPS Trusted Peer Example](http://www.hacksparrow.com/express-js-https-server-client-example.html/comment-page-1)
|
|
* [How to Create a CSR for HTTPS SSL (demo with name.com, node.js)](http://blog.coolaj86.com/articles/how-to-create-a-csr-for-https-tls-ssl-rsa-pems/)
|
|
* [coolaj86/Painless-Self-Signed-Certificates-in-node](https://github.com/coolaj86/node-ssl-root-cas/wiki/Painless-Self-Signed-Certificates-in-node.js)
|