Initial commit after restructure
This commit is contained in:
12
firewall/inspection/tls/verify/FORKED
Normal file
12
firewall/inspection/tls/verify/FORKED
Normal file
@@ -0,0 +1,12 @@
|
||||
from: https://github.com/cloudflare/cfssl
|
||||
commit: https://github.com/cloudflare/cfssl/commit/851e4b77bb0e13a97e7d836a2f069d17ed5478c1
|
||||
license: BSD 2-clause "Simplified" License
|
||||
original files imported:
|
||||
- revoke/revoke.go sha256:95e3b14c1dc3d99a37f3da914c0137df80145e82295a817acf7382f9ce4bcc7e
|
||||
- helpers/helpers.go sha256:f2642d23b3f48a9dd7d0a72d3538f95aeadbc652df30fe2f52e4e9207e256e07
|
||||
|
||||
from: https://github.com/golang/go
|
||||
commit: https://github.com/golang/go/commit/220e0e0f7383a79fda0ba61bd1bf2076f5f74d72
|
||||
license: BSD 3-clause "New" or "Revised" License
|
||||
original files imported:
|
||||
- src/crypto/x509/verify.go sha256:8d31581662e28a35af65166a98e8dadd60de750a83bfa7e25efad2500832b8ed
|
||||
266
firewall/inspection/tls/verify/cert.go
Normal file
266
firewall/inspection/tls/verify/cert.go
Normal file
@@ -0,0 +1,266 @@
|
||||
// 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 verify
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudflare/cfssl/crypto/pkcs7"
|
||||
datastore "github.com/ipfs/go-datastore"
|
||||
|
||||
"github.com/Safing/safing-core/crypto/hash"
|
||||
"github.com/Safing/safing-core/database"
|
||||
)
|
||||
|
||||
// Cert saves a certificate.
|
||||
type Cert struct {
|
||||
database.Base
|
||||
|
||||
cert *x509.Certificate
|
||||
Raw []byte
|
||||
|
||||
RevokedWithCRL bool `*:",omitempty"`
|
||||
RevokedWithOneCRL bool `*:",omitempty"`
|
||||
RevokedWithCRLSet bool `*:",omitempty"`
|
||||
RevokedWithOCSP bool `*:",omitempty"`
|
||||
|
||||
OCSPFailed bool
|
||||
NextOCSPUpdate int64
|
||||
|
||||
LastSeen int64
|
||||
Expires int64
|
||||
}
|
||||
|
||||
var certModel *Cert // only use this as parameter for database.EnsureModel-like functions
|
||||
|
||||
func init() {
|
||||
database.RegisterModel(certModel, func() database.Model { return new(Cert) })
|
||||
}
|
||||
|
||||
// ensureParsed ensures that the certificate is parsed and available in the cert attribute
|
||||
func (m *Cert) ensureParsed() error {
|
||||
if m.cert != nil {
|
||||
if len(m.Raw) == 0 {
|
||||
return errors.New("certificate data not saved")
|
||||
}
|
||||
var err error
|
||||
m.cert, err = x509.ParseCertificate(m.Raw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse certificate: %s", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCertificate returns the underlying x509.Certificate
|
||||
func (m *Cert) GetCertificate() (*x509.Certificate, error) {
|
||||
if err := m.ensureParsed(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m.cert, nil
|
||||
}
|
||||
|
||||
// IsRevoked returns if the certificate has been revoked.
|
||||
func (m *Cert) IsRevoked(hardFail bool) bool {
|
||||
if m.RevokedWithCRL || m.RevokedWithOneCRL || m.RevokedWithCRLSet || m.RevokedWithOCSP {
|
||||
return true
|
||||
}
|
||||
if hardFail && m.OCSPFailed {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// RevocationStatus returns the status of the certificate in form of a string to be appended to something like "The certificate is ".
|
||||
func (m *Cert) RevocationStatus(hardFail bool) string {
|
||||
if !m.IsRevoked(hardFail) {
|
||||
return "not revoked"
|
||||
}
|
||||
var revokedBy []string
|
||||
if m.RevokedWithCRL {
|
||||
revokedBy = append(revokedBy, "CRL")
|
||||
}
|
||||
if m.RevokedWithOneCRL {
|
||||
revokedBy = append(revokedBy, "OneCRL")
|
||||
}
|
||||
if m.RevokedWithCRLSet {
|
||||
revokedBy = append(revokedBy, "CRLSet")
|
||||
}
|
||||
if m.RevokedWithOCSP {
|
||||
revokedBy = append(revokedBy, "OCSP")
|
||||
}
|
||||
if len(revokedBy) > 0 {
|
||||
return fmt.Sprintf("revoked by %s", strings.Join(revokedBy, ", "))
|
||||
}
|
||||
return fmt.Sprintf("possibly revoked (OCSP failed) - hardfailing as requested.")
|
||||
}
|
||||
|
||||
// CreateWithUrl saves Cert in the default namespace using the certificate URL as the key.
|
||||
func (m *Cert) CreateWithUrl(url string) error {
|
||||
return m.CreateObject(&database.CertCache, fmt.Sprintf("U%x", url), m)
|
||||
}
|
||||
|
||||
// CreateWithSPKI saves Cert in the default namespace using the certificate SPKI as the key.
|
||||
func (m *Cert) CreateWithSPKI(spki []byte) error {
|
||||
return m.CreateObject(&database.CertCache, fmt.Sprintf("K%x", hash.Sum(spki, hash.SHA2_256).Safe64()), m)
|
||||
}
|
||||
|
||||
// CreateRevokedCert creates a new Cert in its CA's namespace with its Serial Number
|
||||
func (m *Cert) CreateRevokedCert(caID string, serialNumber *big.Int) error {
|
||||
namespace := database.CARevocationInfoCache.ChildString(fmt.Sprintf("CARevocationInfo:%s", caID))
|
||||
return m.CreateInNamespace(&namespace, fmt.Sprintf("S%x", serialNumber))
|
||||
}
|
||||
|
||||
// CreateInNamespace saves Cert with the provided name in the provided namespace.
|
||||
func (m *Cert) CreateInNamespace(namespace *datastore.Key, name string) error {
|
||||
return m.CreateObject(namespace, name, m)
|
||||
}
|
||||
|
||||
// Save saves Cert.
|
||||
func (m *Cert) Save() error {
|
||||
return m.SaveObject(m)
|
||||
}
|
||||
|
||||
// GetCertWithURL fetches Cert from the default namespace using the certificate URL as the key.
|
||||
func GetCertWithURL(url string) (*Cert, error) {
|
||||
return GetCertFromNamespace(&database.CertCache, fmt.Sprintf("U%x", url))
|
||||
}
|
||||
|
||||
// GetCertWithSPKI fetches Cert from the default namespace using the certificate SPKI as the key.
|
||||
func GetCertWithSPKI(spki []byte) (*Cert, error) {
|
||||
return GetCertFromNamespace(&database.CertCache, fmt.Sprintf("K%x", hash.Sum(spki, hash.SHA2_256).Safe64()))
|
||||
}
|
||||
|
||||
// GetCertFromNamespace gets Cert with the provided name from the provided namespace.
|
||||
func GetCertFromNamespace(namespace *datastore.Key, name string) (*Cert, error) {
|
||||
object, err := database.GetAndEnsureModel(namespace, name, certModel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
model, ok := object.(*Cert)
|
||||
if !ok {
|
||||
return nil, database.NewMismatchError(object, certModel)
|
||||
}
|
||||
return model, nil
|
||||
}
|
||||
|
||||
// GetRevokedCert gets Cert from its CA's namespace with its Serial Number
|
||||
func GetRevokedCert(caID string, serialNumber *big.Int) (*Cert, error) {
|
||||
namespace := database.CARevocationInfoCache.ChildString(fmt.Sprintf("CARevocationInfo:%s", caID))
|
||||
return GetCertFromNamespace(&namespace, fmt.Sprintf("S%x", serialNumber))
|
||||
}
|
||||
|
||||
func GetOrFetchCert(urls []string) (*x509.Certificate, error) {
|
||||
// TODO: handle root CAs
|
||||
for _, url := range urls {
|
||||
cert, err := GetCertWithURL(url)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
certificate, err := x509.ParseCertificate(cert.Raw)
|
||||
if err == nil {
|
||||
return certificate, nil
|
||||
}
|
||||
}
|
||||
return ImportCert(urls)
|
||||
}
|
||||
|
||||
func GetOrFetchIssuer(cert *x509.Certificate) (*x509.Certificate, error) {
|
||||
if len(cert.IssuingCertificateURL) == 0 {
|
||||
return nil, errors.New("no issuing certificate URLs")
|
||||
}
|
||||
return GetOrFetchCert(cert.IssuingCertificateURL)
|
||||
}
|
||||
|
||||
func ImportCert(urls []string) (*x509.Certificate, error) {
|
||||
var err error
|
||||
for _, url := range urls {
|
||||
|
||||
var resp *http.Response
|
||||
resp, err = http.Get(url)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("could not fetch certificate from %s: %s", url, err)
|
||||
continue
|
||||
}
|
||||
|
||||
var data []byte
|
||||
data, err = ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("could not read certificate from %s: %s", url, err)
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
var cert *x509.Certificate
|
||||
cert, err = x509.ParseCertificate(data)
|
||||
if err != nil {
|
||||
cert, err = ParsePEMCertificate(data)
|
||||
}
|
||||
if err != nil {
|
||||
err = fmt.Errorf("could not parse certificate from %s: %s", url, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// verify
|
||||
|
||||
_, err = cert.Verify(x509.VerifyOptions{})
|
||||
// chains, err = cert.Verify(x509.VerifyOptions{})
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to verify certificate from %s: %s", url, err)
|
||||
}
|
||||
|
||||
// check revocation
|
||||
|
||||
// save
|
||||
|
||||
return cert, nil
|
||||
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no or only failing Certs available, last error: %s", err)
|
||||
|
||||
}
|
||||
|
||||
// ParsePEMCertificate parses and returns a PEM-encoded certificate,
|
||||
// can handle PEM encoded PKCS #7 structures.
|
||||
func ParsePEMCertificate(certPEM []byte) (*x509.Certificate, error) {
|
||||
|
||||
block, _ := pem.Decode(bytes.TrimSpace(certPEM))
|
||||
if block == nil {
|
||||
return nil, errors.New("decoding failed")
|
||||
}
|
||||
|
||||
// if len(rest) > 0 {
|
||||
// return nil, errors.New("decoding failed: the PEM data should contain only one object")
|
||||
// }
|
||||
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err == nil {
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
pkcs7data, err := pkcs7.ParsePKCS7(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing failed: %s", err)
|
||||
}
|
||||
if pkcs7data.ContentInfo != "SignedData" {
|
||||
return nil, errors.New("parsing failed: only PKCS #7 Signed Data Content Info supported for certificate parsing")
|
||||
}
|
||||
certs := pkcs7data.Content.SignedData.Certificates
|
||||
if certs == nil {
|
||||
return nil, errors.New("PKCS #7 structure contains no certificates")
|
||||
}
|
||||
// if len(certs) > 1 {
|
||||
// return nil, errors.New("decoding failed: the PKCS7 object in the PEM data should contain only one certificate"))
|
||||
// }
|
||||
return certs[0], nil
|
||||
|
||||
}
|
||||
70
firewall/inspection/tls/verify/cert_test.go
Normal file
70
firewall/inspection/tls/verify/cert_test.go
Normal file
@@ -0,0 +1,70 @@
|
||||
// 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 verify
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// func TestCertFetching(t *testing.T) {
|
||||
//
|
||||
// cert, err := GetOrFetchCert([]string{"http://cert.int-x3.letsencrypt.org/"})
|
||||
// if err != nil {
|
||||
// t.Errorf("failed to GetOrFetchCert: %s", err)
|
||||
// }
|
||||
// fmt.Printf("%v\n", cert)
|
||||
//
|
||||
// GetOrFetchCert([]string{"http://cert.int-x3.letsencrypt.org/"})
|
||||
// if err != nil {
|
||||
// t.Errorf("failed to GetOrFetchCert: %s", err)
|
||||
// }
|
||||
// fmt.Printf("%v\n", cert)
|
||||
//
|
||||
// }
|
||||
|
||||
func TestMissingChain(t *testing.T) {
|
||||
|
||||
certPEM := []byte(`-----BEGIN CERTIFICATE-----
|
||||
MIIEuzCCA6OgAwIBAgIQRMpyC8ARigQMjp7ywd0HOzANBgkqhkiG9w0BAQsFADBD
|
||||
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhhd3RlLCBJbmMuMR0wGwYDVQQDExR0
|
||||
aGF3dGUgU0hBMjU2IFNTTCBDQTAeFw0xNjAxMTkwMDAwMDBaFw0xODAxMTgyMzU5
|
||||
NTlaMHQxCzAJBgNVBAYTAkRFMRYwFAYDVQQIDA1OaWVkZXJzYWNoc2VuMREwDwYD
|
||||
VQQHDAhIYW5ub3ZlcjEjMCEGA1UECgwaSGVpc2UgTWVkaWVuIEdtYkggJiBDby4g
|
||||
S0cxFTATBgNVBAMMDHd3dy5oZWlzZS5kZTCCASIwDQYJKoZIhvcNAQEBBQADggEP
|
||||
ADCCAQoCggEBAL+S5DqFzKXKpDuPKxSkhG/2ap4kFxBXv0u7gmAE30Cya16RASHt
|
||||
oSZCjHPE2yyGhLaLTjnf6kC4AgJ4eQtStPb0Oc7NodEbeFYzn6ei1OyXmYD4V7kL
|
||||
HYwjGIE3TZch4scb6peuNexYotHLB032KL/csScfdtDSpYg6ZEJ7kYI2MSqP4ogo
|
||||
BbakfgVjCdTwi4PWfmRO080t6MUEfJbfqojxcVxO70femsvmteU5/7IaXXNCnnoF
|
||||
KWl/G2WgmD8eBu2+HY9ojRrG5DrbKcz6XcNJCz88khQ+x/1EPsEEWjHiADfvl3HH
|
||||
uWmFf0BBoB3V3oL+v4i1zu2ffZH6UdlekxMCAwEAAaOCAXgwggF0MCEGA1UdEQQa
|
||||
MBiCDHd3dy5oZWlzZS5kZYIIaGVpc2UuZGUwCQYDVR0TBAIwADBuBgNVHSAEZzBl
|
||||
MGMGBmeBDAECAjBZMCYGCCsGAQUFBwIBFhpodHRwczovL3d3dy50aGF3dGUuY29t
|
||||
L2NwczAvBggrBgEFBQcCAjAjDCFodHRwczovL3d3dy50aGF3dGUuY29tL3JlcG9z
|
||||
aXRvcnkwDgYDVR0PAQH/BAQDAgWgMB8GA1UdIwQYMBaAFCuaNa4BGDgw4XB6BeAR
|
||||
dqPOvZAUMCsGA1UdHwQkMCIwIKAeoByGGmh0dHA6Ly90Zy5zeW1jYi5jb20vdGcu
|
||||
Y3JsMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjBXBggrBgEFBQcBAQRL
|
||||
MEkwHwYIKwYBBQUHMAGGE2h0dHA6Ly90Zy5zeW1jZC5jb20wJgYIKwYBBQUHMAKG
|
||||
Gmh0dHA6Ly90Zy5zeW1jYi5jb20vdGcuY3J0MA0GCSqGSIb3DQEBCwUAA4IBAQAy
|
||||
dryRQkVsQIxrhyGlAdGVR9ygOwtJBUq0najtb0+/HNEysN0QZjguNKDlzmHm1pAI
|
||||
gYR5hTlQH93XqL4d1+UVRL61hiKJja0EEkHnDEtde9eRsyvVfBHRrUF/qV6ar3yG
|
||||
0NHQdlZSIGpztKap4Za6RKxwgZid+LC1k67a4envcSPxHnREtR23mDIpe6u0NoQA
|
||||
VhUDXwbAUHO2A/6dKvhQVlPsZES56hg0uPrA6ODCxHQeRId2mn+/HY2VnDkwfJRZ
|
||||
NwVkZSvy4/Mi4cZYkkcW3Z9gePYDiNGe1wBr4/H3ffK63ek6W7Uy3ju2TpMiOyIB
|
||||
gQDY9b+bJOhhWfRQhONh
|
||||
-----END CERTIFICATE-----`)
|
||||
|
||||
cert, err := ParsePEMCertificate(certPEM)
|
||||
if err != nil {
|
||||
t.Errorf("failed to parse cert: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = cert.Verify(x509.VerifyOptions{})
|
||||
// chains, err = cert.Verify(x509.VerifyOptions{})
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to verify certificate: %s", err)
|
||||
}
|
||||
|
||||
}
|
||||
254
firewall/inspection/tls/verify/crl.go
Normal file
254
firewall/inspection/tls/verify/crl.go
Normal file
@@ -0,0 +1,254 @@
|
||||
// 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 verify
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
datastore "github.com/ipfs/go-datastore"
|
||||
|
||||
"github.com/Safing/safing-core/crypto/hash"
|
||||
"github.com/Safing/safing-core/database"
|
||||
"github.com/Safing/safing-core/log"
|
||||
)
|
||||
|
||||
// CARevocationInfo saves Information on revokation of Certificates of a Certificate Authority.
|
||||
type CARevocationInfo struct {
|
||||
database.Base
|
||||
|
||||
CRLDistributionPoints []string
|
||||
OCSPServers []string
|
||||
CertificateURLs []string
|
||||
|
||||
LastCRLUpdate int64
|
||||
NextCRLUpdate int64
|
||||
|
||||
cert *x509.Certificate
|
||||
Raw []byte
|
||||
|
||||
Expires int64
|
||||
}
|
||||
|
||||
var (
|
||||
caRevocationInfoModel *CARevocationInfo // only use this as parameter for database.EnsureModel-like functions
|
||||
|
||||
dupCrlReqMap = make(map[string]*sync.Mutex)
|
||||
dupCrlReqLock sync.Mutex
|
||||
)
|
||||
|
||||
func init() {
|
||||
database.RegisterModel(caRevocationInfoModel, func() database.Model { return new(CARevocationInfo) })
|
||||
}
|
||||
|
||||
// Create saves CARevocationInfo with the provided name in the default namespace.
|
||||
func (m *CARevocationInfo) Create(name string) error {
|
||||
return m.CreateObject(&database.CARevocationInfoCache, name, m)
|
||||
}
|
||||
|
||||
// CreateInNamespace saves CARevocationInfo with the provided name in the provided namespace.
|
||||
func (m *CARevocationInfo) CreateInNamespace(namespace *datastore.Key, name string) error {
|
||||
return m.CreateObject(namespace, name, m)
|
||||
}
|
||||
|
||||
// Save saves CARevocationInfo.
|
||||
func (m *CARevocationInfo) Save() error {
|
||||
return m.SaveObject(m)
|
||||
}
|
||||
|
||||
func (m *CARevocationInfo) GetRevokedCert(serialNumber *big.Int) (*Cert, error) {
|
||||
return GetCertFromNamespace(m.GetKey(), fmt.Sprintf("S%x", serialNumber))
|
||||
}
|
||||
|
||||
func (m *CARevocationInfo) CreateRevokedCert(cert *Cert, serialNumber *big.Int) error {
|
||||
return cert.CreateInNamespace(m.GetKey(), fmt.Sprintf("S%x", serialNumber))
|
||||
}
|
||||
|
||||
// GetCARevocationInfo fetches CARevocationInfo with the provided name from the default namespace.
|
||||
func GetCARevocationInfo(name string) (*CARevocationInfo, error) {
|
||||
return GetCARevocationInfoFromNamespace(&database.CARevocationInfoCache, name)
|
||||
}
|
||||
|
||||
// GetCARevocationInfoFromNamespace fetches CARevocationInfo with the provided name from the provided namespace.
|
||||
func GetCARevocationInfoFromNamespace(namespace *datastore.Key, name string) (*CARevocationInfo, error) {
|
||||
object, err := database.GetAndEnsureModel(namespace, name, caRevocationInfoModel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
model, ok := object.(*CARevocationInfo)
|
||||
if !ok {
|
||||
return nil, database.NewMismatchError(object, caRevocationInfoModel)
|
||||
}
|
||||
return model, nil
|
||||
}
|
||||
|
||||
// ensureCertParsed ensures that the certificate is parsed and available in the cert attribute
|
||||
func (m *CARevocationInfo) ensureCertParsed() error {
|
||||
if m.cert != nil {
|
||||
if len(m.Raw) == 0 {
|
||||
return errors.New("certificate data not saved")
|
||||
}
|
||||
var err error
|
||||
m.cert, err = x509.ParseCertificate(m.Raw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse certificate: %s", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateCRLDistributionPoints updates the CRL Distribution Points with new urls
|
||||
func (m *CARevocationInfo) UpdateCRLDistributionPoints(newCRLDistributionPoints []string) {
|
||||
var found bool
|
||||
sort.Reverse(sort.StringSlice(newCRLDistributionPoints))
|
||||
for _, newEntry := range newCRLDistributionPoints {
|
||||
found = false
|
||||
for _, entry := range m.CRLDistributionPoints {
|
||||
if newEntry == entry {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
m.CRLDistributionPoints = append([]string{newEntry}, m.CRLDistributionPoints...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateCRL fetches and imports the CRL belonging to a CA, if expired.
|
||||
func UpdateCRL(caInfo *CARevocationInfo, ca *x509.Certificate, caID string) error {
|
||||
var err error
|
||||
|
||||
// ensure we have caInfo
|
||||
if caInfo == nil {
|
||||
if ca == nil && caID == "" {
|
||||
return errors.New("verify: UpdateCRL must be called with at least one of: caInfo *CARevocationInfo, ca *x509.Certificate, caID string")
|
||||
}
|
||||
if caID == "" {
|
||||
caID = hash.Sum(ca.RawSubjectPublicKeyInfo, hash.SHA2_256).Safe64()
|
||||
}
|
||||
caInfo, err = GetCARevocationInfo(caID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("verify: could not get CARevocationInfo for caID %s: %s", caID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// don't update if we still have a valid record
|
||||
if caInfo.NextCRLUpdate > time.Now().Unix() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// dedup requests
|
||||
dupCrlReqLock.Lock()
|
||||
mutex, requestActive := dupCrlReqMap[caID]
|
||||
if !requestActive {
|
||||
mutex = new(sync.Mutex)
|
||||
mutex.Lock()
|
||||
dupCrlReqMap[caID] = mutex
|
||||
dupCrlReqLock.Unlock()
|
||||
} else {
|
||||
dupCrlReqLock.Unlock()
|
||||
log.Tracef("verify: waiting for duplicate CRL import for CA %s to complete", caID)
|
||||
mutex.Lock()
|
||||
// only wait until duplicate request is finished, then return
|
||||
mutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
defer func() {
|
||||
dupCrlReqLock.Lock()
|
||||
delete(dupCrlReqMap, caID)
|
||||
dupCrlReqLock.Unlock()
|
||||
mutex.Unlock()
|
||||
}()
|
||||
|
||||
// fetch and import CRL
|
||||
for _, url := range caInfo.CRLDistributionPoints {
|
||||
|
||||
// fetch CRL
|
||||
crl, err := fetchCRL(url)
|
||||
if err != nil {
|
||||
log.Warningf("verify: failed to import CRL from %s: %s", url, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// check CRL signature
|
||||
// TODO: how is revokation checked when verifying CRL signature?
|
||||
err = ca.CheckCRLSignature(crl)
|
||||
if err != nil {
|
||||
log.Warningf("verify: failed to import CRL from %s: %s", url, err)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Infof("verify: importing verified CRL for CA %s from %s", caID, url)
|
||||
|
||||
// save to DB
|
||||
newExpiry := crl.TBSCertList.NextUpdate.Add(720 * time.Hour).Unix()
|
||||
caInfo.LastCRLUpdate = time.Now().Unix()
|
||||
caInfo.NextCRLUpdate = newExpiry
|
||||
for _, entry := range crl.TBSCertList.RevokedCertificates {
|
||||
|
||||
// log.Tracef("verify: importing %d", entry.SerialNumber)
|
||||
|
||||
// fetch or create rCert
|
||||
rCert, err := caInfo.GetRevokedCert(entry.SerialNumber)
|
||||
if err != nil {
|
||||
rCert = new(Cert)
|
||||
}
|
||||
|
||||
// update expiry
|
||||
rCert.RevokedWithCRL = true
|
||||
if newExpiry > rCert.Expires {
|
||||
rCert.Expires = newExpiry
|
||||
}
|
||||
|
||||
// save
|
||||
if rCert.GetKey() == nil {
|
||||
caInfo.CreateRevokedCert(rCert, entry.SerialNumber)
|
||||
} else {
|
||||
rCert.Save()
|
||||
}
|
||||
}
|
||||
|
||||
log.Tracef("verify: import from %s finished.", url)
|
||||
caInfo.Save()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("verify: no or only failing CRLs available for CA %s", caID)
|
||||
}
|
||||
|
||||
func fetchCRL(url string) (*pkix.CertificateList, error) {
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 1 * time.Minute,
|
||||
}
|
||||
resp, err := client.Get(url)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve CRL: %s", err)
|
||||
} else if resp.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("failed to retrieve CRL: non-200 status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read CRL: %s", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
crl, err := x509.ParseCRL(body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse CRL: %s", err)
|
||||
}
|
||||
|
||||
return crl, nil
|
||||
}
|
||||
139
firewall/inspection/tls/verify/ocsp.go
Normal file
139
firewall/inspection/tls/verify/ocsp.go
Normal file
@@ -0,0 +1,139 @@
|
||||
// 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 verify
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ocsp"
|
||||
|
||||
"github.com/Safing/safing-core/crypto/hash"
|
||||
"github.com/Safing/safing-core/log"
|
||||
)
|
||||
|
||||
var (
|
||||
ocspOpts = ocsp.RequestOptions{
|
||||
Hash: crypto.SHA1,
|
||||
}
|
||||
|
||||
dupOcspReqMap = make(map[string]*sync.Mutex)
|
||||
dupOcspReqLock sync.Mutex
|
||||
)
|
||||
|
||||
func UpdateOCSP(rCert *Cert, cert, ca *x509.Certificate, caID string) (*Cert, error) {
|
||||
|
||||
if rCert.NextOCSPUpdate > time.Now().Unix() {
|
||||
return rCert, nil
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
// get CA if necessary
|
||||
if ca == nil {
|
||||
ca, err = GetOrFetchIssuer(cert)
|
||||
if err != nil {
|
||||
return rCert, err
|
||||
}
|
||||
}
|
||||
if caID == "" {
|
||||
caID = hash.Sum(ca.RawSubjectPublicKeyInfo, hash.SHA2_256).Safe64()
|
||||
}
|
||||
|
||||
// dedup requests
|
||||
dupOcspReqLock.Lock()
|
||||
mutex, requestActive := dupOcspReqMap[rCert.FmtKey()]
|
||||
if !requestActive {
|
||||
mutex = new(sync.Mutex)
|
||||
mutex.Lock()
|
||||
dupOcspReqMap[rCert.FmtKey()] = mutex
|
||||
dupOcspReqLock.Unlock()
|
||||
} else {
|
||||
dupOcspReqLock.Unlock()
|
||||
log.Tracef("waiting for duplicate OCSP request for %s to complete", rCert.FmtKey())
|
||||
mutex.Lock()
|
||||
// only wait until duplicate request is finished, then return
|
||||
mutex.Unlock()
|
||||
// refetch rCert
|
||||
rCert, err = GetRevokedCert(caID, cert.SerialNumber)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to refetch rCert %s: %s", rCert.FmtKey(), err)
|
||||
}
|
||||
return rCert, nil
|
||||
}
|
||||
defer func() {
|
||||
dupOcspReqLock.Lock()
|
||||
delete(dupOcspReqMap, rCert.FmtKey())
|
||||
dupOcspReqLock.Unlock()
|
||||
mutex.Unlock()
|
||||
}()
|
||||
|
||||
// create request
|
||||
ocspRequest, err := ocsp.CreateRequest(cert, ca, &ocspOpts)
|
||||
if err != nil {
|
||||
return rCert, err
|
||||
}
|
||||
|
||||
// fetch
|
||||
var resp *ocsp.Response
|
||||
for _, url := range cert.OCSPServer {
|
||||
resp, err = fetchOCSP(url, ocspRequest, cert, ca)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
rCert.NextOCSPUpdate = resp.NextUpdate.Unix()
|
||||
if rCert.NextOCSPUpdate > rCert.Expires {
|
||||
rCert.Expires = rCert.NextOCSPUpdate
|
||||
}
|
||||
rCert.RevokedWithOCSP = resp.Status != ocsp.Good
|
||||
rCert.OCSPFailed = false
|
||||
rCert.Save()
|
||||
return rCert, nil
|
||||
}
|
||||
|
||||
rCert.NextOCSPUpdate = time.Now().Add(120 * time.Second).Unix()
|
||||
rCert.OCSPFailed = true
|
||||
rCert.Save()
|
||||
return rCert, fmt.Errorf("all OCSP servers failed, last error: %s", err)
|
||||
}
|
||||
|
||||
// fetchOCSP attempts to request an OCSP response from the
|
||||
// server. The error only indicates a failure to *fetch* the
|
||||
// certificate, and *does not* mean the certificate is valid.
|
||||
func fetchOCSP(server string, req []byte, cert, ca *x509.Certificate) (*ocsp.Response, error) {
|
||||
|
||||
var resp *http.Response
|
||||
var err error
|
||||
if len(req) > 256 {
|
||||
buf := bytes.NewBuffer(req)
|
||||
resp, err = http.Post(server, "application/ocsp-request", buf)
|
||||
} else {
|
||||
reqURL := server + "/" + base64.StdEncoding.EncodeToString(req)
|
||||
resp, err = http.Get(reqURL)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errors.New("failed to retrieve OSCP")
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
return ocsp.ParseResponseForCert(body, cert, ca)
|
||||
}
|
||||
215
firewall/inspection/tls/verify/verify.go
Normal file
215
firewall/inspection/tls/verify/verify.go
Normal file
@@ -0,0 +1,215 @@
|
||||
// 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 verify
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/Safing/safing-core/configuration"
|
||||
"github.com/Safing/safing-core/crypto/hash"
|
||||
"github.com/Safing/safing-core/database"
|
||||
)
|
||||
|
||||
// useful references:
|
||||
// https://github.com/cloudflare/cfssl/blob/master/revoke/revoke.go
|
||||
// Mozilla OneCRL
|
||||
// https://blog.mozilla.org/security/2015/03/03/revoking-intermediate-certificates-introducing-onecrl/
|
||||
// https://wiki.mozilla.org/CA:ImprovingRevocation
|
||||
// Google CRLSet
|
||||
// https://security.stackexchange.com/questions/55811/how-are-crlsets-more-secure
|
||||
// https://www.imperialviolet.org/2012/02/05/crlsets.html
|
||||
// RE: https://www.grc.com/revocation/crlsets.htm
|
||||
// RE: RE: https://www.imperialviolet.org/2014/04/29/revocationagain.html
|
||||
|
||||
var (
|
||||
config = configuration.Get()
|
||||
)
|
||||
|
||||
// FullCheckBytes does a full certificate check, certificates are provided as raw bytes.
|
||||
// It parses the raw certificates and calls FullCheck.
|
||||
func FullCheckBytes(name string, certBytes [][]byte) (bool, error) {
|
||||
certs := make([]*x509.Certificate, len(certBytes))
|
||||
for key, bytes := range certBytes {
|
||||
cert, err := x509.ParseCertificate(bytes)
|
||||
if err != nil {
|
||||
return false, errors.New("verify: failed to parse certificate: " + err.Error())
|
||||
}
|
||||
certs[key] = cert
|
||||
}
|
||||
|
||||
return FullCheck(name, certs)
|
||||
}
|
||||
|
||||
// FullCheck does a full certificate check.
|
||||
// Calls CheckSignatures, CheckRecovation and CheckCertificateTransparency(TODO).
|
||||
func FullCheck(name string, chain []*x509.Certificate) (bool, error) {
|
||||
verifiedChain, err := CheckSignatures(name, chain)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("verify: certificate invalid: %s", err)
|
||||
}
|
||||
|
||||
return CheckRecovation(verifiedChain)
|
||||
}
|
||||
|
||||
func CheckSignatures(name string, chain []*x509.Certificate) ([]*x509.Certificate, error) {
|
||||
if len(chain) == 0 {
|
||||
return nil, errors.New("no certificates supplied")
|
||||
}
|
||||
|
||||
opts := x509.VerifyOptions{
|
||||
// Roots: c.config.RootCAs,
|
||||
// CurrentTime: time.Now(),
|
||||
DNSName: name,
|
||||
Intermediates: x509.NewCertPool(),
|
||||
}
|
||||
|
||||
for i, cert := range chain {
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
opts.Intermediates.AddCert(cert)
|
||||
}
|
||||
|
||||
verifiedChains, err := chain[0].Verify(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: further process all verified chains (revocation / CT)
|
||||
return verifiedChains[0], nil
|
||||
}
|
||||
|
||||
// TODO
|
||||
// func CheckCertificateTransparency(chain []*x509.Certificate, securityLevel int8) {
|
||||
// }
|
||||
|
||||
func CheckKnownRevocation(verifiedChain []*x509.Certificate) (bool, error) {
|
||||
for i := 0; i < len(verifiedChain)-1; i++ {
|
||||
caID := hash.Sum(verifiedChain[i+1].RawSubjectPublicKeyInfo, hash.SHA2_256).Safe64()
|
||||
rCert, err := GetRevokedCert(caID, verifiedChain[i].SerialNumber)
|
||||
if err != nil {
|
||||
if err != database.ErrNotFound {
|
||||
return true, nil
|
||||
}
|
||||
return true, fmt.Errorf("verify: failed to get rCert from database: %s", err)
|
||||
}
|
||||
if rCert.IsRevoked(false) {
|
||||
return false, nil
|
||||
}
|
||||
if rCert.OCSPFailed {
|
||||
return true, errors.New("verify: OCSP failed in the past")
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func CheckRecovation(verifiedChain []*x509.Certificate) (bool, error) {
|
||||
|
||||
for i := 0; i < len(verifiedChain)-1; i++ {
|
||||
ok, err := checkCertRevocation(verifiedChain[i], verifiedChain[i+1])
|
||||
if !ok {
|
||||
return ok, err
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
|
||||
}
|
||||
|
||||
func checkCertRevocation(cert, ca *x509.Certificate) (bool, error) {
|
||||
|
||||
// proper use?
|
||||
if cert == nil {
|
||||
return false, errors.New("verify: no certificate supplied")
|
||||
}
|
||||
|
||||
// check if recocation is supported
|
||||
if len(cert.CRLDistributionPoints) == 0 && len(cert.OCSPServer) == 0 {
|
||||
return true, fmt.Errorf("verify: certificate does not support OCSP or CRL.")
|
||||
}
|
||||
|
||||
var err error
|
||||
if ca == nil {
|
||||
ca, err = GetOrFetchIssuer(cert)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
}
|
||||
caID := hash.Sum(ca.RawSubjectPublicKeyInfo, hash.SHA2_256).Safe64()
|
||||
|
||||
// check cert
|
||||
rCert, err := GetRevokedCert(caID, cert.SerialNumber)
|
||||
if err != nil {
|
||||
if err != database.ErrNotFound {
|
||||
return true, fmt.Errorf("verify: failed to get Cert: %s", err)
|
||||
}
|
||||
rCert = &Cert{
|
||||
cert: cert,
|
||||
// Raw: cert.Raw,
|
||||
LastSeen: time.Now().Unix(),
|
||||
Expires: time.Now().Add(7 * 24 * time.Hour).Unix(),
|
||||
}
|
||||
rCert.CreateRevokedCert(caID, cert.SerialNumber)
|
||||
} else if rCert.IsRevoked(false) {
|
||||
return false, fmt.Errorf("verify: certificate is %s", rCert.RevocationStatus(false))
|
||||
}
|
||||
|
||||
// update OCSP
|
||||
// TODO: check OCSP stapling data
|
||||
// TODO: is MustStaple already handled by golang? If not, handle it!
|
||||
if len(cert.OCSPServer) > 0 {
|
||||
if rCert == nil {
|
||||
rCert = new(Cert)
|
||||
}
|
||||
rCert, err = UpdateOCSP(rCert, cert, ca, caID)
|
||||
if err == nil && !rCert.OCSPFailed {
|
||||
// update CRL later
|
||||
go checkCRL(rCert, cert, ca, caID, false, true)
|
||||
if rCert.IsRevoked(false) {
|
||||
return false, fmt.Errorf("verify: certificate is %s", rCert.RevocationStatus(false))
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
// update CRL
|
||||
return checkCRL(rCert, cert, ca, caID, false, false)
|
||||
}
|
||||
|
||||
func checkCRL(rCert *Cert, cert, ca *x509.Certificate, caID string, hardFail bool, postpone bool) (bool, error) {
|
||||
|
||||
if postpone {
|
||||
// postpone a bit to finish packet processing
|
||||
// TODO: use microtask management
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
caInfo, err := GetCARevocationInfo(caID)
|
||||
if err != nil {
|
||||
if err != database.ErrNotFound {
|
||||
return true, fmt.Errorf("verify: failed to get CARevocationInfo: %s", err)
|
||||
}
|
||||
caInfo = &CARevocationInfo{
|
||||
CRLDistributionPoints: cert.CRLDistributionPoints,
|
||||
OCSPServers: cert.OCSPServer,
|
||||
CertificateURLs: cert.IssuingCertificateURL,
|
||||
Raw: ca.Raw,
|
||||
Expires: time.Now().Add(30 * 24 * time.Hour).Unix(),
|
||||
}
|
||||
caInfo.Create(caID)
|
||||
}
|
||||
|
||||
UpdateCRL(caInfo, ca, caID)
|
||||
|
||||
rCert, err = GetRevokedCert(caID, cert.SerialNumber)
|
||||
if err != nil {
|
||||
return true, fmt.Errorf("verify: failed to get Cert: %s", err)
|
||||
}
|
||||
if rCert.IsRevoked(false) {
|
||||
return false, fmt.Errorf("verify: certificate is %s", rCert.RevocationStatus(false))
|
||||
}
|
||||
return true, nil
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user