]> git.sur5r.net Git - openldap/commitdiff
ITS#8753 Public key pinning support in libldap
authorOndřej Kuzník <ondra@openldap.org>
Tue, 7 Nov 2017 18:35:33 +0000 (18:35 +0000)
committerOndřej Kuzník <ondra@openldap.org>
Mon, 13 Nov 2017 17:24:49 +0000 (17:24 +0000)
include/ldap.h
libraries/libldap/init.c
libraries/libldap/ldap-int.h
libraries/libldap/ldap-tls.h
libraries/libldap/open.c
libraries/libldap/tls2.c
libraries/libldap/tls_g.c
libraries/libldap/tls_m.c
libraries/libldap/tls_o.c

index e3f292dbaa17634447ad857c1470e02cc7a05e2d..2ca1b84fd6820a93b4b5aa31fdc06df4d5962474 100644 (file)
@@ -165,6 +165,7 @@ LDAP_BEGIN_DECL
 #define LDAP_OPT_X_TLS_CACERT          0x6016
 #define LDAP_OPT_X_TLS_CERT                    0x6017
 #define LDAP_OPT_X_TLS_KEY                     0x6018
+#define LDAP_OPT_X_TLS_PEERKEY_HASH    0x6019
 
 #define LDAP_OPT_X_TLS_NEVER   0
 #define LDAP_OPT_X_TLS_HARD            1
