Skip to content

Commit 13514f1

Browse files
committed
Support selected RCC Transforms from RFC 4771
Roll-over Counter Carrying Transform specified in RFC 4771 defines way to send Roll-over Counter in Authentication Tag of SRTP packet. This allows late joiners to reliably discover current value of ROC. RFC 4771 defines 3 RCC modes. This PR adds support for mode RCCm2 for AES-CM and NULL profiles, and mode RCCm3 for AES-GCM (AEAD) profiles. Modes RCCm1 and RCCm3 disables authentication tag so they decrease security of AES-CM profiles. For AES-GCS profiles modes RCCm1 and RCCm2 would require to calculate SHA-1 MAC in addition to existing AEAD tag, unnecessary causing extra CPU load and increasing SRTP packet size.
1 parent e16ce5b commit 13514f1

14 files changed

+682
-98
lines changed

context.go

Lines changed: 102 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,23 @@ type srtcpSSRCState struct {
4343
replayDetector replaydetector.ReplayDetector
4444
}
4545

46+
// RCCMode is the mode of Roll-over Counter Carrying Transform from RFC 4771.
47+
type RCCMode int
48+
49+
const (
50+
// RCCModeNone is the default mode.
51+
RCCModeNone RCCMode = iota
52+
// RCCMode1 is RCCm1 mode from RFC 4771. In this mode ROC and truncated auth tag is sent every R-th packet,
53+
// and no auth tag in other ones. This mode is not supported by pion/srtp.
54+
RCCMode1
55+
// RCCMode2 is RCCm2 mode from RFC 4771. In this mode ROC and truncated auth tag is sent every R-th packet,
56+
// and full auth tag in other ones. This mode is supported for AES-CM and NULL profiles only.
57+
RCCMode2
58+
// RCCMode3 is RCCm3 mode from RFC 4771. In this mode ROC is sent every R-th packet (without truncated auth tag),
59+
// and no auth tag in other ones. This mode is supported for AES-GCM profiles only.
60+
RCCMode3
61+
)
62+
4663
// Context represents a SRTP cryptographic context.
4764
// Context can only be used for one-way operations.
4865
// it must either used ONLY for encryption or ONLY for decryption.
@@ -67,6 +84,11 @@ type Context struct {
6784

6885
encryptSRTP bool
6986
encryptSRTCP bool
87+
88+
rccMode RCCMode
89+
rocTransmitRate uint16
90+
91+
authTagRTPLen *int
7092
}
7193

7294
// CreateContext creates a new SRTP Context.
@@ -102,6 +124,21 @@ func CreateContext(
102124
}
103125
}
104126

