HowTo:PGP

From Predictive Chemistry
Jump to: navigation, search

Online services are the foundation of distributed computing, but security is often overlooked at the beginnings of these projects. Here, I'll explain a really simple way to lock down access to your precious protocols at the transport layer. There are essentially no changes required to the communication protocol. In addition, the method dovetails with the GnuPG suite for message signature and encryption.

Get Started with GnuPG

Why?

GnuPG uses a chain of public-key certificates, similar to the x.509 certificates that we're all familiar with our web-browsers constantly complaining about. Each certificate has some private information (that the owner keeps) and some identifying public information that is distributed like a user-id, name and birthdate, or the like. The difference with those other numbers is that revealing the public key doesn't compromise your security, like a password social security number would. Instead, the public key is used to check that a communication could only have come from the person knowing the private key.

Things get complicated when more than two people get involved. How do I know a person's public key is trusted to do something? PGP does this by having one key sign another key. If I want my webserver to recognize a set of users, I can create a server private key and use it to sign the public keys of trusted users. Then I have a chain of signatures that validate the user's public key. When the user connects, I use that public key to make sure the user knows the corresponding private key.

Secure internet servers use x.509 certificates in a similar way to prove that you can trust them. Your browser has a list of trusted root certificates from places like Verisign and Thawte. When you connect to a random server using HTTPS, the server gives you a public key. Your browser then checks whether that public key has been signed by one of the trusted root certificates it knows about.

Although similar to x.509 server certificates, PGP doesn't have a centrally based set of trusted root certificate authorities. In fact, anyone can sign for anyone else in the PGP scheme. This makes it much more user-centric. I can, for example, sign source code packages I produce and people who have my public key can verify that they haven't been tampered with. I could produce a hierarchy of keys to sign things like code, purchase orders, or server access. I could use these to delegate those responsibilities. At the same time, I could get those keys signed by other people (like the manager of a coding project, an accountant, or a server admin).

Install

Use a package-manager to look for a gnupg or equivalent. <source lang="bash"> apt-get install gnupg # Trisquel / Debian / Mint / Ubuntu fink install gnupg # OSX + Fink yum install gnupg # RHEL / Centos / Scientific Linux / Fedora </source>

Create a Keyring

GnuPG does almost everything through options to the gpg command. Following along with,[1] <source lang="bash"> gpg --gen-key </source>

gpg (GnuPG) 1.4.13; Copyright (C) 2012 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Please select what kind of key you want:
   (1) RSA and RSA (default)
   (2) DSA and Elgamal
   (3) DSA (sign only)
   (4) RSA (sign only)
Your selection? 1
RSA keys may be between 1024 and 4096 bits long.
What keysize do you want? (2048) 
Requested keysize is 2048 bits
Please specify how long the key should be valid.
         0 = key does not expire
      <n>  = key expires in n days
      <n>w = key expires in n weeks
      <n>m = key expires in n months
      <n>y = key expires in n years
Key is valid for? (0) 
Key does not expire at all
Is this correct? (y/N) y

You need a user ID to identify your key; the software constructs the user ID
from the Real Name, Comment and Email Address in this form:
    "Heinrich Heine (Der Dichter) <heinrichhduesseldorf.de>"

Real name: David M. Rogers
Email address: predictivestatmechgmail.com
Comment: 
You selected this USER-ID:
    "David M. Rogers <predictivestatmechgmail.com>"

Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? o
You need a Passphrase to protect your secret key.

We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
.
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
.
gpg: ~/.gnupg/trustdb.gpg: trustdb created
gpg: key 262B259C marked as ultimately trusted
public and secret key created and signed.

gpg: checking the trustdb
gpg: 3 marginal(s) needed, 1 complete(s) needed, PGP trust model
gpg: depth: 0  valid:   1  signed:   0  trust: 0-, 0q, 0n, 0m, 0f, 1u
pub   2048R/262B259C 2013-12-20
      Key fingerprint = D1CE 9C6A 5099 6753 8FC4  81F5 5B64 9362 262B 259C
uid                  David M. Rogers <predictivestatmechgmail.com>
sub   2048R/E494F149 2013-12-20

Next, you'll want to double-check that it worked and make sure gnupg's directory has no read or write permissions for anyone other than yourself. <source lang="bash"> gpg --list-keys ls -ld ~/.gnupg </source>

For the client/server example, we'll also want to write the public key to a separate file. <source lang="bash"> gpg --export 262B259C --ascii >~/.gnupg/predictivestatmech.asc </source>

Next, we'll generate a certificate belonging to my laptop. I'll choose to use this one for allowing users to connect to a new protocol I'm developing. It can't have a password according the GNUTLS manual. <source lang="bash"> gpg --gen-key </source>