index 3e602dff9ad9f4bf1f80348ae637159499381e08..f48e22e83fa771b1e87dffe174fe40f160316889 100644 (file)
@@ -130,6 +130,7 @@ static const struct ol_attribute {
        {0, ATTR_TLS,   "TLS_RANDFILE",         NULL,   LDAP_OPT_X_TLS_RANDOM_FILE},
        {0, ATTR_TLS,   "TLS_CIPHER_SUITE",     NULL,   LDAP_OPT_X_TLS_CIPHER_SUITE},
        {0, ATTR_TLS,   "TLS_PROTOCOL_MIN",     NULL,   LDAP_OPT_X_TLS_PROTOCOL_MIN},
+       {0, ATTR_TLS,   "TLS_PEERKEY_HASH",     NULL,   LDAP_OPT_X_TLS_PEERKEY_HASH},
 
 #ifdef HAVE_OPENSSL_CRL
        {0, ATTR_TLS,   "TLS_CRLCHECK",         NULL,   LDAP_OPT_X_TLS_CRLCHECK},
index d7d1afada17ecd1f8fef1463efe1ca8263d27fcf..1210f496d30c9d789a0eafa2c0b04eaa2447c8f7 100644 (file)
@@ -268,7 +268,9 @@ struct ldapoptions {
        int                     ldo_tls_require_cert;
        int                     ldo_tls_impl;
        int                     ldo_tls_crlcheck;
-#define LDAP_LDO_TLS_NULLARG ,0,0,0,{0,0,0,0,0,0,0,0,0},0,0,0,0
+       char            *ldo_tls_pin_hashalg;
+       struct berval   ldo_tls_pin;
+#define LDAP_LDO_TLS_NULLARG ,0,0,0,{0,0,0,0,0,0,0,0,0},0,0,0,0,0,{0,0}
 #else
 #define LDAP_LDO_TLS_NULLARG
 #endif
index ec997da635b8795811da08d4fe59503903eb01a8..0b1722cdcddb98463a9308cc1893e0269bc3569a 100644 (file)
@@ -44,6 +44,7 @@ typedef int (TI_session_strength)(tls_session *sess);
 typedef int (TI_session_unique)(tls_session *sess, struct berval *buf, int is_server);
 typedef const char *(TI_session_name)(tls_session *s);
 typedef int (TI_session_peercert)(tls_session *s, struct berval *der);
+typedef int (TI_session_pinning)(LDAP *ld, tls_session *s, char *hashalg, struct berval *hash);
 
 typedef void (TI_thr_init)(void);
 
@@ -71,6 +72,7 @@ typedef struct tls_impl {
        TI_session_name *ti_session_version;
        TI_session_name *ti_session_cipher;
        TI_session_peercert *ti_session_peercert;
+       TI_session_pinning *ti_session_pinning;
 
        Sockbuf_IO *ti_sbio;
 
index b513ad7d37943bbbe19173294fa141cafb858229..f02e91eba257301db30a7842a51fec9220361937 100644 (file)
@@ -151,6 +151,23 @@ ldap_create( LDAP **ldp )
        /* Properly initialize the structs mutex */
        ldap_pvt_thread_mutex_init( &(ld->ld_ldopts_mutex) );
 #endif
+
+#ifdef HAVE_TLS
+       if ( ld->ld_options.ldo_tls_pin_hashalg ) {
+               int len = strlen( gopts->ldo_tls_pin_hashalg );
+
+               ld->ld_options.ldo_tls_pin_hashalg =
+                       LDAP_MALLOC( len + 1 + gopts->ldo_tls_pin.bv_len );
+               if ( !ld->ld_options.ldo_tls_pin_hashalg ) goto nomem;
+
+               ld->ld_options.ldo_tls_pin.bv_val = ld->ld_options.ldo_tls_pin_hashalg
+                       + len + 1;
+               AC_MEMCPY( ld->ld_options.ldo_tls_pin_hashalg, gopts->ldo_tls_pin_hashalg,
+                               len + 1 + gopts->ldo_tls_pin.bv_len );
+       } else if ( !BER_BVISEMPTY(&ld->ld_options.ldo_tls_pin) ) {
+               ber_dupbv( &ld->ld_options.ldo_tls_pin, &gopts->ldo_tls_pin );
+       }
+#endif
        LDAP_MUTEX_UNLOCK( &gopts->ldo_mutex );
 
        ld->ld_valid = LDAP_VALID_SESSION;
@@ -215,6 +232,15 @@ nomem:
        LDAP_FREE( ld->ld_options.ldo_def_sasl_realm );
        LDAP_FREE( ld->ld_options.ldo_def_sasl_mech );
 #endif
+
+#ifdef HAVE_TLS
+       /* tls_pin_hashalg and tls_pin share the same buffer */
+       if ( ld->ld_options.ldo_tls_pin_hashalg ) {
+               LDAP_FREE( ld->ld_options.ldo_tls_pin_hashalg );
+       } else {
+               LDAP_FREE( ld->ld_options.ldo_tls_pin.bv_val );
+       }
+#endif
        LDAP_FREE( (char *)ld );
        return LDAP_NO_MEMORY;
 }
index 04db61234c3bed31f0899d3c19323090622292ee..1feba5befd56ceef025d91c15d2fc0c6313f1ec0 100644 (file)
@@ -138,6 +138,14 @@ ldap_int_tls_destroy( struct ldapoptions *lo )
                LDAP_FREE( lo->ldo_tls_crlfile );
                lo->ldo_tls_crlfile = NULL;
        }
+       /* tls_pin_hashalg and tls_pin share the same buffer */
+       if ( lo->ldo_tls_pin_hashalg ) {
+               LDAP_FREE( lo->ldo_tls_pin_hashalg );
+               lo->ldo_tls_pin_hashalg = NULL;
+       } else {
+               LDAP_FREE( lo->ldo_tls_pin.bv_val );
+       }
+       BER_BVZERO( &lo->ldo_tls_pin );
 }
 
 /*
@@ -518,6 +526,18 @@ ldap_pvt_tls_check_hostname( LDAP *ld, void *s, const char *name_in )
                }
        }
 
+       /*
+        * If instructed to do pinning, do it now
+        */
+       if ( !BER_BVISNULL( &ld->ld_options.ldo_tls_pin ) ) {
+               ld->ld_errno = tls_imp->ti_session_pinning( ld, s,
+                               ld->ld_options.ldo_tls_pin_hashalg,
+                               &ld->ld_options.ldo_tls_pin );
+               if (ld->ld_errno != LDAP_SUCCESS) {
+                       return ld->ld_errno;
+               }
+       }
+
        return LDAP_SUCCESS;
 }
 
