safing-portmaster/firewall/inspection/tls/tls.go
2018-08-13 14:14:27 +02:00

436 lines
15 KiB
Go

// 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
}