gpg (GnuPG) 1.4.13; Copyright (C) 2012 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Please select what kind of key you want:
   (1) RSA and RSA (default)
   (2) DSA and Elgamal
   (3) DSA (sign only)
   (4) RSA (sign only)
Your selection? 4
RSA keys may be between 1024 and 4096 bits long.
What keysize do you want? (2048) 
Requested keysize is 2048 bits
Please specify how long the key should be valid.
         0 = key does not expire
      <n>  = key expires in n days
      <n>w = key expires in n weeks
      <n>m = key expires in n months
      <n>y = key expires in n years
Key is valid for? (0) 3y
Key expires at Mon Dec 19 15:04:37 2016 EST
Is this correct? (y/N) y

You need a user ID to identify your key; the software constructs the user ID
from the Real Name, Comment and Email Address in this form:
    "Heinrich Heine (Der Dichter) <heinrichhduesseldorf.de>"

Real name: kubotan
Email address: davidrogersusf.edu
Comment: MBPR Laptop
You selected this USER-ID:
    "kubotan (MBPR Laptop) <davidrogersusf.edu>"

Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? o
You need a Passphrase to protect your secret key.

You don't want a passphrase - this is probably a *bad* idea!
I will do it anyway.  You can change your passphrase at any time,
using this program with the option "--edit-key".

We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
.
gpg: key 03357392 marked as ultimately trusted
public and secret key created and signed.

gpg: checking the trustdb
gpg: 3 marginal(s) needed, 1 complete(s) needed, PGP trust model
gpg: depth: 0  valid:   2  signed:   0  trust: 0-, 0q, 0n, 0m, 0f, 2u
gpg: next trustdb check due at 2016-12-19
pub   2048R/03357392 2013-12-20 [expires: 2016-12-19]
      Key fingerprint = C417 3765 AB3A 139B 72C3  F093 D35C FD07 0335 7392
uid                  kubotan (MBPR Laptop) <davidrogersusf.edu>

Note that this key cannot be used for encryption.  You may want to use
the command "--edit-key" to generate a subkey for this purpose.

Following the suggestion, I'll sign this one with my user key. <source lang="bash"> gpg --edit-key kubotan </source>

gpg (GnuPG) 1.4.13; Copyright (C) 2012 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Secret key is available.

pub  2048R/03357392  created: 2013-12-20  expires: 2016-12-19  usage: SC  
                     trust: ultimate      validity: ultimate
[ultimate] (1). kubotan (MBPR Laptop) <davidrogersusf.edu>

gpg> sign

pub  2048R/03357392  created: 2013-12-20  expires: 2016-12-19  usage: SC  
                     trust: ultimate      validity: ultimate
 Primary key fingerprint: C417 3765 AB3A 139B 72C3  F093 D35C FD07 0335 7392

     kubotan (MBPR Laptop) <davidrogersusf.edu>

This key is due to expire on 2016-12-19.
Are you sure that you want to sign this key with your
key "David M. Rogers <predictivestatmechgmail.com>" (262B259C)

Really sign? (y/N) y

You need a passphrase to unlock the secret key for
user: "David M. Rogers <predictivestatmechgmail.com>"
2048-bit RSA key, ID 262B259C, created 2013-12-20

                  
gpg> quit
Save changes? (y/N) y

My signature belongs to kubotan's key now, so GPG saves it there. <source lang="bash"> gpg --list-sigs </source>

gpg: checking the trustdb
gpg: 3 marginal(s) needed, 1 complete(s) needed, PGP trust model
gpg: depth: 0  valid:   2  signed:   0  trust: 0-, 0q, 0n, 0m, 0f, 2u
gpg: next trustdb check due at 2016-12-19
/Users/rogers/.gnupg/pubring.gpg
--------------------------------
pub   2048R/262B259C 2013-12-20
uid                  David M. Rogers <predictivestatmechgmail.com>
sig 3        262B259C 2013-12-20  David M. Rogers <predictivestatmechgmail.com>
sub   2048R/E494F149 2013-12-20
sig          262B259C 2013-12-20  David M. Rogers <predictivestatmechgmail.com>

pub   2048R/03357392 2013-12-20 [expires: 2016-12-19]
uid                  kubotan (MBPR Laptop) <davidrogersusf.edu>
sig 3        03357392 2013-12-20  kubotan (MBPR Laptop) <davidrogersusf.edu>
sig          262B259C 2013-12-20  David M. Rogers <predictivestatmechgmail.com>