@@ -534,6 +554,7 @@ ldap_pvt_tls_config( LDAP *ld, int option, const char *arg )
        case LDAP_OPT_X_TLS_RANDOM_FILE:
        case LDAP_OPT_X_TLS_CIPHER_SUITE:
        case LDAP_OPT_X_TLS_DHFILE:
+       case LDAP_OPT_X_TLS_PEERKEY_HASH:
        case LDAP_OPT_X_TLS_CRLFILE:    /* GnuTLS only */
                return ldap_pvt_tls_set_option( ld, option, (void *) arg );
 
@@ -946,6 +967,68 @@ ldap_pvt_tls_set_option( LDAP *ld, int option, void *arg )
                        BER_BVZERO( &lo->ldo_tls_key );
                }
                break;
+       case LDAP_OPT_X_TLS_PEERKEY_HASH: {
+               /* arg = "[hashalg:]pubkey_hash" */
+               struct berval bv;
+               char *p, *pin = arg;
+               int rc = LDAP_SUCCESS;
+
+               if ( !tls_imp->ti_session_pinning ) return -1;
+
+               if ( !pin ) {
+                       if ( lo->ldo_tls_pin_hashalg ) {
+                               LDAP_FREE( lo->ldo_tls_pin_hashalg );
+                       } else if ( lo->ldo_tls_pin.bv_val ) {
+                               LDAP_FREE( lo->ldo_tls_pin.bv_val );
+                       }
+                       lo->ldo_tls_pin_hashalg = NULL;
+                       BER_BVZERO( &lo->ldo_tls_pin );
+                       return rc;
+               }
+
+               pin = LDAP_STRDUP( pin );
+               p = strchr( pin, ':' );
+
+               /* pubkey (its hash) goes in bv, alg in p */
+               if ( p ) {
+                       *p = '\0';
+                       bv.bv_val = p+1;
+                       p = pin;
+               } else {
+                       bv.bv_val = pin;
+               }
+
+               bv.bv_len = strlen(bv.bv_val);
+               if ( ldap_int_decode_b64_inplace( &bv ) ) {
+                       LDAP_FREE( pin );
+                       return -1;
+               }
+
+               if ( ld != NULL ) {
+                       LDAPConn *conn = ld->ld_defconn;
+                       if ( conn != NULL ) {
+                               Sockbuf *sb = conn->lconn_sb;
+                               void *sess = ldap_pvt_tls_sb_ctx( sb );
+                               if ( sess != NULL ) {
+                                       rc = tls_imp->ti_session_pinning( ld, sess, p, &bv );
+                               }
+                       }
+               }
+
+               if ( rc == LDAP_SUCCESS ) {
+                       if ( lo->ldo_tls_pin_hashalg ) {
+                               LDAP_FREE( lo->ldo_tls_pin_hashalg );
+                       } else if ( lo->ldo_tls_pin.bv_val ) {
+                               LDAP_FREE( lo->ldo_tls_pin.bv_val );
+                       }
+                       lo->ldo_tls_pin_hashalg = p;
+                       lo->ldo_tls_pin = bv;
+               } else {
+                       LDAP_FREE( pin );
+               }
+
+               return rc;
+       }
        default:
                return -1;
        }
index 0df32a8e4573800a9b0438d168a02cda8598a86a..adcb6be04076a91d3a0bf94cf8357f4e51f5b9da 100644 (file)
@@ -43,6 +43,8 @@
 
 #include <gnutls/gnutls.h>
 #include <gnutls/x509.h>
