src/sock_gnutls.c
changeset 140 aa390e52eda8
parent 139 55b9dcc2b73a
child 155 c59d3eaff0fb
--- a/src/sock_gnutls.c	Thu Apr 16 01:20:09 2009 +0300
+++ b/src/sock_gnutls.c	Sun Apr 19 04:04:42 2009 +0300
@@ -4,8 +4,13 @@
 // XXX: remove
 #include "log.h"
 
+#include <gnutls/x509.h>
+
 #include <stdlib.h>
-#include <err.h>
+#include <string.h>
+#include <time.h>
+
+#include <assert.h>
 
 /**
  * Register for events based on the session's gnutls_record_get_direction().
@@ -37,10 +42,100 @@
 }
 
 /**
+ * Translate a set of gnutls_certificate_status_t values to a constant error message
+ */
+static const char* sock_gnutls_verify_error (unsigned int status)
+{
+    if (status & GNUTLS_CERT_REVOKED)
+        return "certificate was revoked";
+
+    else if (status & GNUTLS_CERT_INVALID) {
+        if (status & GNUTLS_CERT_SIGNER_NOT_FOUND)
+            return "certificate signer was not found";
+
+        else if (status & GNUTLS_CERT_SIGNER_NOT_CA)
+            return "certificate signer is not a Certificate Authority";
+
+        else if (status & GNUTLS_CERT_INSECURE_ALGORITHM)
+            return "certificate signed using an insecure algorithm";
+
+        else
+            return "certificate could not be verified";
+
+    } else
+        return "unknown error";
+
+}
+
+/**
+ * Perform the certificate validation procedure on the socket.
+ *
+ * Based on the GnuTLS examples/ex-rfc2818.c
+ */
+static err_t sock_gnutls_verify (struct sock_gnutls *sock, struct error_info *err)
+{
+    unsigned int status;
+    const gnutls_datum_t *cert_list;
+    unsigned int cert_list_size;
+    gnutls_x509_crt_t cert = NULL;
+    time_t t, now;
+
+    // init
+    RESET_ERROR(err);
+    now = time(NULL);
+    
+    // inspect the peer's cert chain using the installed trusted CAs
+    if ((ERROR_EXTRA(err) = gnutls_certificate_verify_peers2(sock->session, &status)))
+        JUMP_SET_ERROR(err, ERR_GNUTLS_CERT_VERIFY_PEERS2);
+
+    // verify errors?
+    if (status)
+        JUMP_SET_ERROR_STR(err, ERR_GNUTLS_CERT_VERIFY, sock_gnutls_verify_error(status));
+    
+    // import the main cert
+    assert(gnutls_certificate_type_get(sock->session) == GNUTLS_CRT_X509);
+
+    if ((ERROR_EXTRA(err) = gnutls_x509_crt_init(&cert)))
+        JUMP_SET_ERROR_STR(err, ERR_GNUTLS_CERT_VERIFY, "gnutls_x509_crt_init");
+
+    if ((cert_list = gnutls_certificate_get_peers(sock->session, &cert_list_size)) == NULL)
+        JUMP_SET_ERROR_STR(err, ERR_GNUTLS_CERT_VERIFY, "gnutls_certificate_get_peers");
+
+    if (!cert_list_size)
+        JUMP_SET_ERROR_STR(err, ERR_GNUTLS_CERT_VERIFY, "cert_list_size");
+
+    if ((ERROR_EXTRA(err) = gnutls_x509_crt_import(cert, &cert_list[0], GNUTLS_X509_FMT_DER)))
+        JUMP_SET_ERROR_STR(err, ERR_GNUTLS_CERT_VERIFY, "gnutls_x509_crt_import");
+    
+    // check expire/activate... not sure if we need to do this
+    if ((t = gnutls_x509_crt_get_expiration_time(cert)) == ((time_t) -1) || t < now)
+        JUMP_SET_ERROR_STR(err, ERR_GNUTLS_CERT_VERIFY, "gnutls_x509_crt_get_expiration_time");
+
+    if ((t = gnutls_x509_crt_get_activation_time(cert)) == ((time_t) -1) || t > now)
+        JUMP_SET_ERROR_STR(err, ERR_GNUTLS_CERT_VERIFY, "gnutls_x509_crt_get_activation_time");
+    
+    // check hostname
+    if (!gnutls_x509_crt_check_hostname(cert, sock->hostname))
+        JUMP_SET_ERROR_STR(err, ERR_GNUTLS_CERT_VERIFY, "gnutls_x509_crt_check_hostname");
+
+error:
+    // cleanup
+    if (cert)
+        gnutls_x509_crt_deinit(cert);
+    
+    // should be SUCCESS
+    return ERROR_CODE(err);    
+}
+
+
+/**
  * Our handshake driver. This will execute the next gnutls_handshake step, handling E_AGAIN.
  *
  * This updates the sock_gnutls::handshake state internally, as used by sock_gnutls_event_handler.
  *
+ * If the sock is marked as verify, this will perform the verification, returning on any errors, and then unset the
+ * verify flag - this ensures that the peer cert is only verified once per connection...
+ *
  * @return >0 for finished handshake, 0 for handshake-in-progress, -err_t for errors.
  */
 static int sock_gnutls_handshake (struct sock_gnutls *sock, struct error_info *err)