I need both its public and private sections to use in the server code, so I'll extract them, and mark them self-read-only. <source lang="bash"> gpg --armor --export "kubotan" >~/.gnupg/kubotan.asc gpg --armor --export-secret-key "kubotan" >~/.gnupg/kubotan-secret.asc chmod 400 ~/.gnupg/*.asc </source>

These keys are all you need to sign or encrypt messages as well. To see the usage for those, check the chapters in the GPG Mini-Howto.

Server

Next, let's get started with some client/server code. TLS stands for transport-layer-security. Normal communications (e.g. http/telnet/ftp) merrily go about sending messages (commands, files, output, etc.) over TCP. TLS-enabled communications (e.g. HTTPS/SSH/SFTP) start a connection by figuring out how to encrypt the session. They can also check signatures to authenticate the user and the server. After that, all communications are encrypted to prevent data leaking to the local packet sniffing red-team.

Although incorporating a 'STARTTLS' message into your protocol has been recommended if you need to talk before authenticating, the lazy way is to just authenticate and start TLS at connection. The GNU TLS library makes this especially easy by handling certificates, doing the initial TLS handshake, and encrypting/decrypting messages passed through gnutls_record_send / gnutls_record_recv.


Installation

The GNUTLS library is under heavy development, and I found that the examples in the manual[2] would not run with the package manager's version 2.2. So, I installed nettle 2.7.1 (--enable-shared) and gnutls 3.2.7[3] from source. Apparently someone's been using strerror in gnutls, and someone else has been replacing strerror with repl_strerror to teach them a lesson - so we have to deal with the collateral damage. <source lang="bash">

 sed -i.bak -e 's|strerror([^)]*)|""|g' lib/nettle/egd.c
 sed -i.bak -e 's|strerror([^)]*)|""|g' lib/nettle/rnd.c

</source>

Usage

The server code for OpenPGP authentication and client code for the very similar x.509 authentication are available.

But the best starting place for hacking together a client/server pair authenticating using OpenPGP are the generic implementations in gnutls-3.2.7/src/serv.c and gnutls-3.2.7/src/cli.c. Those commands are bundled with gnutls and are easy to test out with <source lang="bash"> gnutls-serv \

   -p 5556 \
   --priority NORMAL:+CTYPE-OPENPGP \
   --pgpkeyring=$HOME/.gnupg/pubring.gpg \
   --pgpcertfile $HOME/.gnupg/kubotan.asc \
   --pgpkeyfile $HOME/.gnupg/kubotan-secret.asc &

gnutls-cli \

   -p 5556 \
   --priority NORMAL:+CTYPE-OPENPGP \
   --pgpkeyring=$HOME/.gnupg/pubring.gpg \
   --no-ca-verification \
   localhost

</source>

You will get a segfault if you read an ascii key (.asc) in binary mode or vice-versa.

Server Code

I've added the following code to interface with libixp. It contains all the necessary bits to put the libraries together, including keyring initialization, session creation, and certificate validation. <source lang="C"> /* Extra goodies required for using GNUTLS

*  Copyright (C) David M. Rogers
*  This file is released under the GNU LGPLv3
*/
  1. include <stdio.h>
  2. include <stdlib.h>
  3. include <unistd.h>
  4. include <sys/socket.h>
  5. include "ixp_local.h"

char *get_peer_name(gnutls_session_t); gnutls_session_t ixp_init_session(gnutls_certificate_credentials_t cred,

       unsigned int flags);

static char *get_crt_name(gnutls_openpgp_crt_t crt, int idx); static int generate_dh_params(); int ixp_init_dh(void); static int cert_verify_callback(gnutls_session_t session);

/* When the system mutexes are (not) to be used

* gnutls_global_set_mutex() must be called explicitly:
***  gnutls_global_set_mutex(mutex_init, mutex_deinit, 
***                          mutex_lock, mutex_unlock);
*/

ssize_t mbtls_read(IxpConn *c, void *buf, size_t nbyte) {

   if(c->session) {
       return gnutls_record_recv(c->session, buf, nbyte);
   }
   return read(c->fd, buf, nbyte);

}

ssize_t mbtls_write(IxpConn *c, const void *buf, size_t nbyte) {

   if(c->session) {
       return gnutls_record_send(c->session, buf, nbyte);
   }
   return write(c->fd, buf, nbyte);

}

/* These are global */ struct tlsinfo_s libixp_tlsinfo = {NULL, NULL, NULL, NULL, 0, 0};

