Node.js TLS request with specific ciphers - node.js

I have a Node.js app that needs to check the TLS compatibility of external resources. I need to limit the specific ciphers that Node.js will use when making an external TLS request. I'm looking for sample code to achieve this.
More info: Apple is requiring in iOS 9 all outbound connections be encrypted and the allowed cipher list is limited.
The accepted ciphers are:
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA
TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256
TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
My goal is to build a service that will check to make sure external servers meet the Apple requirements.

You could just connect to each resource using that list of ciphers. If the connection is successful, then you know one of those ciphers is being used and thus checks out. An exclusive list of ciphers can be set via the ciphers property. For example:
var ciphers = ['TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384',
'TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256',
'TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384',
'TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA',
'TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256',
'TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA',
'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384',
'TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256',
'TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384',
'TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256',
'TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA'].join(':');
tls.connect({
host: 'example.com',
port: 443,
ciphers: ciphers
}, function() {
// Success!
}).on('error', function(err) {
// Unsuccessful! You may check `err` to make sure it wasn't an unexpected
// error like ECONNREFUSED
});
You can also limit the protocol used by setting the secureProtocol property or the minVersion and maxVersion properties in node v10.16.0 or newer. For example, to use TLSv1.2:
var ciphers = ['TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384',
'TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256',
'TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384',
'TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA',
'TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256',
'TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA',
'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384',
'TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256',
'TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384',
'TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256',
'TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA'].join(':');
tls.connect({
host: 'example.com',
port: 443,
ciphers: ciphers,
secureProtocol: 'TLSv1_2_method',
// or for node v10.16.0+:
// minVersion: 'TLSv1.2',
// maxVersion: 'TLSv1.2',
}, function() {
// Success!
}).on('error', function(err) {
// Unsuccessful! You may check `err` to make sure it wasn't an unexpected
// error like ECONNREFUSED
});

Related

SSL certificates with RedBird.js and Node.js with two domains on one server

Bad(?) news "SSL For Free is joining ZeroSSL". Since their news I renewed my certificates and TLS stopped working. Used to work fine.
I believe free certs are now from something called AutoSSL. Hopefully.
With new certificates I get error "You may need to install an Intermediate/chain certificate to link it to a trusted root certificate" from https://www.sslshopper.com/ssl-checker.html and this error "TLS Certificate is not trusted" from https://www.digicert.com/help.
Browsers are smart enough to mask the problem but my Android app uses an API and it stopped working.
Anyone else getting TLS problems since ZeroSSL got involved?
I'm using redbirdjs on nodejs which is awesome since its so simple (two domains, same server), but Zero provides no installation instructions for my setup. (My domains are small in traffic so using the fastest webservers etc. isn't an issue).
Zero took away the 2 domains in one cert option (gee thanks) so my updated script looks like:
const { constants } = require('crypto');
var redbird = new require('redbird')({ port: 8080, ssl: { port: 443 }});
redbird.register('domain1.com', 'http://127.0.0.1:9443', {
ssl: {
key: 'ssl/domain1/private.key',
cert: 'ssl/domain1/certificate.crt',
ca: 'ssl/domain1/ca_bundle.crt',
secureOptions: constants.SSL_OP_NO_TLSv1 | constants.SSL_OP_NO_TLSv1_1,
}
});
redbird.register('domain2.com', 'http://127.0.0.1:3003', {
ssl: {
key: 'ssl/domain2/private.key',
cert: 'ssl/domain2/certificate.crt',
ca: 'ssl/domain2/ca_bundle.crt',
secureOptions: constants.SSL_OP_NO_TLSv1 | constants.SSL_OP_NO_TLSv1_1,
}
});
Other than separating the domain ssl config, it is the same as what used to work with SSLForFree.
I read somewhere that "free" SSL CA's do not necessarily provide the "full chain".
Anyone know how to get TLS working again with ZeroSSL on redbirdjs and nodejs?
Well, I got it working. I used https://whatsmychaincert.com, which I think just literally joins a couple files together. Either way for redbird fans (like me) here is the script for multiple domains on the same server.
// https://github.com/OptimalBits/redbird
// https://whatsmychaincert.com/
// 9443 is where domain1 server runs locally
// 3003 is where domain2 server runs locally
const { constants } = require('crypto');
var redbird = new require('redbird')({ port: 8080, ssl: { port: 443 }});
redbird.register('domain1.com', 'http://127.0.0.1:9443', {
ssl: {
port: 9443,
key: 'ssl/domain1/private.key',
cert: 'ssl/domain1/domain1.chained.crt', // used whatsmychaincert.com to generate ('enter hostname', no need to include root)
secureOptions: constants.SSL_OP_NO_TLSv1 | constants.SSL_OP_NO_TLSv1_1,
}
});
redbird.register('domain2.net', 'http://127.0.0.1:3003', {
ssl: {
port: 3003,
key: 'ssl/domain2/private.key',
cert: 'ssl/domain2/domain2.chained.crt',
secureOptions: constants.SSL_OP_NO_TLSv1 | constants.SSL_OP_NO_TLSv1_1,
}
});
Of the 3 files downloaded from ZeroSSL, whatsmychaincert.com put the certificate.crt and the ca_bundle.crt (in that order) into one file called domain.chained.crt (as see in the script above).