127+
if err = c.checkRCCMode(); err != nil {
128+
return nil, err
129+
}
130+
131+
if c.authTagRTPLen != nil {
132+
var authKeyLen int
133+
authKeyLen, err = c.profile.AuthKeyLen()
134+
if err != nil {
135+
return nil, err
136+
}
137+
if *c.authTagRTPLen > authKeyLen {
138+
return nil, errTooLongSRTPAuthTag
139+
}
140+
}
141+
105142
c.cipher, err = c.createCipher(c.sendMKI, masterKey, masterSalt, c.encryptSRTP, c.encryptSRTCP)
106143
if err != nil {
107144
return nil, err
@@ -154,16 +191,21 @@ func (c *Context) createCipher(mki, masterKey, masterSalt []byte, encryptSRTP, e
154191
return nil, fmt.Errorf("%w expected(%d) actual(%d)", errShortSrtpMasterSalt, saltLen, masterSaltLen)
155192
}
156193

194+
profileWithArgs := protectionProfileWithArgs{
195+
ProtectionProfile: c.profile,
196+
authTagRTPLen: c.authTagRTPLen,
197+
}
198+
157199
switch c.profile {
158200
case ProtectionProfileAeadAes128Gcm, ProtectionProfileAeadAes256Gcm:
159-
return newSrtpCipherAeadAesGcm(c.profile, masterKey, masterSalt, mki, encryptSRTP, encryptSRTCP)
201+
return newSrtpCipherAeadAesGcm(profileWithArgs, masterKey, masterSalt, mki, encryptSRTP, encryptSRTCP)
160202
case ProtectionProfileAes128CmHmacSha1_32,
161203
ProtectionProfileAes128CmHmacSha1_80,
162204
ProtectionProfileAes256CmHmacSha1_32,
163205
ProtectionProfileAes256CmHmacSha1_80:
164-
return newSrtpCipherAesCmHmacSha1(c.profile, masterKey, masterSalt, mki, encryptSRTP, encryptSRTCP)
206+
return newSrtpCipherAesCmHmacSha1(profileWithArgs, masterKey, masterSalt, mki, encryptSRTP, encryptSRTCP)
165207
case ProtectionProfileNullHmacSha1_32, ProtectionProfileNullHmacSha1_80:
166-
return newSrtpCipherAesCmHmacSha1(c.profile, masterKey, masterSalt, mki, false, false)
208+
return newSrtpCipherAesCmHmacSha1(profileWithArgs, masterKey, masterSalt, mki, false, false)
167209
default:
168210
return nil, fmt.Errorf("%w: %#v", errNoSuchSRTPProfile, c.profile)
169211
}
@@ -197,7 +239,7 @@ func (c *Context) SetSendMKI(mki []byte) error {
197239
}
198240

199241
// https://tools.ietf.org/html/rfc3550#appendix-A.1
200-
func (s *srtpSSRCState) nextRolloverCount(sequenceNumber uint16) (roc uint32, diff int32, overflow bool) {
242+
func (s *srtpSSRCState) nextRolloverCount(sequenceNumber uint16) (roc uint32, diff int64, overflow bool) {
201243
seq := int32(sequenceNumber)
202244
localRoc := uint32(s.index >> 16) //nolint:gosec // G115
203245
localSeq := int32(s.index & (seqNumMax - 1)) //nolint:gosec // G115
@@ -232,17 +274,20 @@ func (s *srtpSSRCState) nextRolloverCount(sequenceNumber uint16) (roc uint32, di
232274
}
233275
}
234276

235-
return guessRoc, difference, (guessRoc == 0 && localRoc == maxROC)
277+
return guessRoc, int64(difference), (guessRoc == 0 && localRoc == maxROC)
236278
}
237279

238-
func (s *srtpSSRCState) updateRolloverCount(sequenceNumber uint16, difference int32) {
239-
if !s.rolloverHasProcessed {
280+
func (s *srtpSSRCState) updateRolloverCount(sequenceNumber uint16, difference int64, hasRemoteRoc bool,
281+
remoteRoc uint32,
282+
) {
283+
switch {
284+
case hasRemoteRoc:
285+
s.index = (uint64(remoteRoc) << 16) | uint64(sequenceNumber)
286+
s.rolloverHasProcessed = true
287+
case !s.rolloverHasProcessed:
240288
s.index |= uint64(sequenceNumber)
241289
s.rolloverHasProcessed = true
242-
243-
return
244-
}
245-
if difference > 0 {
290+
case difference > 0:
246291
s.index += uint64(difference)
247292
}
248293
}
@@ -309,3 +354,49 @@ func (c *Context) SetIndex(ssrc uint32, index uint32) {
309354
s := c.getSRTCPSSRCState(ssrc)
310355
s.srtcpIndex = index % (maxSRTCPIndex + 1)
311356
}
357+
358+
//nolint:cyclop
359+
func (c *Context) checkRCCMode() error {
360+
if c.rccMode == RCCModeNone {
361+
return nil
362+
}
363+
364+
if c.rocTransmitRate == 0 {
365+
return errZeroRocTransmitRate
366+
}
367+
368+
switch c.profile {
369+
case ProtectionProfileAeadAes128Gcm, ProtectionProfileAeadAes256Gcm:
370+
// AEAD profiles support RCCMode3 only
371+
if c.rccMode != RCCMode3 {
372+
return errUnsupportedRccMode
373+
}
374+
375+
case ProtectionProfileAes128CmHmacSha1_32,
376+
ProtectionProfileAes256CmHmacSha1_32,
377+
ProtectionProfileNullHmacSha1_32:
378+
if c.authTagRTPLen == nil {
379+
// ROC completely replaces auth tag for _32 profiles. If you really want to use 4-byte
380+
// SRTP auth tag with RCC, use SRTPAuthenticationTagLength(4) option.
381+
return errTooShortSRTPAuthTag
382+
}
383+
384+
fallthrough // Checks below are common for _32 and _80 profiles.
385+
386+
case ProtectionProfileAes128CmHmacSha1_80,
387+
ProtectionProfileAes256CmHmacSha1_80,
388+
ProtectionProfileNullHmacSha1_80:
389+
// AES-CM and NULL profiles support RCCMode2 only
390+
if c.rccMode != RCCMode2 {
391+
return errUnsupportedRccMode
392+
}
393+
if c.authTagRTPLen != nil && *c.authTagRTPLen < 4 {
394+
return errTooShortSRTPAuthTag
395+
}
396+
397+
default:
398+
return errUnsupportedRccMode
399+
}
400+
401+
return nil
402+
}

context_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,77 @@ func TestContextRemoveMKI(t *testing.T) {
142142
err = ctx.RemoveMKI(mki3)
143143
assert.Error(t, err)
144144
}
145+
146+
func TestInvalidContextOptions(t *testing.T) {
147+
profiles := []ProtectionProfile{
148+
ProtectionProfileAes128CmHmacSha1_80,
149+
ProtectionProfileAes128CmHmacSha1_32,
150+
ProtectionProfileAes256CmHmacSha1_80,
151+
ProtectionProfileAes256CmHmacSha1_32,
152+
ProtectionProfileNullHmacSha1_80,
153+
ProtectionProfileNullHmacSha1_32,
154+
ProtectionProfileAeadAes128Gcm,
155+
ProtectionProfileAeadAes256Gcm,
156+
}
157+
158+
for _, profile := range profiles {
159+
t.Run(profile.String(), func(t *testing.T) {
160+
keyLen, err := profile.KeyLen()
161+
assert.NoError(t, err)
162+
saltLen, err := profile.SaltLen()
163+
assert.NoError(t, err)
164+
authTagLen, err := profile.AuthTagRTPLen()
165+
assert.NoError(t, err)
166+
authKeyLen, err := profile.AuthKeyLen()
167+
assert.NoError(t, err)
168+
169+
masterKey := make([]byte, keyLen)
170+
masterSalt := make([]byte, saltLen)
171+
aeadAuthTagLen, err := profile.AEADAuthTagLen()
172+
assert.NoError(t, err)
173+
174+
t.Run("InvalidRCCContextOptions", func(t *testing.T) {
175+
authTagLenOpt := SRTPAuthenticationTagLength(authTagLen)
176+
177+
_, err = CreateContext(masterKey, masterSalt, profile, RolloverCounterCarryingTransform(RCCMode1, 0),
178+
authTagLenOpt)
179+
assert.ErrorIs(t, err, errZeroRocTransmitRate)
180+
_, err = CreateContext(masterKey, masterSalt, profile, RolloverCounterCarryingTransform(RCCMode2, 0),
181+
authTagLenOpt)
182+
assert.ErrorIs(t, err, errZeroRocTransmitRate)
183+
_, err = CreateContext(masterKey, masterSalt, profile, RolloverCounterCarryingTransform(RCCMode3, 0),
184+
authTagLenOpt)
185+
assert.ErrorIs(t, err, errZeroRocTransmitRate)
186+
187+
_, err = CreateContext(masterKey, masterSalt, profile, RolloverCounterCarryingTransform(RCCMode1, 10),
188+
authTagLenOpt)
189+
assert.ErrorIs(t, err, errUnsupportedRccMode)
190+
if aeadAuthTagLen == 0 { // AES-CM
191+
_, err = CreateContext(masterKey, masterSalt, profile, RolloverCounterCarryingTransform(RCCMode3, 10),
192+
authTagLenOpt)
193+
assert.ErrorIs(t, err, errUnsupportedRccMode)
194+
195+
if authTagLen == 4 {
196+
_, err = CreateContext(masterKey, masterSalt, profile, RolloverCounterCarryingTransform(RCCMode2, 10))
197+
assert.ErrorIs(t, err, errTooShortSRTPAuthTag)
198+
}
199+
200+
for n := 0; n < 4; n++ {
201+
_, err = CreateContext(masterKey, masterSalt, profile, RolloverCounterCarryingTransform(RCCMode2, 10),
202+
SRTPAuthenticationTagLength(n))
203+
assert.ErrorIs(t, err, errTooShortSRTPAuthTag)
204+
}
205+
} else { // AEAD
206+
_, err = CreateContext(masterKey, masterSalt, profile, RolloverCounterCarryingTransform(RCCMode2, 10),
207+
authTagLenOpt)
208+
assert.ErrorIs(t, err, errUnsupportedRccMode)
209+
}
210+
})
211+
212+
t.Run("InvalidSRTPAuthTagLen", func(t *testing.T) {
213+
_, err = CreateContext(masterKey, masterSalt, profile, SRTPAuthenticationTagLength(authKeyLen+1))
214+
assert.ErrorIs(t, err, errTooLongSRTPAuthTag)
215+
})
216+
})
217+
}
218+
}

errors.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,16 @@ var (
3131
errMKIAlreadyInUse = errors.New("MKI already in use")
3232
errMKIIsNotEnabled = errors.New("MKI is not enabled")
3333
errInvalidMKILength = errors.New("invalid MKI length")
34+
errTooLongSRTPAuthTag = errors.New("SRTP auth tag is too long")
35+
errTooShortSRTPAuthTag = errors.New("SRTP auth tag is too short")
3436

3537
errStreamNotInited = errors.New("stream has not been inited, unable to close")
3638
errStreamAlreadyClosed = errors.New("stream is already closed")
3739
errStreamAlreadyInited = errors.New("stream is already inited")
3840
errFailedTypeAssertion = errors.New("failed to cast child")
41+
42+
errZeroRocTransmitRate = errors.New("ROC transmit rate is zero")
43+
errUnsupportedRccMode = errors.New("unsupported RCC mode")
3944
)
4045

4146
type duplicatedError struct {

option.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ func SRTPEncryption() ContextOption { // nolint:revive
104104
// SRTPNoEncryption disables SRTP encryption.
105105
// This option is useful when you want to use NullCipher for SRTP and keep authentication only.
106106
// It simplifies debugging and testing, but it is not recommended for production use.
107+
//
108+
// Note: you can also use SRTPAuthenticationTagLength(0) to disable authentication tag too.
107109
func SRTPNoEncryption() ContextOption { // nolint:revive
108110
return func(c *Context) error {
109111
c.encryptSRTP = false
@@ -131,3 +133,43 @@ func SRTCPNoEncryption() ContextOption {
131133
return nil
132134
}
133135
}
136+
137+
// RolloverCounterCarryingTransform enables Rollover Counter Carrying Transform from RFC 4771.
138+
// ROC value is sent in Authentication Tag of SRTP packets every rocTransmitRate packets.
139+
//
140+
// RFC 4771 defines 3 RCC modes. pion/srtp supports mode RCCm2 for AES-CM and NULL profiles,
141+
// and mode RCCm3 for AES-GCM (AEAD) profiles.
142+
//
143+
// From RFC 4771: "[For modes RCCm1 and and RCCm3] the length of the MAC is shorter than the length
144+
// of the authentication tag. To achieve the same (or less) MAC forgery success probability on all
145+
// packets when using RCCm1 or RCCm2, as with the default integrity transform in RFC 3711,
146+
// the tag-length must be set to 14 octets, which means that the length of MAC_tr is 10 octets."
147+
//
148+
// Protection profiles ProtectionProfile*CmHmacSha1_32 uses 4-byte SRTP auth tag, so in RCCm2 mode
149+
// SRTP packets with ROC will not be integrity protected.
150+
//
151+
// You can increase the length of the authentication tag using SRTPAuthenticationTagLength option
152+
// to mitigate this issue.
153+
func RolloverCounterCarryingTransform(mode RCCMode, rocTransmitRate uint16) ContextOption {
154+
return func(c *Context) error {
155+
c.rccMode = mode
156+
c.rocTransmitRate = rocTransmitRate
157+
158+
return nil
159+
}
160+
}
161+
162+
// SRTPAuthenticationTagLength sets length of SRTP authentication tag in bytes for AES-CM protection
163+
// profiles. Decreasing the length of the authentication tag is not recommended for production use,
164+
// as it decreases integrity protection.
165+
//
166+
// Zero value means that there is no authentication tag, what may be useful for debugging and testing.
167+
//
168+
// This option is ignored for AEAD profiles.
169+
func SRTPAuthenticationTagLength(authTagRTPLen int) ContextOption { // nolint:revive
170+
return func(c *Context) error {
171+
c.authTagRTPLen = &authTagRTPLen
172+
173+
return nil
174+
}
175+
}

protection_profile_with_args.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
2+
// SPDX-License-Identifier: MIT
3+
4+
package srtp
5+
6+
// protectionProfileWithArgs is a wrapper around ProtectionProfile that allows to
7+
// specify additional arguments for the profile.
8+
type protectionProfileWithArgs struct {
9+
ProtectionProfile
10+
authTagRTPLen *int
11+
}
12+
13+
// AuthTagRTPLen returns length of RTP authentication tag in bytes for AES protection profiles.
14+
// For AEAD ones it returns zero.
15+
func (p protectionProfileWithArgs) AuthTagRTPLen() (int, error) {
16+
if p.authTagRTPLen != nil {
17+
return *p.authTagRTPLen, nil
18+
}
19+
20+
return p.ProtectionProfile.AuthTagRTPLen()
21+
}

srtcp.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,16 @@ import (
1010
"github.com/pion/rtcp"
1111
)
1212

13+
/*
14+
Simplified structure of SRTCP Packets:
15+
- RTCP Header
16+
- Payload
17+
- AEAD Auth Tag - used by AEAD profiles only
18+
- E flag and SRTCP Index
19+
- MKI (optional)
20+
- Auth Tag - used by non-AEAD profiles only
21+
*/
22+
1323
const maxSRTCPIndex = 0x7FFFFFFF
1424

1525
const srtcpHeaderSize = 8
@@ -42,7 +52,7 @@ func (c *Context) decryptRTCP(dst, encrypted []byte) ([]byte, error) {
4252
cipher := c.cipher
4353
if len(c.mkis) > 0 {
4454
// Find cipher for MKI
45-
actualMKI := c.cipher.getMKI(encrypted, false)
55+
actualMKI := encrypted[len(encrypted)-mkiLen-authTagLen : len(encrypted)-authTagLen]
4656
cipher, ok = c.mkis[string(actualMKI)]
4757
if !ok {
4858
return nil, ErrMKINotFound

0 commit comments

Comments
 (0)