PR #23724 opened by omkhar URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23724 Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23724.patch
Follow-up to #23249 (which required a=fingerprint to be present -- step a); this is step b. WHIP binds the peer to the SRTP keys through the SDP a=fingerprint (RFC 8842 sec. 5.1): the DTLS peer cert is not CA-validated (verify=0, self-signed) but its hash MUST match the SDP fingerprint. Nothing checked that, so the SRTP keys bound to an unauthenticated peer -- an on-path attacker or malicious server could substitute their own DTLS material. Adds ff_dtls_get_peer_fingerprint() to the OpenSSL backend (retains the peer cert under verify=0) and, after the handshake and before SRTP key export, compares the peer-cert fingerprint to the SDP a=fingerprint, tearing down on mismatch (sec. 5.3). Verified against an OpenSSL DTLS s_server: the extracted fingerprint matches `openssl x509 -fingerprint -sha256`; a matching fingerprint proceeds, a mismatched one is torn down before key export. Happy to split into two commits (tls_openssl helper + whip consumer) if preferred. >From ead97ebfe7cc09da82d35d70f0c453944212d7ce Mon Sep 17 00:00:00 2001 From: Omkhar Arasaratnam <[email protected]> Date: Mon, 6 Jul 2026 14:13:22 -0400 Subject: [PATCH] avformat/whip: verify the DTLS peer certificate against the SDP a=fingerprint WHIP binds the peer identity to the SRTP keying material through the SDP a=fingerprint (RFC 8842 sec. 5.1): the DTLS peer certificate is not validated against a CA (verify=0, self-signed by design) but its hash MUST match the fingerprint advertised in the SDP answer. parse_answer() already requires the fingerprint to be present; until now nothing checked it against the actual peer certificate, so the SRTP keys were derived from a session whose peer was never authenticated -- an on-path attacker or a malicious server could substitute their own DTLS material and receive the publisher's media. Add ff_dtls_get_peer_fingerprint() to the OpenSSL backend (retaining the peer certificate under verify=0 via an accept-all request) and, after the DTLS handshake and before the SRTP keys are exported, compare the peer-certificate fingerprint to the stored SDP a=fingerprint, tearing the session down on mismatch (RFC 8842 sec. 5.3). Signed-off-by: Omkhar Arasaratnam <[email protected]> --- libavformat/tls.h | 7 +++++++ libavformat/tls_openssl.c | 32 ++++++++++++++++++++++++++++++++ libavformat/whip.c | 37 +++++++++++++++++++++++++++++++++++-- 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/libavformat/tls.h b/libavformat/tls.h index f2f4f8991f..41ed0f5e2a 100644 --- a/libavformat/tls.h +++ b/libavformat/tls.h @@ -118,6 +118,13 @@ int ff_tls_set_external_socket(URLContext *h, URLContext *sock); int ff_dtls_export_materials(URLContext *h, char *dtls_srtp_materials, size_t materials_sz); +/** + * Return the SHA-256 fingerprint of the DTLS peer certificate as an uppercase + * colon-separated hex string (the SDP a=fingerprint format). Caller owns + * *fingerprint and must av_free() it. + */ +int ff_dtls_get_peer_fingerprint(URLContext *h, char **fingerprint); + int ff_ssl_read_key_cert(char *key_url, char *cert_url, char *key_buf, size_t key_sz, char *cert_buf, size_t cert_sz, char **fingerprint); int ff_ssl_gen_key_cert(char *key_buf, size_t key_sz, char *cert_buf, size_t cert_sz, char **fingerprint); diff --git a/libavformat/tls_openssl.c b/libavformat/tls_openssl.c index 5d3be96fbb..50d599f068 100644 --- a/libavformat/tls_openssl.c +++ b/libavformat/tls_openssl.c @@ -119,6 +119,17 @@ static int x509_fingerprint(X509 *cert, char **fingerprint) return av_bprint_finalize(&buf, fingerprint); } +/** + * DTLS-SRTP (WHIP) binds the peer by matching its certificate to the SDP + * a=fingerprint, not via a CA. This accept-all callback keeps verification a + * no-op while still requesting the peer certificate, so it is retained for + * ff_dtls_get_peer_fingerprint() even when verify=0. + */ +static int openssl_accept_all_verify_cb(int ok, X509_STORE_CTX *ctx) +{ + return 1; +} + int ff_ssl_read_key_cert(char *key_url, char *cert_url, char *key_buf, size_t key_sz, char *cert_buf, size_t cert_sz, char **fingerprint) { int ret = 0; @@ -464,6 +475,22 @@ int ff_tls_set_external_socket(URLContext *h, URLContext *sock) return 0; } +int ff_dtls_get_peer_fingerprint(URLContext *h, char **fingerprint) +{ + TLSContext *c = h->priv_data; + X509 *peer = SSL_get1_peer_certificate(c->ssl); + int ret; + + *fingerprint = NULL; + if (!peer) { + av_log(c, AV_LOG_ERROR, "TLS: no DTLS peer certificate to fingerprint\n"); + return AVERROR_INVALIDDATA; + } + ret = x509_fingerprint(peer, fingerprint); + X509_free(peer); + return ret; +} + int ff_dtls_export_materials(URLContext *h, char *dtls_srtp_materials, size_t materials_sz) { int ret = 0; @@ -816,6 +843,11 @@ static int tls_open(URLContext *h, const char *uri, int flags, AVDictionary **op SSL_CTX_set_verify(c->ctx, SSL_VERIFY_PEER|SSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL); if (s->is_dtls && s->use_srtp) { + /* Retain the peer certificate for the SDP a=fingerprint check even when + * verify=0 (WHIP): SSL_VERIFY_NONE skips the certificate request on the + * server side, so ask for it with an accept-all callback. */ + if (!s->verify) + SSL_CTX_set_verify(c->ctx, SSL_VERIFY_PEER, openssl_accept_all_verify_cb); /** * The profile for OpenSSL's SRTP is SRTP_AES128_CM_SHA1_80, see ssl/d1_srtp.c. * The profile for FFmpeg's SRTP is SRTP_AES128_CM_HMAC_SHA1_80, see libavformat/srtp.c. diff --git a/libavformat/whip.c b/libavformat/whip.c index ae2c116a4d..60e26a1c58 100644 --- a/libavformat/whip.c +++ b/libavformat/whip.c @@ -292,6 +292,8 @@ typedef struct WHIPContext { char *dtls_fingerprint; /* remote DTLS cert fingerprint from SDP answer (sha-256). */ char *remote_fingerprint; + /* algo token from the SDP a=fingerprint line (must be sha-256). */ + char *remote_fingerprint_algo; /** * This represents the material used to build the SRTP master key. It is * generated by DTLS and has the following layout: @@ -930,8 +932,9 @@ static int parse_answer(AVFormatContext *s) * the algo token, store the hex string for post-handshake compare. */ const char *space = strchr(ptr, ' '); if (space) { - whip->remote_fingerprint = av_strdup(space + 1); - if (!whip->remote_fingerprint) { + whip->remote_fingerprint_algo = av_strndup(ptr, space - ptr); + whip->remote_fingerprint = av_strdup(space + 1); + if (!whip->remote_fingerprint || !whip->remote_fingerprint_algo) { ret = AVERROR(ENOMEM); goto end; } @@ -1432,6 +1435,35 @@ static int setup_srtp(AVFormatContext *s) char *cp = is_dtls_active ? send_key : recv_key; char *sp = is_dtls_active ? recv_key : send_key; + /* RFC 8842 sec. 5.1/5.3: bind the SRTP channel to the SDP identity by matching + * the DTLS peer-cert fingerprint to the SDP a=fingerprint (presence already + * enforced in parse_answer). This must run before the SRTP keys are exported; + * tear the session down on mismatch. */ + if (whip->remote_fingerprint_algo && av_strcasecmp(whip->remote_fingerprint_algo, "sha-256")) { + av_log(whip, AV_LOG_ERROR, "Unsupported SDP fingerprint algo '%s' (only sha-256 " + "is supported); refusing session\n", whip->remote_fingerprint_algo); + ret = AVERROR_PATCHWELCOME; + goto end; + } + + char *peer_fingerprint = NULL; + ret = ff_dtls_get_peer_fingerprint(whip->dtls_uc, &peer_fingerprint); + if (ret < 0 || !peer_fingerprint) { + av_log(whip, AV_LOG_ERROR, "Failed to read the DTLS peer certificate " + "fingerprint; refusing session\n"); + av_freep(&peer_fingerprint); + ret = ret < 0 ? ret : AVERROR_INVALIDDATA; + goto end; + } + if (av_strcasecmp(peer_fingerprint, whip->remote_fingerprint)) { + av_log(whip, AV_LOG_ERROR, "DTLS peer certificate does not match the SDP " + "a=fingerprint; tearing down (RFC 8842 sec. 5.3)\n"); + av_freep(&peer_fingerprint); + ret = AVERROR_INVALIDDATA; + goto end; + } + av_freep(&peer_fingerprint); + ret = ff_dtls_export_materials(whip->dtls_uc, whip->dtls_srtp_materials, sizeof(whip->dtls_srtp_materials)); if (ret < 0) goto end; @@ -2157,6 +2189,7 @@ static av_cold void whip_deinit(AVFormatContext *s) ffurl_closep(&whip->udp); av_freep(&whip->dtls_fingerprint); av_freep(&whip->remote_fingerprint); + av_freep(&whip->remote_fingerprint_algo); } static int whip_check_bitstream(AVFormatContext *s, AVStream *st, const AVPacket *pkt) -- 2.52.0 _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]