+#include <gnutls/abstract.h>
+#include <gnutls/crypto.h>
 
 typedef struct tlsg_ctx {
        gnutls_certificate_credentials_t cred;
@@ -752,6 +754,109 @@ tlsg_session_peercert( tls_session *sess, struct berval *der )
        return 0;
 }
 
+static int
+tlsg_session_pinning( LDAP *ld, tls_session *sess, char *hashalg, struct berval *hash )
+{
+       tlsg_session *s = (tlsg_session *)sess;
+       const gnutls_datum_t *cert_list;
+       unsigned int cert_list_size = 0;
+       gnutls_x509_crt_t crt;
+       gnutls_pubkey_t pubkey;
+       gnutls_datum_t key = {};
+       gnutls_digest_algorithm_t alg;
+       struct berval keyhash;
+       size_t len;
+       int rc = -1;
+
+       if ( hashalg ) {
+               alg = gnutls_digest_get_id( hashalg );
+               if ( alg == GNUTLS_DIG_UNKNOWN ) {
+                       Debug( LDAP_DEBUG_ANY, "tlsg_session_pinning: "
+                                       "unknown hashing algorithm for GnuTLS: '%s'\n",
+                                       hashalg, 0, 0 );
+                       return rc;
+               }
+       }
+
+       cert_list = gnutls_certificate_get_peers( s->session, &cert_list_size );
+       if ( cert_list_size == 0 ) {
+               return rc;
+       }
+
+       if ( gnutls_x509_crt_init( &crt ) < 0 ) {
+               return rc;
+       }
+
+       if ( gnutls_x509_crt_import( crt, &cert_list[0], GNUTLS_X509_FMT_DER ) ) {
+               goto done;
+       }
+
+       if ( gnutls_pubkey_init( &pubkey ) ) {
+               goto done;
+       }
+
+       if ( gnutls_pubkey_import_x509( pubkey, crt, 0 ) < 0 ) {
+               goto done;
+       }
+
+       gnutls_pubkey_export( pubkey, GNUTLS_X509_FMT_DER, key.data, &len );
+       if ( len <= 0 ) {
+               goto done;
+       }
+
+       key.data = LDAP_MALLOC( len );
+       if ( !key.data ) {
+               goto done;
+       }
+
+       key.size = len;
+
+       if ( gnutls_pubkey_export( pubkey, GNUTLS_X509_FMT_DER,
+                               key.data, &len ) < 0 ) {
+               goto done;
+       }
+
+       if ( hashalg ) {
+               keyhash.bv_len = gnutls_hash_get_len( alg );
+               keyhash.bv_val = LDAP_MALLOC( keyhash.bv_len );
+               if ( !keyhash.bv_val || gnutls_fingerprint( alg, &key,
+                                       keyhash.bv_val, &keyhash.bv_len ) < 0 ) {
+                       goto done;
+               }
+       } else {
+               keyhash.bv_val = (char *)key.data;
+               keyhash.bv_len = key.size;
+       }
+
+       if ( ber_bvcmp( hash, &keyhash ) ) {
+               rc = LDAP_CONNECT_ERROR;
+               Debug( LDAP_DEBUG_ANY, "tlsg_session_pinning: "
+                               "public key hash does not match provided pin.\n", 0, 0, 0 );
+               if ( ld->ld_error ) {
+                       LDAP_FREE( ld->ld_error );
+               }
+               ld->ld_error = LDAP_STRDUP(
+                       _("TLS: public key hash does not match provided pin"));
+       } else {
+               rc = LDAP_SUCCESS;
+       }
+
+done:
+       if ( pubkey ) {
+               gnutls_pubkey_deinit( pubkey );
+       }
+       if ( crt ) {
+               gnutls_x509_crt_deinit( crt );
+       }
+       if ( keyhash.bv_val != (char *)key.data ) {
+               LDAP_FREE( keyhash.bv_val );
+       }
+       if ( key.data ) {
+               LDAP_FREE( key.data );
+       }
+       return rc;
+}
+
 /* suites is a string of colon-separated cipher suite names. */
 static int
 tlsg_parse_ciphers( tlsg_ctx *ctx, char *suites )