NodeJS tls.connect() getPeerCertificate() return error (multi) but browser shows fine

I'm building a SSL crawler application where user pass in the domain name and NodeJS use tls library to retrieve the SSL certificate.
First, here is my codes:
server.js
const tls = require('tls');
var rootCas = require('ssl-root-cas/latest').create();
const fs = require('fs');
fs.readdirSync('./keys/intermediate_certs').forEach(file => {
rootCas.addFile('./keys/intermediate_certs/' + file)
});
var secureContext = tls.createSecureContext({
ca: rootCas
});
options = {
host: host, //domain like google.com
port: 443,
secureContext: secureContext,
ca: rootCas,
rejectUnauthorized: true
};
var tlsSocket = tls.connect(options, function () {
let rawCert = tlsSocket.getPeerCertificate()
console.log(rawCert)
})
tlsSocket.on('error', (error) => {
console.log(error)
// [ERR_TLS_CERT_ALTNAME_INVALID] Hostname/IP does not match certificate's altnames: Host: zdns.cn. is not in the cert's altnames: DNS:*.fkw.com, DNS:fkw.com
// unable to verify the first certificate or UNABLE_TO_VERIFY_LEAF_SIGNATURE
});
Problem is the nodejs application throwing error, according to the TLS documentation, the errors were from OpenSSL, however, when browsing the website and view certificate is showing all valid (even the common name matched exactly).
Here are some criteria:
zdns.cn / www.zdns.cn is showing the error: ERR_TLS_CERT_ALTNAME_INVALID; When view cert from browser it show *.zdns.cn
knet.cn / www.knet.cn is showing the error: unable to verify the first certificate; When view cert from browser it show www.knet.cn
Note: I included latest root CA from ssl-root-cas and also downloaded the intermediate certificate manually from CA site.
You are getting that error specifically because of your rejectUnauthorized parameter. The certificate is presenting *.fkw.com as the CN, and it is presenting *.fkw.com and fkw.com as alternate names. None of those match zdns.cn or www.zdns.cn.
If you are just crawling to get the certs, you may want to drop the rejectUnauthorized. Alternatively, the error does seem to display the rest of the certificate information in the error. So you could keep it as is and include in your output information about why the certificate is untrusted/invalid. That seems like valuable information for a crawler pulling certs.

Nodejs how to obtain TLS object from https server

