From 4fb542a47a789d5efdb7fa604541024c7760ccc8 Mon Sep 17 00:00:00 2001 From: Martin Vopatek Date: Tue, 18 Mar 2025 15:23:31 +0100 Subject: [PATCH] Allow to set a crypto policy Following policies are defined: AES_CM_128_HMAC_SHA1_80 AES_CM_128_HMAC_SHA1_32 AES_CM_192_HMAC_SHA1_80 AES_CM_192_HMAC_SHA1_32 AES_CM_256_HMAC_SHA1_80 AES_CM_256_HMAC_SHA1_32 AES_GCM_128_16 AES_GCM_256_16 --- src/_cffi_src/build_srtp.py | 11 ++++ src/pylibsrtp/__init__.py | 85 ++++++++++++++++++++++++---- tests/test_session.py | 110 +++++++++++++++++++++++++++++++++--- 3 files changed, 187 insertions(+), 19 deletions(-) diff --git a/src/_cffi_src/build_srtp.py b/src/_cffi_src/build_srtp.py index e75d5d5..e740333 100644 --- a/src/_cffi_src/build_srtp.py +++ b/src/_cffi_src/build_srtp.py @@ -35,7 +35,11 @@ ssrc_any_outbound = 3 } srtp_ssrc_type_t; +typedef uint32_t srtp_cipher_type_id_t; + typedef struct srtp_crypto_policy_t { + srtp_cipher_type_id_t cipher_type; + int cipher_key_len; ...; } srtp_crypto_policy_t; @@ -64,6 +68,13 @@ void srtp_crypto_policy_set_rtp_default(srtp_crypto_policy_t *p); void srtp_crypto_policy_set_rtcp_default(srtp_crypto_policy_t *p); +void srtp_crypto_policy_set_aes_cm_128_hmac_sha1_32(srtp_crypto_policy_t *p); +void srtp_crypto_policy_set_aes_cm_256_hmac_sha1_80(srtp_crypto_policy_t *p); +void srtp_crypto_policy_set_aes_cm_256_hmac_sha1_32(srtp_crypto_policy_t *p); +void srtp_crypto_policy_set_aes_cm_192_hmac_sha1_80(srtp_crypto_policy_t *p); +void srtp_crypto_policy_set_aes_cm_192_hmac_sha1_32(srtp_crypto_policy_t *p); +void srtp_crypto_policy_set_aes_gcm_128_16_auth(srtp_crypto_policy_t *p); +void srtp_crypto_policy_set_aes_gcm_256_16_auth(srtp_crypto_policy_t *p); srtp_err_status_t srtp_crypto_policy_set_from_profile_for_rtp( srtp_crypto_policy_t *policy, srtp_profile_t profile); diff --git a/src/pylibsrtp/__init__.py b/src/pylibsrtp/__init__.py index d93a20f..4c8743a 100644 --- a/src/pylibsrtp/__init__.py +++ b/src/pylibsrtp/__init__.py @@ -60,6 +60,15 @@ class Policy: Policy for single SRTP stream. """ + AES_CM_128_HMAC_SHA1_80 = 1 + AES_CM_128_HMAC_SHA1_32 = 2 + AES_CM_192_HMAC_SHA1_80 = 3 + AES_CM_192_HMAC_SHA1_32 = 4 + AES_CM_256_HMAC_SHA1_80 = 5 + AES_CM_256_HMAC_SHA1_32 = 6 + AES_GCM_128_16 = 7 + AES_GCM_256_16 = 8 + #: AES-128 CM mode with 80-bit authentication tag (default) SRTP_PROFILE_AES128_CM_SHA1_80 = lib.srtp_profile_aes128_cm_sha1_80 #: AES-128 CM mode with 32-bit authentication tag @@ -83,26 +92,68 @@ def __init__( key: Optional[bytes] = None, ssrc_type: int = SSRC_UNDEFINED, ssrc_value: int = 0, - srtp_profile: int = SRTP_PROFILE_AES128_CM_SHA1_80, + srtp_profile: Optional[int] = None, + crypto_policy: Optional[int] = None, ) -> None: + if srtp_profile is None and crypto_policy is None: + srtp_profile = self.SRTP_PROFILE_AES128_CM_SHA1_80 + self._policy = ffi.new("srtp_policy_t *") self._srtp_profile = srtp_profile + self._crypto_policy = crypto_policy - _srtp_assert( - lib.srtp_crypto_policy_set_from_profile_for_rtp( - ffi.addressof(self._policy.rtp), srtp_profile + if srtp_profile is not None: + _srtp_assert( + lib.srtp_crypto_policy_set_from_profile_for_rtp( + ffi.addressof(self._policy.rtp), srtp_profile + ) ) - ) - _srtp_assert( - lib.srtp_crypto_policy_set_from_profile_for_rtcp( - ffi.addressof(self._policy.rtcp), srtp_profile + _srtp_assert( + lib.srtp_crypto_policy_set_from_profile_for_rtcp( + ffi.addressof(self._policy.rtcp), srtp_profile + ) ) - ) + else: + self.__set_crypto_policy(crypto_policy) self.key = key self.ssrc_type = ssrc_type self.ssrc_value = ssrc_value + def __set_crypto_policy(self, crypto_policy: int) -> None: + rtp = ffi.addressof(self._policy.rtp) + rtcp = ffi.addressof(self._policy.rtcp) + + if crypto_policy == self.AES_CM_128_HMAC_SHA1_80: + lib.srtp_crypto_policy_set_rtp_default(rtp) + lib.srtp_crypto_policy_set_rtcp_default(rtcp) + elif crypto_policy == self.AES_CM_128_HMAC_SHA1_32: + lib.srtp_crypto_policy_set_aes_cm_128_hmac_sha1_32(rtp) + # RTCP use an 80-bit auth tag. + lib.srtp_crypto_policy_set_rtcp_default(rtcp) + elif crypto_policy == self.AES_CM_192_HMAC_SHA1_80: + lib.srtp_crypto_policy_set_aes_cm_192_hmac_sha1_80(rtp) + lib.srtp_crypto_policy_set_aes_cm_192_hmac_sha1_80(rtcp) + elif crypto_policy == self.AES_CM_192_HMAC_SHA1_32: + lib.srtp_crypto_policy_set_aes_cm_192_hmac_sha1_32(rtp) + # RTCP use an 80-bit auth tag. + lib.srtp_crypto_policy_set_aes_cm_192_hmac_sha1_80(rtcp) + elif crypto_policy == self.AES_CM_256_HMAC_SHA1_80: + lib.srtp_crypto_policy_set_aes_cm_256_hmac_sha1_80(rtp) + lib.srtp_crypto_policy_set_aes_cm_256_hmac_sha1_80(rtcp) + elif crypto_policy == self.AES_CM_256_HMAC_SHA1_32: + lib.srtp_crypto_policy_set_aes_cm_256_hmac_sha1_32(rtp) + # RTCP use an 80-bit auth tag. + lib.srtp_crypto_policy_set_aes_cm_256_hmac_sha1_80(rtcp) + elif crypto_policy == self.AES_GCM_128_16: + lib.srtp_crypto_policy_set_aes_gcm_128_16_auth(rtp) + lib.srtp_crypto_policy_set_aes_gcm_128_16_auth(rtcp) + elif crypto_policy == self.AES_GCM_256_16: + lib.srtp_crypto_policy_set_aes_gcm_256_16_auth(rtp) + lib.srtp_crypto_policy_set_aes_gcm_256_16_auth(rtcp) + else: + raise ValueError("Unknown crypto policy") + @property def allow_repeat_tx(self) -> bool: """ @@ -114,6 +165,13 @@ def allow_repeat_tx(self) -> bool: def allow_repeat_tx(self, allow_repeat_tx: bool) -> None: self._policy.allow_repeat_tx = 1 if allow_repeat_tx else 0 + @property + def crypto_policy(self) -> int: + """ + The crypto policy. + """ + return self._crypto_policy + @property def key(self) -> Optional[bytes]: """ @@ -132,9 +190,12 @@ def key(self, key: Optional[bytes]) -> None: return # Check the key is acceptable then assign it. - expected_length = lib.srtp_profile_get_master_key_length( - self._srtp_profile - ) + lib.srtp_profile_get_master_salt_length(self._srtp_profile) + if self._srtp_profile is not None: + expected_length = lib.srtp_profile_get_master_key_length( + self._srtp_profile + ) + lib.srtp_profile_get_master_salt_length(self._srtp_profile) + else: + expected_length = self._policy.rtp.cipher_key_len if not isinstance(key, bytes): raise TypeError("key must be bytes") if len(key) < expected_length: diff --git a/tests/test_session.py b/tests/test_session.py index 55fa460..f865ed9 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -1,9 +1,12 @@ -import dataclasses +import logging import secrets +from typing import Optional from unittest import TestCase from pylibsrtp import Error, Policy, Session +logger = logging.getLogger(__name__) + RTP = ( b"\x80\x08\x00\x00" # version, packet type, sequence number b"\x00\x00\x00\x00" # timestamp @@ -15,12 +18,25 @@ ) -@dataclasses.dataclass class Profile: - key_length: int - protected_rtp_length: int - protected_rtcp_length: int - srtp_profile: int + def __init__(self, + key_length: int, + protected_rtp_length: int, + protected_rtcp_length: int, + srtp_profile: Optional[int] = None, + crypto_policy: Optional[int] = None): + self.key_length = key_length + self.protected_rtp_length = protected_rtp_length + self.protected_rtcp_length = protected_rtcp_length + self.srtp_profile = srtp_profile + self.crypto_policy = crypto_policy + + def __repr__(self): + return (f'Profile(key_length={self.key_length}, ' + f'protected_rtp_length={self.protected_rtp_length}, ' + f'protected_rtcp_length={self.protected_rtcp_length}, ' + f'srtp_profile={self.srtp_profile}, ' + f'crypto_policy={self.crypto_policy})') # AES-GCM may not be supported depending on how libsrtp2 was built. @@ -30,17 +46,72 @@ class Profile: protected_rtp_length=182, protected_rtcp_length=42, srtp_profile=Policy.SRTP_PROFILE_AES128_CM_SHA1_80, + crypto_policy=None, ), Profile( key_length=30, protected_rtp_length=176, protected_rtcp_length=42, srtp_profile=Policy.SRTP_PROFILE_AES128_CM_SHA1_32, + crypto_policy=None, + ), + Profile( + key_length=30, + protected_rtp_length=182, + protected_rtcp_length=42, + srtp_profile=None, + crypto_policy=Policy.AES_CM_128_HMAC_SHA1_80, + ), + Profile( + key_length=30, + protected_rtp_length=176, + protected_rtcp_length=42, + srtp_profile=None, + crypto_policy=Policy.AES_CM_128_HMAC_SHA1_32, + ), + Profile( + key_length=46, + protected_rtp_length=182, + protected_rtcp_length=42, + srtp_profile=None, + crypto_policy=Policy.AES_CM_256_HMAC_SHA1_80, + ), + Profile( + key_length=46, + protected_rtp_length=176, + protected_rtcp_length=42, + srtp_profile=None, + crypto_policy=Policy.AES_CM_256_HMAC_SHA1_32, ), ] + +try: + Session(Policy(key=b'\x00' * 38, crypto_policy=Policy.AES_CM_192_HMAC_SHA1_80)) +except Error: + logger.warning("AES-192 is not supported by this libsrtp.") + pass +else: + SRTP_PROFILES += [ + Profile( + key_length=38, + protected_rtp_length=182, + protected_rtcp_length=42, + srtp_profile=None, + crypto_policy=Policy.AES_CM_192_HMAC_SHA1_80, + ), + Profile( + key_length=38, + protected_rtp_length=176, + protected_rtcp_length=42, + srtp_profile=None, + crypto_policy=Policy.AES_CM_192_HMAC_SHA1_32, + ), + ] + try: Policy(srtp_profile=Policy.SRTP_PROFILE_AEAD_AES_128_GCM) except Error: + logger.warning("GCM is not supported by this libsrtp.") pass else: SRTP_PROFILES += [ @@ -49,12 +120,28 @@ class Profile: protected_rtp_length=188, protected_rtcp_length=48, srtp_profile=Policy.SRTP_PROFILE_AEAD_AES_128_GCM, + crypto_policy=None, ), Profile( key_length=44, protected_rtp_length=188, protected_rtcp_length=48, srtp_profile=Policy.SRTP_PROFILE_AEAD_AES_256_GCM, + crypto_policy=None, + ), + Profile( + key_length=28, + protected_rtp_length=188, + protected_rtcp_length=48, + srtp_profile=None, + crypto_policy=Policy.AES_GCM_128_16, + ), + Profile( + key_length=44, + protected_rtp_length=188, + protected_rtcp_length=48, + srtp_profile=None, + crypto_policy=Policy.AES_GCM_256_16, ), ] @@ -108,7 +195,8 @@ def test_srtp_policy(self): # Valid user-specified profiles. for profile in SRTP_PROFILES: with self.subTest(profile=profile): - policy = Policy(srtp_profile=profile.srtp_profile) + policy = Policy(srtp_profile=profile.srtp_profile, + crypto_policy=profile.crypto_policy) self.assertEqual(policy.srtp_profile, profile.srtp_profile) # Invalid profile. @@ -158,6 +246,7 @@ def test_add_remove_stream(self): srtp_profile=profile.srtp_profile, ssrc_type=Policy.SSRC_SPECIFIC, ssrc_value=12345, + crypto_policy=profile.crypto_policy, ) ) protected = tx_session.protect(RTP) @@ -171,6 +260,7 @@ def test_add_remove_stream(self): srtp_profile=profile.srtp_profile, ssrc_type=Policy.SSRC_SPECIFIC, ssrc_value=12345, + crypto_policy=profile.crypto_policy, ) ) unprotected = rx_session.unprotect(protected) @@ -195,6 +285,7 @@ def test_rtp_any_ssrc(self): key=key, srtp_profile=profile.srtp_profile, ssrc_type=Policy.SSRC_ANY_OUTBOUND, + crypto_policy=profile.crypto_policy, ) ) protected = tx_session.protect(RTP) @@ -216,6 +307,7 @@ def test_rtp_any_ssrc(self): key=key, srtp_profile=profile.srtp_profile, ssrc_type=Policy.SSRC_ANY_INBOUND, + crypto_policy=profile.crypto_policy, ) ) unprotected = rx_session.unprotect(protected) @@ -232,6 +324,7 @@ def test_rtcp_any_ssrc(self): key=key, srtp_profile=profile.srtp_profile, ssrc_type=Policy.SSRC_ANY_OUTBOUND, + crypto_policy=profile.crypto_policy, ) ) protected = tx_session.protect_rtcp(RTCP) @@ -253,6 +346,7 @@ def test_rtcp_any_ssrc(self): key=key, srtp_profile=profile.srtp_profile, ssrc_type=Policy.SSRC_ANY_INBOUND, + crypto_policy=profile.crypto_policy, ) ) unprotected = rx_session.unprotect_rtcp(protected) @@ -270,6 +364,7 @@ def test_rtp_specific_ssrc(self): srtp_profile=profile.srtp_profile, ssrc_type=Policy.SSRC_SPECIFIC, ssrc_value=12345, + crypto_policy=profile.crypto_policy, ) ) protected = tx_session.protect(RTP) @@ -282,6 +377,7 @@ def test_rtp_specific_ssrc(self): srtp_profile=profile.srtp_profile, ssrc_type=Policy.SSRC_SPECIFIC, ssrc_value=12345, + crypto_policy=profile.crypto_policy, ) ) unprotected = rx_session.unprotect(protected)