@@ -1012,6 +1117,7 @@ tls_impl ldap_int_tls_impl = {
        tlsg_session_version,
        tlsg_session_cipher,
        tlsg_session_peercert,
+       tlsg_session_pinning,
 
        &tlsg_sbio,
 
index ad003d29f1979ef2c1f3e23cf88f4d9b88dca528..7e48b8a671c727eddd120feaa74c54bb3b8018cd 100644 (file)
@@ -3386,6 +3386,7 @@ tls_impl ldap_int_tls_impl = {
        tlsm_session_version,
        tlsm_session_cipher,
        tlsm_session_peercert,
+       NULL,
 
        &tlsm_sbio,
 
index 95fb62890c58bc4a6bb8a7ea712c5796b7d38b2f..d3b6ceb35fae5752f8f2382a85b186bf930bc405 100644 (file)
@@ -835,6 +835,69 @@ tlso_session_peercert( tls_session *sess, struct berval *der )
        return 0;
 }
 
+static int
+tlso_session_pinning( LDAP *ld, tls_session *sess, char *hashalg, struct berval *hash )
+{
+       tlso_session *s = (tlso_session *)sess;
+       char *tmp, digest[EVP_MAX_MD_SIZE];
+       struct berval key,
+                                 keyhash = { .bv_val = digest, .bv_len = sizeof(digest) };
+       X509 *cert = SSL_get_peer_certificate(s);
+       int len, rc = LDAP_SUCCESS;
+
+       len = i2d_X509_PUBKEY( X509_get_X509_PUBKEY(cert), NULL );
+
+       key.bv_val = tmp = LDAP_MALLOC( len );
+       if ( !key.bv_val ) {
+               return -1;
+       }
+
+       key.bv_len = i2d_X509_PUBKEY( X509_get_X509_PUBKEY(cert), &tmp );
+
+       if ( hashalg ) {
+               const EVP_MD *md;
+               EVP_MD_CTX *mdctx;
+               unsigned int len = keyhash.bv_len;
+
+               md = EVP_get_digestbyname( hashalg );
+               if ( !md ) {
+                       Debug( LDAP_DEBUG_TRACE, "tlso_session_pinning: "
+                                       "hash %s not recognised by OpenSSL\n", hashalg, 0, 0 );
+                       rc = -1;
+                       goto done;
+               }
+
+               mdctx = EVP_MD_CTX_new();
+               if ( !mdctx ) {
+                       rc = -1;
+                       goto done;
+               }
+
+               EVP_DigestInit_ex( mdctx, md, NULL );
+               EVP_DigestUpdate( mdctx, key.bv_val, key.bv_len );
+               EVP_DigestFinal_ex( mdctx, (unsigned char *)keyhash.bv_val, &len );
+               keyhash.bv_len = len;
+               EVP_MD_CTX_free( mdctx );
+       } else {
+               keyhash = key;
+       }
+
+       if ( ber_bvcmp( hash, &keyhash ) ) {
+               rc = LDAP_CONNECT_ERROR;
+               Debug( LDAP_DEBUG_ANY, "tlso_session_pinning: "
+                               "public key hash does not match provided pin.\n", 0, 0, 0 );
+               if ( ld->ld_error ) {
+                       LDAP_FREE( ld->ld_error );
+               }
+               ld->ld_error = LDAP_STRDUP(
+                       _("TLS: public key hash does not match provided pin"));
+       }
+
+done:
+       LDAP_FREE( key.bv_val );
+       return rc;
+}
+
 /*
  * TLS support for LBER Sockbufs
  */
@@ -1368,6 +1431,7 @@ tls_impl ldap_int_tls_impl = {
        tlso_session_version,
        tlso_session_cipher,
        tlso_session_peercert,
+       tlso_session_pinning,
 
        &tlso_sbio,