I am using a https server using nodejs. For security reasons, I have to change certain ssl parameters. More specifically, I have to set/disable "client renegotiation limit".
As per the standard documentation here, i have to set or change tls.CLIENT_RENEG_LIMIT to my value.
Since, I am using https module, I have access to https server. My question is how to obtain tls object from https server, so that I can set values.
I understand that I can set a few options while creating a https server as below.
const options = {
key: fs.readFileSync('app-key.pem'),
cert: fs.readFileSync('app-cert.pem'),
secureOptions: constants.SSL_OP_NO_SSLV2 | ...,
ciphers: [...]
}
However, I am unsure what exactly to set if I were to add values for tls.CLIENT_RENEG_LIMIT, or tls.CLIENT_RENEG_WINDOW etc. I am assuming there would be some way to obtain a tls handle through which I can set these.
Any help here...?
You can not obtain TLS object from https server object but you can use the built-in module tls to set the required option. Before creating https server, you can set values as:
const tls = require('tls')
tls.CLIENT_RENEG_LIMIT = ' required value'
const options = {
key: fs.readFileSync('app-key.pem'),
cert: fs.readFileSync('app-cert.pem'),
secureOptions: constants.SSL_OP_NO_SSLV2 | ...,
ciphers: [...]
}
const server = https.createServer(options,function(req,res){
res.writeHead(200);
res.end('hello world\n');
})
server.listen(8080);
TLSv1.3 does not support renegotiation.
https module is dependent on tls, So whatever valid value you set to tls will be applicable for https
You can not pass CLIENT_RENEG_LIMIT as https options because
options <Object> Accepts options from
tls.createServer(),tls.createSecureContext() and
http.createServer().
See tls.createServer options

Managed DigitalOcean Redis instance giving Redis AbortError

