1

I have a centralized server and DB that each of my devices polls every ten seconds. The centralized server may respond with a list of "active jobs", which the devices will then run.

I need to ensure the data sent from the server to the devices is encrypted and only each device can decrypt that.

I was thinking I would need to generate a key pair on each device at installation and send the public key to be stored on the centralized server. The server then encrypts data out using the public key, and each device decrypts that data once ingested using the private key.

Does this make sense? Which encryption scheme should I be using? My server is running Laravel (PHP) and as mentioned each device is running a NodeJS daemon.

1

This original answer was to use HTTP/SSL/TLS to encrypt the information between the PHP server and NodeJS client and to use basic authentication in the HTTPS request. However from the conversation in the comments to this answer the server and clients are already using HTTPS to secure the traffic between the server and the client.

Therefore as long as the NodeJS client trusts the SSL Certificate provided by the server, the communications between the client and server should be secure given all of the caveats of all of the answers to this Information Security question: How safe is SSL?

To be extra safe I found an example in NodeJS HTTPS API documentation that shows you how to have your HTTPS request validate that the server's SSL certificate has the correct SHA256 fingerprint and private key.

const tls = require('tls');
const https = require('https');
const crypto = require('crypto');

function sha256(s) {
  return crypto.createHash('sha256').update(s).digest('base64');
}
const options = {
  hostname: 'github.com',
  port: 443,
  path: '/',
  method: 'GET',
  checkServerIdentity: function(host, cert) {
    // Make sure the certificate is issued to the host we are connected to
    const err = tls.checkServerIdentity(host, cert);
    if (err) {
      return err;
    }

    // Pin the public key, similar to HPKP pin-sha25 pinning
    const pubkey256 = 'pL1+qb9HTMRZJmuC/bB/ZI9d302BYrrqiVuRyW+DGrU=';
    if (sha256(cert.pubkey) !== pubkey256) {
      const msg = 'Certificate verification error: ' +
        `The public key of '${cert.subject.CN}' ('${sha256(cert.pubkey)}') ` +
        `does not match our pinned public key ('${pubkey256}') `;
      return new Error(msg);
    }

    // Pin the exact certificate, rather then the pub key
    const cert256 = '25:FE:39:32:D9:63:8C:8A:FC:A1:9A:29:87:' +
      'D8:3E:4C:1D:98:DB:71:E4:1A:48:03:98:EA:22:6A:BD:8B:93:16';
    if (cert.fingerprint256 !== cert256) {
      const msg = 'Certificate verification error: ' +
      `The certificate of '${cert.subject.CN}' ('${cert.fingerprint256}') ` +
      `does not match our pinned fingerprint ('${cert256}')`;
      return new Error(msg);
    }

    // This loop is informational only.
    // Print the certificate and public key fingerprints of all certs in the
    // chain. Its common to pin the public key of the issuer on the public
    // internet, while pinning the public key of the service in sensitive
    // environments.
    do {
      console.log('Subject Common Name:', cert.subject.CN);
      console.log('  Certificate SHA256 fingerprint:', cert.fingerprint256);

      hash = crypto.createHash('sha256');
      console.log('  Public key ping-sha256:', sha256(cert.pubkey));

      lastprint256 = cert.fingerprint256;
      cert = cert.issuerCertificate;
    } while (cert.fingerprint256 !== lastprint256);

  },
};

options.agent = new https.Agent(options);
const req = https.request(options, (res) => {
  console.log('All OK. Server matched our pinned cert or public key');
  console.log('statusCode:', res.statusCode);
  // Print the HPKP values
  console.log('headers:', res.headers['public-key-pins']);

  res.on('data', (d) => {});
});

req.on('error', (e) => {
  console.error(e.message);
});
req.end();

Here's an example running this program. The validations failed because the Github SSL certificate changed. I added displaying the encountered values to the error messages to allow the script to be changed with the current values to validate testing.

pi@rpi1:~/tpf $ node check-cert.js 
Certificate verification error: The public key of 'github.com' ('o5oa5F4LbZEfeZ0kXDgmaU2K3sIPYtbQpT3EQLJZquM=') does not match our pinned private key ('pL1+qb9HTMRZJmuC/bB/ZI9d302BYrrqiVuRyW+DGrU=') 

