mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 18:45:53 +00:00
198c66c6c4
Refs #1673
166 lines
5.7 KiB
Go
166 lines
5.7 KiB
Go
package tlsutil
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/hex"
|
|
"errors"
|
|
"net"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// CertificateTrustStatus is the bounded trust result published for an HTTPS
|
|
// availability observation. It separates an intentionally self-signed leaf
|
|
// from an otherwise untrusted chain so operators can distinguish the two.
|
|
type CertificateTrustStatus string
|
|
|
|
const (
|
|
CertificateTrustTrusted CertificateTrustStatus = "trusted"
|
|
CertificateTrustSelfSigned CertificateTrustStatus = "self-signed"
|
|
CertificateTrustUntrusted CertificateTrustStatus = "untrusted"
|
|
CertificateTrustExpired CertificateTrustStatus = "expired"
|
|
CertificateTrustNotYetValid CertificateTrustStatus = "not-yet-valid"
|
|
)
|
|
|
|
// CertificateObservation is the canonical, secret-free TLS leaf projection
|
|
// shared by local availability checks, remote probe-agent reports and unified
|
|
// resource consumers.
|
|
type CertificateObservation struct {
|
|
Subject string `json:"subject,omitempty"`
|
|
Issuer string `json:"issuer,omitempty"`
|
|
SerialNumber string `json:"serialNumber,omitempty"`
|
|
DNSNames []string `json:"dnsNames,omitempty"`
|
|
ObservedAt time.Time `json:"observedAt"`
|
|
NotBefore time.Time `json:"notBefore"`
|
|
NotAfter time.Time `json:"notAfter"`
|
|
FingerprintSHA256 string `json:"fingerprintSha256"`
|
|
TrustStatus CertificateTrustStatus `json:"trustStatus"`
|
|
ChainValid bool `json:"chainValid"`
|
|
HostnameValid bool `json:"hostnameValid"`
|
|
SelfSigned bool `json:"selfSigned"`
|
|
TrustError string `json:"trustError,omitempty"`
|
|
}
|
|
|
|
// Clone returns an independent observation for registry and report snapshots.
|
|
func (o *CertificateObservation) Clone() *CertificateObservation {
|
|
if o == nil {
|
|
return nil
|
|
}
|
|
out := *o
|
|
out.DNSNames = append([]string(nil), o.DNSNames...)
|
|
return &out
|
|
}
|
|
|
|
// ObservePeerCertificate derives the leaf identity, validity and trust posture
|
|
// from a completed TLS connection. The caller may use an intentionally
|
|
// unverified transport for reachability, while this function performs the
|
|
// explicit trust and hostname checks reported to the operator.
|
|
func ObservePeerCertificate(state *tls.ConnectionState, serverName string, now time.Time) *CertificateObservation {
|
|
if state == nil || len(state.PeerCertificates) == 0 {
|
|
return nil
|
|
}
|
|
leaf := state.PeerCertificates[0]
|
|
if leaf == nil {
|
|
return nil
|
|
}
|
|
if now.IsZero() {
|
|
now = time.Now().UTC()
|
|
} else {
|
|
now = now.UTC()
|
|
}
|
|
serverName = strings.TrimSpace(serverName)
|
|
|
|
fingerprint := sha256.Sum256(leaf.Raw)
|
|
observation := &CertificateObservation{
|
|
Subject: certificateName(leaf.Subject.CommonName, leaf.Subject.String()),
|
|
Issuer: certificateName(leaf.Issuer.CommonName, leaf.Issuer.String()),
|
|
SerialNumber: strings.ToUpper(leaf.SerialNumber.Text(16)),
|
|
DNSNames: append([]string(nil), leaf.DNSNames...),
|
|
ObservedAt: now,
|
|
NotBefore: leaf.NotBefore.UTC(),
|
|
NotAfter: leaf.NotAfter.UTC(),
|
|
FingerprintSHA256: hex.EncodeToString(fingerprint[:]),
|
|
SelfSigned: certificateIsSelfSigned(leaf),
|
|
}
|
|
|
|
hostnameErr := verifyCertificateHostname(leaf, serverName)
|
|
observation.HostnameValid = hostnameErr == nil
|
|
chainErr := verifyCertificateChain(state.PeerCertificates, now)
|
|
observation.ChainValid = chainErr == nil
|
|
|
|
switch {
|
|
case now.Before(leaf.NotBefore):
|
|
observation.TrustStatus = CertificateTrustNotYetValid
|
|
case now.After(leaf.NotAfter):
|
|
observation.TrustStatus = CertificateTrustExpired
|
|
case observation.SelfSigned:
|
|
observation.TrustStatus = CertificateTrustSelfSigned
|
|
case observation.ChainValid && observation.HostnameValid:
|
|
observation.TrustStatus = CertificateTrustTrusted
|
|
default:
|
|
observation.TrustStatus = CertificateTrustUntrusted
|
|
}
|
|
observation.TrustError = certificateTrustError(chainErr, hostnameErr, observation.SelfSigned)
|
|
return observation
|
|
}
|
|
|
|
func certificateName(commonName, distinguishedName string) string {
|
|
if value := strings.TrimSpace(commonName); value != "" {
|
|
return value
|
|
}
|
|
return strings.TrimSpace(distinguishedName)
|
|
}
|
|
|
|
func certificateIsSelfSigned(certificate *x509.Certificate) bool {
|
|
if certificate == nil || certificate.RawSubject == nil || certificate.RawIssuer == nil {
|
|
return false
|
|
}
|
|
if !strings.EqualFold(hex.EncodeToString(certificate.RawSubject), hex.EncodeToString(certificate.RawIssuer)) {
|
|
return false
|
|
}
|
|
return certificate.CheckSignatureFrom(certificate) == nil
|
|
}
|
|
|
|
func verifyCertificateHostname(certificate *x509.Certificate, serverName string) error {
|
|
if certificate == nil {
|
|
return errors.New("certificate is missing")
|
|
}
|
|
if serverName == "" {
|
|
return errors.New("target hostname is missing")
|
|
}
|
|
if ip := net.ParseIP(serverName); ip != nil {
|
|
return certificate.VerifyHostname(ip.String())
|
|
}
|
|
return certificate.VerifyHostname(serverName)
|
|
}
|
|
|
|
func verifyCertificateChain(certificates []*x509.Certificate, now time.Time) error {
|
|
if len(certificates) == 0 || certificates[0] == nil {
|
|
return errors.New("certificate chain is missing")
|
|
}
|
|
intermediates := x509.NewCertPool()
|
|
for _, certificate := range certificates[1:] {
|
|
if certificate != nil {
|
|
intermediates.AddCert(certificate)
|
|
}
|
|
}
|
|
_, err := certificates[0].Verify(x509.VerifyOptions{
|
|
Intermediates: intermediates,
|
|
CurrentTime: now,
|
|
})
|
|
return err
|
|
}
|
|
|
|
func certificateTrustError(chainErr, hostnameErr error, selfSigned bool) string {
|
|
parts := make([]string, 0, 2)
|
|
if chainErr != nil && !selfSigned {
|
|
parts = append(parts, "trust chain: "+chainErr.Error())
|
|
}
|
|
if hostnameErr != nil {
|
|
parts = append(parts, "hostname: "+hostnameErr.Error())
|
|
}
|
|
return strings.Join(parts, ". ")
|
|
}
|