@@ -56,6 +151,16 @@
         // update state
         sock->handshake = false;
 
+        // verify?
+        if (sock->verify) {
+            // perform the validation
+            if (sock_gnutls_verify(sock, err))
+                goto error;
+            
+            // unmark
+            sock->verify = false;
+        }
+
         // handshake done
         return 1;
 
@@ -92,8 +197,8 @@
             // wait for the next handshake step
         
         } else if (SOCK_GNUTLS_BASE(sock)->conn_cb_func) {
-            // the async connect process has now completed, either succesfully or not
-            // invoke the user connect callback directly
+            // the async connect process has now completed, either succesfully or with an error
+            // invoke the user connect callback directly with appropriate error
             sock_stream_invoke_conn_cb(SOCK_GNUTLS_BASE(sock), ERROR_CODE(&err) ? &err : NULL, true);
 
         } else {
@@ -125,6 +230,7 @@
     ret = gnutls_record_recv(sock->session, buf, *len);
     
     // errors
+    // XXX: E_INTERRUPTED, E_REHANDSHAKE?
     if (ret < 0 && ret != GNUTLS_E_AGAIN)
         RETURN_SET_ERROR_EXTRA(err, ERR_GNUTLS_RECORD_RECV, ret);
     
@@ -250,24 +356,11 @@
 };
 
 /*
- * XXX: global shared sock_gnutls_ctx
- */
-struct sock_gnutls_client_ctx _sock_gnutls_client_ctx;
-
-/*
- * Configure the given gnutls socket context to use simple anonymous client credentials
+ * Global shared anonymous client credentials
  */
-static err_t sock_gnutls_client_ctx_anon (struct sock_gnutls_client_ctx *ctx, struct error_info *err)
-{
-    // init to use anonymous x509 cert
-    if ((ERROR_EXTRA(err) = gnutls_certificate_allocate_credentials(&ctx->xcred)) < 0)
-        return SET_ERROR(err, ERR_GNUTLS_CERT_ALLOC_CRED);
+static struct sock_ssl_client_cred sock_gnutls_client_cred_anon = { .x509 = NULL, .verify = false, .refcount = 0 };
 
-    // done
-    return SUCCESS;
-}
-
-// XXX: log func
+// XXX: GnuTLS log func
 void _log (int level, const char *msg)
 {
     printf("gnutls: %d: %s", level, msg);
@@ -279,9 +372,9 @@
     if ((ERROR_EXTRA(err) = gnutls_global_init()) < 0)
         return SET_ERROR(err, ERR_GNUTLS_GLOBAL_INIT);
 
-    // init _sock_gnutls_ctx
-    if (sock_gnutls_client_ctx_anon(&_sock_gnutls_client_ctx, err))
-        return ERROR_CODE(err);
+    // initialize the anon client credentials
+    if ((ERROR_EXTRA(err) = gnutls_certificate_allocate_credentials(&sock_gnutls_client_cred_anon.x509)) < 0)
+        return SET_ERROR(err, ERR_GNUTLS_CERT_ALLOC_CRED);
 
     // XXX: debug
 //    gnutls_global_set_log_function(&_log);
@@ -291,11 +384,83 @@
     return SUCCESS;
 }
 
