#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
{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},
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
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);
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;
/* 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;
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;
}
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 );
}
/*
}
}
+ /*
+ * 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;
}
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 );
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;
}
#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;
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 )
tlsg_session_version,
tlsg_session_cipher,
tlsg_session_peercert,
+ tlsg_session_pinning,
&tlsg_sbio,
tlsm_session_version,
tlsm_session_cipher,
tlsm_session_peercert,
+ NULL,
&tlsm_sbio,
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
*/
tlso_session_version,
tlso_session_cipher,
tlso_session_peercert,
+ tlso_session_pinning,
&tlso_sbio,