mirror of
https://github.com/safing/portmaster
synced 2025-09-01 10:09:11 +00:00
254 lines
6.8 KiB
Go
254 lines
6.8 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 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
|
|
}
|