feat(gateway): validate encrypted Apollo RTSP frames
Verify Data Plane / gateway (push) Canceled after 1m42s

This commit is contained in:
sechmachine
2026-07-29 11:32:56 +07:00
parent 844e548c95
commit 994fe38fb9
6 changed files with 146 additions and 3 deletions
+1 -1
View File
@@ -40,7 +40,7 @@ func TestNativeApolloManagementUsesSessionScopedMTLS(t *testing.T) {
work := protocol.ProviderSessionWork{ work := protocol.ProviderSessionWork{
Version: "1", SessionID: "session-1", GatewayID: "gateway-1", ReconnectSequence: 0, Version: "1", SessionID: "session-1", GatewayID: "gateway-1", ReconnectSequence: 0,
ExpiresAt: "2099-01-01T00:00:00Z", ProviderProfile: ProviderProfileApollo, ExpiresAt: "2099-01-01T00:00:00Z", ProviderProfile: ProviderProfileApollo,
ProviderIdentity: "apollo-server#sha256:" + hex.EncodeToString(pinned[:]), PolicyVersionID: "policy-1", ApplicationID: "1", ProviderIdentity: "apollo-server#sha256:" + hex.EncodeToString(pinned[:]), PolicyVersionID: "policy-1", ApplicationID: "1", ClientID: "paired-client",
ManagementHost: host, ManagementPort: port, StreamHost: host, StreamPort: 47984, ManagementHost: host, ManagementPort: port, StreamHost: host, StreamPort: 47984,
ClientCertificatePem: certificatePEM(t, clientTLS.Certificates[0]), ClientCertificatePem: certificatePEM(t, clientTLS.Certificates[0]),
ClientPrivateKeyPem: privateKeyPEM(t, clientTLS.Certificates[0]), ClientPrivateKeyPem: privateKeyPEM(t, clientTLS.Certificates[0]),
+85
View File
@@ -0,0 +1,85 @@
package gateway
import (
"crypto/aes"
"crypto/cipher"
"encoding/binary"
"errors"
)
const (
encryptedRTSPHeaderSize = 24
encryptedRTSPMaxPayload = 64 << 10
)
var errEncryptedRTSPFrame = errors.New("invalid encrypted RTSP frame")
// encryptedRTSPCodec keeps client and host nonce spaces disjoint. It accepts
// only strictly increasing host sequence numbers, so a replay cannot be fed
// into the RTSP parser after it has already authenticated once.
type encryptedRTSPCodec struct {
aead cipher.AEAD
nextClient uint32
lastHost uint32
hostReceived bool
}
func newEncryptedRTSPCodec(key []byte) (*encryptedRTSPCodec, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
aead, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
return &encryptedRTSPCodec{aead: aead, nextClient: 1}, nil
}
func (codec *encryptedRTSPCodec) SealClient(plaintext []byte) ([]byte, error) {
if codec == nil || codec.aead == nil || len(plaintext) == 0 || len(plaintext) > encryptedRTSPMaxPayload || codec.nextClient == 0 {
return nil, errEncryptedRTSPFrame
}
sequence := codec.nextClient
codec.nextClient++
nonce := encryptedRTSPNonce(sequence, 'C', 'R')
sealed := codec.aead.Seal(nil, nonce[:], plaintext, nil)
ciphertext, tag := sealed[:len(plaintext)], sealed[len(plaintext):]
frame := make([]byte, encryptedRTSPHeaderSize+len(ciphertext))
binary.BigEndian.PutUint32(frame[:4], uint32(len(ciphertext))|0x80000000)
binary.BigEndian.PutUint32(frame[4:8], sequence)
copy(frame[8:24], tag)
copy(frame[24:], ciphertext)
return frame, nil
}
func (codec *encryptedRTSPCodec) OpenHost(frame []byte) ([]byte, error) {
if codec == nil || codec.aead == nil || len(frame) < encryptedRTSPHeaderSize {
return nil, errEncryptedRTSPFrame
}
length := binary.BigEndian.Uint32(frame[:4])
if length&0x80000000 == 0 || int(length&0x7fffffff) > encryptedRTSPMaxPayload || len(frame) != encryptedRTSPHeaderSize+int(length&0x7fffffff) {
return nil, errEncryptedRTSPFrame
}
sequence := binary.BigEndian.Uint32(frame[4:8])
if sequence == 0 || (codec.hostReceived && sequence <= codec.lastHost) {
return nil, errEncryptedRTSPFrame
}
nonce := encryptedRTSPNonce(sequence, 'H', 'R')
sealed := make([]byte, int(length&0x7fffffff)+codec.aead.Overhead())
copy(sealed, frame[24:])
copy(sealed[length&0x7fffffff:], frame[8:24])
plaintext, err := codec.aead.Open(nil, nonce[:], sealed, nil)
if err != nil {
return nil, errEncryptedRTSPFrame
}
codec.lastHost, codec.hostReceived = sequence, true
return plaintext, nil
}
func encryptedRTSPNonce(sequence uint32, origin, protocol byte) [12]byte {
var nonce [12]byte
binary.BigEndian.PutUint32(nonce[:4], sequence)
nonce[10], nonce[11] = origin, protocol
return nonce
}
+56
View File
@@ -0,0 +1,56 @@
package gateway
import (
"crypto/aes"
"crypto/cipher"
"encoding/binary"
"testing"
)
func TestEncryptedRTSPRejectsTagReplayAndReorderedHostFrames(t *testing.T) {
key := []byte("0123456789abcdef")
codec, err := newEncryptedRTSPCodec(key)
if err != nil {
t.Fatal(err)
}
first := hostEncryptedRTSPFrame(t, key, 1, []byte("RTSP/1.0 200 OK\r\n\r\n"))
if plaintext, err := codec.OpenHost(first); err != nil || string(plaintext) != "RTSP/1.0 200 OK\r\n\r\n" {
t.Fatalf("OpenHost() = %q, %v", plaintext, err)
}
if _, err := codec.OpenHost(first); err == nil {
t.Fatal("OpenHost() accepted a replayed frame")
}
tampered := hostEncryptedRTSPFrame(t, key, 2, []byte("RTSP/1.0 200 OK\r\n\r\n"))
tampered[len(tampered)-1] ^= 0x01
if _, err := codec.OpenHost(tampered); err == nil {
t.Fatal("OpenHost() accepted a tag failure")
}
third := hostEncryptedRTSPFrame(t, key, 3, []byte("RTSP/1.0 200 OK\r\n\r\n"))
if _, err := codec.OpenHost(third); err != nil {
t.Fatalf("OpenHost() sequence 3 error = %v", err)
}
second := hostEncryptedRTSPFrame(t, key, 2, []byte("RTSP/1.0 200 OK\r\n\r\n"))
if _, err := codec.OpenHost(second); err == nil {
t.Fatal("OpenHost() accepted an out-of-order frame")
}
}
func hostEncryptedRTSPFrame(t *testing.T, key []byte, sequence uint32, plaintext []byte) []byte {
t.Helper()
block, err := aes.NewCipher(key)
if err != nil {
t.Fatal(err)
}
aead, err := cipher.NewGCM(block)
if err != nil {
t.Fatal(err)
}
nonce := encryptedRTSPNonce(sequence, 'H', 'R')
sealed := aead.Seal(nil, nonce[:], plaintext, nil)
frame := make([]byte, encryptedRTSPHeaderSize+len(plaintext))
binary.BigEndian.PutUint32(frame[:4], uint32(len(plaintext))|0x80000000)
binary.BigEndian.PutUint32(frame[4:8], sequence)
copy(frame[8:24], sealed[len(plaintext):])
copy(frame[24:], sealed[:len(plaintext)])
return frame
}
+1 -1
View File
@@ -361,7 +361,7 @@ func (a *oneTimeAdmission) ProviderWork(_ context.Context, authority protocol.Se
Version: "1", SessionID: authority.SessionID, GatewayID: authority.GatewayID, Version: "1", SessionID: authority.SessionID, GatewayID: authority.GatewayID,
ReconnectSequence: authority.ReconnectSequence, ExpiresAt: authority.ExpiresAt, ReconnectSequence: authority.ReconnectSequence, ExpiresAt: authority.ExpiresAt,
ProviderProfile: ProviderProfileApollo, ProviderIdentity: authority.ProviderIdentity, ProviderProfile: ProviderProfileApollo, ProviderIdentity: authority.ProviderIdentity,
PolicyVersionID: "policy-1", ApplicationID: "1", ManagementHost: "apollo.test", ManagementPort: 47990, PolicyVersionID: "policy-1", ApplicationID: "1", ClientID: "paired-client", ManagementHost: "apollo.test", ManagementPort: 47990,
StreamHost: "apollo.test", StreamPort: 47984, ClientCertificatePem: "certificate", StreamHost: "apollo.test", StreamPort: 47984, ClientCertificatePem: "certificate",
ClientPrivateKeyPem: "private-key", ServerCertificatePem: "server-certificate", ClientPrivateKeyPem: "private-key", ServerCertificatePem: "server-certificate",
}, nil }, nil
+1 -1
View File
@@ -3,7 +3,7 @@ module git.sechmachine.io.vn/sechmachine/VerseVDI-Data-Plane
go 1.26.5 go 1.26.5
require ( require (
git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol v1.0.0-phase3c-gateway-rc.4 git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol v1.0.0-phase3c-gateway-rc.5
github.com/quic-go/quic-go v0.61.0 github.com/quic-go/quic-go v0.61.0
) )
+2
View File
@@ -2,6 +2,8 @@ git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol v1.0.0-phase3c-gateway-rc.3
git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol v1.0.0-phase3c-gateway-rc.3/go.mod h1:7PhFIDhjtr20btWoEb2GqB+7dBpzJt43olrnHVutWoc= git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol v1.0.0-phase3c-gateway-rc.3/go.mod h1:7PhFIDhjtr20btWoEb2GqB+7dBpzJt43olrnHVutWoc=
git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol v1.0.0-phase3c-gateway-rc.4 h1:uwNoKtzRlpdbvq9kHblncKr+K+dck0ydtozzPKItQzA= git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol v1.0.0-phase3c-gateway-rc.4 h1:uwNoKtzRlpdbvq9kHblncKr+K+dck0ydtozzPKItQzA=
git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol v1.0.0-phase3c-gateway-rc.4/go.mod h1:7PhFIDhjtr20btWoEb2GqB+7dBpzJt43olrnHVutWoc= git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol v1.0.0-phase3c-gateway-rc.4/go.mod h1:7PhFIDhjtr20btWoEb2GqB+7dBpzJt43olrnHVutWoc=
git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol v1.0.0-phase3c-gateway-rc.5 h1:F+Ig0OVpHcKr/G+uYy7Vm8BVxfei/Rk1yqYHtX9BcL0=
git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol v1.0.0-phase3c-gateway-rc.5/go.mod h1:7PhFIDhjtr20btWoEb2GqB+7dBpzJt43olrnHVutWoc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=