Initial commit after restructure
This commit is contained in:
436
firewall/inspection/tls/tls.go
Normal file
436
firewall/inspection/tls/tls.go
Normal file
@@ -0,0 +1,436 @@
|
||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
||||
|
||||
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/safing-core/configuration"
|
||||
"github.com/Safing/safing-core/crypto/verify"
|
||||
"github.com/Safing/safing-core/firewall/inspection"
|
||||
"github.com/Safing/safing-core/firewall/inspection/tls/tlslib"
|
||||
"github.com/Safing/safing-core/log"
|
||||
"github.com/Safing/safing-core/network"
|
||||
"github.com/Safing/safing-core/network/netutils"
|
||||
"github.com/Safing/safing-core/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
|
||||
|
||||
config = configuration.Get()
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user