ITS#8753 Public key pinning support in libldap

This commit is contained in:
Ondřej Kuzník 2017-11-07 18:35:33 +00:00
parent 91ebfc82ea
commit 8e34ed8c78
9 changed files with 287 additions and 1 deletions

View 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

View 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},

View 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

View 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;

View 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;
}

View 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;
}

View 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,

View file

@ -3386,6 +3386,7 @@ tls_impl ldap_int_tls_impl = {
tlsm_session_version,
tlsm_session_cipher,
tlsm_session_peercert,
NULL,
&tlsm_sbio,

View 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,