RTSP source: add mandatory parameter sourceFingerprint to validate server certificates and prevent man-in-the-middle attacks (#350)

This commit is contained in:
aler9 2021-04-16 22:46:22 +02:00
parent cbda813e1a
commit 3d1b5296d7
6 changed files with 73 additions and 25 deletions

View File

@ -149,7 +149,7 @@ The configuration can be changed dinamically when the server is running (hot rel
### Encryption ### Encryption
Incoming and outgoing streams can be encrypted with TLS (obtaining the RTSPS protocol). A TLS certificate must be installed on the server; if the server is installed on a machine that is publicly accessible from the internet, a certificate can be requested from a Certificate authority by using tools like [Certbot](https://certbot.eff.org/); otherwise, a self-signed certificate can be generated with openSSL: Incoming and outgoing streams can be encrypted with TLS (obtaining the RTSPS protocol). A self-signed TLS certificate is needed and can be generated with openSSL:
``` ```
openssl genrsa -out server.key 2048 openssl genrsa -out server.key 2048
@ -171,7 +171,7 @@ Streams can then be published and read with the `rtsps` scheme and the `8555` po
ffmpeg -i rtsps://ip:8555/... ffmpeg -i rtsps://ip:8555/...
``` ```
If the client is _GStreamer_ and the server certificate is self signed, remember to disable the certificate validation: If the client is _GStreamer_, disable the certificate validation:
``` ```
gst-launch-1.0 rtspsrc location=rtsps://ip:8555/... tls-validation-flags=0 gst-launch-1.0 rtspsrc location=rtsps://ip:8555/... tls-validation-flags=0

View File

@ -66,34 +66,41 @@ func CheckPathName(name string) error {
// PathConf is a path configuration. // PathConf is a path configuration.
type PathConf struct { type PathConf struct {
Regexp *regexp.Regexp `yaml:"-" json:"-"` Regexp *regexp.Regexp `yaml:"-" json:"-"`
// source
Source string `yaml:"source"` Source string `yaml:"source"`
SourceProtocol string `yaml:"sourceProtocol"` SourceProtocol string `yaml:"sourceProtocol"`
SourceProtocolParsed *gortsplib.StreamProtocol `yaml:"-" json:"-"` SourceProtocolParsed *gortsplib.StreamProtocol `yaml:"-" json:"-"`
SourceFingerprint string `yaml:"sourceFingerprint" json:"sourceFingerprint"`
SourceOnDemand bool `yaml:"sourceOnDemand"` SourceOnDemand bool `yaml:"sourceOnDemand"`
SourceOnDemandStartTimeout time.Duration `yaml:"sourceOnDemandStartTimeout"` SourceOnDemandStartTimeout time.Duration `yaml:"sourceOnDemandStartTimeout"`
SourceOnDemandCloseAfter time.Duration `yaml:"sourceOnDemandCloseAfter"` SourceOnDemandCloseAfter time.Duration `yaml:"sourceOnDemandCloseAfter"`
SourceRedirect string `yaml:"sourceRedirect"` SourceRedirect string `yaml:"sourceRedirect"`
DisablePublisherOverride bool `yaml:"disablePublisherOverride"` DisablePublisherOverride bool `yaml:"disablePublisherOverride"`
Fallback string `yaml:"fallback"` Fallback string `yaml:"fallback"`
RunOnInit string `yaml:"runOnInit"`
RunOnInitRestart bool `yaml:"runOnInitRestart"` // custom commands
RunOnDemand string `yaml:"runOnDemand"` RunOnInit string `yaml:"runOnInit"`
RunOnDemandRestart bool `yaml:"runOnDemandRestart"` RunOnInitRestart bool `yaml:"runOnInitRestart"`
RunOnDemandStartTimeout time.Duration `yaml:"runOnDemandStartTimeout"` RunOnDemand string `yaml:"runOnDemand"`
RunOnDemandCloseAfter time.Duration `yaml:"runOnDemandCloseAfter"` RunOnDemandRestart bool `yaml:"runOnDemandRestart"`
RunOnPublish string `yaml:"runOnPublish"` RunOnDemandStartTimeout time.Duration `yaml:"runOnDemandStartTimeout"`
RunOnPublishRestart bool `yaml:"runOnPublishRestart"` RunOnDemandCloseAfter time.Duration `yaml:"runOnDemandCloseAfter"`
RunOnRead string `yaml:"runOnRead"` RunOnPublish string `yaml:"runOnPublish"`
RunOnReadRestart bool `yaml:"runOnReadRestart"` RunOnPublishRestart bool `yaml:"runOnPublishRestart"`
PublishUser string `yaml:"publishUser"` RunOnRead string `yaml:"runOnRead"`
PublishPass string `yaml:"publishPass"` RunOnReadRestart bool `yaml:"runOnReadRestart"`
PublishIps []string `yaml:"publishIps"`
PublishIpsParsed []interface{} `yaml:"-" json:"-"` // authentication
ReadUser string `yaml:"readUser"` PublishUser string `yaml:"publishUser"`
ReadPass string `yaml:"readPass"` PublishPass string `yaml:"publishPass"`
ReadIps []string `yaml:"readIps"` PublishIps []string `yaml:"publishIps"`
ReadIpsParsed []interface{} `yaml:"-" json:"-"` PublishIpsParsed []interface{} `yaml:"-" json:"-"`
ReadUser string `yaml:"readUser"`
ReadPass string `yaml:"readPass"`
ReadIps []string `yaml:"readIps"`
ReadIpsParsed []interface{} `yaml:"-" json:"-"`
} }
func (pconf *PathConf) fillAndCheck(name string) error { func (pconf *PathConf) fillAndCheck(name string) error {
@ -163,6 +170,10 @@ func (pconf *PathConf) fillAndCheck(name string) error {
return fmt.Errorf("unsupported protocol '%s'", pconf.SourceProtocol) return fmt.Errorf("unsupported protocol '%s'", pconf.SourceProtocol)
} }
if strings.HasPrefix(pconf.Source, "rtsps://") && pconf.SourceFingerprint == "" {
return fmt.Errorf("sourceFingerprint is required with a RTSPS URL")
}
case strings.HasPrefix(pconf.Source, "rtmp://"): case strings.HasPrefix(pconf.Source, "rtmp://"):
if pconf.Regexp != nil { if pconf.Regexp != nil {
return fmt.Errorf("a path with a regular expression (or path 'all') cannot have a RTMP source; use another path") return fmt.Errorf("a path with a regular expression (or path 'all') cannot have a RTMP source; use another path")

View File

@ -406,6 +406,7 @@ func (pa *Path) startExternalSource() {
pa.source = sourcertsp.New( pa.source = sourcertsp.New(
pa.conf.Source, pa.conf.Source,
pa.conf.SourceProtocolParsed, pa.conf.SourceProtocolParsed,
pa.conf.SourceFingerprint,
pa.readTimeout, pa.readTimeout,
pa.writeTimeout, pa.writeTimeout,
pa.readBufferCount, pa.readBufferCount,

View File

@ -1,6 +1,11 @@
package sourcertsp package sourcertsp
import ( import (
"crypto/sha256"
"crypto/tls"
"encoding/hex"
"fmt"
"strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
@ -28,6 +33,7 @@ type Parent interface {
type Source struct { type Source struct {
ur string ur string
proto *gortsplib.StreamProtocol proto *gortsplib.StreamProtocol
fingerprint string
readTimeout time.Duration readTimeout time.Duration
writeTimeout time.Duration writeTimeout time.Duration
readBufferCount int readBufferCount int
@ -41,8 +47,10 @@ type Source struct {
} }
// New allocates a Source. // New allocates a Source.
func New(ur string, func New(
ur string,
proto *gortsplib.StreamProtocol, proto *gortsplib.StreamProtocol,
fingerprint string,
readTimeout time.Duration, readTimeout time.Duration,
writeTimeout time.Duration, writeTimeout time.Duration,
readBufferCount int, readBufferCount int,
@ -53,6 +61,7 @@ func New(ur string,
s := &Source{ s := &Source{
ur: ur, ur: ur,
proto: proto, proto: proto,
fingerprint: fingerprint,
readTimeout: readTimeout, readTimeout: readTimeout,
writeTimeout: writeTimeout, writeTimeout: writeTimeout,
readBufferCount: readBufferCount, readBufferCount: readBufferCount,
@ -121,7 +130,23 @@ func (s *Source) runInner() bool {
defer close(dialDone) defer close(dialDone)
conf := gortsplib.ClientConf{ conf := gortsplib.ClientConf{
StreamProtocol: s.proto, StreamProtocol: s.proto,
TLSConfig: &tls.Config{
InsecureSkipVerify: true,
VerifyConnection: func(cs tls.ConnectionState) error {
h := sha256.New()
h.Write(cs.PeerCertificates[0].Raw)
hstr := hex.EncodeToString(h.Sum(nil))
fingerprintLower := strings.ToLower(s.fingerprint)
if hstr != fingerprintLower {
return fmt.Errorf("server fingerprint do not match: expected %s, got %s",
fingerprintLower, hstr)
}
return nil
},
},
ReadTimeout: s.readTimeout, ReadTimeout: s.readTimeout,
WriteTimeout: s.writeTimeout, WriteTimeout: s.writeTimeout,
ReadBufferCount: s.readBufferCount, ReadBufferCount: s.readBufferCount,

View File

@ -98,6 +98,7 @@ func TestSourceRTSP(t *testing.T) {
"paths:\n" + "paths:\n" +
" proxied:\n" + " proxied:\n" +
" source: rtsps://testuser:testpass@localhost:8555/teststream\n" + " source: rtsps://testuser:testpass@localhost:8555/teststream\n" +
" sourceFingerprint: 33949E05FFFB5FF3E8AA16F8213A6251B4D9363804BA53233C4DA9A46D6F2739\n" +
" sourceOnDemand: yes\n") " sourceOnDemand: yes\n")
require.Equal(t, true, ok) require.Equal(t, true, ok)
defer p2.close() defer p2.close()

View File

@ -60,6 +60,9 @@ rtpPort: 8000
# port of the UDP/RTCP listener. This is used only if "udp" is in protocols. # port of the UDP/RTCP listener. This is used only if "udp" is in protocols.
rtcpPort: 8001 rtcpPort: 8001
# path to the server key. This is used only if encryption is "strict" or "optional". # path to the server key. This is used only if encryption is "strict" or "optional".
# this can be generated with:
# openssl genrsa -out server.key 2048
# openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650
serverKey: server.key serverKey: server.key
# path to the server certificate. This is used only if encryption is "strict" or "optional". # path to the server certificate. This is used only if encryption is "strict" or "optional".
serverCert: server.crt serverCert: server.crt
@ -92,16 +95,23 @@ paths:
# source of the stream - this can be: # source of the stream - this can be:
# * record -> the stream is published by a RTSP or RTMP client # * record -> the stream is published by a RTSP or RTMP client
# * rtsp://existing-url -> the stream is pulled from another RTSP server # * rtsp://existing-url -> the stream is pulled from another RTSP server
# * rtsps://existing-url -> the stream is pulled from another RTSP server # * rtsps://existing-url -> the stream is pulled from another RTSP server, with RTSPS
# * rtmp://existing-url -> the stream is pulled from a RTMP server # * rtmp://existing-url -> the stream is pulled from a RTMP server
# * redirect -> the stream is provided by another path or server # * redirect -> the stream is provided by another path or server
source: record source: record
# if the source is an RTSP URL, this is the protocol that will be used to # if the source is an RTSP or RTSPS URL, this is the protocol that will be used to
# pull the stream. available options are "automatic", "udp", "tcp". # pull the stream. available options are "automatic", "udp", "tcp".
# the tcp protocol can help to overcome the error "no UDP packets received recently". # the tcp protocol can help to overcome the error "no UDP packets received recently".
sourceProtocol: automatic sourceProtocol: automatic
# if the source is an RTSPS URL, the fingerprint of the certificate of the source
# must be provided in order to prevent man-in-the-middle attacks.
# it can be obtained from the source by running:
# openssl s_client -connect source_ip:source_port </dev/null 2>/dev/null | sed -n '/BEGIN/,/END/p' > server.crt
# openssl x509 -in server.crt -noout -fingerprint -sha256 | cut -d "=" -f2 | tr -d ':'
sourceFingerprint:
# if the source is an RTSP or RTMP URL, it will be pulled only when at least # if the source is an RTSP or RTMP URL, it will be pulled only when at least
# one reader is connected, saving bandwidth. # one reader is connected, saving bandwidth.
sourceOnDemand: no sourceOnDemand: no