Skip to content

Protocol Violation: c5 DTLS-SRTP protection profile negotiation mismatch leads to policy inconsistency #275

Description

@songxpu

1. Problem Overview

libpeer allows NULL encryption profiles to enter the negotiable set during DTLS-SRTP negotiation, and after negotiation completes it does not map the actually-selected profile to the SRTP/SRTCP policy used by libsrtp. The media layer always uses a fixed default policy. When the remote side actually selects a NULL profile or a non-default profile (such as AES128-CM-HMAC-SHA1-32), libpeer's media path violates the RFC 8827 prohibition on plaintext media or causes an SRTP parameter mismatch.

2. Specification Requirements

RFC 8827 §6.5 "Communications Security" states:

"Media traffic MUST NOT be sent over plain (unencrypted) RTP or RTCP; that is, implementations MUST NOT negotiate cipher suites with NULL encryption modes."

RFC 5764 §4.1.2 "SRTP Protection Profiles" states:

"A DTLS-SRTP SRTP Protection Profile defines the parameters and options that are in effect for the SRTP processing."

Therefore the actually-selected profile must be consistent with the cipher, authentication algorithm, and tag length of the subsequent SRTP policy.

3. Code Analysis

When the negotiation condition holds, the following path exposes two violation points at the same time. The NULL profile is configured as an acceptable result, while the actually-obtained profile after the handshake is not passed to the policy configuration; the key-export callback directly creates a fixed default context.

/* src/dtls_srtp.c:148-154 — dtls_srtp_init */
static const mbedtls_ssl_srtp_profile default_profiles[] = {
    MBEDTLS_TLS_SRTP_AES128_CM_HMAC_SHA1_80,
    MBEDTLS_TLS_SRTP_AES128_CM_HMAC_SHA1_32,
    MBEDTLS_TLS_SRTP_NULL_HMAC_SHA1_80,
    MBEDTLS_TLS_SRTP_NULL_HMAC_SHA1_32,
    MBEDTLS_TLS_SRTP_UNSET};

/* src/dtls_srtp.c:210-216 — dtls_srtp_init */
mbedtls_ssl_conf_dtls_srtp_protection_profiles(&dtls_srtp->conf, default_profiles);
mbedtls_ssl_conf_srtp_mki_value_supported(&dtls_srtp->conf, MBEDTLS_SSL_DTLS_SRTP_MKI_UNSUPPORTED);
mbedtls_ssl_setup(&dtls_srtp->ssl, &dtls_srtp->conf);

/* src/dtls_srtp.c:369-385 — dtls_srtp_do_handshake */
#if CONFIG_MBEDTLS_2_X
mbedtls_ssl_conf_export_keys_ext_cb(&dtls_srtp->conf, dtls_srtp_key_derivation_cb, dtls_srtp);
#else
mbedtls_ssl_set_export_keys_cb(&dtls_srtp->ssl, dtls_srtp_key_derivation_cb, dtls_srtp);
#endif
ret = mbedtls_ssl_handshake(&dtls_srtp->ssl);

/* src/dtls_srtp.c:291-314 — dtls_srtp_key_derivation */
memset(&dtls_srtp->remote_policy, 0, sizeof(dtls_srtp->remote_policy));
srtp_crypto_policy_set_rtp_default(&dtls_srtp->remote_policy.rtp);
srtp_crypto_policy_set_rtcp_default(&dtls_srtp->remote_policy.rtcp);
memset(&dtls_srtp->local_policy, 0, sizeof(dtls_srtp->local_policy));
srtp_crypto_policy_set_rtp_default(&dtls_srtp->local_policy.rtp);
srtp_crypto_policy_set_rtcp_default(&dtls_srtp->local_policy.rtcp);

/* src/dtls_srtp.c:303-304, 325-326 — dtls_srtp_key_derivation */
srtp_create(&dtls_srtp->srtp_in, &dtls_srtp->remote_policy);
srtp_create(&dtls_srtp->srtp_out, &dtls_srtp->local_policy);

/* src/dtls_srtp.c:461-464 — dtls_srtp_handshake */
mbedtls_dtls_srtp_info dtls_srtp_negotiation_result;
mbedtls_ssl_get_dtls_srtp_negotiation_result(&dtls_srtp->ssl, &dtls_srtp_negotiation_result);
return ret;

/* src/dtls_srtp.c:509-522 — media consumers */
srtp_unprotect(dtls_srtp->srtp_in, packet, bytes);
srtp_protect(dtls_srtp->srtp_out, packet, bytes);
srtp_protect_rtcp(dtls_srtp->srtp_out, packet, bytes);

4. Impact Analysis

This is a functional bug. If the remote side selects AES32 or another profile different from the default policy, the two sides may have different understandings of the authentication tag length or cipher parameters, and SRTP packets may fail authentication or cannot be decrypted, causing persistent media failure. It is also a security bug: if the remote side actually selects NULL encryption, the media session violates WebRTC's prohibition on plaintext media and may lose confidentiality. The error enters the media context after the DTLS handshake; retrying the same negotiation does not repair the missing profile-to-policy mapping.

5. Dynamic Verification

Server command:

cd /8T/xiangpu/Code/webrtcfuzzing/aiortc

python3 examples/libpeer/c5_dtls_srtp_protection_profile_probe.py \
  --port 9457 \
  --cert /tmp/libpeer-c3-cert.pem \
  --key /tmp/libpeer-c3-key.pem

The client still uses libpeer's bundled sample:

cd /home/xiangpu/Code/webrtc_evaluation/projects/libpeer

timeout 20s stdbuf -oL env LD_LIBRARY_PATH=build_aiortc_c3/src \
  ./build_aiortc_c3/examples/generic/sample \
  -u https://127.0.0.1:9457/whip

Key server logs:

forced ClientHello profile=SRTP_AES128_CM_SHA1_32
DTLS ServerHello selected use_srtp profile=SRTP_AES128_CM_SHA1_32
C5 TRIGGER: libpeer selected AES128_CM_HMAC_SHA1_32; compare with its fixed AES128_CM_HMAC_SHA1_80 policy

libpeer logs:

Created inbound SRTP session
Created outbound SRTP session
DTLS server handshake done
DTLS-SRTP handshake done
state is changed: completed

The logs show:

  1. The test side only offers AES32 in the DTLS ClientHello.
  2. DTLS ServerHello selected ... AES128_CM_HMAC_SHA1_32 is the actual negotiation result.
  3. libpeer then completes ICE/DTLS and creates its own SRTP contexts.
  4. But libpeer's source code always calls:
srtp_crypto_policy_set_rtp_default()
srtp_crypto_policy_set_rtcp_default()

These default policies are AES128-CM/HMAC-SHA1-80, not the negotiated AES32. Therefore:

Actually negotiated: AES128_CM_HMAC_SHA1_32
Actual policy: AES128_CM_HMAC_SHA1_80

This proves the C5 problem.

Supplementary Material

c5_DTLS_MEDIA_srtp_protection_profile.zip

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions