send server name (SNI) when opening TLS connections (#4973)

This commit is contained in:
Alessandro Ros
2025-09-15 19:38:36 +02:00
committed by GitHub
parent 22aad7bbd0
commit 35aceaa4a9
9 changed files with 197 additions and 46 deletions

View File

@@ -273,8 +273,13 @@ func (m *Manager) pullJWTJWKS() (jwt.Keyfunc, error) {
defer m.mutex.Unlock()
if now.Sub(m.jwksLastRefresh) >= jwksRefreshPeriod {
u, err := url.Parse(m.JWTJWKS)
if err != nil {
return nil, err
}
tr := &http.Transport{
TLSClientConfig: tls.ConfigForFingerprint(m.JWTJWKSFingerprint),
TLSClientConfig: tls.MakeConfig(u.Hostname(), m.JWTJWKSFingerprint),
}
defer tr.CloseIdleConnections()

View File

@@ -0,0 +1,38 @@
// Package tls contains TLS utilities.
package tls
import (
"crypto/sha256"
"crypto/tls"
"encoding/hex"
"fmt"
"strings"
)
// MakeConfig returns a tls.Config with:
// - server name indicator (SNI) support
// - fingerprint support
func MakeConfig(serverName string, fingerprint string) *tls.Config {
conf := &tls.Config{
ServerName: serverName,
}
if fingerprint != "" {
fingerprintLower := strings.ToLower(fingerprint)
conf.InsecureSkipVerify = true
conf.VerifyConnection = func(cs tls.ConnectionState) error {
h := sha256.New()
h.Write(cs.PeerCertificates[0].Raw)
hstr := hex.EncodeToString(h.Sum(nil))
if hstr != fingerprintLower {
return fmt.Errorf("source fingerprint does not match: expected %s, got %s",
fingerprintLower, hstr)
}
return nil
}
}
return conf
}

View File

@@ -0,0 +1,137 @@
package tls
import (
"crypto/tls"
"net"
"testing"
"github.com/stretchr/testify/require"
)
var testTLSCertPub = []byte(`-----BEGIN CERTIFICATE-----
MIIDazCCAlOgAwIBAgIUXw1hEC3LFpTsllv7D3ARJyEq7sIwDQYJKoZIhvcNAQEL
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDEyMTMxNzQ0NThaFw0zMDEy
MTExNzQ0NThaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQDG8DyyS51810GsGwgWr5rjJK7OE1kTTLSNEEKax8Bj
zOyiaz8rA2JGl2VUEpi2UjDr9Cm7nd+YIEVs91IIBOb7LGqObBh1kGF3u5aZxLkv
NJE+HrLVvUhaDobK2NU+Wibqc/EI3DfUkt1rSINvv9flwTFu1qHeuLWhoySzDKEp
OzYxpFhwjVSokZIjT4Red3OtFz7gl2E6OAWe2qoh5CwLYVdMWtKR0Xuw3BkDPk9I
qkQKx3fqv97LPEzhyZYjDT5WvGrgZ1WDAN3booxXF3oA1H3GHQc4m/vcLatOtb8e
nI59gMQLEbnp08cl873bAuNuM95EZieXTHNbwUnq5iybAgMBAAGjUzBRMB0GA1Ud
DgQWBBQBKhJh8eWu0a4au9X/2fKhkFX2vjAfBgNVHSMEGDAWgBQBKhJh8eWu0a4a
u9X/2fKhkFX2vjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBj
3aCW0YPKukYgVK9cwN0IbVy/D0C1UPT4nupJcy/E0iC7MXPZ9D/SZxYQoAkdptdO
xfI+RXkpQZLdODNx9uvV+cHyZHZyjtE5ENu/i5Rer2cWI/mSLZm5lUQyx+0KZ2Yu
tEI1bsebDK30msa8QSTn0WidW9XhFnl3gRi4wRdimcQapOWYVs7ih+nAlSvng7NI
XpAyRs8PIEbpDDBMWnldrX4TP6EWYUi49gCp8OUDRREKX3l6Ls1vZ02F34yHIt/7
7IV/XSKG096bhW+icKBWV0IpcEsgTzPK1J1hMxgjhzIMxGboAeUU+kidthOob6Sd
XQxaORfgM//NzX9LhUPk
-----END CERTIFICATE-----
`)
var testTLSCertKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAxvA8skudfNdBrBsIFq+a4ySuzhNZE0y0jRBCmsfAY8zsoms/
KwNiRpdlVBKYtlIw6/Qpu53fmCBFbPdSCATm+yxqjmwYdZBhd7uWmcS5LzSRPh6y
1b1IWg6GytjVPlom6nPxCNw31JLda0iDb7/X5cExbtah3ri1oaMkswyhKTs2MaRY
cI1UqJGSI0+EXndzrRc+4JdhOjgFntqqIeQsC2FXTFrSkdF7sNwZAz5PSKpECsd3
6r/eyzxM4cmWIw0+Vrxq4GdVgwDd26KMVxd6ANR9xh0HOJv73C2rTrW/HpyOfYDE
CxG56dPHJfO92wLjbjPeRGYnl0xzW8FJ6uYsmwIDAQABAoIBACi0BKcyQ3HElSJC
kaAao+Uvnzh4yvPg8Nwf5JDIp/uDdTMyIEWLtrLczRWrjGVZYbsVROinP5VfnPTT
kYwkfKINj2u+gC6lsNuPnRuvHXikF8eO/mYvCTur1zZvsQnF5kp4GGwIqr+qoPUP
bB0UMndG1PdpoMryHe+JcrvTrLHDmCeH10TqOwMsQMLHYLkowvxwJWsmTY7/Qr5S
Wm3PPpOcW2i0uyPVuyuv4yD1368fqnqJ8QFsQp1K6QtYsNnJ71Hut1/IoxK/e6hj
5Z+byKtHVtmcLnABuoOT7BhleJNFBksX9sh83jid4tMBgci+zXNeGmgqo2EmaWAb
agQslkECgYEA8B1rzjOHVQx/vwSzDa4XOrpoHQRfyElrGNz9JVBvnoC7AorezBXQ
M9WTHQIFTGMjzD8pb+YJGi3gj93VN51r0SmJRxBaBRh1ZZI9kFiFzngYev8POgD3
ygmlS3kTHCNxCK/CJkB+/jMBgtPj5ygDpCWVcTSuWlQFphePkW7jaaECgYEA1Blz
ulqgAyJHZaqgcbcCsI2q6m527hVr9pjzNjIVmkwu38yS9RTCgdlbEVVDnS0hoifl
+jVMEGXjF3xjyMvL50BKbQUH+KAa+V4n1WGlnZOxX9TMny8MBjEuSX2+362vQ3BX
4vOlX00gvoc+sY+lrzvfx/OdPCHQGVYzoKCxhLsCgYA07HcviuIAV/HsO2/vyvhp
xF5gTu+BqNUHNOZDDDid+ge+Jre2yfQLCL8VPLXIQW3Jff53IH/PGl+NtjphuLvj
7UDJvgvpZZuymIojP6+2c3gJ3CASC9aR3JBnUzdoE1O9s2eaoMqc4scpe+SWtZYf
3vzSZ+cqF6zrD/Rf/M35IQKBgHTU4E6ShPm09CcoaeC5sp2WK8OevZw/6IyZi78a
r5Oiy18zzO97U/k6xVMy6F+38ILl/2Rn31JZDVJujniY6eSkIVsUHmPxrWoXV1HO
y++U32uuSFiXDcSLarfIsE992MEJLSAynbF1Rsgsr3gXbGiuToJRyxbIeVy7gwzD
94TpAoGAY4/PejWQj9psZfAhyk5dRGra++gYRQ/gK1IIc1g+Dd2/BxbT/RHr05GK
6vwrfjsoRyMWteC1SsNs/CurjfQ/jqCfHNP5XPvxgd5Ec8sRJIiV7V5RTuWJsPu1
+3K6cnKEyg+0ekYmLertRFIY6SwWmY1fyKgTvxudMcsBY7dC4xs=
-----END RSA PRIVATE KEY-----
`)
func TestMakeConfigSNI(t *testing.T) {
l, err := net.Listen("tcp", "localhost:8556")
require.NoError(t, err)
defer l.Close()
serverDone := make(chan struct{})
defer func() { <-serverDone }()
go func() {
defer close(serverDone)
nconn, err2 := l.Accept()
require.NoError(t, err2)
defer nconn.Close()
cert, err2 := tls.X509KeyPair(testTLSCertPub, testTLSCertKey)
require.NoError(t, err2)
tnconn := tls.Server(nconn, &tls.Config{
Certificates: []tls.Certificate{cert},
InsecureSkipVerify: true,
VerifyConnection: func(cs tls.ConnectionState) error {
require.Equal(t, "myhost", cs.ServerName)
return nil
},
})
err2 = tnconn.Handshake()
require.EqualError(t, err2, "remote error: tls: bad certificate")
}()
conf := MakeConfig("myhost", "")
_, err = tls.Dial("tcp", "localhost:8556", conf)
require.EqualError(t, err, "tls: failed to verify certificate: x509: "+
"certificate is not valid for any names, but wanted to match myhost")
}
func TestMakeConfigFingerprint(t *testing.T) {
l, err := net.Listen("tcp", "localhost:8556")
require.NoError(t, err)
defer l.Close()
serverDone := make(chan struct{})
defer func() { <-serverDone }()
go func() {
defer close(serverDone)
nconn, err2 := l.Accept()
require.NoError(t, err2)
defer nconn.Close()
cert, err2 := tls.X509KeyPair(testTLSCertPub, testTLSCertKey)
require.NoError(t, err2)
tnconn := tls.Server(nconn, &tls.Config{
Certificates: []tls.Certificate{cert},
InsecureSkipVerify: true,
VerifyConnection: func(cs tls.ConnectionState) error {
require.Equal(t, "myhost", cs.ServerName)
return nil
},
})
err2 = tnconn.Handshake()
require.NoError(t, err2)
}()
conf := MakeConfig("myhost", "33949e05fffb5ff3e8aa16f8213a6251b4d9363804ba53233c4da9a46d6f2739")
conn, err := tls.Dial("tcp", "localhost:8556", conf)
require.NoError(t, err)
defer conn.Close() //nolint:errcheck
}

View File

@@ -1,35 +0,0 @@
// Package tls contains TLS utilities.
package tls
import (
"crypto/sha256"
"crypto/tls"
"encoding/hex"
"fmt"
"strings"
)
// ConfigForFingerprint returns a tls.Config that supports given fingerprint.
func ConfigForFingerprint(fingerprint string) *tls.Config {
if fingerprint == "" {
return nil
}
fingerprintLower := strings.ToLower(fingerprint)
return &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))
if hstr != fingerprintLower {
return fmt.Errorf("source fingerprint does not match: expected %s, got %s",
fingerprintLower, hstr)
}
return nil
},
}
}

View File

@@ -3,6 +3,7 @@ package hls
import (
"net/http"
"net/url"
"time"
"github.com/bluenviron/gohlslib/v2"
@@ -60,8 +61,13 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error {
decodeErrors.Start()
defer decodeErrors.Stop()
u, err := url.Parse(params.ResolvedSource)
if err != nil {
return err
}
tr := &http.Transport{
TLSClientConfig: tls.ConfigForFingerprint(params.Conf.SourceFingerprint),
TLSClientConfig: tls.MakeConfig(u.Hostname(), params.Conf.SourceFingerprint),
}
defer tr.CloseIdleConnections()
@@ -88,9 +94,9 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error {
decodeErrors.Increase()
},
OnTracks: func(tracks []*gohlslib.Track) error {
medias, err := hls.ToStream(c, tracks, &stream)
if err != nil {
return err
medias, err2 := hls.ToStream(c, tracks, &stream)
if err2 != nil {
return err2
}
res := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{
@@ -107,7 +113,7 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error {
},
}
err := c.Start()
err = c.Start()
if err != nil {
return err
}

View File

@@ -83,7 +83,7 @@ func (s *Source) runReader(ctx context.Context, u *url.URL, fingerprint string)
connectCtx, connectCtxCancel := context.WithTimeout(ctx, time.Duration(s.ReadTimeout))
conn := &gortmplib.Client{
URL: u,
TLSConfig: tls.ConfigForFingerprint(fingerprint),
TLSConfig: tls.MakeConfig(u.Hostname(), fingerprint),
Publish: false,
}
err := conn.Initialize(connectCtx)

View File

@@ -125,7 +125,7 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error {
Scheme: u.Scheme,
Host: u.Host,
Transport: params.Conf.RTSPTransport.Transport,
TLSConfig: tls.ConfigForFingerprint(params.Conf.SourceFingerprint),
TLSConfig: tls.MakeConfig(u.Hostname(), params.Conf.SourceFingerprint),
ReadTimeout: time.Duration(s.ReadTimeout),
WriteTimeout: time.Duration(s.WriteTimeout),
WriteQueueSize: s.WriteQueueSize,

View File

@@ -48,17 +48,17 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error {
u.Scheme = strings.ReplaceAll(u.Scheme, "whep", "http")
tr := &http.Transport{
TLSClientConfig: tls.ConfigForFingerprint(params.Conf.SourceFingerprint),
TLSClientConfig: tls.MakeConfig(u.Hostname(), params.Conf.SourceFingerprint),
}
defer tr.CloseIdleConnections()
client := whip.Client{
URL: u,
HTTPClient: &http.Client{
Timeout: time.Duration(s.ReadTimeout),
Transport: tr,
},
UseAbsoluteTimestamp: params.Conf.UseAbsoluteTimestamp,
URL: u,
Log: s,
}
err = client.Initialize(params.Context)