mirror of
https://github.com/safing/portmaster
synced 2025-09-01 10:09:11 +00:00
431 lines
15 KiB
Go
431 lines
15 KiB
Go
package tls
|
|
|
|
import (
|
|
"crypto/x509"
|
|
"fmt"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"github.com/google/gopacket"
|
|
"github.com/google/gopacket/layers"
|
|
"github.com/google/gopacket/tcpassembly"
|
|
|
|
"github.com/Safing/portbase/log"
|
|
"github.com/Safing/portmaster/firewall/inspection"
|
|
"github.com/Safing/portmaster/firewall/inspection/tls/tlslib"
|
|
"github.com/Safing/portmaster/firewall/inspection/tls/verify"
|
|
"github.com/Safing/portmaster/network"
|
|
"github.com/Safing/portmaster/network/netutils"
|
|
"github.com/Safing/portmaster/network/packet"
|
|
)
|
|
|
|
// TODO:
|
|
// - delete insecure cipher suites from clienthello as of level (-> configuration)
|
|
// - reject connection if serverhello initiates insecure cipher suite (-> configuration)
|
|
// - reject TLS without SNI as of level (-> configuration)
|
|
|
|
var (
|
|
tlsInspectorIndex int
|
|
assemblerManager *netutils.SimpleStreamAssemblerManager
|
|
assembler *tcpassembly.Assembler
|
|
)
|
|
|
|
const (
|
|
statusWaitForMore uint8 = iota
|
|
statusNotTLS
|
|
statusProtocolViolation
|
|
statusInvalidCertificate
|
|
statusVerified
|
|
)
|
|
|
|
func init() {
|
|
tlsInspectorIndex = inspection.RegisterInspector("TLS", inspector, network.ACCEPT)
|
|
|
|
assemblerManager = new(netutils.SimpleStreamAssemblerManager)
|
|
streamPool := tcpassembly.NewStreamPool(assemblerManager)
|
|
assembler = tcpassembly.NewAssembler(streamPool)
|
|
}
|
|
|
|
type TLSInspection struct {
|
|
AssemblerI *netutils.SimpleStreamAssembler
|
|
AssemblerO *netutils.SimpleStreamAssembler
|
|
PacketsSeen uint
|
|
ServerName string
|
|
Resuming bool
|
|
WaitingForVerification bool
|
|
verification chan bool
|
|
SecurityLevel int8
|
|
EnforceCT bool
|
|
EnforceRevocation bool
|
|
DenyInsecureTLS bool
|
|
DenyTLSWithoutSNI bool
|
|
}
|
|
|
|
func inspector(pkt packet.Packet, link *network.Link) uint8 {
|
|
|
|
// obviously, only inspect TCP
|
|
if pkt.GetIPHeader().Protocol != packet.TCP {
|
|
return inspection.STOP_INSPECTING
|
|
}
|
|
|
|
// only check outgoing
|
|
if link.Connection().Direction == network.Inbound {
|
|
return inspection.STOP_INSPECTING
|
|
}
|
|
|
|
// get or create link-specific inspection data
|
|
var tlsInspection *TLSInspection
|
|
inspectorData, ok := link.InspectorData[uint8(tlsInspectorIndex)]
|
|
if ok {
|
|
tlsInspection, ok = inspectorData.(*TLSInspection)
|
|
}
|
|
if !ok {
|
|
tlsInspection = new(TLSInspection)
|
|
link.InspectorData[uint8(tlsInspectorIndex)] = tlsInspection
|
|
|
|
// load config for link
|
|
tlsInspection.SecurityLevel = link.Connection().Process().Profile.SecurityLevel
|
|
config.Changed()
|
|
config.RLock()
|
|
tlsInspection.EnforceCT = config.EnforceCT.IsSetWithLevel(tlsInspection.SecurityLevel)
|
|
tlsInspection.EnforceRevocation = config.EnforceRevocation.IsSetWithLevel(tlsInspection.SecurityLevel)
|
|
tlsInspection.DenyInsecureTLS = config.DenyInsecureTLS.IsSetWithLevel(tlsInspection.SecurityLevel)
|
|
tlsInspection.DenyTLSWithoutSNI = config.DenyTLSWithoutSNI.IsSetWithLevel(tlsInspection.SecurityLevel)
|
|
config.RUnlock()
|
|
|
|
}
|
|
|
|
if tlsInspection.WaitingForVerification {
|
|
select {
|
|
case verified := <-tlsInspection.verification:
|
|
if verified {
|
|
return inspection.STOP_INSPECTING
|
|
}
|
|
return inspection.BLOCK_LINK
|
|
default:
|
|
return inspection.DO_NOTHING
|
|
}
|
|
}
|
|
|
|
var err error
|
|
var parser *gopacket.DecodingLayerParser
|
|
var decoded []gopacket.LayerType
|
|
|
|
// TODO: pool allocated space for reuse -> performance!
|
|
var ip4 layers.IPv4
|
|
var ip6 layers.IPv6
|
|
var tcp layers.TCP
|
|
|
|
var payload gopacket.Payload
|
|
|
|
switch pkt.IPVersion() {
|
|
case packet.IPv4:
|
|
parser = gopacket.NewDecodingLayerParser(layers.LayerTypeIPv4, &ip4, &tcp, &payload)
|
|
err = parser.DecodeLayers(*pkt.GetPayload(), &decoded)
|
|
case packet.IPv6:
|
|
parser = gopacket.NewDecodingLayerParser(layers.LayerTypeIPv6, &ip6, &tcp, &payload)
|
|
err = parser.DecodeLayers(*pkt.GetPayload(), &decoded)
|
|
default:
|
|
log.Warningf("TLS inspector: %s: not IPv4 or IPv6", pkt.IPVersion().String())
|
|
return inspection.DO_NOTHING
|
|
}
|
|
|
|
if err != nil {
|
|
log.Warningf("TLS inspector: %s: failed to parse packet: %s", pkt, err)
|
|
return inspection.DO_NOTHING
|
|
}
|
|
|
|
// Stop after 10 packets
|
|
tlsInspection.PacketsSeen += 1
|
|
if tlsInspection.PacketsSeen > 20 {
|
|
if tlsInspection.Resuming {
|
|
log.Debugf("TLS inspector: resumed TLS session: %s", link.String())
|
|
} else {
|
|
log.Debugf("TLS inspector: not TLS: %s", link.String())
|
|
}
|
|
return inspection.STOP_INSPECTING
|
|
}
|
|
|
|
// TCP Stream building
|
|
|
|
var streamAssembler *netutils.SimpleStreamAssembler
|
|
inbound := pkt.IsInbound()
|
|
|
|
// load assembler
|
|
if inbound {
|
|
streamAssembler = tlsInspection.AssemblerI
|
|
} else {
|
|
streamAssembler = tlsInspection.AssemblerO
|
|
}
|
|
|
|
// BUG:
|
|
// panic: runtime error: index out of range
|
|
//
|
|
// goroutine 1120 [running]:
|
|
// safing/vendor/github.com/google/gopacket/tcpassembly.(*Assembler).sendToConnection(0xc4200d2e40, 0xc42172f140)
|
|
// /home/dr/.go/src/safing/vendor/github.com/google/gopacket/tcpassembly/assembly.go:631 +0xc2
|
|
// safing/vendor/github.com/google/gopacket/tcpassembly.(*Assembler).AssembleWithTimestamp(0xc4200d2e40, 0x1, 0x4, 0x4, 0x97fa8c0, 0x0, 0x559063c1, 0x0, 0xc42126ec80, 0xed0c9eb39, ...)
|
|
// /home/dr/.go/src/safing/vendor/github.com/google/gopacket/tcpassembly/assembly.go:605 +0x3d8
|
|
// safing/vendor/github.com/google/gopacket/tcpassembly.(*Assembler).Assemble(0xc4200d2e40, 0x1, 0x4, 0x4, 0x97fa8c0, 0x0, 0x559063c1, 0x0, 0xc42126ec80)
|
|
// /home/dr/.go/src/safing/vendor/github.com/google/gopacket/tcpassembly/assembly.go:518 +0x82
|
|
// safing/firewall/inspection/tls.inspector(0xec5f00, 0xc421b88780, 0xc422c20f30, 0xc423d75380)
|
|
// /home/dr/.go/src/safing/firewall/inspection/tls/tls.go:122 +0x414
|
|
// safing/firewall/inspection.RunInspectors(0xec5f00, 0xc421b88780, 0xc422c20f30, 0xc421545fb8)
|
|
// /home/dr/.go/src/safing/firewall/inspection/inspection.go:62 +0xfe
|
|
// safing/firewall.inspectThenVerdict(0xec5f00, 0xc421b88780, 0xc422c20f30)
|
|
// /home/dr/.go/src/safing/firewall/firewall.go:144 +0x43
|
|
// safing/network.(*Link).packetHandler(0xc422c20f30)
|
|
// /home/dr/.go/src/safing/network/link.go:109 +0x89
|
|
// created by safing/network.(*Link).SetFirewallHandler
|
|
// /home/dr/.go/src/safing/network/link.go:61 +0xdd
|
|
|
|
// assemble and save assembler if first time
|
|
if streamAssembler != nil {
|
|
if pkt.IPVersion() == packet.IPv4 {
|
|
assembler.Assemble(ip4.NetworkFlow(), &tcp)
|
|
// FIXME: panic: runtime error: index out of range
|
|
//
|
|
// goroutine 772 [running]:
|
|
// safing/vendor/github.com/google/gopacket/tcpassembly.(*Assembler).sendToConnection(0xc4200eed80, 0xc4222b1fa0)
|
|
// /home/dr/.go/src/safing/vendor/github.com/google/gopacket/tcpassembly/assembly.go:631 +0xc2
|
|
// safing/vendor/github.com/google/gopacket/tcpassembly.(*Assembler).AssembleWithTimestamp(0xc4200eed80, 0x1, 0x4, 0x4, 0x97fa8c0, 0x0, 0x8e17d9ac, 0x0, 0xc42175a8c0, 0xed0e03a0a, ...)
|
|
// /home/dr/.go/src/safing/vendor/github.com/google/gopacket/tcpassembly/assembly.go:605 +0x3d8
|
|
// safing/vendor/github.com/google/gopacket/tcpassembly.(*Assembler).Assemble(0xc4200eed80, 0x1, 0x4, 0x4, 0x97fa8c0, 0x0, 0x8e17d9ac, 0x0, 0xc42175a8c0)
|
|
// /home/dr/.go/src/safing/vendor/github.com/google/gopacket/tcpassembly/assembly.go:518 +0x82
|
|
// safing/firewall/inspection/tls.inspector(0xed40c0, 0xc421cce780, 0xc4229e42d0, 0xc42155f6c0)
|
|
// /home/dr/.go/src/safing/firewall/inspection/tls/tls.go:143 +0x414
|
|
// safing/firewall/inspection.RunInspectors(0xed40c0, 0xc421cce780, 0xc4229e42d0, 0xc421d19fb8)
|
|
// /home/dr/.go/src/safing/firewall/inspection/inspection.go:62 +0xfe
|
|
// safing/firewall.inspectThenVerdict(0xed40c0, 0xc421cce780, 0xc4229e42d0)
|
|
// /home/dr/.go/src/safing/firewall/firewall.go:149 +0x43
|
|
// safing/network.(*Link).packetHandler(0xc4229e42d0)
|
|
// /home/dr/.go/src/safing/network/link.go:110 +0x8c
|
|
// created by safing/network.(*Link).SetFirewallHandler
|
|
// /home/dr/.go/src/safing/network/link.go:61 +0xe7
|
|
} else {
|
|
assembler.Assemble(ip6.NetworkFlow(), &tcp)
|
|
}
|
|
} else {
|
|
assemblerManager.InitLock.Lock()
|
|
if pkt.IPVersion() == packet.IPv4 {
|
|
assembler.Assemble(ip4.NetworkFlow(), &tcp)
|
|
} else {
|
|
assembler.Assemble(ip6.NetworkFlow(), &tcp)
|
|
}
|
|
if inbound {
|
|
streamAssembler = assemblerManager.GetLastAssembler()
|
|
tlsInspection.AssemblerI = streamAssembler
|
|
} else {
|
|
streamAssembler = assemblerManager.GetLastAssembler()
|
|
tlsInspection.AssemblerO = streamAssembler
|
|
}
|
|
assemblerManager.InitLock.Unlock()
|
|
}
|
|
|
|
if streamAssembler == nil {
|
|
return inspection.DO_NOTHING
|
|
}
|
|
|
|
for {
|
|
|
|
// check if we have a possible tls message header
|
|
if len(streamAssembler.Cumulated) < 5 {
|
|
return inspection.DO_NOTHING
|
|
}
|
|
|
|
// get tls message length
|
|
tlsMessageLen := int(streamAssembler.Cumulated[3])*256 + int(streamAssembler.Cumulated[4]) + 5
|
|
|
|
// check if we have full tls message
|
|
if len(streamAssembler.Cumulated) < tlsMessageLen {
|
|
return inspection.DO_NOTHING
|
|
}
|
|
|
|
action := processMessage(tlsInspection, streamAssembler.Cumulated[:tlsMessageLen], pkt, link)
|
|
// BUG: slice bounds out of range
|
|
if tlsMessageLen == len(streamAssembler.Cumulated) {
|
|
streamAssembler.Cumulated = make([]byte, 0, 0)
|
|
} else if tlsMessageLen < len(streamAssembler.Cumulated) {
|
|
streamAssembler.Cumulated = streamAssembler.Cumulated[tlsMessageLen:]
|
|
} else {
|
|
log.Warningf("TLS inspector: processed more than available, resetting buffer")
|
|
streamAssembler.Cumulated = make([]byte, 0, 0)
|
|
}
|
|
|
|
if action != inspection.DO_NOTHING {
|
|
return action
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
func processMessage(tlsInspection *TLSInspection, data []byte, pkt packet.Packet, link *network.Link) (action uint8) {
|
|
|
|
// we are only interested in handshake messages
|
|
if data[0] != uint8(tlslib.RecordTypeHandshake) {
|
|
// log.Tracef("TLS inspector: %s: got %s", pkt, tlsRecordType)
|
|
return inspection.DO_NOTHING
|
|
}
|
|
|
|
// TODO: handle tls session resumption: session tickets / session id
|
|
_, ok := tlsHandshakeTypeNames[data[5]]
|
|
if !ok {
|
|
// tlsHandshakeType = "UNKNOWN"
|
|
log.Tracef("TLS inspector: %s: UNKNOWN handshake %d:", pkt, data[5])
|
|
}
|
|
// log.Tracef("TLS inspector: %s: got %s", pkt, tlsHandshakeType)
|
|
|
|
if pkt.IsOutbound() {
|
|
switch data[5] {
|
|
|
|
// ClientHello
|
|
case tlslib.TypeClientHello:
|
|
var msg tlslib.ClientHelloMsg
|
|
if msg.Unmarshal(data[5:]) {
|
|
// log.Tracef("TLS inspector: %s: ClientHello: %s", pkt, msg.ServerName)
|
|
|
|
if tlsInspection.DenyTLSWithoutSNI && msg.ServerName == "" {
|
|
log.Infof("TLS inspector: %s does not use SNI, blocking", link)
|
|
link.AddReason(fmt.Sprintf("TLS does not use SNI"))
|
|
return inspection.BLOCK_LINK
|
|
}
|
|
|
|
if tlsInspection.DenyInsecureTLS && msg.Vers < 0x0301 {
|
|
log.Infof("TLS inspector: %s uses version prior TLS1.0, blocking", link)
|
|
link.AddReason(fmt.Sprintf("TLS uses version prior to TLS1.0"))
|
|
return inspection.BLOCK_LINK
|
|
}
|
|
|
|
tlsInspection.ServerName = msg.ServerName
|
|
if len(msg.SessionId) > 0 || len(msg.SessionTicket) > 0 {
|
|
tlsInspection.Resuming = true
|
|
}
|
|
|
|
return
|
|
}
|
|
log.Warningf("TLS inspector: %s: failed to parse ClientHello", pkt)
|
|
|
|
}
|
|
} else {
|
|
switch data[5] {
|
|
|
|
// ServerHello
|
|
case tlslib.TypeServerHello:
|
|
var msg tlslib.ServerHelloMsg
|
|
if msg.Unmarshal(data[5:]) {
|
|
|
|
if tlsInspection.DenyInsecureTLS {
|
|
cs, ok := cipherSuiteNames[msg.CipherSuite]
|
|
if !ok {
|
|
log.Infof("TLS inspector: %s uses unknown cipher suite, blocking", link)
|
|
link.AddReason(fmt.Sprintf("TLS uses unknown cipher suite"))
|
|
return inspection.BLOCK_LINK
|
|
}
|
|
|
|
// We think matching this way is more secure, as it leaves less possibility for bugs, makes the code more readable, and easier to verify.
|
|
switch {
|
|
case strings.HasSuffix(cs, "MD5"):
|
|
case strings.HasSuffix(cs, "SHA"):
|
|
case strings.Contains(cs, "NULL"):
|
|
case strings.Contains(cs, "RC4"):
|
|
case strings.Contains(cs, "DES"):
|
|
case strings.Contains(cs, "IDEA"):
|
|
case strings.Contains(cs, "anon"):
|
|
default:
|
|
return
|
|
}
|
|
|
|
log.Infof("TLS inspector: %s uses insecure cipher suite, blocking", link)
|
|
link.AddReason(fmt.Sprintf("TLS uses insecure cipher suite"))
|
|
return inspection.BLOCK_LINK
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
log.Warningf("TLS inspector: %s: failed to parse server hello", pkt)
|
|
|
|
// Certificate from server
|
|
case tlslib.TypeCertificate:
|
|
var msg tlslib.CertificateMsg
|
|
if msg.Unmarshal(data[5:]) {
|
|
|
|
// parse certificates
|
|
certs := make([]*x509.Certificate, len(msg.Certificates))
|
|
for key, bytes := range msg.Certificates {
|
|
cert, err := x509.ParseCertificate(bytes)
|
|
if err != nil {
|
|
if tlsInspection.EnforceRevocation {
|
|
log.Infof("TLS inspector: %s: failed to parse cert, denying")
|
|
link.AddReason("failed to parse TLS certificates")
|
|
return inspection.BLOCK_LINK
|
|
}
|
|
return inspection.STOP_INSPECTING
|
|
}
|
|
certs[key] = cert
|
|
}
|
|
|
|
// always check signatures
|
|
if tlsInspection.ServerName == "" && len(certs[0].DNSNames) > 0 {
|
|
// ignore missing SNI, as we already checked for that earlier
|
|
tlsInspection.ServerName = certs[0].DNSNames[0]
|
|
}
|
|
verifiedChain, err := verify.CheckSignatures(tlsInspection.ServerName, certs)
|
|
if err != nil {
|
|
log.Infof("TLS inspector: certificate invalid: %s, denying", err)
|
|
link.AddReason(fmt.Sprintf("certificate invalid: %s", err))
|
|
return inspection.BLOCK_LINK
|
|
}
|
|
|
|
// always check if we already know of cert revocation
|
|
ok, _ := verify.CheckKnownRevocation(verifiedChain)
|
|
if !ok {
|
|
log.Infof("TLS inspector: certificate is revoked, denying", err)
|
|
link.AddReason("certificate is revoked")
|
|
return inspection.BLOCK_LINK
|
|
}
|
|
|
|
// check recocation, either now or later
|
|
if tlsInspection.EnforceRevocation {
|
|
ok, err := verify.CheckRecovation(verifiedChain)
|
|
if !ok {
|
|
log.Infof("TLS inspector: %s: failed to check revocation: %s", link, err)
|
|
link.AddReason(fmt.Sprintf("failed to check revocation: %s", err))
|
|
return inspection.BLOCK_LINK
|
|
}
|
|
if err != nil {
|
|
log.Infof("TLS inspector: %s: softfailed to check revocation: %s", link, err)
|
|
return inspection.STOP_INSPECTING
|
|
}
|
|
log.Infof("TLS inspector: %s: verified certificate", link)
|
|
return inspection.STOP_INSPECTING
|
|
} else {
|
|
tlsInspection.verification = make(chan bool, 1)
|
|
tlsInspection.WaitingForVerification = true
|
|
go func() {
|
|
runtime.Gosched()
|
|
ok, err := verify.CheckRecovation(verifiedChain)
|
|
if !ok {
|
|
log.Infof("TLS inspector (delayed): %s: failed to check revocation: %s", link, err)
|
|
// TODO: think about locking Reason, right now
|
|
link.AddReason(fmt.Sprintf("failed to check revocation: %s", err))
|
|
} else if err != nil {
|
|
log.Infof("TLS inspector (delayed): %s: softfailed to check revocation: %s", link, err)
|
|
} else {
|
|
log.Infof("TLS inspector (delayed): %s: verified certificate", link)
|
|
}
|
|
tlsInspection.verification <- ok
|
|
}()
|
|
return inspection.DO_NOTHING
|
|
}
|
|
|
|
}
|
|
log.Warningf("TLS inspector: %s: failed to parse server cert msg", link)
|
|
|
|
}
|
|
}
|
|
|
|
return
|
|
|
|
}
|