hammer2 - further crypto cleanup
[dragonfly.git] / sbin / hammer2 / crypto.c
index 947a0cf..fd5ac6f 100644 (file)
@@ -4,6 +4,7 @@
  * This code is derived from software contributed to The DragonFly Project
  * by Matthew Dillon <dillon@dragonflybsd.org>
  * by Venkatesh Srinivas <vsrinivas@dragonflybsd.org>
+ * by Alex Hornung <alexh@dragonflybsd.org>
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions
  */
 
 #include "hammer2.h"
+#include <sys/endian.h>
 
-#include <openssl/rsa.h>
-#include <openssl/pem.h>
-#include <openssl/err.h>
+/*
+ * Setup crypto for pthreads
+ */
+static pthread_mutex_t *crypto_locks;
+int crypto_count;
+
+static int hammer2_crypto_gcm_init(hammer2_ioq_t *, char *, int, char *, int, int);
+static int hammer2_crypto_gcm_encrypt_chunk(hammer2_ioq_t *, char *, char *, int, int *);
+static int hammer2_crypto_gcm_decrypt_chunk(hammer2_ioq_t *, char *, char *, int, int *);
+
+/*
+ * NOTE: the order of this table needs to match the HAMMER2_CRYPTO_ALGO_*_IDX
+ *       defines in network.h.
+ */
+static struct crypto_algo crypto_algos[] = {
+       {
+               .name      = "aes-256-gcm",
+               .keylen    = HAMMER2_CRYPTO_GCM_KEY_SIZE,
+               .taglen    = HAMMER2_CRYPTO_GCM_TAG_SIZE,
+               .init      = hammer2_crypto_gcm_init,
+               .enc_chunk = hammer2_crypto_gcm_encrypt_chunk,
+               .dec_chunk = hammer2_crypto_gcm_decrypt_chunk
+       },
+       { NULL, 0, 0, NULL, NULL, NULL }
+};
+
+static
+unsigned long
+hammer2_crypto_id_callback(void)
+{
+       return ((unsigned long)(uintptr_t)pthread_self());
+}
+
+static
+void
+hammer2_crypto_locking_callback(int mode, int type,
+                               const char *file __unused, int line __unused)
+{
+       assert(type >= 0 && type < crypto_count);
+       if (mode & CRYPTO_LOCK) {
+               pthread_mutex_lock(&crypto_locks[type]);
+       } else {
+               pthread_mutex_unlock(&crypto_locks[type]);
+       }
+}
+
+void
+hammer2_crypto_setup(void)
+{
+       crypto_count = CRYPTO_num_locks();
+       crypto_locks = calloc(crypto_count, sizeof(crypto_locks[0]));
+       CRYPTO_set_id_callback(hammer2_crypto_id_callback);
+       CRYPTO_set_locking_callback(hammer2_crypto_locking_callback);
+}
+
+static
+int
+hammer2_crypto_gcm_init(hammer2_ioq_t *ioq, char *key, int klen,
+                       char *iv_fixed, int ivlen, int enc)
+{
+       int i, ok;
+
+       if (klen < HAMMER2_CRYPTO_GCM_KEY_SIZE ||
+           ivlen < HAMMER2_CRYPTO_GCM_IV_FIXED_SIZE) {
+               if (DebugOpt)
+                       fprintf(stderr, "Not enough key or iv material\n");
+               return -1;
+       }
+
+       printf("%s key: ", enc ? "Encryption" : "Decryption");
+       for (i = 0; i < HAMMER2_CRYPTO_GCM_KEY_SIZE; ++i)
+               printf("%02x", (unsigned char)key[i]);
+       printf("\n");
+
+       printf("%s iv:  ", enc ? "Encryption" : "Decryption");
+       for (i = 0; i < HAMMER2_CRYPTO_GCM_IV_FIXED_SIZE; ++i)
+               printf("%02x", (unsigned char)iv_fixed[i]);
+       printf(" (fixed part only)\n");
+
+       EVP_CIPHER_CTX_init(&ioq->ctx);
+
+       if (enc)
+               ok = EVP_EncryptInit_ex(&ioq->ctx, EVP_aes_256_gcm(), NULL,
+                                       key, NULL);
+       else
+               ok = EVP_DecryptInit_ex(&ioq->ctx, EVP_aes_256_gcm(), NULL,
+                                       key, NULL);
+       if (!ok)
+               goto fail;
+
+       /*
+        * According to the original Galois/Counter Mode of Operation (GCM)
+        * proposal, only IVs that are exactly 96 bits get used without any
+        * further processing. Other IV sizes cause the GHASH() operation
+        * to be applied to the IV, which is more costly.
+        *
+        * The NIST SP 800-38D also recommends using a 96 bit IV for the same
+        * reasons. We actually follow the deterministic construction
+        * recommended in NIST SP 800-38D with a 64 bit invocation field as an
+        * integer counter and a random, session-specific fixed field.
+        *
+        * This means that we can essentially use the same session key and
+        * IV fixed field for up to 2^64 invocations of the authenticated
+        * encryption or decryption.
+        *
+        * With a chunk size of 64 bytes, this adds up to 1 zettabyte of
+        * traffic.
+        */
+       ok = EVP_CIPHER_CTX_ctrl(&ioq->ctx, EVP_CTRL_GCM_SET_IVLEN,
+                                HAMMER2_CRYPTO_GCM_IV_SIZE, NULL);
+       if (!ok)
+               goto fail;
+
+       memset(ioq->iv, 0, HAMMER2_CRYPTO_GCM_IV_SIZE);
+       memcpy(ioq->iv, iv_fixed, HAMMER2_CRYPTO_GCM_IV_FIXED_SIZE);
+
+       /*
+        * Strictly speaking, padding is irrelevant with a counter mode
+        * encryption.
+        *
+        * However, setting padding to 0, even if using a counter mode such
+        * as GCM, will cause an error in _finish if the pt/ct size is not
+        * a multiple of the cipher block size.
+        */
+       EVP_CIPHER_CTX_set_padding(&ioq->ctx, 0);
+
+       return 0;
+
+fail:
+       if (DebugOpt)
+               fprintf(stderr, "Error during _gcm_init\n");
+       return -1;
+}
+
+static
+int
+_gcm_iv_increment(char *iv)
+{
+       /*
+        * Deterministic construction according to NIST SP 800-38D, with
+        * 64 bit invocation field as integer counter.
+        *
+        * In other words, our 96 bit IV consists of a 32 bit fixed field
+        * unique to the session and a 64 bit integer counter.
+        */
+
+       uint64_t *c = (uint64_t *)(&iv[HAMMER2_CRYPTO_GCM_IV_FIXED_SIZE]);
+
+       /* Increment invocation field integer counter */
+       *c = htobe64(be64toh(*c)+1);
+
+       /*
+        * Detect wrap-around, which means it is time to renegotiate
+        * the session to get a new key and/or fixed field.
+        */
+       return (c == 0) ? 0 : 1;
+}
+
+static
+int
+hammer2_crypto_gcm_encrypt_chunk(hammer2_ioq_t *ioq, char *ct, char *pt,
+                                int in_size, int *out_size)
+{
+       int ok;
+       int u_len, f_len;
+
+       *out_size = 0;
+
+       /* Re-initialize with new IV (but without redoing the key schedule) */
+       ok = EVP_EncryptInit_ex(&ioq->ctx, NULL, NULL, NULL, ioq->iv);
+       if (!ok)
+               goto fail;
+
+       ok = EVP_EncryptUpdate(&ioq->ctx, ct, &u_len, pt, in_size);
+       if (!ok)
+               goto fail;
+
+       ok = EVP_EncryptFinal(&ioq->ctx, ct + u_len, &f_len);
+       if (!ok)
+               goto fail;
+
+       /* Retrieve auth tag */
+       ok = EVP_CIPHER_CTX_ctrl(&ioq->ctx, EVP_CTRL_GCM_GET_TAG,
+                                HAMMER2_CRYPTO_GCM_TAG_SIZE,
+                                ct + u_len + f_len);
+       if (!ok)
+               goto fail;
+
+       ok = _gcm_iv_increment(ioq->iv);
+       if (!ok) {
+               ioq->error = HAMMER2_IOQ_ERROR_IVWRAP;
+               goto fail_out;
+       }
+
+       *out_size = u_len + f_len + HAMMER2_CRYPTO_GCM_TAG_SIZE;
+
+       return 0;
+
+fail:
+       ioq->error = HAMMER2_IOQ_ERROR_ALGO;
+fail_out:
+       if (DebugOpt)
+               fprintf(stderr, "error during encrypt_chunk\n");
+       return -1;
+}
+
+static
+int
+hammer2_crypto_gcm_decrypt_chunk(hammer2_ioq_t *ioq, char *ct, char *pt,
+                                int out_size, int *consume_size)
+{
+       int ok;
+       int u_len, f_len;
+
+       *consume_size = 0;
+
+       /* Re-initialize with new IV (but without redoing the key schedule) */
+       ok = EVP_DecryptInit_ex(&ioq->ctx, NULL, NULL, NULL, ioq->iv);
+       if (!ok) {
+               ioq->error = HAMMER2_IOQ_ERROR_ALGO;
+               goto fail_out;
+       }
+
+       ok = EVP_CIPHER_CTX_ctrl(&ioq->ctx, EVP_CTRL_GCM_SET_TAG,
+                                HAMMER2_CRYPTO_GCM_TAG_SIZE,
+                                ct + out_size);
+       if (!ok) {
+               ioq->error = HAMMER2_IOQ_ERROR_ALGO;
+               goto fail_out;
+       }
+
+       ok = EVP_DecryptUpdate(&ioq->ctx, pt, &u_len, ct, out_size);
+       if (!ok)
+               goto fail;
+
+       ok = EVP_DecryptFinal(&ioq->ctx, pt + u_len, &f_len);
+       if (!ok)
+               goto fail;
+
+       ok = _gcm_iv_increment(ioq->iv);
+       if (!ok) {
+               ioq->error = HAMMER2_IOQ_ERROR_IVWRAP;
+               goto fail_out;
+       }
+
+       *consume_size = u_len + f_len + HAMMER2_CRYPTO_GCM_TAG_SIZE;
+
+       return 0;
+
+fail:
+       ioq->error = HAMMER2_IOQ_ERROR_MACFAIL;
+fail_out:
+       if (DebugOpt)
+               fprintf(stderr, "error during decrypt_chunk (likely authentication error)\n");
+       return -1;
+}
 
 /*
  * Synchronously negotiate crypto for a new session.  This must occur
@@ -92,7 +347,8 @@ hammer2_crypto_negotiate(hammer2_iocom_t *iocom)
        char realname[128];
        hammer2_handshake_t handtx;
        hammer2_handshake_t handrx;
-       char buf[sizeof(handtx)];
+       char buf1[sizeof(handtx)];
+       char buf2[sizeof(handtx)];
        char *ptr;
        char *path;
        struct stat st;
@@ -103,6 +359,7 @@ hammer2_crypto_negotiate(hammer2_iocom_t *iocom)
        size_t blkmask;
        ssize_t n;
        int fd;
+       int error;
 
        /*
         * Get the peer IP address for the connection as a string.
@@ -134,6 +391,9 @@ hammer2_crypto_negotiate(hammer2_iocom_t *iocom)
 
        /*
         * Find the remote host's public key
+        *
+        * If the link is not to be encrypted (<ip>.none located) we shortcut
+        * the handshake entirely.  No buffers are exchanged.
         */
        asprintf(&path, "%s/%s.pub", HAMMER2_PATH_REMOTE, peername);
        if ((fp = fopen(path, "r")) == NULL) {
@@ -149,6 +409,7 @@ hammer2_crypto_negotiate(hammer2_iocom_t *iocom)
                }
                if (DebugOpt)
                        fprintf(stderr, "auth succeeded, unencrypted link\n");
