mirror of
https://git.openldap.org/openldap/openldap.git
synced 2026-01-04 06:01:23 -05:00
ITS#8753 Public key pinning support in libldap
This commit is contained in:
parent
91ebfc82ea
commit
8e34ed8c78
9 changed files with 287 additions and 1 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
||||
|
|
|
|||
|
|
@ -3386,6 +3386,7 @@ tls_impl ldap_int_tls_impl = {
|
|||
tlsm_session_version,
|
||||
tlsm_session_cipher,
|
||||
tlsm_session_peercert,
|
||||
NULL,
|
||||
|
||||
&tlsm_sbio,
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue