diff --git a/examples/tls_backend_testcases.sh b/examples/tls_backend_testcases.sh index 8def4269..3dc07ba8 100755 --- a/examples/tls_backend_testcases.sh +++ b/examples/tls_backend_testcases.sh @@ -9,11 +9,12 @@ # - PSK client identity-hint callback # - PSK server identity callback # - wrong PSK negative path -# - client first-flight loss -# - server HelloVerifyRequest loss -# - server ServerHello flight loss -# - client flight loss timeout -# - server flight loss timeout +# - Client first-flight loss +# - Server HelloVerifyRequest loss +# - Server ServerHello flight loss +# - Client flight loss timeout +# - Server flight loss timeout +# - PSK SNI key selection # - TLS-over-TCP PSK handshake success # - TLS-over-TCP peer close handling # - TLS-over-TCP PKI handshake success @@ -21,6 +22,9 @@ # - PKI intermediate CA chain loading # - PKI certificate chain depth limit success # - PKI certificate chain depth exceeded negative path +# - PKI self-signed leaf allowed +# - PKI self-signed with common CA required negative +# - PKI non-self-signed with override open negative # - PKI server PEM buffer loading # - PKI mutual authentication success # - PKI root CA file mutual authentication @@ -30,7 +34,11 @@ # - concurrent PSK and PKI server configuration # - PKI missing client certificate negative path # - PKI SAN preferred over CN +# - PKI second SAN preferred over CN # - PKI CN fallback without SAN +# - PKI bad returned CN or SAN +# - PKI Wildcard Certificate +# - PKI wrong Wildcard Certificate # - PKI SNI certificate selection # - wrong PKI CA negative path # @@ -160,6 +168,7 @@ SNI_URI=coaps://$SNI_URI_HOST:$SECURE_PORT/.well-known/core TLS_URI=coaps+tcp://$TARGET_URI_HOST:$SECURE_PORT/.well-known/core PKI_URI=coaps://$SNI_URI_HOST:$SECURE_PORT/.well-known/core TLS_PKI_URI=coaps+tcp://$SNI_URI_HOST:$SECURE_PORT/.well-known/core +WILDCARD_URI=coaps://server.example:$SECURE_PORT/.well-known/core if command -v mktemp >/dev/null 2>&1; then LOGDIR=$(mktemp -d "${TMPDIR:-/tmp}"/libcoap-tls-backend.XXXXXX) @@ -325,6 +334,9 @@ generate_pki_files () { server_conf=$pki_dir/server.cnf alt_server_conf=$pki_dir/alt_server.cnf san_server_conf=$pki_dir/san_server.cnf + second_san_server_conf=$pki_dir/second_san_server.cnf + wildcard_server_conf=$pki_dir/wildcard_server.cnf + wildcard_bad_conf=$pki_dir/wildcard_bad.cnf cn_server_conf=$pki_dir/cn_server.cnf sni_server_conf=$pki_dir/sni_server.cnf client_conf=$pki_dir/client.cnf @@ -337,6 +349,12 @@ generate_pki_files () { [ -f "$pki_dir/alt_server.pem" ] && [ -f "$pki_dir/san_server.pem" ] && [ -f "$pki_dir/san_server.key" ] && + [ -f "$pki_dir/second_san_server.pem" ] && + [ -f "$pki_dir/second_san_server.key" ] && + [ -f "$pki_dir/wildcard_server.pem" ] && + [ -f "$pki_dir/wildcard_server.key" ] && + [ -f "$pki_dir/wildcard_bad.pem" ] && + [ -f "$pki_dir/wildcard_bad.key" ] && [ -f "$pki_dir/cn_server.pem" ] && [ -f "$pki_dir/cn_server.key" ] && [ -f "$pki_dir/sni_combined.pem" ] && @@ -419,6 +437,56 @@ subjectAltName = @alt_names [alt_names] DNS.1 = $SNI_HOST +EOF + + cat > "$second_san_server_conf" < "$wildcard_server_conf" < "$wildcard_bad_conf" < "$cn_server_conf" <> "$LOGDIR/$case_name.openssl" 2>&1 || return 1 + openssl req -new -newkey rsa:2048 -nodes -sha256 \ + -keyout "$pki_dir/second_san_server.key" -out "$pki_dir/second_san_server.csr" \ + -config "$second_san_server_conf" >> "$LOGDIR/$case_name.openssl" 2>&1 || return 1 + openssl x509 -req -in "$pki_dir/second_san_server.csr" \ + -CA "$pki_dir/ca.pem" -CAkey "$pki_dir/ca.key" -CAcreateserial \ + -out "$pki_dir/second_san_server.pem" -days 1 -sha256 \ + -extensions v3_req -extfile "$second_san_server_conf" \ + >> "$LOGDIR/$case_name.openssl" 2>&1 || return 1 + openssl req -new -newkey rsa:2048 -nodes -sha256 \ + -keyout "$pki_dir/wildcard_server.key" -out "$pki_dir/wildcard_server.csr" \ + -config "$wildcard_server_conf" >> "$LOGDIR/$case_name.openssl" 2>&1 || return 1 + openssl x509 -req -in "$pki_dir/wildcard_server.csr" \ + -CA "$pki_dir/ca.pem" -CAkey "$pki_dir/ca.key" -CAcreateserial \ + -out "$pki_dir/wildcard_server.pem" -days 1 -sha256 \ + -extensions v3_req -extfile "$wildcard_server_conf" \ + >> "$LOGDIR/$case_name.openssl" 2>&1 || return 1 + openssl req -new -newkey rsa:2048 -nodes -sha256 \ + -keyout "$pki_dir/wildcard_bad.key" -out "$pki_dir/wildcard_bad.csr" \ + -config "$wildcard_bad_conf" >> "$LOGDIR/$case_name.openssl" 2>&1 || return 1 + openssl x509 -req -in "$pki_dir/wildcard_bad.csr" \ + -CA "$pki_dir/ca.pem" -CAkey "$pki_dir/ca.key" -CAcreateserial \ + -out "$pki_dir/wildcard_bad.pem" -days 1 -sha256 \ + -extensions v3_req -extfile "$wildcard_bad_conf" \ + >> "$LOGDIR/$case_name.openssl" 2>&1 || return 1 openssl req -new -newkey rsa:2048 -nodes -sha256 \ -keyout "$pki_dir/cn_server.key" -out "$pki_dir/cn_server.csr" \ -config "$cn_server_conf" >> "$LOGDIR/$case_name.openssl" 2>&1 || return 1 @@ -1556,6 +1648,33 @@ run_pki_san_preferred_over_cn () { fi } +run_pki_second_san_preferred_over_cn () { + case_name=pki_second_san_preferred_over_cn + echo -n "PKI second SAN preferred over CN - " + pki_dir=$LOGDIR/pki + + if ! generate_pki_files "$case_name"; then + fail_case "$case_name" "certificate generation failed" + return + fi + if ! start_pki_server "$case_name" "$pki_dir/second_san_server.pem" \ + "$pki_dir/second_san_server.key"; then + fail_case "$case_name" "server did not start" + return + fi + + run_pki_client "$case_name" "$pki_dir/ca.pem" "$CLIENT_TIMEOUT" + + if assert_contains "$LOGDIR/$case_name.client" "COAP_EVENT_DTLS_CONNECTED" && + assert_contains "$LOGDIR/$case_name.client" "2\\.05" && + assert_contains "$LOGDIR/$case_name.client" "CN 'default.invalid|localhost' presented by server" && + assert_contains "$LOGDIR/$case_name.server" "call handler for pseudo resource '.well-known/core'"; then + pass_case + else + fail_case "$case_name" "PKI second SAN not chosen" + fi +} + run_pki_cn_fallback () { case_name=pki_cn_fallback echo -n "PKI CN fallback without SAN - " @@ -1601,8 +1720,7 @@ run_pki_bad_cn_or_san () { run_pki_client "$case_name" "$pki_dir/ca.pem" "$CLIENT_TIMEOUT" if assert_not_contains "$LOGDIR/$case_name.client" "COAP_EVENT_DTLS_CONNECTED" && - assert_not_contains "$LOGDIR/$case_name.client" "2\\.05" && - assert_contains "$LOGDIR/$case_name.client" "CN 'default.invalid' presented by server" && + assert_contains "$LOGDIR/$case_name.client" "SNI 'localhost' not returned in certificate" && assert_not_contains "$LOGDIR/$case_name.server" "call handler for pseudo resource '.well-known/core'"; then pass_case else @@ -1610,6 +1728,63 @@ run_pki_bad_cn_or_san () { fi } +run_pki_wildcard () { + case_name=pki_wildcard + echo -n "PKI Wildcard Certificate - " + pki_dir=$LOGDIR/pki + + if ! generate_pki_files "$case_name"; then + fail_case "$case_name" "certificate generation failed" + return + fi + if ! start_pki_server "$case_name" "$pki_dir/wildcard_server.pem" \ + "$pki_dir/wildcard_server.key"; then + fail_case "$case_name" "server did not start" + return + fi + + run_pki_client "$case_name" "$pki_dir/ca.pem" "$CLIENT_TIMEOUT" "" "" $WILDCARD_URI + + if assert_not_contains "/etc/hosts" "127\\.0\\.0\\.1[[:space:]]+server\\.example"; then + skip_case "/etc/hosts does not contain '127.0.0.1 server.example'. Add it in." + elif assert_contains "$LOGDIR/$case_name.client" "COAP_EVENT_DTLS_CONNECTED" && + assert_contains "$LOGDIR/$case_name.client" "2\\.05" && + assert_contains "$LOGDIR/$case_name.client" "CN '\\*.example' presented by server" && + assert_contains "$LOGDIR/$case_name.server" "call handler for pseudo resource '.well-known/core'"; then + pass_case + else + fail_case "$case_name" "PKI Wildcard Certificate not accepted" + fi +} + +run_pki_wildcard_bad () { + case_name=pki_wildcard_bad + echo -n "PKI wrong Wildcard Certificate - " + pki_dir=$LOGDIR/pki + + if ! generate_pki_files "$case_name"; then + fail_case "$case_name" "certificate generation failed" + return + fi + if ! start_pki_server "$case_name" "$pki_dir/wildcard_bad.pem" \ + "$pki_dir/wildcard_bad.key"; then + fail_case "$case_name" "server did not start" + return + fi + + run_pki_client "$case_name" "$pki_dir/ca.pem" "$CLIENT_TIMEOUT" "" "" $WILDCARD_URI + + if assert_not_contains "/etc/hosts" "127\\.0\\.0\\.1[[:space:]]+server\\.example"; then + skip_case "/etc/hosts does not contain '127.0.0.1 server.example'. Add it in." + elif assert_not_contains "$LOGDIR/$case_name.client" "COAP_EVENT_DTLS_CONNECTED" && + assert_contains "$LOGDIR/$case_name.client" "SNI 'server.example' not returned in certificate" && + assert_not_contains "$LOGDIR/$case_name.server" "call handler for pseudo resource '.well-known/core'"; then + pass_case + else + fail_case "$case_name" "PKI bad Wildcard Certificate accepted" + fi +} + run_pki_sni () { case_name=pki_sni @@ -1698,8 +1873,11 @@ run_pki_root_ca_dir_invalid run_psk_pki_dual_mode run_pki_missing_client_cert run_pki_san_preferred_over_cn +run_pki_second_san_preferred_over_cn run_pki_cn_fallback run_pki_bad_cn_or_san +run_pki_wildcard +run_pki_wildcard_bad run_pki_sni run_wrong_pki_ca diff --git a/include/coap3/coap_dtls_internal.h b/include/coap3/coap_dtls_internal.h index bccb5813..e4d46dd5 100644 --- a/include/coap3/coap_dtls_internal.h +++ b/include/coap3/coap_dtls_internal.h @@ -512,6 +512,17 @@ int coap_dtls_define_issue(coap_define_issue_key_t type, */ int coap_dtls_set_cid_tuple_change(coap_context_t *context, uint8_t every); +/** + * Check if the PKI name entry is a (wildcard) match for the requested SNI. + * + * @param name_entry The current name entry from the Certificate. + * @param name_length The length of the current name entry from the Certificate. + * @param sni_match The requested host name in the SNI request. + * + * @return @c 1 if a match, else @c 0. + */ +int coap_pki_name_match_sni(const char *name_entry, size_t name_length, const char *sni_match); + /** @} */ #ifdef __cplusplus diff --git a/src/coap_dtls.c b/src/coap_dtls.c index 8c730b68..237dd4ed 100644 --- a/src/coap_dtls.c +++ b/src/coap_dtls.c @@ -290,6 +290,28 @@ coap_dtls_close(coap_session_t *session) { session->sock.lfunc[COAP_LAYER_TLS].l_close(session); } +int +coap_pki_name_match_sni(const char *name_entry, size_t name_length, const char *sni_match) { + if (!sni_match) + return 1; + + if (name_length >= 2 && name_entry[0] == '*' && name_entry[1] == '.') { + /* Check for wildcard match */ + const char *cp = strchr(sni_match, '.'); + + if (!cp || strlen(cp) != name_length -1 || memcmp(&name_entry[1], cp, name_length -1)) { + /* Not an wildcard match */ + return 0; + } + } else { + if (strlen(sni_match) != name_length || memcmp(name_entry, sni_match, name_length)) { + /* Not an absolute match */ + return 0; + } + } + return 1; +} + #if !COAP_DISABLE_TCP void coap_tls_establish(coap_session_t *session) { diff --git a/src/coap_gnutls.c b/src/coap_gnutls.c index 91019391..c0cb7358 100644 --- a/src/coap_gnutls.c +++ b/src/coap_gnutls.c @@ -705,14 +705,15 @@ typedef struct { * certificate. GNUTLS_CRT_UNKNOWN if failure. */ static gnutls_certificate_type_t -get_san_or_cn(gnutls_session_t g_session, - coap_gnutls_certificate_info_t *cert_info) { +get_san_or_cn(coap_session_t *session, gnutls_session_t g_session, + coap_gnutls_certificate_info_t *cert_info, const char *sni_match) { gnutls_x509_crt_t cert; char dn[256]; size_t size; int n; char *cn; int ret; + unsigned int seq; #if (GNUTLS_VERSION_NUMBER >= 0x030606) cert_info->certificate_type = gnutls_certificate_type_get2(g_session, @@ -740,21 +741,31 @@ get_san_or_cn(gnutls_session_t g_session, cert_info->self_signed = gnutls_x509_crt_check_issuer(cert, cert); - size = sizeof(dn) -1; /* See if there is a Subject Alt Name first */ - ret = gnutls_x509_crt_get_subject_alt_name(cert, 0, dn, &size, NULL); - if (ret >= 0) { - dn[size] = '\000'; - gnutls_x509_crt_deinit(cert); - cert_info->san_or_cn = gnutls_strdup(dn); - return cert_info->certificate_type; + for (seq = 0; seq < 100; seq++) { + size = sizeof(dn) -1; + ret = gnutls_x509_crt_get_subject_alt_name(cert, seq, dn, &size, NULL); + if (ret == GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE) { + /* Reached the end of the SAN list */ + break; + } + if (ret >= 0) { + dn[size] = '\000'; + if (sni_match) { + if (strlen(sni_match) != size || + memcmp(dn, sni_match, size)) { + continue; + } + } + gnutls_x509_crt_deinit(cert); + cert_info->san_or_cn = gnutls_strdup(dn); + return cert_info->certificate_type; + } } size = sizeof(dn); G_CHECK(gnutls_x509_crt_get_dn(cert, dn, &size), "gnutls_x509_crt_get_dn"); - gnutls_x509_crt_deinit(cert); - /* Need to emulate strcasestr() here. Looking for CN= */ n = strlen(dn) - 3; cn = dn; @@ -773,9 +784,37 @@ get_san_or_cn(gnutls_session_t g_session, if (ecn) { cn[ecn-cn] = '\000'; } + if (sni_match) { + if (!coap_pki_name_match_sni(cn, strlen(cn), sni_match)) { + coap_log_debug(" %s: got CN '%s'\n", + coap_session_str(session), cn); + goto no_match; + } + } + + gnutls_x509_crt_deinit(cert); cert_info->san_or_cn = gnutls_strdup(cn); return cert_info->certificate_type; } +no_match: + n = 0; + for (seq = 0; seq < 100; seq++) { + size = sizeof(dn) -1; + ret = gnutls_x509_crt_get_subject_alt_name(cert, seq, dn, &size, NULL); + if (ret == GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE) { + /* Reached the end of the SAN list */ + break; + } + if (ret >= 0) { + dn[size] = '\000'; + coap_log_debug(" %s: got DNS.%d '%s'\n", + coap_session_str(session), n + 1, dn); + + } + n++; + continue; + } + gnutls_x509_crt_deinit(cert); return GNUTLS_CRT_UNKNOWN; fail: @@ -849,7 +888,20 @@ cert_verify_gnutls(gnutls_session_t g_session) { gnutls_certificate_type_t cert_type; memset(&cert_info, 0, sizeof(cert_info)); - cert_type = get_san_or_cn(g_session, &cert_info); + if (!g_context->setup_data.allow_sni_cn_mismatch && + c_session->type == COAP_SESSION_TYPE_CLIENT && + g_context->setup_data.client_sni) { + cert_type = get_san_or_cn(c_session, g_session, &cert_info, g_context->setup_data.client_sni); + if (cert_type == GNUTLS_CRT_UNKNOWN) { + coap_log_info(" %s: SNI '%s' not returned in certificate\n", + coap_session_str(c_session), g_context->setup_data.client_sni); + alert = GNUTLS_A_ACCESS_DENIED; + goto fail; + } + } + + if (!cert_info.san_or_cn) + cert_type = get_san_or_cn(c_session, g_session, &cert_info, NULL); #if (GNUTLS_VERSION_NUMBER >= 0x030606) if (cert_type == GNUTLS_CRT_RAW) { if (!check_rpk_cert(g_context, &cert_info, c_session)) { @@ -867,18 +919,8 @@ cert_verify_gnutls(gnutls_session_t g_session) { goto fail; } - if (c_session->type == COAP_SESSION_TYPE_CLIENT && g_context->setup_data.client_sni) { - gnutls_typed_vdata_st host; - - host.type = GNUTLS_DT_DNS_HOSTNAME; - host.data = (uint8_t *)g_context->setup_data.client_sni; - host.size = strlen(g_context->setup_data.client_sni); - G_CHECK(gnutls_certificate_verify_peers(g_session, &host, 1, &status), - "gnutls_certificate_verify_peers"); - } else { - G_CHECK(gnutls_certificate_verify_peers(g_session, NULL, 0, &status), - "gnutls_certificate_verify_peers"); - } + G_CHECK(gnutls_certificate_verify_peers(g_session, NULL, 0, &status), + "gnutls_certificate_verify_peers"); coap_dtls_log(COAP_LOG_DEBUG, "error %x cert '%s'\n", status, cert_info.san_or_cn); diff --git a/src/coap_mbedtls.c b/src/coap_mbedtls.c index 69f5c3de..736bc89a 100644 --- a/src/coap_mbedtls.c +++ b/src/coap_mbedtls.c @@ -610,19 +610,29 @@ psk_server_callback(void *p_info, mbedtls_ssl_context *ssl, #endif /* MBEDTLS_KEY_EXCHANGE__SOME__PSK_ENABLED && MBEDTLS_SSL_SRV_C */ static char * -get_san_or_cn_from_cert(mbedtls_x509_crt *crt) { +get_san_or_cn_from_cert(coap_session_t *session, mbedtls_x509_crt *crt, const char *sni_match) { if (crt) { const mbedtls_asn1_named_data *cn_data; + (void)session; if (crt->ext_types & MBEDTLS_X509_EXT_SUBJECT_ALT_NAME) { mbedtls_asn1_sequence *seq = &crt->subject_alt_names; - while (seq && seq->buf.p == NULL) { - seq = seq->next; - } - if (seq) { - /* Return the Subject Alt Name */ - return mbedtls_strndup((const char *)seq->buf.p, - seq->buf.len); + while (seq) { + while (seq->buf.p == NULL) { + seq = seq->next; + } + if (seq) { + if (sni_match) { + if (strlen(sni_match) != seq->buf.len || + memcmp(seq->buf.p, sni_match, seq->buf.len)) { + seq = seq->next; + continue; + } + } + /* Return the Subject Alt Name */ + return mbedtls_strndup((const char *)seq->buf.p, + seq->buf.len); + } } } @@ -630,10 +640,40 @@ get_san_or_cn_from_cert(mbedtls_x509_crt *crt) { MBEDTLS_OID_AT_CN, MBEDTLS_OID_SIZE(MBEDTLS_OID_AT_CN)); if (cn_data) { + if (sni_match) { + if (!coap_pki_name_match_sni((char *)cn_data->val.p, cn_data->val.len, sni_match)) { + coap_log_debug(" %s: got CN '%*.*s'\n", + coap_session_str(session), (int)cn_data->val.len, + (int)cn_data->val.len, cn_data->val.p); + goto no_match; + } + } /* Return the Common Name */ return mbedtls_strndup((const char *)cn_data->val.p, cn_data->val.len); } +no_match: + if (!sni_match) { + return NULL; + } + if (crt->ext_types & MBEDTLS_X509_EXT_SUBJECT_ALT_NAME) { + mbedtls_asn1_sequence *seq = &crt->subject_alt_names; + int n = 0; + + while (seq) { + while (seq->buf.p == NULL) { + seq = seq->next; + } + if (seq) { + coap_log_debug(" %s: got DNS.%d '%*.*s'\n", + coap_session_str(session), n + 1, (int)seq->buf.len, + (int)seq->buf.len, seq->buf.p); + seq = seq->next; + n++; + continue; + } + } + } } return NULL; } @@ -678,13 +718,21 @@ cert_verify_callback_mbedtls(void *data, mbedtls_x509_crt *crt, coap_dtls_pki_t *setup_data = &m_context->setup_data; char *cn = NULL; - cn = get_san_or_cn_from_cert(crt); + if (!setup_data->allow_sni_cn_mismatch && depth == 0 && + c_session->type == COAP_SESSION_TYPE_CLIENT && setup_data->client_sni) { + + cn = get_san_or_cn_from_cert(c_session, crt, setup_data->client_sni); + if (!cn) { + coap_log_info(" %s: SNI '%s' not returned in certificate\n", + coap_session_str(c_session), setup_data->client_sni); + *flags |= MBEDTLS_X509_BADCERT_CN_MISMATCH; + } + } + if (!cn) { + cn = get_san_or_cn_from_cert(c_session, crt, NULL); + } coap_dtls_log(COAP_LOG_DEBUG, "depth %d flags %x cert '%s'\n", depth, *flags, cn); - if (depth == 0 && c_session->type == COAP_SESSION_TYPE_CLIENT && setup_data->client_sni && cn && - strcmp(cn, setup_data->client_sni)) { - *flags |= MBEDTLS_X509_BADCERT_CN_MISMATCH; - } if (setup_data->validate_cn_call_back) { int ret; diff --git a/src/coap_openhitls.c b/src/coap_openhitls.c index c553fb00..4e17b51f 100644 --- a/src/coap_openhitls.c +++ b/src/coap_openhitls.c @@ -805,13 +805,16 @@ coap_hitls_copy_name(const uint8_t *data, uint32_t data_len) { } static char * -coap_hitls_get_san_from_cert(HITLS_X509_Cert *cert) { +coap_hitls_get_san_from_cert(coap_session_t *session, HITLS_X509_Cert *cert, + const char *sni_match, int report_san) { HITLS_X509_ExtSan san = {0}; char *dns_name = NULL; if (HITLS_X509_CertCtrl(cert, HITLS_X509_EXT_GET_SAN, &san, sizeof(san)) == HITLS_PKI_SUCCESS && san.names) { + int n = 0; + for (BslListNode *name_node = BSL_LIST_FirstNode(san.names); name_node != NULL; name_node = BSL_LIST_GetNextNode(san.names, name_node)) { @@ -823,6 +826,19 @@ coap_hitls_get_san_from_cert(HITLS_X509_Cert *cert) { if (name->value.dataLen && memchr(name->value.data, '\000', name->value.dataLen)) continue; + if (report_san) { + coap_log_debug(" %s: got DNS.%d '%*.*s'\n", + coap_session_str(session), n + 1, (int)name->value.dataLen, (int)name->value.dataLen, + name->value.data); + n++; + continue; + } + if (sni_match) { + if (strlen(sni_match) != name->value.dataLen || + memcmp(name->value.data, sni_match, name->value.dataLen)) { + continue; + } + } dns_name = coap_hitls_copy_name(name->value.data, name->value.dataLen); break; } @@ -833,13 +849,19 @@ coap_hitls_get_san_from_cert(HITLS_X509_Cert *cert) { } static char * -coap_hitls_get_cn_from_cert(HITLS_X509_Cert *cert) { +coap_hitls_get_cn_from_cert(coap_session_t *session, HITLS_X509_Cert *cert, const char *sni_match) { BSL_Buffer cn = {0}; char *cn_name = NULL; if (HITLS_X509_CertCtrl(cert, HITLS_X509_GET_SUBJECT_CN_STR, &cn, sizeof(cn)) == HITLS_PKI_SUCCESS) { - cn_name = coap_hitls_copy_name(cn.data, cn.dataLen); + if (cn.data && coap_pki_name_match_sni((char *)cn.data, cn.dataLen, sni_match)) { + cn_name = coap_hitls_copy_name(cn.data, cn.dataLen); + } + } + if (cn.data && !cn_name) { + coap_log_debug(" %s: got CN '%*.*s'\n", + coap_session_str(session), (int)cn.dataLen, (int)cn.dataLen, cn.data); } if (cn.data) BSL_SAL_Free(cn.data); @@ -847,15 +869,20 @@ coap_hitls_get_cn_from_cert(HITLS_X509_Cert *cert) { } static char * -coap_hitls_get_san_or_cn_from_cert(HITLS_X509_Cert *cert) { +coap_hitls_get_san_or_cn_from_cert(coap_session_t *session, HITLS_X509_Cert *cert, + const char *sni_match) { char *name; if (!cert) return NULL; - name = coap_hitls_get_san_from_cert(cert); + name = coap_hitls_get_san_from_cert(session, cert, sni_match, 0); if (name) return name; - return coap_hitls_get_cn_from_cert(cert); + name = coap_hitls_get_cn_from_cert(session, cert, sni_match); + if (!name) { + name = coap_hitls_get_san_from_cert(session, cert, sni_match, 1); + } + return name; } static int @@ -863,10 +890,10 @@ coap_hitls_validate_cn_cert(coap_session_t *session, const coap_dtls_pki_t *setup_data, HITLS_X509_Cert *cert, unsigned depth, - int validated) { + int validated, + char **san_or_cn) { uint8_t *der = NULL; uint32_t der_len = 0; - char *san_or_cn; int ret = 1; (void)HITLS_X509_CertCtrl(cert, HITLS_X509_GET_ENCODELEN, @@ -874,22 +901,16 @@ coap_hitls_validate_cn_cert(coap_session_t *session, if (der_len) (void)HITLS_X509_CertCtrl(cert, HITLS_X509_GET_ENCODE, &der, sizeof(der)); - san_or_cn = coap_hitls_get_san_or_cn_from_cert(cert); + if (!*san_or_cn) + *san_or_cn = coap_hitls_get_san_or_cn_from_cert(session, cert, NULL); if (setup_data->validate_cn_call_back) { coap_lock_callback_ret(ret, setup_data->validate_cn_call_back( - san_or_cn ? san_or_cn : "", + *san_or_cn ? *san_or_cn : "", der, der_len, session, depth, validated, setup_data->cn_call_back_arg)); } - if (depth == 0 && session->type == COAP_SESSION_TYPE_CLIENT && - setup_data->client_sni && !setup_data->allow_sni_cn_mismatch && - san_or_cn && strcmp(san_or_cn, setup_data->client_sni)) { - ret = 0; - } - if (san_or_cn) - coap_free_type(COAP_STRING, san_or_cn); return ret; } @@ -901,6 +922,7 @@ coap_hitls_verify_cb(int32_t err_code, HITLS_CERT_StoreCtx *store_ctx) { HITLS_X509_Cert *cert; int32_t depth; int allowed; + char *san_or_cn = NULL; if (!coap_hitls_get_verify_session(store_ctx, &session) || !session->context || !session->context->dtls_context) @@ -908,6 +930,37 @@ coap_hitls_verify_cb(int32_t err_code, HITLS_CERT_StoreCtx *store_ctx) { context = (coap_hitls_context_t *)session->context->dtls_context; setup_data = &context->setup_data; + + if (!coap_hitls_get_verify_cert(store_ctx, &cert, &depth)) { + return err_code; + } + if (!setup_data->allow_sni_cn_mismatch && depth <= 0 && + session->type == COAP_SESSION_TYPE_CLIENT && setup_data->client_sni) { + + san_or_cn = coap_hitls_get_san_or_cn_from_cert(session, cert, setup_data->client_sni); + if (!san_or_cn) { + /* No match on returned Cert for requested host */ + coap_log_warn("* %s: SNI '%s' not returned in certificate\n", + coap_session_str(session), + setup_data->client_sni); + return HITLS_X509_ERR_VFY_HOSTNAME_FAIL; + } + } + if (setup_data->validate_cn_call_back) { + if (!coap_hitls_validate_cn_cert(session, setup_data, cert, + depth < 0 ? 0 : (unsigned)depth, + err_code == HITLS_PKI_SUCCESS, &san_or_cn)) { + coap_log_warn("* %s: certificate verification failed: %s\n", + coap_session_str(session), + coap_hitls_verify_err_str(HITLS_X509_ERR_VFY_HOSTNAME_FAIL)); + if (san_or_cn) + coap_free_type(COAP_STRING, san_or_cn); + return HITLS_X509_ERR_VFY_HOSTNAME_FAIL; + } + } + if (san_or_cn) + coap_free_type(COAP_STRING, san_or_cn); + if (err_code == HITLS_X509_ERR_ISSUE_CERT_NOT_FOUND || err_code == HITLS_X509_ERR_ROOT_CERT_NOT_FOUND) { allowed = coap_hitls_verify_cb_self_signed_allowed(setup_data, @@ -922,18 +975,6 @@ coap_hitls_verify_cb(int32_t err_code, HITLS_CERT_StoreCtx *store_ctx) { return err_code; } - if (setup_data->validate_cn_call_back && - coap_hitls_get_verify_cert(store_ctx, &cert, &depth)) { - if (!coap_hitls_validate_cn_cert(session, setup_data, cert, - depth < 0 ? 0 : (unsigned)depth, - err_code == HITLS_PKI_SUCCESS)) { - coap_log_warn("* %s: certificate verification failed: %s\n", - coap_session_str(session), - coap_hitls_verify_err_str(HITLS_X509_ERR_VFY_HOSTNAME_FAIL)); - return HITLS_X509_ERR_VFY_HOSTNAME_FAIL; - } - } - return HITLS_PKI_SUCCESS; } @@ -968,6 +1009,7 @@ coap_hitls_app_verify_cb(HITLS_CERT_StoreCtx *store_ctx, ret == HITLS_X509_ERR_ROOT_CERT_NOT_FOUND) && coap_hitls_self_signed_leaf_allowed(setup_data, store_ctx, &leaf_cert)) { + char *san_or_cn = NULL; if (!setup_data->allow_expired_certs) { int32_t time_ret = coap_hitls_cert_time_valid(leaf_cert); @@ -981,8 +1023,13 @@ coap_hitls_app_verify_cb(HITLS_CERT_StoreCtx *store_ctx, coap_log_info(" %s: %s: overridden: 'self-signed' depth=0\n", coap_session_str(session), coap_hitls_verify_err_str(ret)); - if (!coap_hitls_validate_cn_cert(session, setup_data, leaf_cert, 0, 0)) + if (!coap_hitls_validate_cn_cert(session, setup_data, leaf_cert, 0, 0, &san_or_cn)) { + if (san_or_cn) + coap_free_type(COAP_STRING, san_or_cn); return HITLS_X509_ERR_VFY_HOSTNAME_FAIL; + } + if (san_or_cn) + coap_free_type(COAP_STRING, san_or_cn); return HITLS_APP_VERIFY_CALLBACK_SUCCESS; } diff --git a/src/coap_openssl.c b/src/coap_openssl.c index 72909d78..4028c706 100644 --- a/src/coap_openssl.c +++ b/src/coap_openssl.c @@ -2409,13 +2409,14 @@ fail_ns_ca: #endif /* OPENSSL_VERSION_NUMBER >= 0x10101000L || COAP_CLIENT_SUPPORT */ static char * -get_san_or_cn_from_cert(X509 *x509) { +get_san_or_cn_from_cert(coap_session_t *session, X509 *x509, const char *sni_match) { if (x509) { char *cn; int n; STACK_OF(GENERAL_NAME) *san_list; char buffer[256]; + (void)session; san_list = X509_get_ext_d2i(x509, NID_subject_alt_name, NULL, NULL); if (san_list) { int san_count = sk_GENERAL_NAME_num(san_list); @@ -2429,6 +2430,11 @@ get_san_or_cn_from_cert(X509 *x509) { /* Make sure that there is not an embedded NUL in the dns_name */ if (ASN1_STRING_length(name->d.dNSName) != (int)strlen(dns_name)) continue; + if (sni_match) { + if (strcmp(dns_name, sni_match)) { + continue; + } + } cn = OPENSSL_strdup(dns_name); sk_GENERAL_NAME_pop_free(san_list, GENERAL_NAME_free); return cn; @@ -2455,10 +2461,39 @@ get_san_or_cn_from_cert(X509 *x509) { if (n > 0) { char *ecn = strchr(cn, '/'); if (ecn) { - return OPENSSL_strndup(cn, ecn-cn); - } else { - return OPENSSL_strdup(cn); + cn[ecn-cn] = '\000'; } + if (sni_match) { + if (!coap_pki_name_match_sni(cn, strlen(cn), sni_match)) { + coap_log_debug(" %s: got CN '%s'\n", + coap_session_str(session), cn); + goto no_match; + } + } + return OPENSSL_strdup(cn); + } +no_match: + if (!sni_match) { + return NULL; + } + san_list = X509_get_ext_d2i(x509, NID_subject_alt_name, NULL, NULL); + if (san_list) { + int san_count = sk_GENERAL_NAME_num(san_list); + + for (n = 0; n < san_count; n++) { + const GENERAL_NAME *name = sk_GENERAL_NAME_value(san_list, n); + + if (name && name->type == GEN_DNS) { + const char *dns_name = (const char *)ASN1_STRING_get0_data(name->d.dNSName); + + /* Make sure that there is not an embedded NUL in the dns_name */ + if (ASN1_STRING_length(name->d.dNSName) != (int)strlen(dns_name)) + continue; + coap_log_debug(" %s: got DNS.%d '%s'\n", + coap_session_str(session), n + 1, dns_name); + } + } + sk_GENERAL_NAME_pop_free(san_list, GENERAL_NAME_free); } } return NULL; @@ -2475,20 +2510,57 @@ tls_verify_call_back(int preverify_ok, X509_STORE_CTX *ctx) { int depth = X509_STORE_CTX_get_error_depth(ctx); int err = X509_STORE_CTX_get_error(ctx); X509 *x509 = X509_STORE_CTX_get_current_cert(ctx); - char *cn = x509 ? get_san_or_cn_from_cert(x509) : NULL; + char *cn = NULL; - coap_dtls_log(COAP_LOG_DEBUG, "depth %d error %x preverify %d cert '%s'\n", - depth, err, preverify_ok, cn); - if (!setup_data) { + if (!setup_data || !x509) { X509_STORE_CTX_set_error(ctx, X509_V_ERR_UNSPECIFIED); - OPENSSL_free(cn); return 0; } if (depth == 0 && session->type == COAP_SESSION_TYPE_CLIENT && setup_data->client_sni && - !setup_data->allow_sni_cn_mismatch && cn && strcmp(cn, setup_data->client_sni)) { - preverify_ok = 0; - X509_STORE_CTX_set_error(ctx, X509_V_ERR_SUBJECT_ISSUER_MISMATCH); - err = X509_V_ERR_SUBJECT_ISSUER_MISMATCH; + !setup_data->allow_sni_cn_mismatch) { + cn = get_san_or_cn_from_cert(session, x509, setup_data->client_sni); + if (!cn) { + coap_log_info(" %s: SNI '%s' not returned in certificate\n", + coap_session_str(session), setup_data->client_sni); + preverify_ok = 0; + X509_STORE_CTX_set_error(ctx, X509_V_ERR_SUBJECT_ISSUER_MISMATCH); + err = X509_V_ERR_SUBJECT_ISSUER_MISMATCH; + } + } + if (!cn) + cn = get_san_or_cn_from_cert(session, x509, NULL); + + coap_dtls_log(COAP_LOG_DEBUG, "depth %d error %x preverify %d cert '%s'\n", + depth, err, preverify_ok, cn); + + /* Certificate - depth == 0 is the Client Cert */ + if (setup_data->validate_cn_call_back) { + int length = i2d_X509(x509, NULL); + uint8_t *base_buf; + uint8_t *base_buf2 = base_buf = length > 0 ? OPENSSL_malloc(length) : NULL; + int ret; + + if (base_buf) { + /* base_buf2 gets moved to the end */ + assert(i2d_X509(x509, &base_buf2) > 0); + (void)base_buf2; + coap_lock_callback_ret(ret, + setup_data->validate_cn_call_back(cn, base_buf, length, session, + depth, preverify_ok, + setup_data->cn_call_back_arg)); + if (!ret) { + if (depth == 0) { + X509_STORE_CTX_set_error(ctx, X509_V_ERR_CERT_REJECTED); + } else { + X509_STORE_CTX_set_error(ctx, X509_V_ERR_INVALID_CA); + } + preverify_ok = 0; + } + OPENSSL_free(base_buf); + } else { + X509_STORE_CTX_set_error(ctx, X509_V_ERR_UNSPECIFIED); + preverify_ok = 0; + } } if (!preverify_ok) { switch (err) { @@ -2549,35 +2621,6 @@ tls_verify_call_back(int preverify_ok, X509_STORE_CTX *ctx) { X509_verify_cert_error_string(err), cn ? cn : "?", depth); } } - /* Certificate - depth == 0 is the Client Cert */ - if (setup_data->validate_cn_call_back) { - int length = i2d_X509(x509, NULL); - uint8_t *base_buf; - uint8_t *base_buf2 = base_buf = length > 0 ? OPENSSL_malloc(length) : NULL; - int ret; - - if (base_buf) { - /* base_buf2 gets moved to the end */ - assert(i2d_X509(x509, &base_buf2) > 0); - (void)base_buf2; - coap_lock_callback_ret(ret, - setup_data->validate_cn_call_back(cn, base_buf, length, session, - depth, preverify_ok, - setup_data->cn_call_back_arg)); - if (!ret) { - if (depth == 0) { - X509_STORE_CTX_set_error(ctx, X509_V_ERR_CERT_REJECTED); - } else { - X509_STORE_CTX_set_error(ctx, X509_V_ERR_INVALID_CA); - } - preverify_ok = 0; - } - OPENSSL_free(base_buf); - } else { - X509_STORE_CTX_set_error(ctx, X509_V_ERR_UNSPECIFIED); - preverify_ok = 0; - } - } OPENSSL_free(cn); return preverify_ok; } diff --git a/src/coap_wolfssl.c b/src/coap_wolfssl.c index cf6f135a..c299c556 100644 --- a/src/coap_wolfssl.c +++ b/src/coap_wolfssl.c @@ -167,18 +167,6 @@ wolfssl_strdup(const char *str) { return ret; } -static char * -wolfssl_strndup(const char *str, size_t n) { - size_t len = strnlen(str, n); - char *ret = (char *)wolfssl_malloc(len + 1); - - if (ret) { - strncpy(ret, str, len); - ret[len] = '\0'; - } - return ret; -} - static coap_wolfssl_env_t * coap_dtls_new_wolfssl_env(coap_session_t *c_session, coap_dtls_role_t role) { coap_wolfssl_env_t *w_env = (coap_wolfssl_env_t *)c_session->tls; @@ -1483,13 +1471,14 @@ setup_pki_ssl(WOLFSSL *ssl, } static char * -get_san_or_cn_from_cert(WOLFSSL_X509 *x509) { +get_san_or_cn_from_cert(coap_session_t *session, WOLFSSL_X509 *x509, const char *sni_match) { if (x509) { char *cn; int n; WOLF_STACK_OF(WOLFSSL_GENERAL_NAME) *san_list; char buffer[256]; + (void)session; buffer[0] = '\000'; san_list = wolfSSL_X509_get_ext_d2i(x509, NID_subject_alt_name, NULL, NULL); if (san_list) { @@ -1504,6 +1493,11 @@ get_san_or_cn_from_cert(WOLFSSL_X509 *x509) { /* Make sure that there is not an embedded NUL in the dns_name */ if (wolfSSL_ASN1_STRING_length(name->d.dNSName) != (int)strlen(dns_name)) continue; + if (sni_match) { + if (strcmp(dns_name, sni_match)) { + continue; + } + } cn = wolfssl_strdup(dns_name); wolfSSL_sk_GENERAL_NAME_pop_free(san_list, wolfSSL_GENERAL_NAME_free); return cn; @@ -1531,10 +1525,39 @@ get_san_or_cn_from_cert(WOLFSSL_X509 *x509) { if (n > 0) { char *ecn = strchr(cn, '/'); if (ecn) { - return wolfssl_strndup(cn, ecn-cn); - } else { - return wolfssl_strdup(cn); + cn[ecn-cn] = '\000'; } + if (sni_match) { + if (!coap_pki_name_match_sni(cn, strlen(cn), sni_match)) { + coap_log_debug(" %s: got CN '%s'\n", + coap_session_str(session), cn); + goto no_match; + } + } + return wolfssl_strdup(cn); + } +no_match: + if (!sni_match) { + return NULL; + } + san_list = X509_get_ext_d2i(x509, NID_subject_alt_name, NULL, NULL); + if (san_list) { + int san_count = sk_GENERAL_NAME_num(san_list); + + for (n = 0; n < san_count; n++) { + const GENERAL_NAME *name = sk_GENERAL_NAME_value(san_list, n); + + if (name && name->type == GEN_DNS) { + const char *dns_name = (const char *)ASN1_STRING_get0_data(name->d.dNSName); + + /* Make sure that there is not an embedded NUL in the dns_name */ + if (ASN1_STRING_length(name->d.dNSName) != (int)strlen(dns_name)) + continue; + coap_log_debug(" %s: got DNS.%d '%s'\n", + coap_session_str(session), n + 1, dns_name); + } + } + sk_GENERAL_NAME_pop_free(san_list, GENERAL_NAME_free); } } return NULL; @@ -1553,7 +1576,7 @@ tls_verify_call_back(int preverify_ok, WOLFSSL_X509_STORE_CTX *ctx) { WOLFSSL_X509 *x509 = wolfSSL_X509_STORE_CTX_get_current_cert(ctx); char *cn = NULL; - if (!setup_data) { + if (!setup_data || !x509) { wolfSSL_X509_STORE_CTX_set_error(ctx, X509_V_ERR_UNSPECIFIED); return 0; } @@ -1561,15 +1584,53 @@ tls_verify_call_back(int preverify_ok, WOLFSSL_X509_STORE_CTX *ctx) { if (setup_data->is_rpk_not_cert) { cn = wolfssl_strdup("RPK"); } else { - cn = x509 ? get_san_or_cn_from_cert(x509) : NULL; + if (depth == 0 && session->type == COAP_SESSION_TYPE_CLIENT && setup_data->client_sni && + !setup_data->allow_sni_cn_mismatch) { + cn = get_san_or_cn_from_cert(session, x509, setup_data->client_sni); + + if (!cn) { + coap_log_info(" %s: SNI '%s' not returned in certificate\n", + coap_session_str(session), setup_data->client_sni); + preverify_ok = 0; + X509_STORE_CTX_set_error(ctx, X509_V_ERR_SUBJECT_ISSUER_MISMATCH); + err = X509_V_ERR_SUBJECT_ISSUER_MISMATCH; + } + } + if (!cn) + cn = get_san_or_cn_from_cert(session, x509, NULL); } coap_dtls_log(COAP_LOG_DEBUG, "depth %d error %x preverify %d cert '%s'\n", depth, err, preverify_ok, cn ? cn : ""); - if (depth == 0 && session->type == COAP_SESSION_TYPE_CLIENT && setup_data->client_sni && cn && - strcmp(cn, setup_data->client_sni)) { - preverify_ok = 0; - X509_STORE_CTX_set_error(ctx, X509_V_ERR_SUBJECT_ISSUER_MISMATCH); - err = X509_V_ERR_SUBJECT_ISSUER_MISMATCH; + /* Certificate - depth == 0 is the Client Cert */ + if (setup_data->validate_cn_call_back) { + int length = wolfSSL_i2d_X509(x509, NULL); + + if (length > 0) { + uint8_t *base_buf; + uint8_t *base_buf2 = base_buf = wolfssl_malloc(length); + int ret; + + if (base_buf) { + /* base_buf2 gets moved to the end */ + wolfSSL_i2d_X509(x509, &base_buf2); + coap_lock_callback_ret(ret, + setup_data->validate_cn_call_back(cn, base_buf, length, session, + depth, preverify_ok, + setup_data->cn_call_back_arg)); + if (!ret) { + if (depth == 0) { + wolfSSL_X509_STORE_CTX_set_error(ctx, X509_V_ERR_CERT_REJECTED); + } else { + wolfSSL_X509_STORE_CTX_set_error(ctx, X509_V_ERR_INVALID_CA); + } + preverify_ok = 0; + } + wolfssl_free(base_buf); + } else { + wolfSSL_X509_STORE_CTX_set_error(ctx, X509_V_ERR_UNSPECIFIED); + preverify_ok = 0; + } + } } if (!preverify_ok) { switch (err) { @@ -1632,37 +1693,6 @@ tls_verify_call_back(int preverify_ok, WOLFSSL_X509_STORE_CTX *ctx) { wolfSSL_X509_verify_cert_error_string(err), cn ? cn : "?", depth); } } - /* Certificate - depth == 0 is the Client Cert */ - if (setup_data->validate_cn_call_back) { - int length = wolfSSL_i2d_X509(x509, NULL); - - if (length > 0) { - uint8_t *base_buf; - uint8_t *base_buf2 = base_buf = wolfssl_malloc(length); - int ret; - - if (base_buf) { - /* base_buf2 gets moved to the end */ - wolfSSL_i2d_X509(x509, &base_buf2); - coap_lock_callback_ret(ret, - setup_data->validate_cn_call_back(cn, base_buf, length, session, - depth, preverify_ok, - setup_data->cn_call_back_arg)); - if (!ret) { - if (depth == 0) { - wolfSSL_X509_STORE_CTX_set_error(ctx, X509_V_ERR_CERT_REJECTED); - } else { - wolfSSL_X509_STORE_CTX_set_error(ctx, X509_V_ERR_INVALID_CA); - } - preverify_ok = 0; - } - wolfssl_free(base_buf); - } else { - wolfSSL_X509_STORE_CTX_set_error(ctx, X509_V_ERR_UNSPECIFIED); - preverify_ok = 0; - } - } - } wolfssl_free(cn); return preverify_ok; } @@ -2118,8 +2148,8 @@ setup_client_ssl_session(coap_session_t *session, WOLFSSL *ssl) { } #endif /* !COAP_DISABLE_TCP */ coap_log_debug("CoAP Client restricted to (D)TLS1.2 with Identity Hint callback\n"); - } - if (COAP_PROTO_NOT_RELIABLE(session->proto)) { + set_ciphersuites(ssl, COAP_ENC_PSK); + } else if (COAP_PROTO_NOT_RELIABLE(session->proto)) { set_ciphersuites(ssl, COAP_ENC_PSK); }