Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
radsecproxy/tlscommon.c
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
1239 lines (1104 sloc)
37.5 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* Copyright (c) 2007-2009, UNINETT AS | |
* Copyright (c) 2010-2011,2015-2016, NORDUnet A/S */ | |
/* See LICENSE for licensing information. */ | |
#if defined(RADPROT_TLS) || defined(RADPROT_DTLS) | |
#define _GNU_SOURCE | |
#include <stdio.h> | |
#include <signal.h> | |
#include <sys/socket.h> | |
#include <netinet/in.h> | |
#include <netdb.h> | |
#include <string.h> | |
#include <unistd.h> | |
#include <limits.h> | |
#include <fcntl.h> | |
#include <poll.h> | |
#include <sys/time.h> | |
#include <sys/types.h> | |
#include <ctype.h> | |
#include <sys/wait.h> | |
#include <arpa/inet.h> | |
#include <regex.h> | |
#include <libgen.h> | |
#include <pthread.h> | |
#include <openssl/ssl.h> | |
#include <openssl/rand.h> | |
#include <openssl/err.h> | |
#include <openssl/md5.h> | |
#include <openssl/x509v3.h> | |
#include "debug.h" | |
#include "hash.h" | |
#include "util.h" | |
#include "hostport.h" | |
#include "radsecproxy.h" | |
static struct hash *tlsconfs = NULL; | |
#define COOKIE_SECRET_LENGTH 16 | |
static unsigned char cookie_secret[COOKIE_SECRET_LENGTH]; | |
static uint8_t cookie_secret_initialized = 0; | |
struct certattrmatch { | |
int (*matchfn)(GENERAL_NAME *, struct certattrmatch *); | |
int type; | |
char * exact; | |
regex_t *regex; | |
ASN1_OBJECT *oid; | |
struct in6_addr ipaddr; | |
int af; | |
char * debugname; | |
}; | |
/* callbacks for making OpenSSL < 1.1 thread safe */ | |
#if OPENSSL_VERSION_NUMBER < 0x10100000 | |
static pthread_mutex_t *ssl_locks = NULL; | |
#if OPENSSL_VERSION_NUMBER < 0x10000000 | |
unsigned long ssl_thread_id() { | |
return (unsigned long)pthread_self(); | |
} | |
#else | |
void ssl_thread_id(CRYPTO_THREADID *id) { | |
CRYPTO_THREADID_set_numeric(id, (unsigned long)pthread_self()); | |
} | |
#endif | |
void ssl_locking_callback(int mode, int type, const char *file, int line) { | |
if (mode & CRYPTO_LOCK) | |
pthread_mutex_lock(&ssl_locks[type]); | |
else | |
pthread_mutex_unlock(&ssl_locks[type]); | |
} | |
#endif | |
void sslinit() { | |
#if OPENSSL_VERSION_NUMBER < 0x10100000 | |
int i; | |
SSL_library_init(); | |
ssl_locks = OPENSSL_malloc(CRYPTO_num_locks() * sizeof(pthread_mutex_t)); | |
if (!ssl_locks) | |
debugx(1, DBG_ERR, "malloc failed"); | |
for (i = 0; i < CRYPTO_num_locks(); i++) { | |
pthread_mutex_init(&ssl_locks[i], NULL); | |
} | |
#if OPENSSL_VERSION_NUMBER < 0x10000000 | |
CRYPTO_set_id_callback(ssl_thread_id); | |
#else | |
CRYPTO_THREADID_set_callback(ssl_thread_id); | |
#endif | |
CRYPTO_set_locking_callback(ssl_locking_callback); | |
SSL_load_error_strings(); | |
#else | |
OPENSSL_init_ssl(0, NULL); | |
#endif | |
} | |
/** | |
* Print a human readable form of X509_NAME | |
* | |
* This is a direct replacement for X509_NAME_oneline() which should no longer be used. | |
* | |
* @param name The X509_Name to be printed. | |
*/ | |
static char *print_x509_name(X509_NAME *name) { | |
BIO *bio; | |
char *buf; | |
bio = BIO_new(BIO_s_mem()); | |
if (!bio) { | |
debug(DBG_ERR, "getcertsubject: BIO_new failed"); | |
return NULL; | |
} | |
X509_NAME_print_ex(bio, name, 0, XN_FLAG_RFC2253); | |
buf = malloc(BIO_number_written(bio)+1); | |
if (buf) { | |
BIO_read(bio, buf, BIO_number_written(bio)); | |
buf[BIO_number_written(bio)] = '\0'; | |
} else { | |
debug(DBG_ERR, "getcertsubject: malloc failed"); | |
} | |
BIO_free(bio); | |
return buf; | |
} | |
static int pem_passwd_cb(char *buf, int size, int rwflag, void *userdata) { | |
int pwdlen = strlen(userdata); | |
if (rwflag != 0 || pwdlen > size) /* not for decryption or too large */ | |
return 0; | |
memcpy(buf, userdata, pwdlen); | |
return pwdlen; | |
} | |
static int verify_cb(int ok, X509_STORE_CTX *ctx) { | |
char *buf = NULL; | |
X509 *err_cert; | |
int err, depth; | |
err_cert = X509_STORE_CTX_get_current_cert(ctx); | |
err = X509_STORE_CTX_get_error(ctx); | |
depth = X509_STORE_CTX_get_error_depth(ctx); | |
if (depth > MAX_CERT_DEPTH) { | |
ok = 0; | |
err = X509_V_ERR_CERT_CHAIN_TOO_LONG; | |
X509_STORE_CTX_set_error(ctx, err); | |
} | |
if (!ok) { | |
if (err_cert) | |
buf = print_x509_name(X509_get_subject_name(err_cert)); | |
debug(DBG_WARN, "verify error: num=%d:%s:depth=%d:%s", err, X509_verify_cert_error_string(err), depth, buf ? buf : ""); | |
free(buf); | |
buf = NULL; | |
switch (err) { | |
case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT: | |
if (err_cert) { | |
buf = print_x509_name(X509_get_issuer_name(err_cert)); | |
if (buf) { | |
debug(DBG_WARN, "\tIssuer=%s", buf); | |
free(buf); | |
buf = NULL; | |
} | |
} | |
break; | |
case X509_V_ERR_CERT_NOT_YET_VALID: | |
case X509_V_ERR_ERROR_IN_CERT_NOT_BEFORE_FIELD: | |
debug(DBG_WARN, "\tCertificate not yet valid"); | |
break; | |
case X509_V_ERR_CERT_HAS_EXPIRED: | |
debug(DBG_WARN, "Certificate has expired"); | |
break; | |
case X509_V_ERR_ERROR_IN_CERT_NOT_AFTER_FIELD: | |
debug(DBG_WARN, "Certificate no longer valid (after notAfter)"); | |
break; | |
case X509_V_ERR_NO_EXPLICIT_POLICY: | |
debug(DBG_WARN, "No Explicit Certificate Policy"); | |
break; | |
} | |
} | |
#ifdef DEBUG | |
printf("certificate verify returns %d\n", ok); | |
#endif | |
return ok; | |
} | |
static int cookie_calculate_hash(struct sockaddr *peer, time_t time, uint8_t *result, unsigned int *resultlength) { | |
uint8_t *buf; | |
int length; | |
length = SOCKADDRP_SIZE(peer) + sizeof(time_t); | |
buf = OPENSSL_malloc(length); | |
if (!buf) { | |
debug(DBG_ERR, "cookie_calculate_hash: malloc failed"); | |
return 0; | |
} | |
memcpy(buf, &time, sizeof(time_t)); | |
memcpy(buf+sizeof(time_t), peer, SOCKADDRP_SIZE(peer)); | |
HMAC(EVP_sha256(), (const void*) cookie_secret, COOKIE_SECRET_LENGTH, | |
buf, length, result, resultlength); | |
OPENSSL_free(buf); | |
return 1; | |
} | |
static int cookie_generate_cb(SSL *ssl, unsigned char *cookie, unsigned int *cookie_len) { | |
struct sockaddr_storage peer; | |
struct timeval now; | |
uint8_t result[EVP_MAX_MD_SIZE]; | |
unsigned int resultlength; | |
if (!cookie_secret_initialized) { | |
if (!RAND_bytes(cookie_secret, COOKIE_SECRET_LENGTH)) | |
debugx(1,DBG_ERR, "cookie_generate_cg: error generating random secret"); | |
cookie_secret_initialized = 1; | |
} | |
if (BIO_dgram_get_peer(SSL_get_rbio(ssl), &peer) <= 0) | |
return 0; | |
gettimeofday(&now, NULL); | |
if (!cookie_calculate_hash((struct sockaddr *)&peer, now.tv_sec, result, &resultlength)) | |
return 0; | |
memcpy(cookie, &now.tv_sec, sizeof(time_t)); | |
memcpy(cookie + sizeof(time_t), result, resultlength); | |
*cookie_len = resultlength + sizeof(time_t); | |
return 1; | |
} | |
#if OPENSSL_VERSION_NUMBER < 0x10100000 | |
static int cookie_verify_cb(SSL *ssl, unsigned char *cookie, unsigned int cookie_len) { | |
#else | |
static int cookie_verify_cb(SSL *ssl, const unsigned char *cookie, unsigned int cookie_len) { | |
#endif | |
struct sockaddr_storage peer; | |
struct timeval now; | |
time_t cookie_time; | |
uint8_t result[EVP_MAX_MD_SIZE]; | |
unsigned int resultlength; | |
if (!cookie_secret_initialized) | |
return 0; | |
if (cookie_len < sizeof(time_t)) { | |
debug(DBG_DBG, "cookie_verify_cb: cookie too short. ignoring."); | |
return 0; | |
} | |
gettimeofday(&now, NULL); | |
cookie_time = *(time_t *)cookie; | |
if (now.tv_sec - cookie_time > 5) { | |
debug(DBG_DBG, "cookie_verify_cb: cookie invalid or older than 5s. ignoring."); | |
return 0; | |
} | |
if (BIO_dgram_get_peer(SSL_get_rbio(ssl), &peer) <= 0) | |
return 0; | |
if (!cookie_calculate_hash((struct sockaddr *)&peer, cookie_time, result, &resultlength)) | |
return 0; | |
if (resultlength + sizeof(time_t) != cookie_len) { | |
debug(DBG_DBG, "cookie_verify_cb: invalid cookie length. ignoring."); | |
return 0; | |
} | |
if (memcmp(cookie + sizeof(time_t), result, resultlength)) { | |
debug(DBG_DBG, "cookie_verify_cb: cookie not valid. ignoring."); | |
return 0; | |
} | |
return 1; | |
} | |
#ifdef DEBUG | |
static void ssl_info_callback(const SSL *ssl, int where, int ret) { | |
const char *s; | |
int w; | |
w = where & ~SSL_ST_MASK; | |
if (w & SSL_ST_CONNECT) | |
s = "SSL_connect"; | |
else if (w & SSL_ST_ACCEPT) | |
s = "SSL_accept"; | |
else | |
s = "undefined"; | |
if (where & SSL_CB_LOOP) | |
debug(DBG_DBG, "%s:%s\n", s, SSL_state_string_long(ssl)); | |
else if (where & SSL_CB_ALERT) { | |
s = (where & SSL_CB_READ) ? "read" : "write"; | |
debug(DBG_DBG, "SSL3 alert %s:%s:%s\n", s, SSL_alert_type_string_long(ret), SSL_alert_desc_string_long(ret)); | |
} | |
else if (where & SSL_CB_EXIT) { | |
if (ret == 0) | |
debug(DBG_DBG, "%s:failed in %s\n", s, SSL_state_string_long(ssl)); | |
else if (ret < 0) | |
debug(DBG_DBG, "%s:error in %s\n", s, SSL_state_string_long(ssl)); | |
} | |
} | |
#endif | |
static X509_VERIFY_PARAM *createverifyparams(char **poids) { | |
X509_VERIFY_PARAM *pm; | |
ASN1_OBJECT *pobject; | |
int i; | |
pm = X509_VERIFY_PARAM_new(); | |
if (!pm) | |
return NULL; | |
for (i = 0; poids[i]; i++) { | |
pobject = OBJ_txt2obj(poids[i], 0); | |
if (!pobject) { | |
X509_VERIFY_PARAM_free(pm); | |
return NULL; | |
} | |
X509_VERIFY_PARAM_add0_policy(pm, pobject); | |
} | |
X509_VERIFY_PARAM_set_flags(pm, X509_V_FLAG_POLICY_CHECK | X509_V_FLAG_EXPLICIT_POLICY); | |
return pm; | |
} | |
static int tlsaddcacrl(SSL_CTX *ctx, struct tls *conf) { | |
STACK_OF(X509_NAME) *calist; | |
X509_STORE *x509_s; | |
unsigned long error; | |
SSL_CTX_set_cert_store(ctx, X509_STORE_new()); | |
if (!SSL_CTX_load_verify_locations(ctx, conf->cacertfile, conf->cacertpath)) { | |
while ((error = ERR_get_error())) | |
debug(DBG_ERR, "SSL: %s", ERR_error_string(error, NULL)); | |
debug(DBG_ERR, "tlsaddcacrl: Error updating TLS context %s", conf->name); | |
return 0; | |
} | |
calist = conf->cacertfile ? SSL_load_client_CA_file(conf->cacertfile) : NULL; | |
if (!conf->cacertfile || calist) { | |
if (conf->cacertpath) { | |
if (!calist) | |
calist = sk_X509_NAME_new_null(); | |
if (!SSL_add_dir_cert_subjects_to_stack(calist, conf->cacertpath)) { | |
sk_X509_NAME_free(calist); | |
calist = NULL; | |
} | |
} | |
} | |
if (!calist) { | |
while ((error = ERR_get_error())) | |
debug(DBG_ERR, "SSL: %s", ERR_error_string(error, NULL)); | |
debug(DBG_ERR, "tlsaddcacrl: Error adding CA subjects in TLS context %s", conf->name); | |
return 0; | |
} | |
ERR_clear_error(); /* add_dir_cert_subj returns errors on success */ | |
SSL_CTX_set_client_CA_list(ctx, calist); | |
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, verify_cb); | |
SSL_CTX_set_verify_depth(ctx, MAX_CERT_DEPTH + 1); | |
SSL_CTX_set_cookie_generate_cb(ctx, cookie_generate_cb); | |
SSL_CTX_set_cookie_verify_cb(ctx, cookie_verify_cb); | |
if (conf->crlcheck || conf->vpm) { | |
x509_s = SSL_CTX_get_cert_store(ctx); | |
if (conf->crlcheck) | |
X509_STORE_set_flags(x509_s, X509_V_FLAG_CRL_CHECK | X509_V_FLAG_CRL_CHECK_ALL); | |
if (conf->vpm) | |
X509_STORE_set1_param(x509_s, conf->vpm); | |
} | |
debug(DBG_DBG, "tlsaddcacrl: updated TLS context %s", conf->name); | |
return 1; | |
} | |
static SSL_CTX *tlscreatectx(uint8_t type, struct tls *conf) { | |
SSL_CTX *ctx = NULL; | |
unsigned long error; | |
switch (type) { | |
#ifdef RADPROT_TLS | |
case RAD_TLS: | |
#if OPENSSL_VERSION_NUMBER >= 0x10100000 | |
/* TLS_method() was introduced in OpenSSL 1.1.0. */ | |
ctx = SSL_CTX_new(TLS_method()); | |
if (conf->tlsminversion >= 0) | |
SSL_CTX_set_min_proto_version(ctx, conf->tlsminversion); | |
if (conf->tlsmaxversion >= 0) | |
SSL_CTX_set_max_proto_version(ctx, conf->tlsmaxversion); | |
#else | |
/* No TLS_method(), use SSLv23_method() and disable SSLv2 and SSLv3. */ | |
ctx = SSL_CTX_new(SSLv23_method()); | |
SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3); | |
#endif | |
#ifdef DEBUG | |
SSL_CTX_set_info_callback(ctx, ssl_info_callback); | |
#endif | |
break; | |
#endif | |
#ifdef RADPROT_DTLS | |
case RAD_DTLS: | |
#if OPENSSL_VERSION_NUMBER >= 0x10002000 | |
/* DTLS_method() seems to have been introduced in OpenSSL 1.0.2. */ | |
ctx = SSL_CTX_new(DTLS_method()); | |
#if OPENSSL_VERSION_NUMBER >= 0x10100000 | |
if (conf->dtlsminversion >= 0) | |
SSL_CTX_set_min_proto_version(ctx, conf->dtlsminversion); | |
if (conf->dtlsmaxversion >= 0) | |
SSL_CTX_set_max_proto_version(ctx, conf->dtlsmaxversion); | |
#endif | |
#else | |
ctx = SSL_CTX_new(DTLSv1_method()); | |
#endif | |
#ifdef DEBUG | |
SSL_CTX_set_info_callback(ctx, ssl_info_callback); | |
#endif | |
SSL_CTX_set_read_ahead(ctx, 1); | |
break; | |
#endif | |
} | |
if (!ctx) { | |
debug(DBG_ERR, "tlscreatectx: Error initialising SSL/TLS in TLS context %s", conf->name); | |
return NULL; | |
} | |
#if OPENSSL_VERSION_NUMBER < 0x10100000L | |
{ | |
long sslversion = SSLeay(); | |
if (sslversion < 0x00908100L || | |
(sslversion >= 0x10000000L && sslversion < 0x10000020L)) { | |
debug(DBG_WARN, "%s: %s seems to be of a version with a " | |
"certain security critical bug (fixed in OpenSSL 0.9.8p and " | |
"1.0.0b). Disabling OpenSSL session caching for context %p.", | |
__func__, SSLeay_version(SSLEAY_VERSION), ctx); | |
SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_OFF); | |
} | |
} | |
#endif | |
if (conf->certkeypwd) { | |
SSL_CTX_set_default_passwd_cb_userdata(ctx, conf->certkeypwd); | |
SSL_CTX_set_default_passwd_cb(ctx, pem_passwd_cb); | |
} | |
if (!SSL_CTX_use_certificate_chain_file(ctx, conf->certfile) || | |
!SSL_CTX_use_PrivateKey_file(ctx, conf->certkeyfile, SSL_FILETYPE_PEM) || | |
!SSL_CTX_check_private_key(ctx)) { | |
while ((error = ERR_get_error())) | |
debug(DBG_ERR, "SSL: %s", ERR_error_string(error, NULL)); | |
debug(DBG_ERR, "tlscreatectx: Error initialising SSL/TLS in TLS context %s", conf->name); | |
SSL_CTX_free(ctx); | |
return NULL; | |
} | |
if (conf->policyoids) { | |
if (!conf->vpm) { | |
conf->vpm = createverifyparams(conf->policyoids); | |
if (!conf->vpm) { | |
debug(DBG_ERR, "tlscreatectx: Failed to add policyOIDs in TLS context %s", conf->name); | |
SSL_CTX_free(ctx); | |
return NULL; | |
} | |
} | |
} | |
if (!tlsaddcacrl(ctx, conf)) { | |
if (conf->vpm) { | |
X509_VERIFY_PARAM_free(conf->vpm); | |
conf->vpm = NULL; | |
} | |
SSL_CTX_free(ctx); | |
return NULL; | |
} | |
if (conf->cipherlist) { | |
if (!SSL_CTX_set_cipher_list(ctx, conf->cipherlist)) { | |
debug(DBG_ERR, "tlscreatectx: Failed to set cipher list in TLS context %s", conf->name); | |
SSL_CTX_free(ctx); | |
return NULL; | |
} | |
} | |
#if OPENSSL_VERSION_NUMBER >= 0x10101000 | |
if (conf->ciphersuites) { | |
if (!SSL_CTX_set_ciphersuites(ctx, conf->ciphersuites)) { | |
debug(DBG_ERR, "tlscreatectx: Failed to set ciphersuites in TLS context %s", conf->name); | |
SSL_CTX_free(ctx); | |
return NULL; | |
} | |
} | |
#endif | |
if (conf->dhparam) { | |
if (!SSL_CTX_set_tmp_dh(ctx, conf->dhparam)) { | |
while ((error = ERR_get_error())) | |
debug(DBG_WARN, "tlscreatectx: SSL: %s", ERR_error_string(error, NULL)); | |
debug(DBG_WARN, "tlscreatectx: Failed to set dh params. Can continue, but some ciphers might not be available."); | |
} | |
} | |
debug(DBG_DBG, "tlscreatectx: created TLS context %s", conf->name); | |
return ctx; | |
} | |
struct tls *tlsgettls(char *alt1, char *alt2) { | |
struct tls *t; | |
t = hash_read(tlsconfs, alt1, strlen(alt1)); | |
if (!t && alt2) | |
t = hash_read(tlsconfs, alt2, strlen(alt2)); | |
return t; | |
} | |
SSL_CTX *tlsgetctx(uint8_t type, struct tls *t) { | |
struct timeval now; | |
if (!t) | |
return NULL; | |
gettimeofday(&now, NULL); | |
switch (type) { | |
#ifdef RADPROT_TLS | |
case RAD_TLS: | |
if (t->tlsexpiry && t->tlsctx) { | |
if (t->tlsexpiry < now.tv_sec) { | |
t->tlsexpiry = now.tv_sec + t->cacheexpiry; | |
tlsaddcacrl(t->tlsctx, t); | |
} | |
} | |
if (!t->tlsctx) { | |
t->tlsctx = tlscreatectx(RAD_TLS, t); | |
if (t->cacheexpiry) | |
t->tlsexpiry = now.tv_sec + t->cacheexpiry; | |
} | |
return t->tlsctx; | |
#endif | |
#ifdef RADPROT_DTLS | |
case RAD_DTLS: | |
if (t->dtlsexpiry && t->dtlsctx) { | |
if (t->dtlsexpiry < now.tv_sec) { | |
t->dtlsexpiry = now.tv_sec + t->cacheexpiry; | |
tlsaddcacrl(t->dtlsctx, t); | |
} | |
} | |
if (!t->dtlsctx) { | |
t->dtlsctx = tlscreatectx(RAD_DTLS, t); | |
if (t->cacheexpiry) | |
t->dtlsexpiry = now.tv_sec + t->cacheexpiry; | |
} | |
return t->dtlsctx; | |
#endif | |
} | |
return NULL; | |
} | |
void tlsreloadcrls() { | |
struct tls *conf; | |
struct hash_entry *entry; | |
struct timeval now; | |
debug (DBG_NOTICE, "reloading CRLs"); | |
gettimeofday(&now, NULL); | |
for (entry = hash_first(tlsconfs); entry; entry = hash_next(entry)) { | |
conf = (struct tls *)entry->data; | |
#ifdef RADPROT_TLS | |
if (conf->tlsctx) { | |
if (conf->tlsexpiry) | |
conf->tlsexpiry = now.tv_sec + conf->cacheexpiry; | |
tlsaddcacrl(conf->tlsctx, conf); | |
} | |
#endif | |
#ifdef RADPROT_DTLS | |
if (conf->dtlsctx) { | |
if (conf->dtlsexpiry) | |
conf->dtlsexpiry = now.tv_sec + conf->cacheexpiry; | |
tlsaddcacrl(conf->dtlsctx, conf); | |
} | |
#endif | |
} | |
} | |
X509 *verifytlscert(SSL *ssl) { | |
X509 *cert; | |
unsigned long error; | |
if (SSL_get_verify_result(ssl) != X509_V_OK) { | |
debug(DBG_ERR, "verifytlscert: basic validation failed"); | |
while ((error = ERR_get_error())) | |
debug(DBG_ERR, "verifytlscert: TLS: %s", ERR_error_string(error, NULL)); | |
return NULL; | |
} | |
cert = SSL_get_peer_certificate(ssl); | |
if (!cert) | |
debug(DBG_ERR, "verifytlscert: failed to obtain certificate"); | |
return cert; | |
} | |
static int certattr_matchrid(GENERAL_NAME *gn, struct certattrmatch *match) { | |
return OBJ_cmp(gn->d.registeredID, match->oid) == 0 ? 1 : 0; | |
} | |
static int certattr_matchip(GENERAL_NAME *gn, struct certattrmatch *match){ | |
int l = ASN1_STRING_length(gn->d.iPAddress); | |
return (((match->af == AF_INET && l == sizeof(struct in_addr)) || (match->af == AF_INET6 && l == sizeof(struct in6_addr))) | |
&& !memcmp(ASN1_STRING_get0_data(gn->d.iPAddress), &match->ipaddr, l)) ? 1 : 0 ; | |
} | |
static int _general_name_regex_match(char *v, int l, struct certattrmatch *match) { | |
char *s; | |
if (l <= 0 ) | |
return 0; | |
if (match->exact) { | |
if (l == strlen(match->exact) && memcmp(v, match->exact, l) == 0) | |
return 1; | |
return 0; | |
} | |
s = stringcopy((char *)v, l); | |
if (!s) { | |
debug(DBG_ERR, "malloc failed"); | |
return 0; | |
} | |
debug(DBG_DBG, "matchtregex: matching %s", s); | |
if (regexec(match->regex, s, 0, NULL, 0) == 0) { | |
free(s); | |
return 1; | |
} | |
free(s); | |
return 0; | |
} | |
static int certattr_matchregex(GENERAL_NAME *gn, struct certattrmatch *match) { | |
return _general_name_regex_match((char *)ASN1_STRING_get0_data(gn->d.ia5), ASN1_STRING_length(gn->d.ia5), match); | |
} | |
static int certattr_matchothername(GENERAL_NAME *gn, struct certattrmatch *match) { | |
if (OBJ_cmp(gn->d.otherName->type_id, match->oid) != 0) | |
return 0; | |
return _general_name_regex_match((char *)ASN1_STRING_get0_data(gn->d.otherName->value->value.octet_string), | |
ASN1_STRING_length(gn->d.otherName->value->value.octet_string), | |
match); | |
} | |
static int certattr_matchcn(X509 *cert, struct certattrmatch *match){ | |
int loc; | |
X509_NAME *nm; | |
X509_NAME_ENTRY *e; | |
ASN1_STRING *t; | |
nm = X509_get_subject_name(cert); | |
loc = -1; | |
for (;;) { | |
loc = X509_NAME_get_index_by_NID(nm, NID_commonName, loc); | |
if (loc == -1) | |
break; | |
e = X509_NAME_get_entry(nm, loc); | |
t = X509_NAME_ENTRY_get_data(e); | |
if (_general_name_regex_match((char *) ASN1_STRING_get0_data(t), ASN1_STRING_length(t), match)) | |
return 1; | |
} | |
return 0; | |
} | |
/* returns | |
1 if expected type is present and matches | |
0 if expected type is not present | |
-1 if expected type is present but does not match */ | |
static int matchsubjaltname(X509 *cert, struct certattrmatch* match) { | |
GENERAL_NAME *gn; | |
int loc, n,i,r = 0; | |
char *fail = NULL, *tmp, *s; | |
STACK_OF(GENERAL_NAME) *alt; | |
/*special case: don't search in SAN, but CN field in subject */ | |
if (match->type == -1) | |
return certattr_matchcn(cert, match); | |
loc = X509_get_ext_by_NID(cert, NID_subject_alt_name, -1); | |
if (loc < 0) | |
return 0; | |
alt = X509V3_EXT_d2i(X509_get_ext(cert, loc)); | |
if (!alt) | |
return 0; | |
n = sk_GENERAL_NAME_num(alt); | |
for (i = 0; i < n; i++) { | |
gn = sk_GENERAL_NAME_value(alt, i); | |
if (gn->type == match->type) { | |
r = match->matchfn(gn, match); | |
if (r) | |
break; | |
r = -1; | |
} | |
/*legacy print non-matching SAN*/ | |
if (gn->type == GEN_DNS || gn->type == GEN_URI) { | |
s = stringcopy((char *)ASN1_STRING_get0_data(gn->d.ia5), ASN1_STRING_length(gn->d.ia5)); | |
if (!s) continue; | |
tmp = fail; | |
if (asprintf(&fail, "%s%s%s", tmp ? tmp : "", tmp ? ", " : "", s) >= 0) | |
free(tmp); | |
else | |
fail = tmp; | |
free(s); | |
} | |
} | |
if (r<1) | |
debug(DBG_DBG, "matchsubjaltname: no matching Subject Alt Name found! (%s)", fail); | |
free(fail); | |
GENERAL_NAMES_free(alt); | |
return r; | |
} | |
int certnamecheck(X509 *cert, struct hostportres *hp) { | |
int r = 0; | |
struct certattrmatch match; | |
memset(&match, 0, sizeof(struct certattrmatch)); | |
r = 0; | |
if (hp->prefixlen != 255) { | |
/* we disable the check for prefixes */ | |
return 1; | |
} | |
if (inet_pton(AF_INET, hp->host, &match.ipaddr)) | |
match.af = AF_INET; | |
else if (inet_pton(AF_INET6, hp->host, &match.ipaddr)) | |
match.af = AF_INET6; | |
else | |
match.af = 0; | |
match.exact = hp->host; | |
if (match.af) { | |
match.matchfn = &certattr_matchip; | |
match.type = GEN_IPADD; | |
r = matchsubjaltname(cert, &match); | |
} | |
if (!r) { | |
match.matchfn = &certattr_matchregex; | |
match.type = GEN_DNS; | |
r = matchsubjaltname(cert, &match); | |
} | |
if (r) { | |
if (r > 0) { | |
debug(DBG_DBG, "certnamecheck: Found subjectaltname matching %s %s", match.af ? "address" : "host", hp->host); | |
return 1; | |
} | |
debug(DBG_WARN, "certnamecheck: No subjectaltname matching %s %s", match.af ? "address" : "host", hp->host); | |
} else { /* as per RFC 6125 6.4.4: CN MUST NOT be matched if SAN is present */ | |
if (certattr_matchcn(cert, &match)) { | |
debug(DBG_DBG, "certnamecheck: Found cn matching host %s", hp->host); | |
return 1; | |
} | |
debug(DBG_WARN, "certnamecheck: cn not matching host %s", hp->host); | |
} | |
return 0; | |
} | |
int certnamecheckany(X509 *cert, struct list *hostports) { | |
struct list_node *entry; | |
for (entry = list_first(hostports); entry; entry = list_next(entry)) { | |
if (certnamecheck(cert, (struct hostportres *)entry->data)) return 1; | |
} | |
return 0; | |
} | |
int verifyconfcert(X509 *cert, struct clsrvconf *conf, struct hostportres *hpconnected) { | |
char *subject; | |
int ok = 1; | |
struct list_node *entry; | |
subject = getcertsubject(cert); | |
debug(DBG_DBG, "verifyconfcert: verify certificate for host %s, subject %s", conf->name, subject); | |
if (conf->certnamecheck) { | |
debug(DBG_DBG, "verifyconfcert: verify hostname"); | |
if (hpconnected) { | |
if (!certnamecheck(cert, hpconnected)) { | |
debug(DBG_WARN, "verifyconfcert: certificate name check failed for host %s (%s)", conf->name, hpconnected->host); | |
ok = 0; | |
} | |
} else { | |
if (!certnamecheckany(cert, conf->hostports)) { | |
debug(DBG_DBG, "verifyconfcert: no matching CN or SAN found for host %s", conf->name); | |
ok = 0; | |
} | |
} | |
} | |
for (entry = list_first(conf->matchcertattrs); entry; entry = list_next(entry)) { | |
if (matchsubjaltname(cert, (struct certattrmatch *)entry->data) < 1) { | |
debug(DBG_WARN, "verifyconfcert: %s not matching for host %s (%s)", ((struct certattrmatch *)entry->data)->debugname, conf->name, subject); | |
ok = 0; | |
} else { | |
debug(DBG_DBG, "verifyconfcert: %s matching for host %s (%s)", ((struct certattrmatch *)entry->data)->debugname, conf->name, subject); | |
} | |
} | |
free(subject); | |
return ok; | |
} | |
char *getcertsubject(X509 *cert) { | |
return print_x509_name(X509_get_subject_name(cert)); | |
} | |
#if OPENSSL_VERSION_NUMBER >= 0x10100000 | |
static int parse_tls_version(const char* version) { | |
if (!strcasecmp("SSL3", version)) { | |
return SSL3_VERSION; | |
} else if (!strcasecmp("TLS1", version)) { | |
return TLS1_VERSION; | |
} else if (!strcasecmp("TLS1_1", version)) { | |
return TLS1_1_VERSION; | |
} else if (!strcasecmp("TLS1_2", version)) { | |
return TLS1_2_VERSION; | |
#if OPENSSL_VERSION_NUMBER >= 0x10101000 | |
} else if (!strcasecmp("TLS1_3", version)) { | |
return TLS1_3_VERSION; | |
#endif | |
} else if (!strcasecmp("DTLS1", version)) { | |
return DTLS1_VERSION; | |
} else if (!strcasecmp("DTLS1_2", version)) { | |
return DTLS1_2_VERSION; | |
} else if (!strcasecmp("", version)) { | |
return 0; | |
} else { | |
return -1; | |
} | |
} | |
static int conf_tls_version(const char *version, int *min, int *max) { | |
char *ver, *s, *smin, *smax; | |
ver = stringcopy(version, strlen(version)); | |
s = strchr(ver, ':'); | |
if (!s) { | |
smin = smax = ver; | |
} else { | |
*s = '\0'; | |
smin = ver; | |
smax = s+1; | |
} | |
*min = parse_tls_version(smin); | |
*max = parse_tls_version(smax); | |
free(ver); | |
return *min >=0 && *max >=0 && (*max == 0 || *min <= *max); | |
} | |
#endif | |
int conftls_cb(struct gconffile **cf, void *arg, char *block, char *opt, char *val) { | |
struct tls *conf; | |
long int expiry = LONG_MIN; | |
char *tlsversion = NULL; | |
char *dtlsversion = NULL; | |
char *dhfile = NULL; | |
unsigned long error; | |
debug(DBG_DBG, "conftls_cb called for %s", block); | |
conf = malloc(sizeof(struct tls)); | |
if (!conf) { | |
debug(DBG_ERR, "conftls_cb: malloc failed"); | |
return 0; | |
} | |
memset(conf, 0, sizeof(struct tls)); | |
if (!getgenericconfig(cf, block, | |
"CACertificateFile", CONF_STR, &conf->cacertfile, | |
"CACertificatePath", CONF_STR, &conf->cacertpath, | |
"CertificateFile", CONF_STR, &conf->certfile, | |
"CertificateKeyFile", CONF_STR, &conf->certkeyfile, | |
"CertificateKeyPassword", CONF_STR, &conf->certkeypwd, | |
"CacheExpiry", CONF_LINT, &expiry, | |
"CRLCheck", CONF_BLN, &conf->crlcheck, | |
"PolicyOID", CONF_MSTR, &conf->policyoids, | |
"CipherList", CONF_STR, &conf->cipherlist, | |
"CipherSuites", CONF_STR, &conf->ciphersuites, | |
"TlsVersion", CONF_STR, &tlsversion, | |
"DtlsVersion", CONF_STR, &dtlsversion, | |
"DhFile", CONF_STR, &dhfile, | |
NULL | |
)) { | |
debug(DBG_ERR, "conftls_cb: configuration error in block %s", val); | |
goto errexit; | |
} | |
if (!conf->certfile || !conf->certkeyfile) { | |
debug(DBG_ERR, "conftls_cb: TLSCertificateFile and TLSCertificateKeyFile must be specified in block %s", val); | |
goto errexit; | |
} | |
if (!conf->cacertfile && !conf->cacertpath) { | |
debug(DBG_ERR, "conftls_cb: CA Certificate file or path need to be specified in block %s", val); | |
goto errexit; | |
} | |
if (expiry != LONG_MIN) { | |
if (expiry < 0) { | |
debug(DBG_ERR, "error in block %s, value of option CacheExpiry is %ld, may not be negative", val, expiry); | |
goto errexit; | |
} | |
conf->cacheexpiry = expiry; | |
} | |
#if OPENSSL_VERSION_NUMBER >= 0x10100000 | |
/* use -1 as 'not set' value */ | |
conf->tlsminversion = conf->tlsmaxversion = conf->dtlsminversion = conf->dtlsmaxversion = -1; | |
if (tlsversion) { | |
if(!conf_tls_version(tlsversion, &conf->tlsminversion, &conf->tlsmaxversion)) { | |
debug(DBG_ERR, "error in block %s, invalid TlsVersion %s", val, tlsversion); | |
goto errexit; | |
} | |
free (tlsversion); | |
tlsversion = NULL; | |
} | |
if (dtlsversion) { | |
if(!conf_tls_version(dtlsversion, &conf->dtlsminversion, &conf->dtlsmaxversion)) { | |
debug(DBG_ERR, "error in block %s, invalid DtlsVersion %s", val, dtlsversion); | |
goto errexit; | |
} | |
free (dtlsversion); | |
dtlsversion = NULL; | |
} | |
#else | |
if (tlsversion || dtlsversion) { | |
debug(DBG_ERR, "error in block %s, setting tls/dtls version requires openssl 1.1.0 or later", val); | |
goto errexit; | |
} | |
#endif | |
if (dhfile) { | |
FILE *dhfp = fopen(dhfile, "r"); | |
if (dhfp) { | |
conf->dhparam = PEM_read_DHparams(dhfp, NULL, NULL, NULL); | |
fclose(dhfp); | |
if (!conf->dhparam) { | |
while ((error = ERR_get_error())) | |
debug(DBG_ERR, "SSL: %s", ERR_error_string(error, NULL)); | |
debug(DBG_ERR, "error in block %s: Failed to load DhFile %s.", val, dhfile); | |
goto errexit; | |
} | |
} else { | |
debug(DBG_ERR, "error in block %s, DhFile: can't open file %s", val, dhfile); | |
goto errexit; | |
} | |
free(dhfile); | |
dhfile = NULL; | |
} | |
conf->name = stringcopy(val, 0); | |
if (!conf->name) { | |
debug(DBG_ERR, "conftls_cb: malloc failed"); | |
goto errexit; | |
} | |
pthread_mutex_init(&conf->lock, NULL); | |
if (!tlsconfs) | |
tlsconfs = hash_create(); | |
if (!hash_insert(tlsconfs, val, strlen(val), conf)) { | |
debug(DBG_ERR, "conftls_cb: malloc failed"); | |
goto errexit; | |
} | |
if (!tlsgetctx(RAD_TLS, conf)) | |
debug(DBG_ERR, "conftls_cb: error creating ctx for TLS block %s", val); | |
debug(DBG_DBG, "conftls_cb: added TLS block %s", val); | |
return 1; | |
errexit: | |
free(conf->cacertfile); | |
free(conf->cacertpath); | |
free(conf->certfile); | |
free(conf->certkeyfile); | |
free(conf->certkeypwd); | |
freegconfmstr(conf->policyoids); | |
free(tlsversion); | |
free(dtlsversion); | |
free(dhfile); | |
DH_free(conf->dhparam); | |
free(conf); | |
return 0; | |
} | |
static regex_t *compileregex(char *regstr) { | |
regex_t *result; | |
if (regstr[0] != '/') | |
return NULL; | |
regstr++; | |
if (regstr[strlen(regstr) - 1] == '/') | |
regstr[strlen(regstr) - 1] = '\0'; | |
if (!*regstr) | |
return NULL; | |
result = malloc(sizeof(regex_t)); | |
if (!result) { | |
debug(DBG_ERR, "malloc failed"); | |
return NULL; | |
} | |
if (regcomp(result, regstr, REG_EXTENDED | REG_ICASE | REG_NOSUB)) { | |
free(result); | |
debug(DBG_ERR, "failed to compile regular expression %s", regstr); | |
return NULL; | |
} | |
return result; | |
} | |
int addmatchcertattr(struct clsrvconf *conf, const char *match) { | |
struct certattrmatch *certattrmatch; | |
char *pos, *colon, *matchcopy; | |
if (!conf->matchcertattrs) { | |
conf->matchcertattrs = list_create(); | |
} | |
certattrmatch = malloc(sizeof(struct certattrmatch)); | |
if (!certattrmatch) return 0; | |
memset(certattrmatch, 0, sizeof(struct certattrmatch)); | |
matchcopy = stringcopy(match,0); | |
pos = matchcopy; | |
colon = strchr(pos, ':'); | |
if (!colon) goto errexit; | |
if (strncasecmp(pos, "CN", colon - pos) == 0) { | |
if(!(certattrmatch->regex = compileregex(colon+1))) goto errexit; | |
certattrmatch->type = -1; | |
certattrmatch->matchfn = NULL; /*special case: don't search in SAN, but CN field in subject */ | |
} | |
else if (strncasecmp(pos, "SubjectAltName", colon - pos) == 0) { | |
pos = colon+1; | |
colon = strchr(pos, ':'); | |
if (!colon) goto errexit; | |
if (strncasecmp(pos, "IP", colon - pos) == 0) { | |
pos = colon+1; | |
if (inet_pton(AF_INET, pos, &certattrmatch->ipaddr)) | |
certattrmatch->af = AF_INET; | |
else if (inet_pton(AF_INET6, pos, &certattrmatch->ipaddr)) | |
certattrmatch->af = AF_INET6; | |
else | |
goto errexit; | |
certattrmatch->type = GEN_IPADD; | |
certattrmatch->matchfn = &certattr_matchip; | |
} | |
else if(strncasecmp(pos, "URI", colon - pos) == 0) { | |
if(!(certattrmatch->regex = compileregex(colon+1))) goto errexit; | |
certattrmatch->type = GEN_URI; | |
certattrmatch->matchfn = &certattr_matchregex; | |
} | |
else if(strncasecmp(pos, "DNS", colon - pos) == 0) { | |
if(!(certattrmatch->regex = compileregex(colon+1))) goto errexit; | |
certattrmatch->type = GEN_DNS; | |
certattrmatch->matchfn = &certattr_matchregex; | |
} | |
else if(strncasecmp(pos, "rID", colon - pos) == 0) { | |
certattrmatch->oid = OBJ_txt2obj(colon+1, 0); | |
if (!certattrmatch->oid) goto errexit; | |
certattrmatch->type = GEN_RID; | |
certattrmatch->matchfn = &certattr_matchrid; | |
} | |
else if(strncasecmp(pos, "otherNAme", colon - pos) == 0){ | |
pos = colon+1; | |
colon = strchr(pos, ':'); | |
if(!colon) goto errexit; | |
*colon = '\0'; | |
if(!(certattrmatch->oid = OBJ_txt2obj(pos,0))) goto errexit; | |
if(!(certattrmatch->regex = compileregex(colon+1))) goto errexit; | |
certattrmatch->type = GEN_OTHERNAME; | |
certattrmatch->matchfn = &certattr_matchothername; | |
} | |
else goto errexit; | |
} | |
else goto errexit; | |
certattrmatch->debugname = stringcopy(match, 0); | |
if(!list_push(conf->matchcertattrs, certattrmatch)) goto errexit; | |
free(matchcopy); | |
return 1; | |
errexit: | |
free(certattrmatch); | |
free(matchcopy); | |
return 0; | |
} | |
void freematchcertattr(struct clsrvconf *conf) { | |
struct list_node *entry; | |
struct certattrmatch *match; | |
if (conf->matchcertattrs) { | |
for (entry = list_first(conf->matchcertattrs); entry; entry=list_next(entry)) { | |
match = ((struct certattrmatch*)entry->data); | |
free(match->debugname); | |
free(match->exact); | |
ASN1_OBJECT_free(match->oid); | |
if(match->regex) | |
regfree(match->regex); | |
free(match->regex); | |
} | |
list_destroy(conf->matchcertattrs); | |
conf->matchcertattrs = NULL; | |
} | |
} | |
int sslaccepttimeout (SSL *ssl, int timeout) { | |
int socket, origflags, ndesc, r = -1, sockerr = 0; | |
socklen_t errlen = sizeof(sockerr); | |
struct pollfd fds[1]; | |
uint8_t want_write = 1; | |
socket = SSL_get_fd(ssl); | |
origflags = fcntl(socket, F_GETFL, 0); | |
if (origflags == -1) { | |
debugerrno(errno, DBG_WARN, "Failed to get flags"); | |
return -1; | |
} | |
if (fcntl(socket, F_SETFL, origflags | O_NONBLOCK) == -1) { | |
debugerrno(errno, DBG_WARN, "Failed to set O_NONBLOCK"); | |
return -1; | |
} | |
while (r < 1) { | |
fds[0].fd = socket; | |
fds[0].events = POLLIN; | |
if (want_write) { | |
fds[0].events |= POLLOUT; | |
want_write = 0; | |
} | |
if ((ndesc = poll(fds, 1, timeout * 1000)) < 1) { | |
if (ndesc == 0) | |
debug(DBG_DBG, "sslaccepttimeout: timeout during SSL_accept"); | |
else | |
debugerrno(errno, DBG_DBG, "sslaccepttimeout: poll error"); | |
break; | |
} | |
if (fds[0].revents & POLLERR) { | |
if(!getsockopt(socket, SOL_SOCKET, SO_ERROR, (void *)&sockerr, &errlen)) | |
debug(DBG_WARN, "SSL Accept failed: %s", strerror(sockerr)); | |
else | |
debug(DBG_WARN, "SSL Accept failed: unknown error"); | |
} else if (fds[0].revents & POLLHUP) { | |
debug(DBG_WARN, "SSL Accept error: hang up"); | |
} else if (fds[0].revents & POLLNVAL) { | |
debug(DBG_WARN, "SSL Accept error: fd not open"); | |
} else { | |
r = SSL_accept(ssl); | |
if (r <= 0) { | |
switch (SSL_get_error(ssl, r)) { | |
case SSL_ERROR_WANT_WRITE: | |
want_write = 1; | |
case SSL_ERROR_WANT_READ: | |
continue; | |
} | |
} | |
} | |
break; | |
} | |
if (fcntl(socket, F_SETFL, origflags) == -1) | |
debugerrno(errno, DBG_WARN, "Failed to set original flags back"); | |
return r; | |
} | |
int sslconnecttimeout(SSL *ssl, int timeout) { | |
int socket, origflags, ndesc, r = -1, sockerr = 0; | |
socklen_t errlen = sizeof(sockerr); | |
struct pollfd fds[1]; | |
uint8_t want_write = 1; | |
socket = SSL_get_fd(ssl); | |
origflags = fcntl(socket, F_GETFL, 0); | |
if (origflags == -1) { | |
debugerrno(errno, DBG_WARN, "Failed to get flags"); | |
return -1; | |
} | |
if (fcntl(socket, F_SETFL, origflags | O_NONBLOCK) == -1) { | |
debugerrno(errno, DBG_WARN, "Failed to set O_NONBLOCK"); | |
return -1; | |
} | |
while (r < 1) { | |
fds[0].fd = socket; | |
fds[0].events = POLLIN; | |
if (want_write) { | |
fds[0].events |= POLLOUT; | |
want_write = 0; | |
} | |
if ((ndesc = poll(fds, 1, timeout * 1000)) < 1) { | |
if (ndesc == 0) | |
debug(DBG_DBG, "sslconnecttimeout: timeout during SSL_connect"); | |
else | |
debugerrno(errno, DBG_DBG, "sslconnecttimeout: poll error"); | |
break; | |
} | |
if (fds[0].revents & POLLERR) { | |
if(!getsockopt(socket, SOL_SOCKET, SO_ERROR, (void *)&sockerr, &errlen)) | |
debug(DBG_WARN, "SSL Connection failed: %s", strerror(sockerr)); | |
else | |
debug(DBG_WARN, "SSL Connection failed: unknown error"); | |
} else if (fds[0].revents & POLLHUP) { | |
debug(DBG_WARN, "SSL Connect error: hang up"); | |
} else if (fds[0].revents & POLLNVAL) { | |
debug(DBG_WARN, "SSL Connect error: fd not open"); | |
} else { | |
r = SSL_connect(ssl); | |
if (r <= 0) { | |
switch (SSL_get_error(ssl, r)) { | |
case SSL_ERROR_WANT_WRITE: | |
want_write = 1; | |
case SSL_ERROR_WANT_READ: | |
continue; | |
} | |
} | |
} | |
break; | |
} | |
if (fcntl(socket, F_SETFL, origflags) == -1) | |
debugerrno(errno, DBG_WARN, "Failed to set original flags back"); | |
return r; | |
} | |
#else | |
/* Just to makes file non-empty, should rather avoid compiling this file when not needed */ | |
static void tlsdummy() { | |
} | |
#endif | |
/* Local Variables: */ | |
/* c-file-style: "stroustrup" */ | |
/* End: */ |