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
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
@ -171,7 +171,7 @@ Streams can then be published and read with the `rtsps` scheme and the `8555` po
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

View File

@ -66,34 +66,41 @@ func CheckPathName(name string) error {
// PathConf is a path configuration.
type PathConf struct {
Regexp *regexp.Regexp `yaml:"-" json:"-"`
Regexp *regexp.Regexp `yaml:"-" json:"-"`
// source
Source string `yaml:"source"`
SourceProtocol string `yaml:"sourceProtocol"`
SourceProtocolParsed *gortsplib.StreamProtocol `yaml:"-" json:"-"`
SourceFingerprint string `yaml:"sourceFingerprint" json:"sourceFingerprint"`
SourceOnDemand bool `yaml:"sourceOnDemand"`
SourceOnDemandStartTimeout time.Duration `yaml:"sourceOnDemandStartTimeout"`
SourceOnDemandCloseAfter time.Duration `yaml:"sourceOnDemandCloseAfter"`
SourceRedirect string `yaml:"sourceRedirect"`
DisablePublisherOverride bool `yaml:"disablePublisherOverride"`
Fallback string `yaml:"fallback"`
RunOnInit string `yaml:"runOnInit"`
RunOnInitRestart bool `yaml:"runOnInitRestart"`
RunOnDemand string `yaml:"runOnDemand"`
RunOnDemandRestart bool `yaml:"runOnDemandRestart"`
RunOnDemandStartTimeout time.Duration `yaml:"runOnDemandStartTimeout"`
RunOnDemandCloseAfter time.Duration `yaml:"runOnDemandCloseAfter"`
RunOnPublish string `yaml:"runOnPublish"`
RunOnPublishRestart bool `yaml:"runOnPublishRestart"`
RunOnRead string `yaml:"runOnRead"`
RunOnReadRestart bool `yaml:"runOnReadRestart"`
PublishUser string `yaml:"publishUser"`
PublishPass string `yaml:"publishPass"`
PublishIps []string `yaml:"publishIps"`
PublishIpsParsed []interface{} `yaml:"-" json:"-"`
ReadUser string `yaml:"readUser"`
ReadPass string `yaml:"readPass"`
ReadIps []string `yaml:"readIps"`
ReadIpsParsed []interface{} `yaml:"-" json:"-"`
// custom commands
RunOnInit string `yaml:"runOnInit"`
RunOnInitRestart bool `yaml:"runOnInitRestart"`
RunOnDemand string `yaml:"runOnDemand"`
RunOnDemandRestart bool `yaml:"runOnDemandRestart"`
RunOnDemandStartTimeout time.Duration `yaml:"runOnDemandStartTimeout"`
RunOnDemandCloseAfter time.Duration `yaml:"runOnDemandCloseAfter"`
RunOnPublish string `yaml:"runOnPublish"`
RunOnPublishRestart bool `yaml:"runOnPublishRestart"`
RunOnRead string `yaml:"runOnRead"`
RunOnReadRestart bool `yaml:"runOnReadRestart"`
// authentication
PublishUser string `yaml:"publishUser"`
PublishPass string `yaml:"publishPass"`
PublishIps []string `yaml:"publishIps"`
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 {
@ -163,6 +170,10 @@ func (pconf *PathConf) fillAndCheck(name string) error {
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://"):
if pconf.Regexp != nil {
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.conf.Source,
pa.conf.SourceProtocolParsed,
pa.conf.SourceFingerprint,
pa.readTimeout,
pa.writeTimeout,
pa.readBufferCount,

View File

@ -1,6 +1,11 @@
package sourcertsp
import (
"crypto/sha256"
"crypto/tls"
"encoding/hex"
"fmt"
"strings"
"sync"
"sync/atomic"
"time"
@ -28,6 +33,7 @@ type Parent interface {
type Source struct {
ur string
proto *gortsplib.StreamProtocol
fingerprint string
readTimeout time.Duration
writeTimeout time.Duration
readBufferCount int
@ -41,8 +47,10 @@ type Source struct {
}
// New allocates a Source.
func New(ur string,
func New(
ur string,
proto *gortsplib.StreamProtocol,
fingerprint string,
readTimeout time.Duration,
writeTimeout time.Duration,
readBufferCount int,
@ -53,6 +61,7 @@ func New(ur string,
s := &Source{
ur: ur,
proto: proto,
fingerprint: fingerprint,
readTimeout: readTimeout,
writeTimeout: writeTimeout,
readBufferCount: readBufferCount,
@ -121,7 +130,23 @@ func (s *Source) runInner() bool {
defer close(dialDone)
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,
WriteTimeout: s.writeTimeout,
ReadBufferCount: s.readBufferCount,

View File

@ -98,6 +98,7 @@ func TestSourceRTSP(t *testing.T) {
"paths:\n" +
" proxied:\n" +
" source: rtsps://testuser:testpass@localhost:8555/teststream\n" +
" sourceFingerprint: 33949E05FFFB5FF3E8AA16F8213A6251B4D9363804BA53233C4DA9A46D6F2739\n" +
" sourceOnDemand: yes\n")
require.Equal(t, true, ok)
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.
rtcpPort: 8001
# 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
# path to the server certificate. This is used only if encryption is "strict" or "optional".
serverCert: server.crt
@ -92,16 +95,23 @@ paths:
# source of the stream - this can be:
# * record -> the stream is published by a RTSP or RTMP client
# * 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
# * redirect -> the stream is provided by another path or server
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".
# the tcp protocol can help to overcome the error "no UDP packets received recently".
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
# one reader is connected, saving bandwidth.
sourceOnDemand: no