From 6128ea36acb6ec5621b3666fb6fc6104ad7183a0 Mon Sep 17 00:00:00 2001 From: Gerhard Rieger Date: Thu, 31 Dec 2020 12:06:32 +0100 Subject: [PATCH] OpenSSL client checks SubjectAltName IP addresses --- CHANGES | 10 ++- test.sh | 206 +++++++++++++++++++++++++++++++++++++++++++++++--- xio-ip6.c | 26 +++++++ xio-ip6.h | 5 ++ xio-openssl.c | 101 ++++++++++++++++++------- 5 files changed, 305 insertions(+), 43 deletions(-) diff --git a/CHANGES b/CHANGES index fa28372..bc8c249 100644 --- a/CHANGES +++ b/CHANGES @@ -120,22 +120,24 @@ Testing: * renamed testaddrs() to testfeats(), and introduced new testaddrs() New features: -<<<<<<< HEAD GOPEN and UNIX-CLIENT addresses now support sockets of type SEQPACKET. Test: GOPENUNIXSEQPACKET Feature suggested by vi0oss. -Features: The generic setsockopt-int and related options are, in case of listening/accepting addresses, applied to the connected socket(s). To enable setting options on the listening socket, a new option setsockopt-listen has been implemented. See the documentation for info on data types. Tests: SETSOCKOPT SETSOCKOPT_LISTEN Thanks to Steven Danna and Korian Edeline for reporting this issue. -======= + Filan option -S gives short description like -s but with improved format ->>>>>>> 7cd82d9... Fixed filan -s, added -S + + Socat OpenSSL client, when server was specified using IP address, did + not verify connection on certificates SubjectAltName IP entries. + Tests: OPENSSL_SERVERALTAUTH OPENSSL_SERVERALTIP4AUTH OPENSSL_SERVERALTIP6AUTH + Fixes Red Hat bug 1805132 ####################### V 1.7.3.4: diff --git a/test.sh b/test.sh index 434d912..e30b385 100755 --- a/test.sh +++ b/test.sh @@ -99,6 +99,7 @@ REUSEADDR=reuseaddr # use this with LISTEN addresses and bind options # SSL certificate contents TESTCERT_CONF=testcert.conf TESTCERT6_CONF=testcert6.conf +TESTALT_CONF=testalt.conf # TESTCERT_COMMONNAME="$LOCALHOST" TESTCERT_COMMONNAME6="$LOCALHOST6" @@ -123,6 +124,7 @@ commonName=$TESTCERT_COMMONNAME O=$TESTCERT_ORGANIZATIONNAME OU=$TESTCERT_ORGANIZATIONALUNITNAME L=$TESTCERT_LOCALITYNAME + EOF cat >$TESTCERT6_CONF <$TESTALT_CONF </dev/null 2>&1 + #openssl req -new -config $TESTCERT_CONF -key $name.key -x509 -out $name.crt -days 3653 -extensions v3_ca >/dev/null 2>&1 openssl req -new -config $TESTCERT_CONF -key $name.key -x509 -out $name.crt -days 3653 >/dev/null 2>&1 cat $name.key $name.crt testcert.dh >$name.pem } @@ -2437,6 +2470,18 @@ gentestcert6 () { cat $name.key $name.crt >$name.pem } +# generate a server certificate and key with SubjectAltName +gentestaltcert () { + local name="$1" + if ! [ -f testcert.dh ]; then + openssl dhparam -out testcert.dh $RSABITS + fi + if [ -s $name.key -a -s $name.crt -a -s $name.pem ]; then return; fi + openssl genrsa $OPENSSL_RAND -out $name.key $RSABITS >/dev/null 2>&1 + openssl req -new -config $TESTALT_CONF -key $name.key -x509 -out $name.crt -days 3653 >/dev/null 2>&1 + cat $name.key $name.crt testcert.dh >$name.pem +} + NAME=UNISTDIO case "$TESTS " in @@ -4209,7 +4254,6 @@ te="$td/test$N.stderr" tdiff="$td/test$N.diff" da="test$N $(date) $RANDOM" CMD2="$TRACE $SOCAT $opts exec:'openssl s_server -accept "$PORT" -quiet -cert testsrv.pem' pipe" -#! CMD="$TRACE $SOCAT $opts - openssl:$LOCALHOST:$PORT" CMD="$TRACE $SOCAT $opts - openssl:$LOCALHOST:$PORT,pf=ip4,verify=0,$SOCAT_EGD" printf "test $F_n $TEST... " $N eval "$CMD2 2>\"${te}1\" &" @@ -4413,7 +4457,7 @@ OPENSSL6SERVER OPENSSL tcp6 OPENSSL-LISTEN:\$PORT,pf=ip6,$SOCAT_EGD,cert=tests NAME=OPENSSL_SERVERAUTH case "$TESTS" in *%$N%*|*%functions%*|*%openssl%*|*%tcp%*|*%tcp4%*|*%ip4%*|*%$NAME%*) -TEST="$NAME: openssl server authentication" +TEST="$NAME: OpenSSL server authentication (hostname)" if ! eval $NUMCOND; then :; elif ! testfeats openssl >/dev/null; then $PRINTF "test $F_n $TEST... ${YELLOW}OPENSSL not available${NORMAL}\n" $N @@ -4430,19 +4474,19 @@ tf="$td/test$N.stdout" te="$td/test$N.stderr" tdiff="$td/test$N.diff" da="test$N $(date) $RANDOM" -CMD2="$TRACE $SOCAT $opts OPENSSL-LISTEN:$PORT,$REUSEADDR,$SOCAT_EGD,cert=testsrv.crt,key=testsrv.key,verify=0 pipe" -CMD="$TRACE $SOCAT $opts - openssl:$LOCALHOST:$PORT,verify=1,cafile=testsrv.crt,$SOCAT_EGD" +CMD0="$TRACE $SOCAT $opts OPENSSL-LISTEN:$PORT,$REUSEADDR,$SOCAT_EGD,cert=testsrv.crt,key=testsrv.key,verify=0 pipe" +CMD1="$TRACE $SOCAT $opts - OPENSSL:$LOCALHOST:$PORT,verify=1,cafile=testsrv.crt,$SOCAT_EGD" printf "test $F_n $TEST... " $N -eval "$CMD2 2>\"${te}1\" &" +eval "$CMD0 2>\"${te}0\" &" pid=$! # background process id waittcp4port $PORT -echo "$da" |$CMD >$tf 2>"${te}2" +echo "$da" |$CMD1 >$tf 2>"${te}1" if ! echo "$da" |diff - "$tf" >"$tdiff"; then $PRINTF "$FAILED: $TRACE $SOCAT:\n" - echo "$CMD2 &" - echo "$CMD" + echo "$CMD0 &" + cat "${te}0" + echo "$CMD1" cat "${te}1" - cat "${te}2" cat "$tdiff" numFAIL=$((numFAIL+1)) listFAIL="$listFAIL $N" @@ -14069,6 +14113,148 @@ PORT=$((PORT+1)) N=$((N+1)) +NAME=OPENSSL_SERVERALTAUTH +case "$TESTS" in +*%$N%*|*%functions%*|*%openssl%*|*%tcp%*|*%tcp4%*|*%ip4%*|*%$NAME%*) +TEST="$NAME: OpenSSL server authentication with SubjectAltName (hostname)" +if ! eval $NUMCOND; then :; +elif ! testfeats openssl >/dev/null; then + $PRINTF "test $F_n $TEST... ${YELLOW}OPENSSL not available${NORMAL}\n" $N + numCANT=$((numCANT+1)) + listCANT="$listCANT $N" +elif ! testfeats listen tcp ip4 >/dev/null || ! runsip4 >/dev/null; then + $PRINTF "test $F_n $TEST... ${YELLOW}TCP/IPv4 not available${NORMAL}\n" $N + numCANT=$((numCANT+1)) + listCANT="$listCANT $N" +else +gentestaltcert testalt +tf="$td/test$N.stdout" +te="$td/test$N.stderr" +tdiff="$td/test$N.diff" +da="test$N $(date) $RANDOM" +CMD0="$TRACE $SOCAT $opts OPENSSL-LISTEN:$PORT,$REUSEADDR,$SOCAT_EGD,cert=testalt.crt,key=testalt.key,verify=0 pipe" +CMD1="$TRACE $SOCAT $opts - OPENSSL:$LOCALHOST:$PORT,verify=1,cafile=testalt.crt,$SOCAT_EGD" +printf "test $F_n $TEST... " $N +eval "$CMD0 2>\"${te}0\" &" +pid=$! # background process id +waittcp4port $PORT +echo "$da" |$CMD1 >$tf 2>"${te}1" +if ! echo "$da" |diff - "$tf" >"$tdiff"; then + $PRINTF "$FAILED: $TRACE $SOCAT:\n" + echo "$CMD0 &" >&2 + cat "${te}0" >&2 + echo "$CMD1" >&2 + cat "${te}1" >&2 + cat "$tdiff" >&2 + numFAIL=$((numFAIL+1)) + listFAIL="$listFAIL $N" +else + $PRINTF "$OK\n" + if [ -n "$debug" ]; then cat "${te}1" "${te}2"; fi + numOK=$((numOK+1)) +fi +kill $pid 2>/dev/null +wait +fi ;; # NUMCOND, feats +esac +PORT=$((PORT+1)) +N=$((N+1)) + +NAME=OPENSSL_SERVERALTIP4AUTH +case "$TESTS" in +*%$N%*|*%functions%*|*%openssl%*|*%tcp%*|*%tcp4%*|*%ip4%*|*%$NAME%*) +TEST="$NAME: OpenSSL server authentication with SubjectAltName (IPv4 address)" +if ! eval $NUMCOND; then :; +elif ! testfeats openssl >/dev/null; then + $PRINTF "test $F_n $TEST... ${YELLOW}OPENSSL not available${NORMAL}\n" $N + numCANT=$((numCANT+1)) + listCANT="$listCANT $N" +elif ! testfeats listen tcp ip4 openssl >/dev/null || ! runsip4 >/dev/null; then + $PRINTF "test $F_n $TEST... ${YELLOW}TCP/IPv4 not available${NORMAL}\n" $N + numCANT=$((numCANT+1)) + listCANT="$listCANT $N" +else +gentestaltcert testalt +tf="$td/test$N.stdout" +te="$td/test$N.stderr" +tdiff="$td/test$N.diff" +da="test$N $(date) $RANDOM" +CMD0="$TRACE $SOCAT $opts OPENSSL-LISTEN:$PORT,$REUSEADDR,$SOCAT_EGD,cert=testalt.crt,key=testalt.key,verify=0 pipe" +CMD1="$TRACE $SOCAT $opts - OPENSSL:127.0.0.1:$PORT,verify=1,cafile=testalt.crt,$SOCAT_EGD" +printf "test $F_n $TEST... " $N +eval "$CMD0 2>\"${te}0\" &" +pid=$! # background process id +waittcp4port $PORT +echo "$da" |$CMD1 >$tf 2>"${te}1" +if ! echo "$da" |diff - "$tf" >"$tdiff"; then + $PRINTF "$FAILED: $TRACE $SOCAT:\n" + echo "$CMD0 &" >&2 + cat "${te}0" >&2 + echo "$CMD1" >&2 + cat "${te}1" >&2 + cat "$tdiff" >&2 + numFAIL=$((numFAIL+1)) + listFAIL="$listFAIL $N" +else + $PRINTF "$OK\n" + if [ -n "$debug" ]; then cat "${te}1" "${te}2"; fi + numOK=$((numOK+1)) +fi +kill $pid 2>/dev/null +wait +fi ;; # NUMCOND, feats +esac +PORT=$((PORT+1)) +N=$((N+1)) + +NAME=OPENSSL_SERVERALTIP6AUTH +case "$TESTS" in +*%$N%*|*%functions%*|*%openssl%*|*%tcp%*|*%tcp6%*|*%ip6%*|*%$NAME%*) +TEST="$NAME: OpenSSL server authentication with SubjectAltName (IPv6 address)" +if ! eval $NUMCOND; then :; +elif ! testfeats openssl >/dev/null; then + $PRINTF "test $F_n $TEST... ${YELLOW}OPENSSL not available${NORMAL}\n" $N + numCANT=$((numCANT+1)) + listCANT="$listCANT $N" +elif ! testfeats listen tcp ip6 openssl >/dev/null || ! runsip6 >/dev/null; then + $PRINTF "test $F_n $TEST... ${YELLOW}TCP/IPv6 not available${NORMAL}\n" $N + numCANT=$((numCANT+1)) + listCANT="$listCANT $N" +else +gentestaltcert testalt +tf="$td/test$N.stdout" +te="$td/test$N.stderr" +tdiff="$td/test$N.diff" +da="test$N $(date) $RANDOM" +CMD0="$TRACE $SOCAT $opts OPENSSL-LISTEN:$PORT,pf=ip6,$REUSEADDR,$SOCAT_EGD,cert=testalt.crt,key=testalt.key,verify=0 pipe" +CMD1="$TRACE $SOCAT $opts - OPENSSL:[::1]:$PORT,verify=1,cafile=testalt.crt,$SOCAT_EGD" +printf "test $F_n $TEST... " $N +eval "$CMD0 2>\"${te}0\" &" +pid=$! # background process id +waittcp6port $PORT +echo "$da" |$CMD1 >$tf 2>"${te}1" +if ! echo "$da" |diff - "$tf" >"$tdiff"; then + $PRINTF "$FAILED: $TRACE $SOCAT:\n" + echo "$CMD0 &" >&2 + cat "${te}0" >&2 + echo "$CMD1" >&2 + cat "${te}1" >&2 + cat "$tdiff" >&2 + numFAIL=$((numFAIL+1)) + listFAIL="$listFAIL $N" +else + $PRINTF "$OK\n" + if [ -n "$debug" ]; then cat "${te}1" "${te}2"; fi + numOK=$((numOK+1)) +fi +kill $pid 2>/dev/null +wait +fi ;; # NUMCOND, feats +esac +PORT=$((PORT+1)) +N=$((N+1)) + + ################################################################################## #================================================================================= # here come tests that might affect your systems integrity. Put normal tests diff --git a/xio-ip6.c b/xio-ip6.c index e051797..8754198 100644 --- a/xio-ip6.c +++ b/xio-ip6.c @@ -75,6 +75,32 @@ const struct optdesc opt_ipv6_recvtclass = { "ipv6-recvtclass", "recvtclass", OP const struct optdesc opt_ipv6_recvpathmtu = { "ipv6-recvpathmtu", "recvpathmtu", OPT_IPV6_RECVPATHMTU, GROUP_SOCK_IP6, PH_PASTSOCKET, TYPE_INT, OFUNC_SOCKOPT, SOL_IPV6, IPV6_RECVPATHMTU }; #endif +/* Returns canonical form of IPv6 address. + IPv6 address may bei enclose in brackets. + Returns STAT_OK on success, STAT_NORETRY on failure. */ +int xioip6_pton(const char *src, struct in6_addr *dst) { + union sockaddr_union sockaddr; + socklen_t sockaddrlen = sizeof(sockaddr); + + if (src[0] == '[') { + char plainaddr[INET6_ADDRSTRLEN]; + char *clos; + + strncpy(plainaddr, src+1, INET6_ADDRSTRLEN); + plainaddr[INET6_ADDRSTRLEN-1] = '\0'; + if ((clos = strchr(plainaddr, ']')) != NULL) + *clos = '\0'; + return xioip6_pton(plainaddr, dst); + } + if (xiogetaddrinfo(src, NULL, PF_INET6, 0, 0, &sockaddr, &sockaddrlen, + 0, 0) + != STAT_OK) { + return STAT_NORETRY; + } + *dst = sockaddr.ip6.sin6_addr; + return STAT_OK; +} + int xioparsenetwork_ip6(const char *rangename, struct xiorange *range) { char *delimpos; /* absolute address of delimiter */ size_t delimind; /* index of delimiter in string */ diff --git a/xio-ip6.h b/xio-ip6.h index 905ca08..e99e559 100644 --- a/xio-ip6.h +++ b/xio-ip6.h @@ -7,6 +7,10 @@ #if WITH_IP6 +#ifndef INET6_ADDRSTRLEN +# define INET6_ADDRSTRLEN 46 +#endif + extern const struct optdesc opt_ipv6_v6only; extern const struct optdesc opt_ipv6_join_group; extern const struct optdesc opt_ipv6_pktinfo; @@ -27,6 +31,7 @@ extern const struct optdesc opt_ipv6_tclass; extern const struct optdesc opt_ipv6_recvtclass; extern const struct optdesc opt_ipv6_recvpathmtu; +extern int xioip6_pton(const char *src, struct in6_addr *dst); extern int xioparsenetwork_ip6(const char *rangename, struct xiorange *range); extern int xiorange_ip6andmask(struct xiorange *range); diff --git a/xio-openssl.c b/xio-openssl.c index 78369d7..6cb926d 100644 --- a/xio-openssl.c +++ b/xio-openssl.c @@ -16,6 +16,8 @@ #include "xio-listen.h" #include "xio-udp.h" #include "xio-ipapp.h" +#include "xio-ip6.h" + #include "xio-openssl.h" /* the openssl library requires a file descriptor for external communications. @@ -1534,31 +1536,30 @@ static int openssl_setenv_cert_fields(const char *field, X509_NAME *name) { supports wildcard cn like *.domain which matches domain and host.domain returns true on match */ -static bool openssl_check_name(const char *cn, const char *peername) { +static bool openssl_check_name(const char *nametype, const char *cn, const char *peername) { const char *dotp; if (peername == NULL) { - Info1("commonName \"%s\": no peername", cn); + Info2("%s \"%s\": no peername", nametype, cn); return false; } else if (peername[0] == '\0') { - Info1("commonName \"%s\": matched by empty peername", cn); + Info2("%s \"%s\": matched by empty peername", nametype, cn); return true; } if (! (cn[0] == '*' && cn[1] == '.')) { /* normal server name - this is simple */ - Debug1("commonName \"%s\" has no wildcard", cn); if (strcmp(cn, peername) == 0) { - Debug2("commonName \"%s\" matches peername \"%s\"", cn, peername); + Debug3("%s \"%s\" matches peername \"%s\"", nametype, cn, peername); return true; } else { - Info2("commonName \"%s\" does not match peername \"%s\"", cn, peername); + Info3("%s \"%s\" does not match peername \"%s\"", nametype, cn, peername); return false; } } /* wildcard cert */ - Debug1("commonName \"%s\" is a wildcard name", cn); + Debug2("%s \"%s\" is a wildcard name", nametype, cn); /* case: just the base domain */ if (strcmp(cn+2, peername) == 0) { - Debug2("wildcard commonName \"%s\" matches base domain \"%s\"", cn, peername); + Debug3("wildcard %s \"%s\" matches base domain \"%s\"", nametype, cn, peername); return true; } /* case: subdomain; only one level! */ @@ -1569,10 +1570,10 @@ static bool openssl_check_name(const char *cn, const char *peername) { return false; } if (strcmp(cn+1, dotp) != 0) { - Info2("commonName \"%s\" does not match subdomain peername \"%s\"", cn, peername); + Info3("%s \"%s\" does not match subdomain peername \"%s\"", nametype, cn, peername); return false; } - Debug2("commonName \"%s\" matches subdomain peername \"%s\"", cn, peername); + Debug3("%s \"%s\" matches subdomain peername \"%s\"", nametype, cn, peername); return true; } @@ -1595,7 +1596,7 @@ static bool openssl_check_peername(X509_NAME *name, const char *peername) { #else text = ASN1_STRING_data(data); #endif - return openssl_check_name((const char *)text, peername); + return openssl_check_name("commonName", (const char *)text, peername); } /* retrieves certificate provided by peer, sets env vars containing @@ -1605,6 +1606,9 @@ static bool openssl_check_peername(X509_NAME *name, const char *peername) { http://etutorials.org/Programming/secure+programming/Chapter+10.+Public+Key+Infrastructure/10.8+Adding+Hostname+Checking+to+Certificate+Verification/ The code examples in this tutorial do not seem to have explicit license restrictions. */ +/* peername is, with OpenSSL client, the server name, or the value of option + commonname if provided; + With OpenSSL server, it is the value of option commonname */ static int openssl_handle_peer_certificate(struct single *xfd, const char *peername, bool opt_ver, int level) { @@ -1658,9 +1662,17 @@ static int openssl_handle_peer_certificate(struct single *xfd, openssl_setenv_cert_name("issuer", issuername); } + if (!opt_ver) { + Notice("option openssl-verify disabled, no check of certificate"); + X509_free(peer_cert); + return STAT_OK; + } + /* check peername against cert's subjectAltName DNS entries */ /* this code is based on example from Gerhard Gappmeier in http://openssl.6102.n7.nabble.com/How-to-extract-subjectAltName-td17236.html + and the GEN_IPADD from + http://openssl.6102.n7.nabble.com/reading-IP-addresses-from-Subject-Alternate-Name-extension-td29245.html */ if ((extcount = X509_get_ext_count(peer_cert)) > 0) { for (i = 0; !ok && i < extcount; ++i) { @@ -1680,50 +1692,81 @@ static int openssl_handle_peer_certificate(struct single *xfd, /* get amount of alternatives, RFC2459 claims there MUST be at least one, but we don't depend on it... */ numalts = sk_GENERAL_NAME_num ( names ); /* loop through all alternatives */ - for ( i=0; ( itype ) { + switch (pName->type) { case GEN_DNS: - ASN1_STRING_to_UTF8(&pBuffer, -pName->d.ia5); + ASN1_STRING_to_UTF8(&pBuffer, pName->d.ia5); xiosetenv("OPENSSL_X509V3_SUBJECTALTNAME_DNS", (char *)pBuffer, 2, " // "); if (peername != NULL && - openssl_check_name((char *)pBuffer, /*const char*/peername)) { + openssl_check_name("subjectAltName", (char *)pBuffer, /*const char*/peername)) { ok = 1; } OPENSSL_free(pBuffer); break; - default: continue; + case GEN_IPADD: + { + /* binary address format */ + const unsigned char *data = pName->d.iPAddress->data; + size_t len = pName->d.iPAddress->length; + char aBuffer[INET6_ADDRSTRLEN]; /* canonical peername */ + struct in6_addr ip6bin; + + switch (len) { + case 4: /* IPv4 */ + snprintf(aBuffer, sizeof(aBuffer), "%u.%u.%u.%u", data[0], data[1], data[2], data[3]); + if (peername != NULL && + openssl_check_name("subjectAltName", aBuffer, /*const char*/peername)) { + ok = 1; + } + break; + case 16: /* IPv6 */ + inet_ntop(AF_INET6, data, aBuffer, sizeof(aBuffer)); + xioip6_pton(peername, &ip6bin); + if (memcmp(data, &ip6bin, sizeof(ip6bin)) == 0) { + Debug2("subjectAltName \"%s\" matches peername \"%s\"", + aBuffer, peername); + ok = 1; + } else { + Info2("subjectAltName \"%s\" does not match peername \"%s\"", + aBuffer, peername); + } + break; + } + xiosetenv("OPENSSL_X509V3_SUBJECTALTNAME_IPADD", (char *)aBuffer, 2, " // "); + } + break; + default: Warn3("Unknown subject type %d (GEN_DNS=%d, GEN_IPADD=%d", + pName->type, GEN_DNS, GEN_IPADD); + continue; } + if (ok) { break; } } } } } } - if (!opt_ver) { - Notice("option openssl-verify disabled, no check of certificate"); - X509_free(peer_cert); - return STAT_OK; - } - if (peername == NULL || peername[0] == '\0') { - Notice("trusting certificate, no check of commonName"); - X509_free(peer_cert); - return STAT_OK; - } if (ok) { Notice("trusting certificate, commonName matches"); X509_free(peer_cert); return STAT_OK; } + if (peername == NULL || peername[0] == '\0') { + Notice("trusting certificate, no check of commonName"); + X509_free(peer_cert); + return STAT_OK; + } + /* here: all envs set; opt_ver, cert verified, no subjAltName match -> check subject CN */ if (!openssl_check_peername(/*X509_NAME*/subjectname, /*const char*/peername)) { - Error("certificate is valid but its commonName does not match hostname"); + Error1("certificate is valid but its commonName does not match hostname \"%s\"", + peername); status = STAT_NORETRY; } else { Notice("trusting certificate, commonName matches");