mirror of
https://github.com/safing/portmaster
synced 2025-09-01 10:09:11 +00:00
264 lines
7.3 KiB
Go
264 lines
7.3 KiB
Go
package verify
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"math/big"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/cloudflare/cfssl/crypto/pkcs7"
|
|
|
|
"github.com/Safing/portbase/crypto/hash"
|
|
"github.com/Safing/portbase/database"
|
|
"github.com/Safing/portbase/database/record"
|
|
)
|
|
|
|
// Cert saves a certificate.
|
|
type Cert struct {
|
|
record.Record
|
|
|
|
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 string, 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 string, 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
|
|
|
|
}
|