1

Goal: The client should send data to trusted server (through self-signed certificates) and the server the same.

I’m running a Node.js TLS server and have many embedded clients which run openSSL TLS client in C. I have the entire setup working and I can see the data is encrypted (through Wireshark) but I’m not convinced I’m doing it the right way (especially the way I'm handling certificates).

What I have so far

  1. On the Server, I generated a 2048 private key (private-key.pem) and a self-signed certificate (public-cert.pem)
  2. Copied over the self-signed certificate (public-cert.pem) to the client

Node.js server code snippet

var tls = require('tls');
var fs = require('fs');

var options = {
    key: fs.readFileSync('private-key.pem'),
    cert: fs.readFileSync('public-cert.pem')
};
tls.createServer(options, function (socket) {

socket.on('data', function(data) {
    // do something
});

socket.on('close', function() {
    socket.destroy();
});

}).listen(PORT);

C Client code snippet

     SSL_library_init(); 
     SSL_load_error_strings();
     ERR_load_BIO_strings();
     OpenSSL_add_all_algorithms();

     // create new context object
     ctx = SSL_CTX_new(TLSv1_2_client_method());

    //load trust cert

    if(! SSL_CTX_load_verify_locations(ctx, "public-cert.pem", NULL)){

          printf("sslInitialize() Error: Cannot load certificate\n");
          ERR_print_errors_fp(stderr);

          if(ctx != NULL) {

            SSL_CTX_free(ctx);

          }


          return;
    } 

    bio = BIO_new_ssl_connect(ctx);

    BIO_get_ssl(bio, & ssl);


    if (ssl == NULL) {

         printf("sslInitialize() Error: Can't locate SSL pointer\n");
         ERR_print_errors_fp(stderr);

         if(ctx != NULL){

            SSL_CTX_free(ctx);

        } 


        if(bio != NULL){

         BIO_free_all(bio);

        }
         return;
    }

    BIO_set_conn_hostname(bio, HOST);

   int connectStatus;                            

   if((connectStatus = BIO_do_connect(bio)) <= 0) {

    printf("Connect Status: %d",connectStatus);
     printf("sslInitialize() Error: Cannot connect to server\n");
     ERR_print_errors_fp(stderr);

    sslCloseConnection();

     return;
   }

Observations

  1. I changed the certificate on the client to same random certificate and it still works (the certificate the server sent during ServerHello TLS handhake and the client loaded using SSL_CTX_load_verify_locations() were different). The makes me wonder as to how the client is trusting the certificate. How can I sort this out?
  2. As of now I've setup it up in such a way that the client verifies the server (although it isn't working) and sends data and the server blindly accepts it. How can I make the client send a certificate (self-signed by client) and the server only accepts it if it has that particular certificate on file.
  3. On the client-end I use SSL_CTX_load_verify_locations(ctx, cert, NULL). The documentation says

specifies the locations for ctx, at which CA certificates for verification purposes are located. The certificates available via CAfile and CApath are trusted.

Does this mean the client checks this against the certificate received in ServerHello message of the TLS handshake? If so, is there another function I should call to do this check?

  1. As always, I've gone through a lot of resources online (SO posts and openSSL man page) before asking this question. Node.js_TLS,openSSL,SO_1, SO_2 to name a few. I've also omitted header files and other boiler-plate code.

Any help will be appreciated. Thanks!

Community
  • 1
  • 1
am3
  • 611
  • 1
  • 11
  • 28
  • 1
    Have a look at [TLS Client](http://wiki.openssl.org/index.php/SSL/TLS_Client) on the OpenSSL wiki. It has information you may find useful. – jww Jul 24 '16 at 06:25
  • Thanks! This was helpful. – am3 Jul 24 '16 at 21:44

2 Answers2

2

For the C client, you may want to ensure that the client is verifying the server certificate by calling OpenSSL's SSL_CTX_set_verify() function explicitly:

SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);

before calling BIO_new_ssl_connect(). Note that this may not be necessary.

And yes, the certificates you set via SSL_CTX_load_verify_locations() are the ones used for verifying the server certificate (which is part of the ServerHello). The checking of the server certificate against those verification certificates happens as part of the default OpenSSL verification callback.

Per the Nodejs tls createServer docs, you might need to provide a couple more options for your TLS server, specially the ca array and the requestCert and rejectUnauthorized booleans:

var options = {
    key: fs.readFileSync('private-key.pem'),
    cert: fs.readFileSync('public-cert.pem'),
    ca: [ fs.readFileSync('ca-cert.pem') ],
    requestCert: true,
    rejectUnauthorized: true
};
tls.createServer(options, function (socket) {

The requestCert boolean instructs the server to request the client's certificate. In TLS, the server has to explicitly request a cert from the client; the client does not supply one unless asked by the server. And without setting rejectUnauthorized to true, your TLS server would request the cert, but ignore any validation error and allow the connection to proceed, which is not desirable. And the ca array configures the list of verification certificates (similar to OpenSSL's SSL_CTX_load_verify_locations() that Nodejs will use for verifying the client certificate).

Ideally, rather than using a self-signed certificate for the server, and a separate self-signed certificate for the client, you would have three certificates: a CA certificate (which is self-signed by definition), a server certificate (which was issued/signed by that CA), and a separate client certificate (also issued/signed by the CA). Since you control both the clients and the server, there is no particular need for you to buy these certificates from a public CA; those are needed for browsers (since the public CA's certificates are in the browsers' trust stores), but that doesn't sound like it's needed for your use case (and thus you can save some money by generating/using your own CA). This site provides example commands for doing just this.

This way, your server would have its certificate (and private key), and the CA cert; your client would have its certificate (and private key), and the CA cert. The CA cert, then, would be the certificate configured in the SSL_CTX_load_verify_locations() paths/files, and in the ca option for tls.createServer.

As a bonus, you might consider adding support TLS session caching, as described here.

Hope this helps!

Castaglia
  • 2,872
  • 5
  • 27
  • 48
  • Couple of pointers 1) Correct me if I'm wrong, I don't think I need to call `SSL_CTX_set_verify()` explicitly. It looks like OpenSSL verifies but I need to reject the connection based on the result. `SSL_get_verify_result` returns the result of the verification. 2) In your Node.js snippet, I think the `ca` should be the certificate from the client and not the one from the server re-sent. This means the certificate has to generate its own self-signed certificate and the server should have a copy of it. – am3 Jul 24 '16 at 21:38
  • The `ca` on the server side should be any of the certs needed for verifying the client's certificate. In your case, your client certificate being self-signed means that a _good_ server **will not** verify that client -- it's tantamount to trusting whatever the client says (which is not a good idea). So _assuming_ that you'd use that same self-signed cert to issue a cert for your client, then that self-signed cert _would_ be the `ca` to use on the server end. – Castaglia Jul 24 '16 at 21:46
  • I think if client uses a self-signed certificate, a copy of that certificate should be on the server. The server then validates the certificate sent by the client against the certificate it already has. Code snippet from Node.js TLS documentation `const options = { key: fs.readFileSync('server-key.pem'), cert: fs.readFileSync('server-cert.pem'), // This is necessary only if using the client certificate authentication. requestCert: true, // This is necessary only if the client uses the self-signed certificate. ca: [ fs.readFileSync('client-cert.pem') ] };` – am3 Jul 24 '16 at 21:50
  • I'd **strongly recommend** that you **not** use self-signed certificates for either the client or the server certificates. That's a very uncommon (and not secure, in the sense of trust) configuration, and not representative of the best practices for these sorts of things. So trying to make self-signed certs work this way is not in line with your stated goal of doing this correctly and properly. – Castaglia Jul 24 '16 at 22:01
  • Thanks. I realize the security concerns. I'm working at TCP level with communication between machines (M2M/IoT communication). Having CA signed certificates for each client will become expensive. – am3 Jul 24 '16 at 23:10
  • Since you control the client and the server certs, your self-signed cert can act as a CA; you don't necessarily need to purchase certs from a public CA. So I'm not sure I understand where the expense part comes into play, with what you're currently doing. – Castaglia Jul 24 '16 at 23:15
  • Guess I didn't understand your comment where you recommended against using self-signed certificates for client/server. This is what I have in mind. 1) server generates a self-signed certificate and this is distributed to all the clients in a secure way. This is used by the client to verify the server. 2) Each client generates a self-signed certificate and a copy of this is distributed to the server in a secure way. This is used by the server to verify each client. Is this a good practice? – am3 Jul 24 '16 at 23:19
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/118168/discussion-between-castaglia-and-agm). – Castaglia Jul 25 '16 at 00:57
1

I'm answering question 1) under the Observations section. It looks like

 SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);

is not necessary. You can check the result of

SSL_get_verify_result(const SSL *ssl);

and reject the connection based on this. It returns a number of codes which can be found here. 0 is a successful verification.

Code snippet would look like this

BIO_set_conn_hostname(bio, HOST);
if(BIO_do_connect(bio) <= 0) {
    ERR_print_errors_fp(stderr);
    BIO_free_all(bio);
    SSL_CTX_free(ctx);
    return;
}
switch(SSL_get_verify_result(ssl)){


            case 0:
                    printf("X509_V_OK\n");
                    break;


            case 2: 
                    printf("X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT\n");
                    //write code to close connection
                    break;

               :
               :
}
am3
  • 611
  • 1
  • 11
  • 28