-err_t sock_ssl_connect_async (struct sock_stream **sock_ptr, const char *host, const char *service, 
-        sock_stream_connect_cb cb_func, void *cb_arg, struct error_info *err)
+static void sock_ssl_client_cred_destroy (struct sock_ssl_client_cred *cred)
+{
+    // simple
+    gnutls_certificate_free_credentials(cred->x509);
+
+    free(cred);
+}
+
+err_t sock_ssl_client_cred_create (struct sock_ssl_client_cred **ctx_cred,
+        const char *cafile_path, bool verify,
+        const char *cert_path, const char *pkey_path,
+        struct error_info *err
+) {
+    struct sock_ssl_client_cred *cred;
+
+    // alloc it
+    if ((cred = calloc(1, sizeof(*cred))) == NULL)
+        return SET_ERROR(err, ERR_CALLOC);
+
+    // create the cert
+    if ((ERROR_EXTRA(err) = gnutls_certificate_allocate_credentials(&cred->x509)) < 0)
+        JUMP_SET_ERROR(err, ERR_GNUTLS_CERT_ALLOC_CRED);
+    
+    // load the trusted ca certs?
+    if (cafile_path) {
+        // load them
+        if ((ERROR_EXTRA(err) = gnutls_certificate_set_x509_trust_file(cred->x509, cafile_path, GNUTLS_X509_FMT_PEM)) < 0)
+            JUMP_SET_ERROR(err, ERR_GNUTLS_CERT_SET_X509_TRUST_FILE);
+
+    }
+
+    // set the verify flags?
+    cred->verify = verify;
+    gnutls_certificate_set_verify_flags(cred->x509, 0);
+
+    // load the client cert?
+    if (cert_path || pkey_path) {
+        // need both...
+        assert(cert_path && pkey_path);
+
+        // load
+        if ((ERROR_EXTRA(err) = gnutls_certificate_set_x509_key_file(cred->x509, cert_path, pkey_path, GNUTLS_X509_FMT_PEM)))
+            JUMP_SET_ERROR(err, ERR_GNUTLS_CERT_SET_X509_KEY_FILE);
+    }
+
+    // ok
+    cred->refcount = 1;
+    *ctx_cred = cred;
+
+    return SUCCESS;
+
+error:
+    // release
+    sock_ssl_client_cred_destroy(cred);
+
+    return ERROR_CODE(err);
+}
+
+void sock_ssl_client_cred_get (struct sock_ssl_client_cred *cred)
+{
+    cred->refcount++;
+}
+
+void sock_ssl_client_cred_put (struct sock_ssl_client_cred *cred)
+{
+    if (--cred->refcount == 0)
+        sock_ssl_client_cred_destroy(cred);
+}
+
+err_t sock_ssl_connect_async (struct sock_stream **sock_ptr, 
+        const char *hostname, const char *service,
+        struct sock_ssl_client_cred *cred,
+        sock_stream_connect_cb cb_func, void *cb_arg, 
+        struct error_info *err
+    )
 {
     struct sock_gnutls *sock = NULL;
-    struct sock_gnutls_client_ctx *ctx = &_sock_gnutls_client_ctx;
 
     // alloc
     if ((sock = calloc(1, sizeof(*sock))) == NULL)
@@ -304,6 +469,24 @@
     // initialize base
     sock_stream_init(SOCK_GNUTLS_BASE(sock), &sock_gnutls_type, cb_func, cb_arg);
 
+    if (!cred) {
+        // default credentials
+        cred = &sock_gnutls_client_cred_anon;
+    
+    } else {
+        // take a ref
+        sock->cred = cred;
+        cred->refcount++;
+    };
+
+    // do verify?
+    if (cred->verify)
+        sock->verify = true;
+
+    // init
+    if ((sock->hostname = strdup(hostname)) == NULL)
+        JUMP_SET_ERROR(err, ERR_STRDUP);
+
     // initialize client session
     if ((ERROR_EXTRA(err) = gnutls_init(&sock->session, GNUTLS_CLIENT)) < 0)
         JUMP_SET_ERROR(err, ERR_GNUTLS_INIT);
@@ -312,12 +495,15 @@
     if ((ERROR_EXTRA(err) = gnutls_set_default_priority(sock->session)))
         JUMP_SET_ERROR(err, ERR_GNUTLS_SET_DEFAULT_PRIORITY);
 
-    // bind anon credentials
-    if ((ERROR_EXTRA(err) = gnutls_credentials_set(sock->session, GNUTLS_CRD_CERTIFICATE, ctx->xcred)))
+    // XXX: silly hack for OpenSSL interop
+    gnutls_dh_set_prime_bits(sock->session, 512);
+
+    // bind credentials
+    if ((ERROR_EXTRA(err) = gnutls_credentials_set(sock->session, GNUTLS_CRD_CERTIFICATE, cred->x509)))
         JUMP_SET_ERROR(err, ERR_GNUTLS_CRED_SET);
 
     // TCP connect
-    if (sock_tcp_connect_async_begin(SOCK_GNUTLS_TCP(sock), host, service, err))
+    if (sock_tcp_connect_async_begin(SOCK_GNUTLS_TCP(sock), hostname, service, err))
         goto error;
 
     // done, wait for the connect to complete
@@ -338,10 +524,14 @@
     sock_fd_close(SOCK_GNUTLS_FD(sock));
 
     // close the session rudely
-    // XXX: does this actually do everything we need it to? Don't want to call gnutls_bye here, since we're void...
     gnutls_deinit(sock->session);
     
+    if (sock->cred)
+        // drop the cred ref
+        sock_ssl_client_cred_put(sock->cred);
+
     // free
+    free(sock->hostname);
     free(sock);
 }