int ixp_set_keyring(char *ringfile) {

   int ret;
   if( (ret = gnutls_certificate_set_openpgp_keyring_file(libixp_tlsinfo.cred,
                   ringfile, GNUTLS_OPENPGP_FMT_RAW))) {
       fprintf(stderr, "Error setting keyring from file '%s': %s\n",
               ringfile, gnutls_strerror(ret));
       return 1;
   }
   libixp_tlsinfo.keyring = 1;
   return 0;

}

static int generate_dh_params() {

       unsigned int bits = gnutls_sec_param_to_pk_bits(GNUTLS_PK_DH,
                                                       GNUTLS_SEC_PARAM_NORMAL);
       /* Generate Diffie-Hellman parameters - for use with DHE
        * kx algorithms. These should be discarded and regenerated
        * once a day, once a week or once a month. Depending on the
        * security requirements.
        */
       if(gnutls_dh_params_init(&libixp_tlsinfo.dh_params) < 0) {
           perror("Error in dh parameter initialization");
           return 1;
       }
       if(gnutls_dh_params_generate2(libixp_tlsinfo.dh_params, bits) < 0) {
           perror("Error in prime generation");
           return 1;
       }
       return 0;

}

int ixp_init_dh() {

       if(generate_dh_params()) return 1;
       gnutls_certificate_set_dh_params(libixp_tlsinfo.cred,
                                        libixp_tlsinfo.dh_params);
       return 0;

}

/* Set the certificate and keyfile required for ixp credentials.

*/

int ixp_set_credentials(char *certfile, char *keyfile) {

   int ret;
   gnutls_certificate_credentials_t cred;
   if(!libixp_tlsinfo.global) {
       gnutls_global_init();
       libixp_tlsinfo.global = 1;
   }
   if(gnutls_certificate_allocate_credentials(&cred)) {
       return 1;
   }
   
   if( (ret = gnutls_certificate_set_openpgp_key_file(cred,
                   certfile, keyfile, GNUTLS_OPENPGP_FMT_BASE64)) < 0) {
       const char *err = gnutls_strerror(ret);
       fprintf(stderr, "Error decoding privkey: %s\n", err);
       gnutls_certificate_free_credentials(cred);
       return 1;
   }
   gnutls_certificate_set_verify_function(cred,
                                          cert_verify_callback);
   if(libixp_tlsinfo.cred) {
       // TODO: check that current cred is not in use / refcount
       gnutls_certificate_free_credentials(libixp_tlsinfo.cred);
   }
   libixp_tlsinfo.cred = cred;
   return 0;

}

/* Flags should be set to either GNUTLS_CLIENT or GNUTLS_SERVER.

*/

gnutls_session_t ixp_init_session(gnutls_certificate_credentials_t cred,

                       unsigned int flags) {

gnutls_session_t session; int ret;

       const static char priorities[] = "NORMAL:+CTYPE-OPENPGP";

const char *err;

       gnutls_init(&session, flags);

if(gnutls_priority_set_direct(session, priorities, &err) < 0) { fprintf(stderr, "Syntax error at: %s\n", err); exit(1); }

if( (ret = gnutls_credentials_set(session,

                                         GNUTLS_CRD_CERTIFICATE, cred))) {
           fprintf(stderr, "gnutls_credentials_set: %s\n",
                   gnutls_strerror(ret));
           return NULL;
       }

gnutls_certificate_server_set_request(session, GNUTLS_CERT_REQUIRE);

/*if (HAVE_OPT(HEARTBEAT)) gnutls_heartbeat_enable(session, GNUTLS_HB_PEER_ALLOWED_TO_SEND);

       */

return session; }

static char *get_crt_name(gnutls_openpgp_crt_t crt, int idx) {

   char *name = NULL, *tmp;
   int ret;
   size_t sz = 64;
   if( (name = malloc(sz)) == NULL) {
       return NULL;
   }
   if( (ret = gnutls_openpgp_crt_get_name(crt, idx, name, &sz))
               == GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE) {
       free(name);
       return NULL;
   }
   if(!ret) return name;
   if( (tmp = realloc(name, sz+1)) == NULL) {
       free(name);
       return NULL;
   }
   name = tmp;
   sz = sz+1 > 64 ? sz+1 : 64;
   if(gnutls_openpgp_crt_get_name(crt, idx, name, &sz) ) {
       free(name);
       return NULL;
   }
   return name;

}