Update script to use 'o5oa5F4LbZEfeZ0kXDgmaU2K3sIPYtbQpT3EQLJZquM=' for the private key.

pi@rpi1:~/tpf $ node check-cert.js 
Certificate verification error: The certificate of 'github.com' ('31:11:50:0C:4A:66:01:2C:DA:E3:33:EC:3F:CA:1C:9D:DE:45:C9:54:44:0E:7E:E4:13:71:6B:FF:36:63:C0:74') does not match our pinned fingerprint ('25:FE:39:32:D9:63:8C:8A:FC:A1:9A:29:87:D8:3E:4C:1D:98:DB:71:E4:1A:48:03:98:EA:22:6A:BD:8B:93:16')

Update script to use the displayed fingerprint which allows the script to continue and display all of the certificates in the chain provided.

pi@rpi1:~/tpf $ node check-cert.js 
Subject Common Name: github.com
  Certificate SHA256 fingerprint: 31:11:50:0C:4A:66:01:2C:DA:E3:33:EC:3F:CA:1C:9D:DE:45:C9:54:44:0E:7E:E4:13:71:6B:FF:36:63:C0:74
  Public key ping-sha256: o5oa5F4LbZEfeZ0kXDgmaU2K3sIPYtbQpT3EQLJZquM=
Subject Common Name: DigiCert SHA2 Extended Validation Server CA
  Certificate SHA256 fingerprint: 40:3E:06:2A:26:53:05:91:13:28:5B:AF:80:A0:D4:AE:42:2C:84:8C:9F:78:FA:D0:1F:C9:4B:C5:B8:7F:EF:1A
  Public key ping-sha256: RRM1dGqnDFsCJXBTHky16vi1obOlCgFFn/yOhI/y+ho=
Subject Common Name: DigiCert High Assurance EV Root CA
  Certificate SHA256 fingerprint: 74:31:E5:F4:C3:C1:CE:46:90:77:4F:0B:61:E0:54:40:88:3B:A9:A0:1E:D0:0B:A6:AB:D7:80:6E:D3:B1:18:CF
  Public key ping-sha256: WoiWRyIOVNa9ihaBciRSC7XHjliYS9VwUGOIud4PB18=
All OK. Server matched our pinned cert or public key
statusCode: 200
headers: undefined
  • Thanks for the reply! My centralized server is serving content over HTTPS. However any browser can hit the GET endpoint and receive the list of active jobs in plain JSON. I would like this to be encrypted and only allow a specific device to decrypt it. Does that make sense? Am I overthinking this? – Rail24 Jun 20 at 15:25
  • Just so I understand, your endpoints are running NodeJS and they do support HTTPS, but they also support HTTP? That has to be on two different TCP ports, right? SSL on one port (maybe 443) and unencrypted on a 2nd port (maybe 80). However, your real issue is that you only want authenticated clients to receive data from the endpoint. – HeatfanJohn Jun 20 at 15:49
  • Endpoints are being served with a PHP server only over HTTPS. I am running NodeJS on multiple devices that uniquely pull active jobs from the PHP server (active jobs are unique to each device). They do not "authenticate" because I cannot pass a password from each device to the PHP server in case of MITM attacks. I would like to (on install) generate a key-pair on each device, send the public key to the PHP server, and use that key to encrypt data out. Each device then uses the private key it generated originally to decrypt this data. – Rail24 Jun 20 at 15:58
  • Ok, when you say "that any browser can hit the endpoint", you really man hit your PHP server. If you're using SSL and your endpoints trust the PHP server's certificate, you really can't have a MITM attack because the MITM doesn't have your PHP server's private key. That being said you should be able to use client certificates on your endpoints to get an end-to-end secure authenticated solution. Let me search a little for info on how to use a client certificate in a NodeJS HTTPS get request. – HeatfanJohn Jun 20 at 16:11
  • So if my devices are all running NodeJS HTTPS servers, they can make a "POST" request to my PHP server with a password to authenticate themselves, and no one can intercept the password? – Rail24 Jun 20 at 16:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.