diff --git a/gateway/apollo_native_test.go b/gateway/apollo_native_test.go index 63a4543..75e1c52 100644 --- a/gateway/apollo_native_test.go +++ b/gateway/apollo_native_test.go @@ -40,7 +40,7 @@ func TestNativeApolloManagementUsesSessionScopedMTLS(t *testing.T) { work := protocol.ProviderSessionWork{ Version: "1", SessionID: "session-1", GatewayID: "gateway-1", ReconnectSequence: 0, 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, ClientCertificatePem: certificatePEM(t, clientTLS.Certificates[0]), ClientPrivateKeyPem: privateKeyPEM(t, clientTLS.Certificates[0]), diff --git a/gateway/apollo_rtsp.go b/gateway/apollo_rtsp.go new file mode 100644 index 0000000..5d6c5a8 --- /dev/null +++ b/gateway/apollo_rtsp.go @@ -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 +} diff --git a/gateway/apollo_rtsp_test.go b/gateway/apollo_rtsp_test.go new file mode 100644 index 0000000..4fe6462 --- /dev/null +++ b/gateway/apollo_rtsp_test.go @@ -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 +} diff --git a/gateway/gateway_test.go b/gateway/gateway_test.go index 561db78..fdf4168 100644 --- a/gateway/gateway_test.go +++ b/gateway/gateway_test.go @@ -361,7 +361,7 @@ func (a *oneTimeAdmission) ProviderWork(_ context.Context, authority protocol.Se Version: "1", SessionID: authority.SessionID, GatewayID: authority.GatewayID, ReconnectSequence: authority.ReconnectSequence, ExpiresAt: authority.ExpiresAt, 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", ClientPrivateKeyPem: "private-key", ServerCertificatePem: "server-certificate", }, nil diff --git a/go.mod b/go.mod index 03c9996..2429404 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module git.sechmachine.io.vn/sechmachine/VerseVDI-Data-Plane go 1.26.5 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 ) diff --git a/go.sum b/go.sum index 689d616..322aae8 100644 --- a/go.sum +++ b/go.sum @@ -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.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.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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=