char *get_peer_name(gnutls_session_t session) {

       int ret;
       unsigned int list_size;
       const gnutls_datum_t *cert_list;
       gnutls_openpgp_crt_t crt;
       char *name;
       // get the peer's certificate
       cert_list = gnutls_certificate_get_peers(session, &list_size);
       if (cert_list == NULL || list_size < 1) {
               fprintf(stderr, "Cannot obtain peer's certificate!\n");
               return NULL;
       }
       gnutls_openpgp_crt_init(&crt);
       if( (ret = gnutls_openpgp_crt_import(crt, &cert_list[0],
                                       GNUTLS_OPENPGP_FMT_RAW) < 0)) {
           fprintf(stderr, "Decoding error: %s\n", gnutls_strerror(ret));
           gnutls_openpgp_crt_deinit(crt);
           return NULL;
       }
       name = get_crt_name(crt, 0);
       gnutls_openpgp_crt_deinit(crt);
       return name;

}

/* This function will verify the peer's certificate, and check

* if the hostname matches, as well as the activation, expiration dates.
*/

static int cert_verify_callback(gnutls_session_t session) {

       unsigned int status;
       int ret, type;
       unsigned int list_size;
       const gnutls_datum_t *cert_list;
       gnutls_openpgp_crt_t crt;
       gnutls_datum_t out;
       // verify session type
       type = gnutls_certificate_type_get(session);
       if(type != GNUTLS_CRT_OPENPGP) {
           fprintf(stderr, "Warning: Session is not using PGP "
                           "- no verification done!\n");
           return 0;
       }
       // get the peer's certificate
       cert_list = gnutls_certificate_get_peers(session, &list_size);
       if (cert_list == NULL || list_size < 1) {
               fprintf(stderr, "Cannot obtain peer's certificate!\n");
               return GNUTLS_E_CERTIFICATE_ERROR;
       }
       gnutls_openpgp_crt_init(&crt);
       if( (ret = gnutls_openpgp_crt_import(crt, &cert_list[0],
                                       GNUTLS_OPENPGP_FMT_RAW) < 0)) {
           fprintf(stderr, "Decoding error: %s\n", gnutls_strerror(ret));
           gnutls_openpgp_crt_deinit(crt);
           return GNUTLS_E_CERTIFICATE_ERROR;
       }
       if(libixp_tlsinfo.keyring) {
           if( (ret = gnutls_certificate_verify_peers3(session, NULL, &status)) < 0) {
               fprintf(stderr, "Error validating peer crt: %s\n",
                       gnutls_strerror(ret));
               goto err;
           }
       } else if( (ret = gnutls_openpgp_crt_verify_self(crt, 0, &status))) {
           fprintf(stderr, "Error validating peer self-sign: %s\n",
                   gnutls_strerror(ret));
           goto err;
       }
       if (status != 0) {      /* Certificate is not trusted */
           if( (ret = gnutls_certificate_verification_status_print(status,
                                                       type, &out, 0)) < 0) {
               fprintf(stderr, "Error printing verification status: %s\n",
                                gnutls_strerror(ret));
           } else {
               printf("%s\n", out.data);
               gnutls_free(out.data);
           }
           goto err;
       }
       gnutls_openpgp_crt_deinit(crt);
       return 0;

err:

       gnutls_openpgp_crt_deinit(crt);
       return GNUTLS_E_CERTIFICATE_ERROR;

}

void tls_hangup(IxpConn *c) {

   if(c->session)
       gnutls_bye(c->session, GNUTLS_SHUT_RDWR);
   else
       shutdown(c->fd, SHUT_RDWR);

} </source> On startup, the client and server should both make calls to

ixp_set_credentials(char *certfile, char *keyfile)

which takes the file paths for the ascii-encoded secret key and the binary-encoded keyring. Just after accepting a socket connection, both client and server call

 ixp_init_session(gnutls_certificate_credentials_t cred, unsigned int flags)

to create a session object. Note that the server has to have previously called ixp_init_dh() and set flags to GNUTLS_SERVER. The client just has to set flags to GNUTLS_CLIENT.

Peer validation happens during ixp_init_session. The validation is performed by cert_verify_callback, which our code tells gnutls about way back when ixp_set_credentials was called. We have cert_verify_callback setup to test whether the peer (client of the server or server of the client) is in the keyring. If no keyring was loaded, we just check that the peer knows how to sign their own name. Note that this is just a membership test. In this function, we only have the peer's public key. The actual cryptographic validation happened before cert_verify_callback got involved, by checking that the peer possesses the corresponding private key. There's also a nicety function

char *get_peer_name(gnutls_session_t)

that either client/server can call to get the name listed on the peer's (server/client) certificate. Once the gnutls session is setup, it acts like the file descriptor, so I had to replace the fd with a struct holding the session object in ixp. You can see that the read and write calls are now mbtls_read and mbtls_write. The connection is closed cleanly with tls_hangup.

There it is, the client and server code in one neat step. Hopefully this makes adding transport layer security to your protocol that much easier. Please let me know if it does!