PR #23394 opened by ThomasDevoogdt URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23394 Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23394.patch
Add a minimal MIKEY (RFC 3830) PSK-INIT decoder (ff_mikey_parse) that extracts SRTP master key and salt from SDP a=key-mgmt:mikey attributes (RFC 4567). Only the subset required for RTSPS is implemented: PSK-INIT messages (data_type 0) with NULL encryption and NULL MAC inside the KEMAC payload, carrying a TEK or TEK+SALT key data sub-payload (RFC 3830 s6.13). Any unsupported variant returns AVERROR_PATCHWELCOME so unhandled configurations are never silently ignored. The decoded key material is fed into ff_srtp_set_crypto() via the existing crypto_suite/crypto_params fields on RTSPStream, following the same path as RFC 4568 a=crypto:. Also fix the RTSPS handler unconditionally forcing lower_transport_mask to TCP. RTSPS secures only the RTSP signalling channel; the RTP/SRTP media transport is negotiated independently. Forcing TCP interleaved prevents UDP-based SRTP streams from being established. Leave lower_transport_mask untouched so the normal negotiation order applies and an explicit -rtsp_transport flag is honoured. Fixes: #10871 Signed-off-by: Thomas Devoogdt <[email protected]> >From f441dd99bc5749901e89cf3fc4d57614481cddba Mon Sep 17 00:00:00 2001 From: Thomas Devoogdt <[email protected]> Date: Fri, 5 Jun 2026 23:28:22 +0200 Subject: [PATCH] avformat/rtsp: add MIKEY PSK-INIT key management for SRTP over RTSPS Add a minimal MIKEY (RFC 3830) PSK-INIT decoder (ff_mikey_parse) that extracts SRTP master key and salt from SDP a=key-mgmt:mikey attributes (RFC 4567). Only the subset required for RTSPS is implemented: PSK-INIT messages (data_type 0) with NULL encryption and NULL MAC inside the KEMAC payload, carrying a TEK or TEK+SALT key data sub-payload (RFC 3830 s6.13). Any unsupported variant returns AVERROR_PATCHWELCOME so unhandled configurations are never silently ignored. The decoded key material is fed into ff_srtp_set_crypto() via the existing crypto_suite/crypto_params fields on RTSPStream, following the same path as RFC 4568 a=crypto:. Also fix the RTSPS handler unconditionally forcing lower_transport_mask to TCP. RTSPS secures only the RTSP signalling channel; the RTP/SRTP media transport is negotiated independently. Forcing TCP interleaved prevents UDP-based SRTP streams from being established. Leave lower_transport_mask untouched so the normal negotiation order applies and an explicit -rtsp_transport flag is honoured. Fixes: #10871 Signed-off-by: Thomas Devoogdt <[email protected]> --- libavformat/Makefile | 2 +- libavformat/mikey.c | 412 +++++++++++++++++++++++++++++++++++++++++++ libavformat/mikey.h | 44 +++++ libavformat/rtsp.c | 25 ++- 4 files changed, 479 insertions(+), 4 deletions(-) create mode 100644 libavformat/mikey.c create mode 100644 libavformat/mikey.h diff --git a/libavformat/Makefile b/libavformat/Makefile index 0db0c7c2a9..12da915688 100644 --- a/libavformat/Makefile +++ b/libavformat/Makefile @@ -546,7 +546,7 @@ OBJS-$(CONFIG_RTP_MUXER) += rtp.o \ rtpenc_vp8.o \ rtpenc_vp9.o \ rtpenc_xiph.o -OBJS-$(CONFIG_RTSP_DEMUXER) += rtsp.o rtspdec.o httpauth.o +OBJS-$(CONFIG_RTSP_DEMUXER) += rtsp.o rtspdec.o httpauth.o mikey.o OBJS-$(CONFIG_RTSP_MUXER) += rtsp.o rtspenc.o httpauth.o OBJS-$(CONFIG_S337M_DEMUXER) += s337m.o spdif.o OBJS-$(CONFIG_SAMI_DEMUXER) += samidec.o subtitles.o diff --git a/libavformat/mikey.c b/libavformat/mikey.c new file mode 100644 index 0000000000..93af635048 --- /dev/null +++ b/libavformat/mikey.c @@ -0,0 +1,412 @@ +/* + * MIKEY (RFC 3830) PSK-INIT key management for SRTP + * + * This file is part of FFmpeg. + * + * FFmpeg is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * FFmpeg is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with FFmpeg; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/** + * Minimal MIKEY PSK-INIT decoder (RFC 3830). + * + * Only the subset needed to extract SRTP master key and salt from a + * PSK-INIT message carried over RTSPS is implemented: + * - data_type 0 (PSK-INIT) + * - payload chain: HDR -> T -> RAND -> KEMAC + * - KEMAC encryption: NULL (ENC_NULL = 0) + * - KEMAC MAC: NULL (MAC_NULL = 0) + * - KEY_DATA type: TEK (2) or TEK+salt (3), KV = NULL (0) + * + * Any field value outside this subset is logged at AV_LOG_ERROR and + * causes the parser to return AVERROR_PATCHWELCOME so that unsupported + * configurations are never silently ignored. + * + * Wire format is byte-aligned (each logical field occupies one or more + * full octets). See the field layout notes beside each parse step. + */ + +#include <stdio.h> +#include <string.h> + +#include "libavutil/base64.h" +#include "libavutil/error.h" +#include "libavutil/intreadwrite.h" +#include "libavutil/log.h" + +#include "mikey.h" + +/* RFC 3830 6.1 Table 6.1.b - next-payload type identifiers */ +#define MIKEY_PT_LAST 0 +#define MIKEY_PT_KEMAC 1 +#define MIKEY_PT_T 5 +#define MIKEY_PT_RAND 11 + +/* RFC 3830 6.1 Table 6.1.a - data_type values */ +#define MIKEY_TYPE_PSK_INIT 0 + +/* RFC 3830 6.1 Table 6.1.c - PRF function */ +#define MIKEY_PRF_MIKEY_1 0 + +/* RFC 3830 6.1 Table 6.1.d - CS ID map type */ +#define MIKEY_MAP_TYPE_SRTP 0 + +/* RFC 3830 6.2 Table 6.2.a - KEMAC encryption algorithm */ +#define MIKEY_ENC_NULL 0 + +/* RFC 3830 6.2 Table 6.2.b - KEMAC MAC algorithm */ +#define MIKEY_MAC_NULL 0 + +/* RFC 3830 6.6 Table 6.6 - timestamp type */ +#define MIKEY_TS_NTP_UTC 0 + +/* RFC 3830 6.13 Table 6.13.a - KEY_DATA Type (upper nibble of byte 1). + * TGK (0) and TGK+SALT (1) require TEK derivation (Section 4.1.3), + * which is not implemented. Only TEK (2) and TEK+SALT (3) are accepted. */ +#define MIKEY_KD_TEK 2 +#define MIKEY_KD_TEK_WITH_SALT 3 + +/* Fixed sizes */ +#define MIKEY_HDR_SIZE 10 /* common header without CS ID map */ +#define MIKEY_CS_SRTP_SIZE 9 /* per-CS SRTP map: policy(1)+SSRC(4)+ROC(4) */ +#define MIKEY_RAND_MIN_LEN 16 /* RFC 3830 6.11: RAND SHOULD be >= 16 bytes */ +#define MIKEY_T_NTP_SIZE 8 /* RFC 3830 6.6: NTP-UTC timestamp */ + +/* Maximum supported key/salt sizes (AES-128-CM) */ +#define MIKEY_MAX_KEY_LEN 16 +#define MIKEY_MAX_SALT_LEN 14 + +int ff_mikey_parse(const char *mikey_b64, + char *suite_out, int suite_size, + char *params_out, int params_size, + void *logctx) +{ + uint8_t raw[512]; + uint8_t key[MIKEY_MAX_KEY_LEN]; + uint8_t salt[MIKEY_MAX_SALT_LEN]; + uint8_t combined[MIKEY_MAX_KEY_LEN + MIKEY_MAX_SALT_LEN]; + const uint8_t *d; + int raw_len, remaining, key_len, salt_len, n_cs, saw_t, saw_rand; + uint8_t next_payload; + + raw_len = av_base64_decode(raw, mikey_b64, sizeof(raw)); + if (raw_len < 0) { + av_log(logctx, AV_LOG_ERROR, "MIKEY: base64 decode failed\n"); + return AVERROR_INVALIDDATA; + } + + if (raw_len < MIKEY_HDR_SIZE) { + av_log(logctx, AV_LOG_ERROR, + "MIKEY: message too short (%d bytes)\n", raw_len); + return AVERROR_INVALIDDATA; + } + + d = raw; + remaining = raw_len; + key_len = 0; + salt_len = 0; + saw_t = 0; + saw_rand = 0; + + /* RFC 3830 6.1: common header + * [0] version (must be 1) + * [1] data_type (0 = PSK-INIT) + * [2] next_payload (type of first payload after header) + * [3] V(1b) | PRF_func(7b) + * [4-7] CSB_ID (32-bit big-endian, not needed for key extraction) + * [8] #CS (number of crypto sessions) + * [9] CS_ID_map_type + */ + if (d[0] != 1) { + av_log(logctx, AV_LOG_ERROR, + "MIKEY: unsupported version %u\n", d[0]); + return AVERROR_INVALIDDATA; + } + + if (d[1] != MIKEY_TYPE_PSK_INIT) { + av_log(logctx, AV_LOG_ERROR, + "MIKEY: unsupported data_type %u; only PSK-INIT (%u) supported\n", + d[1], MIKEY_TYPE_PSK_INIT); + return AVERROR_PATCHWELCOME; + } + + next_payload = d[2]; + + /* RFC 3830 6.1: V flag means the initiator expects a verification-response + * message. It has no bearing on key extraction; ignore it. */ + + if ((d[3] & 0x7F) != MIKEY_PRF_MIKEY_1) { + av_log(logctx, AV_LOG_ERROR, + "MIKEY: unsupported PRF function %u\n", d[3] & 0x7F); + return AVERROR_PATCHWELCOME; + } + + n_cs = d[8]; + + if (d[9] != MIKEY_MAP_TYPE_SRTP) { + av_log(logctx, AV_LOG_ERROR, + "MIKEY: unsupported CS ID map type %u\n", d[9]); + return AVERROR_PATCHWELCOME; + } + + if (n_cs != 1) { + av_log(logctx, AV_LOG_ERROR, + "MIKEY: only single-CS messages supported (got %d)\n", n_cs); + return AVERROR_PATCHWELCOME; + } + + if (remaining < MIKEY_HDR_SIZE + n_cs * MIKEY_CS_SRTP_SIZE) { + av_log(logctx, AV_LOG_ERROR, + "MIKEY: message truncated in CS ID map\n"); + return AVERROR_INVALIDDATA; + } + + /* + * RFC 3830 6.1.1: SRTP CS ID map entry (9 bytes per CS): + * [0] policy_no + * [1-4] SSRC (32-bit big-endian, not used here) + * [5-8] ROC (32-bit big-endian, not used here) + */ + d += MIKEY_HDR_SIZE + n_cs * MIKEY_CS_SRTP_SIZE; + remaining -= MIKEY_HDR_SIZE + n_cs * MIKEY_CS_SRTP_SIZE; + + /* RFC 3830 5.3: walk the payload chain */ + while (next_payload != MIKEY_PT_LAST && remaining > 0) { + uint8_t pt = next_payload; + + switch (pt) { + + case MIKEY_PT_T: { + /* + * RFC 3830 6.6: timestamp payload + * [0] next_payload + * [1] TS_type (0 = NTP-UTC) + * [2-9] 8-byte NTP-UTC timestamp + */ + if (remaining < 2) + goto err_truncated; + next_payload = d[0]; + if (d[1] != MIKEY_TS_NTP_UTC) { + av_log(logctx, AV_LOG_ERROR, + "MIKEY: unsupported timestamp type %u\n", d[1]); + return AVERROR_PATCHWELCOME; + } + if (remaining < 2 + MIKEY_T_NTP_SIZE) + goto err_truncated; + saw_t = 1; + d += 2 + MIKEY_T_NTP_SIZE; + remaining -= 2 + MIKEY_T_NTP_SIZE; + break; + } + + case MIKEY_PT_RAND: { + /* + * RFC 3830 6.11: RAND payload + * [0] next_payload + * [1] rand_len + * [2 .. 2+rand_len-1] random bytes + */ + uint8_t rand_len; + if (remaining < 2) + goto err_truncated; + next_payload = d[0]; + rand_len = d[1]; + if (rand_len < MIKEY_RAND_MIN_LEN) + av_log(logctx, AV_LOG_WARNING, + "MIKEY: RAND too short (%u < %u)\n", + rand_len, MIKEY_RAND_MIN_LEN); + if (remaining < 2 + (int)rand_len) + goto err_truncated; + saw_rand = 1; + d += 2 + rand_len; + remaining -= 2 + rand_len; + break; + } + + case MIKEY_PT_KEMAC: { + /* + * RFC 3830 6.2: KEMAC payload + * [0] next_payload (must be PT_LAST for PSK-INIT) + * [1] enc_alg (must be ENC_NULL) + * [2-3] enc_len (length of encrypted data, BE16) + * [4 .. 4+enc_len-1] encrypted key-data region + * [4+enc_len] mac_alg (must be MAC_NULL) + * + * RFC 3830 6.13: KEY_DATA sub-payload inside the encrypted region + * [0] Next payload (0 = last; only one sub-payload supported) + * [1] Type(4b) | KV(4b) + * [2-3] Key data len (BE16) + * [4 .. 4+key_len-1] Key data + * if type == MIKEY_KD_TEK_WITH_SALT: + * [4+key_len .. 4+key_len+1] salt_len (BE16) + * [4+key_len+2 .. ...] salt bytes + */ + const uint8_t *enc_data; + uint8_t enc_alg, mac_alg, sub_next, type_kv, kd_type, kv_type; + uint16_t enc_len, kd_key_len; + int has_salt, pos; + + if (remaining < 5) + goto err_truncated; + + /* RFC 3830 3.1: PSK-INIT ordering requires T and RAND before KEMAC */ + if (!saw_t || !saw_rand) { + av_log(logctx, AV_LOG_ERROR, + "MIKEY: T and RAND payloads must precede KEMAC\n"); + return AVERROR_INVALIDDATA; + } + + enc_alg = d[1]; + enc_len = AV_RB16(d + 2); + + /* RFC 3830 4.2.3: assert NULL encryption */ + if (enc_alg != MIKEY_ENC_NULL) { + av_log(logctx, AV_LOG_ERROR, + "MIKEY: KEMAC encryption alg %u not supported; " + "only NULL (%u) is accepted over RTSPS\n", + enc_alg, MIKEY_ENC_NULL); + return AVERROR_PATCHWELCOME; + } + + if (remaining < 5 + (int)enc_len) + goto err_truncated; + + mac_alg = d[4 + enc_len]; + + /* RFC 3830 4.2.4: assert NULL MAC */ + if (mac_alg != MIKEY_MAC_NULL) { + av_log(logctx, AV_LOG_ERROR, + "MIKEY: KEMAC MAC alg %u not supported; " + "only NULL (%u) is accepted over RTSPS\n", + mac_alg, MIKEY_MAC_NULL); + return AVERROR_PATCHWELCOME; + } + + if (d[0] != MIKEY_PT_LAST) { + av_log(logctx, AV_LOG_ERROR, + "MIKEY: additional payloads after KEMAC not supported\n"); + return AVERROR_PATCHWELCOME; + } + + /* Parse KEY_DATA sub-payload */ + enc_data = d + 4; + if (enc_len < 4) + goto err_truncated; + + sub_next = enc_data[0]; + if (sub_next != MIKEY_PT_LAST) { + av_log(logctx, AV_LOG_ERROR, + "MIKEY: multiple KEY_DATA sub-payloads not supported\n"); + return AVERROR_PATCHWELCOME; + } + + type_kv = enc_data[1]; + kd_type = (type_kv >> 4) & 0x0F; + kv_type = type_kv & 0x0F; + + if (kv_type != 0) { + av_log(logctx, AV_LOG_ERROR, + "MIKEY: KEY_DATA KV type %u not supported; " + "only NULL (0)\n", kv_type); + return AVERROR_PATCHWELCOME; + } + + if (kd_type != MIKEY_KD_TEK && kd_type != MIKEY_KD_TEK_WITH_SALT) { + av_log(logctx, AV_LOG_ERROR, + "MIKEY: unsupported KEY_DATA type %u\n", kd_type); + return AVERROR_PATCHWELCOME; + } + + has_salt = (kd_type == MIKEY_KD_TEK_WITH_SALT); + kd_key_len = AV_RB16(enc_data + 2); + + if (!kd_key_len || (int)enc_len < 4 + (int)kd_key_len) + goto err_truncated; + + if (kd_key_len > MIKEY_MAX_KEY_LEN) { + av_log(logctx, AV_LOG_ERROR, + "MIKEY: key too large (%u > %d)\n", + kd_key_len, MIKEY_MAX_KEY_LEN); + return AVERROR_INVALIDDATA; + } + + key_len = kd_key_len; + memcpy(key, enc_data + 4, key_len); + pos = 4 + key_len; + + if (has_salt) { + uint16_t kd_salt_len; + if ((int)enc_len < pos + 2) + goto err_truncated; + kd_salt_len = AV_RB16(enc_data + pos); + pos += 2; + if (!kd_salt_len || (int)enc_len < pos + (int)kd_salt_len) + goto err_truncated; + if (kd_salt_len > MIKEY_MAX_SALT_LEN) { + av_log(logctx, AV_LOG_ERROR, + "MIKEY: salt too large (%u > %d)\n", + kd_salt_len, MIKEY_MAX_SALT_LEN); + return AVERROR_INVALIDDATA; + } + salt_len = kd_salt_len; + memcpy(salt, enc_data + pos, salt_len); + } else { + av_log(logctx, AV_LOG_WARNING, + "MIKEY: no salt in KEY_DATA, using zero salt\n"); + salt_len = MIKEY_MAX_SALT_LEN; + memset(salt, 0, salt_len); + } + + goto done; + } + + default: + av_log(logctx, AV_LOG_ERROR, + "MIKEY: unknown payload type %u\n", pt); + return AVERROR_INVALIDDATA; + } + } + + av_log(logctx, AV_LOG_ERROR, "MIKEY: no KEMAC payload found\n"); + return AVERROR_INVALIDDATA; + +err_truncated: + av_log(logctx, AV_LOG_ERROR, "MIKEY: message truncated\n"); + return AVERROR_INVALIDDATA; + +done: + /* Derive SRTP suite name from key length */ + if (key_len == 16) { + snprintf(suite_out, suite_size, "AES_CM_128_HMAC_SHA1_80"); + } else { + av_log(logctx, AV_LOG_ERROR, + "MIKEY: unsupported key length %d\n", key_len); + return AVERROR_PATCHWELCOME; + } + + /* + * ff_srtp_set_crypto() expects params as base64(master_key || master_salt), + * i.e. 16 + 14 = 30 raw bytes -> 40 base64 characters. + */ + memcpy(combined, key, key_len); + memcpy(combined + key_len, salt, salt_len); + + if (!av_base64_encode(params_out, params_size, combined, key_len + salt_len)) { + av_log(logctx, AV_LOG_ERROR, + "MIKEY: params output buffer too small\n"); + return AVERROR(EINVAL); + } + + return 0; +} diff --git a/libavformat/mikey.h b/libavformat/mikey.h new file mode 100644 index 0000000000..069acabf94 --- /dev/null +++ b/libavformat/mikey.h @@ -0,0 +1,44 @@ +/* + * MIKEY (RFC 3830) PSK-INIT key management for SRTP + * + * This file is part of FFmpeg. + * + * FFmpeg is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * FFmpeg is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with FFmpeg; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef AVFORMAT_MIKEY_H +#define AVFORMAT_MIKEY_H + +/** + * Parse a base64-encoded MIKEY PSK-INIT message from an SDP a=key-mgmt:mikey + * attribute and extract SRTP key material for ff_srtp_set_crypto(). + * + * Supported: PSK-INIT (data_type 0), NULL encryption, NULL MAC (RFC 3830). + * Any other variant returns AVERROR_PATCHWELCOME. + * + * @param mikey_b64 NUL-terminated base64 MIKEY PDU. + * @param suite_out Output buffer for the SRTP suite name (e.g. "AES_CM_128_HMAC_SHA1_80"). + * @param suite_size Size of suite_out in bytes. + * @param params_out Output buffer for the base64-encoded key || salt string. + * @param params_size Size of params_out in bytes. + * @param logctx Logging context (may be NULL). + * @return 0 on success, negative AVERROR on failure. + */ +int ff_mikey_parse(const char *mikey_b64, + char *suite_out, int suite_size, + char *params_out, int params_size, + void *logctx); + +#endif /* AVFORMAT_MIKEY_H */ diff --git a/libavformat/rtsp.c b/libavformat/rtsp.c index 45b62c4188..8a4ae7600f 100644 --- a/libavformat/rtsp.c +++ b/libavformat/rtsp.c @@ -56,6 +56,7 @@ #include "tls.h" #include "rtpenc.h" #include "mpegts.h" +#include "mikey.h" #include "version.h" /* Default timeout values for read packet in seconds */ @@ -691,6 +692,25 @@ static void sdp_parse_line(AVFormatContext *s, SDPParseState *s1, p += strspn(p, SPACE_CHARS); if (av_strstart(p, "inline:", &p)) get_word(rtsp_st->crypto_params, sizeof(rtsp_st->crypto_params), &p); + } else if (av_strstart(p, "key-mgmt:", &p) && s->nb_streams > 0) { + // RFC 4567 + char keymgmt_b64[512]; + rtsp_st = rt->rtsp_streams[rt->nb_rtsp_streams - 1]; + p += strspn(p, SPACE_CHARS); + if (av_strstart(p, "mikey", &p)) { + p += strspn(p, SPACE_CHARS); + get_word(keymgmt_b64, sizeof(keymgmt_b64), &p); + if (ff_mikey_parse(keymgmt_b64, + rtsp_st->crypto_suite, + sizeof(rtsp_st->crypto_suite), + rtsp_st->crypto_params, + sizeof(rtsp_st->crypto_params), s) < 0) + av_log(s, AV_LOG_WARNING, + "Failed to parse MIKEY key-mgmt attribute\n"); + } else { + av_log(s, AV_LOG_WARNING, + "Unsupported key-mgmt protocol in SDP: %.20s\n", p); + } } else if (av_strstart(p, "source-filter:", &p)) { int exclude = 0; get_word(buf1, sizeof(buf1), &p); @@ -1914,9 +1934,8 @@ redirect: host, sizeof(host), &port, path, sizeof(path), s->url); if (!strcmp(proto, "rtsps")) { - lower_rtsp_proto = "tls"; - default_port = RTSPS_DEFAULT_PORT; - rt->lower_transport_mask = 1 << RTSP_LOWER_TRANSPORT_TCP; + lower_rtsp_proto = "tls"; + default_port = RTSPS_DEFAULT_PORT; } else if (!strcmp(proto, "satip")) { av_strlcpy(proto, "rtsp", sizeof(proto)); rt->server_type = RTSP_SERVER_SATIP; -- 2.52.0 _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]
