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]

Reply via email to