I setup managed redis and managed postgres on digital ocean. Digital ocean gave me a .crt file, I don't know what to do with this, so didn't do anything with it. Can this be the root of the problem below:
Or do I have to allow docker container to reach outside of the container on the rediss protocol?
I dockerized a node app and then put this container onto my droplet. I have my droplet and managed redis and postgres in same region (SFO2). It connects to redis using this url:
url: 'rediss://default:REMOVED_THIS_PASSWORD#my-new-app-sfo2-do-user-5053627-0.db.ondigitalocean.com:25061/0',
I then did ran my docker container with docker run.
It then gives me error:
node_redis: WARNING: You passed "rediss" as protocol instead of the "redis" protocol!
events.js:186
throw er; // Unhandled 'error' event
^
AbortError: Connection forcefully ended and command aborted. It might have been processed.
at RedisClient.flush_and_error (/opt/apps/mynewapp/node_modules/redis/index.js:362:23)
at RedisClient.end (/opt/apps/mynewapp/node_modules/redis/lib/extendedApi.js:52:14)
at RedisClient.onPreConnectionEnd (/opt/apps/mynewapp/node_modules/machinepack-redis/machines/get-connection.js:157:14)
at RedisClient.emit (events.js:209:13)
at RedisClient.connection_gone (/opt/apps/mynewapp/node_modules/redis/index.js:590:14)
at Socket.<anonymous> (/opt/apps/mynewapp/node_modules/redis/index.js:293:14)
at Object.onceWrapper (events.js:298:28)
at Socket.emit (events.js:214:15)
at endReadableNT (_stream_readable.js:1178:12)
at processTicksAndRejections (internal/process/task_queues.js:80:21)
Emitted 'error' event on RedisClient instance at:
at /opt/apps/mynewapp/node_modules/redis/index.js:310:22
at Object.callbackOrEmit [as callback_or_emit] (/opt/apps/mynewapp/node_modules/redis/lib/utils.js:89:9)
at Command.callback (/opt/apps/mynewapp/node_modules/redis/lib/individualCommands.js:199:15)
at RedisClient.flush_and_error (/opt/apps/mynewapp/node_modules/redis/index.js:374:29)
at RedisClient.end (/opt/apps/mynewapp/node_modules/redis/lib/extendedApi.js:52:14)
[... lines matching original stack trace ...]
at processTicksAndRejections (internal/process/task_queues.js:80:21) {
code: 'NR_CLOSED',
command: 'AUTH',
args: [ 'REMOVED_I_DONT_KNOW_IF_THIS_IS_SENSITIVE' ]
The redis protocol is different from rediss because the latter uses TLS connection. DigitalOcean Managed Redis requires the connections to be made over TLS, so you have to use rediss. However, I couldn't find any info about the TLS certificate provided by DigitalOcean to connect to the Managed Redis service.
Based on your error message, I presumed you're using this redis package. If that's the case, you can pass empty TLS object option in the connection string like so:
const Redis = require('redis')
const host = 'db-redis.db.ondigitalocean.com'
const port = '25061'
const username = 'user'
const password = 'secret'
const url = `${username}:${password}#${host}:${port}`
const client = Redis.createClient(url, {tls: {}})
Further reading/source:
SSL connections arrive for Redis on Compose
Connecting to IBM Cloud Databases for Redis from Node.js
I solved this. Below are snippets from config/env/production.js
Sockets
For sockets, to enable rediss you have to pass in all options through adapterOptions like this:
sockets: {
onlyAllowOrigins: ['https://my-website.com'],
// pass in as adapterOptions so it gets through to redis-adapter
// as i need it "rediss" but this url is not supported i get an error.
// so i need to pass in `tls` empty object. and i see he moves things into
// `adapterOptions` here here - https://github.com/balderdashy/sails-hook-sockets/blob/master/lib/configure.js#L128
adapterOptions: {
user: 'username',
pass: 'password',
host: 'host',
port: 9999,
db: 2, // pick a number
tls: {},
},
adapter: '#sailshq/socket.io-redis',
},
Session
For session, pass tls: {} empty object to config:
session: {
pass: 'password',
host: 'host',
port: 9999,
db: 1, // pick a number not used by sockets
tls: {},
cookie: {
secure: true,
maxAge: 24 * 60 * 60 * 1000, // 24 hours
},
},

sending OSC between machines on a LAN using Node.js and OSC.js

Has anyone created a working setup where OSC is being sent between machines on a LAN using Node.js? Ideally, using Colin Clark's osc.js package?
I have what I think should be a pretty simple example, except that it doesn't work - I get an EADDRNOTAVAIL error, which implies that the remote address isn't available. I can ping the other laptop successfully, however.
Here's the code and the error, for reference:
Sending code (laptop at 192.168.0.5):
var osc = require("osc");
var udp = new osc.UDPPort({
localAddress: "127.0.0.1", // shouldn't matter here
localPort: 5000, // not receiving, but here's a port anyway
remoteAddress: "192.168.0.7", // the other laptop
remotePort: 9999 // the port to send to
});
udp.open();
udp.on("ready", function () {
console.log("ready");
setInterval(function () {
udp.send({
address: "/sending/every/second",
args: [1, 2, 3]
})
}, 1000);
});
Receive code (on laptop at 192.168.0.7):
var osc = require("osc");
var udp = new osc.UDPPort({
localAddress: "192.168.0.7",
localPort: 9999
});
udp.open();
udp.on("ready", function () {
console.log("receiver is ready");
});
udp.on("message", function(message, timetag, info) {
console.log(message);
});
Here's the error I get when I run the sending code:
ready
events.js:141
throw er; // Unhandled 'error' event
^
Error: send EADDRNOTAVAIL 192.168.0.7:9999
at Object.exports._errnoException (util.js:907:11)
at exports._exceptionWithHostPort (util.js:930:20)
at SendWrap.afterSend [as oncomplete] (dgram.js:345:11)
The issue is that the osc.UDPPort that you are using to send OSC messages has its localAddress bound to the loopback address, which is limited to connections within your local computer. As a result, your sender can't find your receiver.
The solution is to bind your sender's localAddress to an appropriate network interface. If your 192.168.0.5 IP address is stable and you don't need to worry about it changing when you connect your laptops to another network (say, for a gig or a gallery installation), then you can just use that. Otherwise, you might want to use an mDNS name ("foo.local") or the "all interfaces" address, 0.0.0.0.
This change to your "Sender code" worked for me when I tried it on my network:
var osc = require("osc");
var udp = new osc.UDPPort({
localAddress: "0.0.0.0", // Totally does matter here :)
localPort: 5000,
remoteAddress: "192.168.0.7", // the other laptop
remotePort: 9999 // the port to send to
});
udp.open();
udp.on("ready", function () {
console.log("ready");
setInterval(function () {
udp.send({
address: "/sending/every/second",
args: [1, 2, 3]
})
}, 1000);
});
As a side note, osc.js's behaviour does differ from a regular Node.js UDP socket, since if the local address is omitted Node will default to 0.0.0.0. An osc.UDPPort, however, will always bind to 127.0.0.1 if localAddress is omitted (which seemed a little safer to me when originally implementing osc.js, but I can see how it might be confusing).
This issue is also discussed on the osc.js issue tracker, and I will update the documentation to prevent the kind of confusion you encountered here. Best of luck with your project!

Resources