From 8e34ed8c786a1f786976da046907f7dbd5d3458d Mon Sep 17 00:00:00 2001 From: =?utf8?q?Ond=C5=99ej=20Kuzn=C3=ADk?= Date: Tue, 7 Nov 2017 18:35:33 +0000 Subject: [PATCH] ITS#8753 Public key pinning support in libldap --- include/ldap.h | 1 + libraries/libldap/init.c | 1 + libraries/libldap/ldap-int.h | 4 +- libraries/libldap/ldap-tls.h | 2 + libraries/libldap/open.c | 26 +++++++++ libraries/libldap/tls2.c | 83 +++++++++++++++++++++++++++ libraries/libldap/tls_g.c | 106 +++++++++++++++++++++++++++++++++++ libraries/libldap/tls_m.c | 1 + libraries/libldap/tls_o.c | 64 +++++++++++++++++++++ 9 files changed, 287 insertions(+), 1 deletion(-) diff --git a/include/ldap.h b/include/ldap.h index e3f292dbaa..2ca1b84fd6 100644 --- a/include/ldap.h +++ b/include/ldap.h @@ -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 diff --git a/libraries/libldap/init.c b/libraries/libldap/init.c index 3e602dff9a..f48e22e83f 100644 --- a/libraries/libldap/init.c +++ b/libraries/libldap/init.c @@ -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}, diff --git a/libraries/libldap/ldap-int.h b/libraries/libldap/ldap-int.h index d7d1afada1..1210f496d3 100644 --- a/libraries/libldap/ldap-int.h +++ b/libraries/libldap/ldap-int.h @@ -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 diff --git a/libraries/libldap/ldap-tls.h b/libraries/libldap/ldap-tls.h index ec997da635..0b1722cdcd 100644 --- a/libraries/libldap/ldap-tls.h +++ b/libraries/libldap/ldap-tls.h @@ -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; diff --git a/libraries/libldap/open.c b/libraries/libldap/open.c index b513ad7d37..f02e91eba2 100644 --- a/libraries/libldap/open.c +++ b/libraries/libldap/open.c @@ -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; } diff --git a/libraries/libldap/tls2.c b/libraries/libldap/tls2.c index 04db61234c..1feba5befd 100644 --- a/libraries/libldap/tls2.c +++ b/libraries/libldap/tls2.c @@ -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; } diff --git a/libraries/libldap/tls_g.c b/libraries/libldap/tls_g.c index 0df32a8e45..adcb6be040 100644 --- a/libraries/libldap/tls_g.c +++ b/libraries/libldap/tls_g.c @@ -43,6 +43,8 @@ #include #include +#include +#include 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, diff --git a/libraries/libldap/tls_m.c b/libraries/libldap/tls_m.c index ad003d29f1..7e48b8a671 100644 --- a/libraries/libldap/tls_m.c +++ b/libraries/libldap/tls_m.c @@ -3386,6 +3386,7 @@ tls_impl ldap_int_tls_impl = { tlsm_session_version, tlsm_session_cipher, tlsm_session_peercert, + NULL, &tlsm_sbio, diff --git a/libraries/libldap/tls_o.c b/libraries/libldap/tls_o.c index 95fb62890c..d3b6ceb35f 100644 --- a/libraries/libldap/tls_o.c +++ b/libraries/libldap/tls_o.c @@ -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, -- 2.39.5