+               goto done;
        }
        if (fp) {
                keys[0] = PEM_read_RSA_PUBKEY(fp, NULL, NULL, NULL);
@@ -248,7 +509,7 @@ urandfail:
        if (bcmp(&handrx, &handtx, sizeof(handtx)) == 0)
                goto urandfail;                 /* read all zeros */
        close(fd);
-       ERR_load_crypto_strings();
+       /* ERR_load_crypto_strings(); openssl debugging */
 
        /*
         * Handshake with the remote.
@@ -290,20 +551,20 @@ urandfail:
                         */
                        do {
                                ++*(int *)(ptr + 4);
-                               if (RSA_private_encrypt(blksize, ptr, buf,
+                               if (RSA_private_encrypt(blksize, ptr, buf1,
                                            keys[2], RSA_NO_PADDING) < 0) {
                                        iocom->ioq_rx.error =
                                                HAMMER2_IOQ_ERROR_KEYXCHGFAIL;
                                }
-                       } while (buf[0] & 0xC0);
+                       } while (buf1[0] & 0xC0);
 
-                       if (RSA_public_encrypt(blksize, buf, ptr,
+                       if (RSA_public_encrypt(blksize, buf1, buf2,
                                            keys[0], RSA_NO_PADDING) < 0) {
                                iocom->ioq_rx.error =
                                        HAMMER2_IOQ_ERROR_KEYXCHGFAIL;
                        }
                }
-               if (write(iocom->sock_fd, ptr, blksize) != (ssize_t)blksize) {
+               if (write(iocom->sock_fd, buf2, blksize) != (ssize_t)blksize) {
                        fprintf(stderr, "WRITE ERROR\n");
                }
        }
@@ -327,11 +588,11 @@ urandfail:
                ptr -= (i & blkmask);
                i += n;
                if (keys[0] && (i & blkmask) == 0) {
-                       if (RSA_private_decrypt(blksize, ptr, buf,
+                       if (RSA_private_decrypt(blksize, ptr, buf1,
                                           keys[2], RSA_NO_PADDING) < 0)
                                iocom->ioq_rx.error =
                                                HAMMER2_IOQ_ERROR_KEYXCHGFAIL;
-                       if (RSA_public_decrypt(blksize, buf, ptr,
+                       if (RSA_public_decrypt(blksize, buf1, ptr,
                                           keys[0], RSA_NO_PADDING) < 0)
                                iocom->ioq_rx.error =
                                                HAMMER2_IOQ_ERROR_KEYXCHGFAIL;
@@ -372,9 +633,30 @@ keyxchgfail:
        if (n != 0)
                goto keyxchgfail;
 
-       if (DebugOpt) {
-               fprintf(stderr, "Remote data: %s\n", handrx.quickmsg);
-       }
+       /*
+        * Use separate session keys and session fixed IVs for receive and
+        * transmit.
+        */
+       error = crypto_algos[HAMMER2_CRYPTO_ALGO].init(&iocom->ioq_rx, handrx.sess,
+           crypto_algos[HAMMER2_CRYPTO_ALGO].keylen,
+           handrx.sess + crypto_algos[HAMMER2_CRYPTO_ALGO].keylen,
+           sizeof(handrx.sess) - crypto_algos[HAMMER2_CRYPTO_ALGO].keylen,
+           0 /* decryption */);
+       if (error)
+               goto keyxchgfail;
+
+       error = crypto_algos[HAMMER2_CRYPTO_ALGO].init(&iocom->ioq_tx, handtx.sess,
+           crypto_algos[HAMMER2_CRYPTO_ALGO].keylen,
+           handtx.sess + crypto_algos[HAMMER2_CRYPTO_ALGO].keylen,
+           sizeof(handtx.sess) - crypto_algos[HAMMER2_CRYPTO_ALGO].keylen,
+           1 /* encryption */);
+       if (error)
+               goto keyxchgfail;
+
+       iocom->flags |= HAMMER2_IOCOMF_CRYPTED;
+
+       if (DebugOpt)
+               fprintf(stderr, "auth success: %s\n", handrx.quickmsg);
 done:
        if (path)
                free(path);
@@ -385,3 +667,101 @@ done:
        if (keys[1])
                RSA_free(keys[2]);
 }
+
+/*
+ * Decrypt pending data in the ioq's fifo.  The data is decrypted in-place.
+ */
+void
+hammer2_crypto_decrypt(hammer2_iocom_t *iocom __unused, hammer2_ioq_t *ioq)
+{
+       int p_len;
+       int used;
+       int error;
+       char buf[512];
+
+       /*
+        * fifo_beg to fifo_cdx is data already decrypted.
+        * fifo_cdn to fifo_end is data not yet decrypted.
+        */
+       p_len = ioq->fifo_end - ioq->fifo_cdn; /* data not yet decrypted */
+
+       if (p_len == 0)
+               return;
+
+       while (p_len >= crypto_algos[HAMMER2_CRYPTO_ALGO].taglen +
+           HAMMER2_CRYPTO_CHUNK_SIZE) {
+               bcopy(ioq->buf + ioq->fifo_cdn, buf,
+                     crypto_algos[HAMMER2_CRYPTO_ALGO].taglen +
+                     HAMMER2_CRYPTO_CHUNK_SIZE);
+               error = crypto_algos[HAMMER2_CRYPTO_ALGO].dec_chunk(
+                   ioq, buf,
+                   ioq->buf + ioq->fifo_cdx,
+                   HAMMER2_CRYPTO_CHUNK_SIZE,
+                   &used);
+#ifdef CRYPTO_DEBUG
+               printf("dec: p_len: %d, used: %d, fifo_cdn: %ju, fifo_cdx: %ju\n",
+                      p_len, used, ioq->fifo_cdn, ioq->fifo_cdx);
+#endif
+               p_len -= used;
+               ioq->fifo_cdn += used;
+               ioq->fifo_cdx += HAMMER2_CRYPTO_CHUNK_SIZE;
+#ifdef CRYPTO_DEBUG
+               printf("dec: p_len: %d, used: %d, fifo_cdn: %ju, fifo_cdx: %ju\n",
+                      p_len, used, ioq->fifo_cdn, ioq->fifo_cdx);
+#endif
+       }
+}
+
+/*
+ * *nactp is set to the number of ORIGINAL bytes consumed by the encrypter.
+ * The FIFO may contain more data.
+ */
+int
+hammer2_crypto_encrypt(hammer2_iocom_t *iocom __unused, hammer2_ioq_t *ioq,
+                      struct iovec *iov, int n, size_t *nactp)
+{
+       int p_len, used, ct_used;
+       int i;
+       int error;
+       size_t nmax;
+
+       nmax = sizeof(ioq->buf) - ioq->fifo_end;        /* max new bytes */
+
+       *nactp = 0;
+       for (i = 0; i < n && nmax; ++i) {
+               used = 0;
+               p_len = iov[i].iov_len;
+               assert((p_len & HAMMER2_MSG_ALIGNMASK) == 0);
+
+               while (p_len >= HAMMER2_CRYPTO_CHUNK_SIZE &&
+                   nmax >= HAMMER2_CRYPTO_CHUNK_SIZE +
+                   (size_t)crypto_algos[HAMMER2_CRYPTO_ALGO].taglen) {
+                       error = crypto_algos[HAMMER2_CRYPTO_ALGO].enc_chunk(
+                           ioq,
+                           ioq->buf + ioq->fifo_cdx,
+                           (char *)iov[i].iov_base + used,
+                           HAMMER2_CRYPTO_CHUNK_SIZE, &ct_used);
+#ifdef CRYPTO_DEBUG
+                       printf("nactp: %ju, p_len: %d, ct_used: %d, used: %d, nmax: %ju\n",
+                              *nactp, p_len, ct_used, used, nmax);
+#endif
+
+                       *nactp += (size_t)HAMMER2_CRYPTO_CHUNK_SIZE;    /* plaintext count */
+                       used += HAMMER2_CRYPTO_CHUNK_SIZE;
+                       p_len -= HAMMER2_CRYPTO_CHUNK_SIZE;
+
+                       ioq->fifo_cdx += (size_t)ct_used;       /* crypted count */
+                       ioq->fifo_cdn += (size_t)ct_used;       /* crypted count */
+                       ioq->fifo_end += (size_t)ct_used;
+                       nmax -= (size_t)ct_used;
+#ifdef CRYPTO_DEBUG
+                       printf("nactp: %ju, p_len: %d, ct_used: %d, used: %d, nmax: %ju\n",
+                              *nactp, p_len, ct_used, used, nmax);
+#endif
+               }
+       }
+       iov[0].iov_base = ioq->buf + ioq->fifo_beg;
+       iov[0].iov_len = ioq->fifo_cdx - ioq->fifo_beg;
+
+       return (1);
+}