140 lines
4.9 KiB
Go
140 lines
4.9 KiB
Go
// Copyright (C) 2025 Opsmate, Inc.
|
|
//
|
|
// This Source Code Form is subject to the terms of the Mozilla
|
|
// Public License, v. 2.0. If a copy of the MPL was not distributed
|
|
// with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
//
|
|
// This software is distributed WITHOUT A WARRANTY OF ANY KIND.
|
|
// See the Mozilla Public License for details.
|
|
|
|
package monitor
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"software.sslmate.com/src/certspotter"
|
|
"software.sslmate.com/src/certspotter/ctclient"
|
|
"software.sslmate.com/src/certspotter/cttypes"
|
|
"software.sslmate.com/src/certspotter/loglist"
|
|
)
|
|
|
|
type LogEntry struct {
|
|
ctclient.Entry
|
|
Index uint64
|
|
Log *loglist.Log
|
|
}
|
|
|
|
func processLogEntry(ctx context.Context, config *Config, issuerGetter ctclient.IssuerGetter, entry *LogEntry) error {
|
|
leaf, err := cttypes.ParseLeafInput(entry.LeafInput())
|
|
if err != nil {
|
|
return processMalformedLogEntry(ctx, config, entry, fmt.Errorf("error parsing Merkle Tree Leaf: %w", err))
|
|
}
|
|
switch leaf.TimestampedEntry.EntryType {
|
|
case cttypes.X509EntryType:
|
|
return processX509LogEntry(ctx, config, issuerGetter, entry, leaf.TimestampedEntry.SignedEntryASN1Cert)
|
|
case cttypes.PrecertEntryType:
|
|
return processPrecertLogEntry(ctx, config, issuerGetter, entry, leaf.TimestampedEntry.SignedEntryPreCert)
|
|
default:
|
|
return processMalformedLogEntry(ctx, config, entry, fmt.Errorf("unknown log entry type %d", leaf.TimestampedEntry.EntryType))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func processX509LogEntry(ctx context.Context, config *Config, issuerGetter ctclient.IssuerGetter, entry *LogEntry, cert *cttypes.ASN1Cert) error {
|
|
certInfo, err := certspotter.MakeCertInfoFromRawCert(*cert)
|
|
if err != nil {
|
|
return processMalformedLogEntry(ctx, config, entry, fmt.Errorf("error parsing X.509 certificate: %w", err))
|
|
}
|
|
if precertTBS, err := certspotter.ReconstructPrecertTBS(certInfo.TBS); err == nil {
|
|
certInfo.TBS = precertTBS
|
|
} else {
|
|
return processMalformedLogEntry(ctx, config, entry, fmt.Errorf("error reconstructing precertificate TBSCertificate: %w", err))
|
|
}
|
|
|
|
getChain := func(ctx context.Context) ([]cttypes.ASN1Cert, error) {
|
|
var (
|
|
chain = []cttypes.ASN1Cert{*cert}
|
|
errs = []error{}
|
|
)
|
|
if issuers, err := entry.GetChain(ctx, issuerGetter); err == nil {
|
|
chain = append(chain, issuers...)
|
|
} else {
|
|
errs = append(errs, err)
|
|
}
|
|
return chain, errors.Join(errs...)
|
|
}
|
|
return processCertificate(ctx, config, entry, certInfo, getChain)
|
|
}
|
|
|
|
func processPrecertLogEntry(ctx context.Context, config *Config, issuerGetter ctclient.IssuerGetter, entry *LogEntry, precert *cttypes.PreCert) error {
|
|
certInfo, err := certspotter.MakeCertInfoFromRawTBS(precert.TBSCertificate)
|
|
if err != nil {
|
|
return processMalformedLogEntry(ctx, config, entry, fmt.Errorf("error parsing precert TBSCertificate: %w", err))
|
|
}
|
|
precertBytes, err := entry.Precertificate()
|
|
if err != nil {
|
|
return processMalformedLogEntry(ctx, config, entry, fmt.Errorf("error getting precert entry's precertificate: %w", err))
|
|
}
|
|
|
|
getChain := func(ctx context.Context) ([]cttypes.ASN1Cert, error) {
|
|
var (
|
|
chain = []cttypes.ASN1Cert{precertBytes}
|
|
errs = []error{}
|
|
)
|
|
if issuers, err := entry.GetChain(ctx, issuerGetter); err == nil {
|
|
chain = append(chain, issuers...)
|
|
} else {
|
|
errs = append(errs, err)
|
|
}
|
|
if _, err := certspotter.ValidatePrecert(precertBytes, precert.TBSCertificate); err != nil {
|
|
errs = append(errs, fmt.Errorf("precertificate in extra_data does not match TBSCertificate in leaf_input: %w", err))
|
|
}
|
|
return chain, errors.Join(errs...)
|
|
}
|
|
return processCertificate(ctx, config, entry, certInfo, getChain)
|
|
}
|
|
|
|
func processCertificate(ctx context.Context, config *Config, entry *LogEntry, certInfo *certspotter.CertInfo, getChain func(context.Context) ([]cttypes.ASN1Cert, error)) error {
|
|
identifiers, err := certInfo.ParseIdentifiers()
|
|
if err != nil {
|
|
return processMalformedLogEntry(ctx, config, entry, err)
|
|
}
|
|
matched, watchItem := config.WatchList.Matches(identifiers)
|
|
if !matched {
|
|
return nil
|
|
}
|
|
|
|
chain, chainErr := getChain(ctx)
|
|
if errors.Is(chainErr, context.Canceled) {
|
|
return chainErr
|
|
}
|
|
|
|
cert := &DiscoveredCert{
|
|
WatchItem: watchItem,
|
|
LogEntry: entry,
|
|
Info: certInfo,
|
|
Chain: chain,
|
|
ChainError: chainErr,
|
|
TBSSHA256: sha256.Sum256(certInfo.TBS.Raw),
|
|
SHA256: sha256.Sum256(chain[0]),
|
|
PubkeySHA256: sha256.Sum256(certInfo.TBS.PublicKey.FullBytes),
|
|
Identifiers: identifiers,
|
|
}
|
|
|
|
if err := config.State.NotifyCert(ctx, cert); err != nil {
|
|
return fmt.Errorf("error notifying about certificate %x: %w", cert.SHA256, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func processMalformedLogEntry(ctx context.Context, config *Config, entry *LogEntry, parseError error) error {
|
|
if err := config.State.NotifyMalformedEntry(ctx, entry, parseError); err != nil {
|
|
return fmt.Errorf("error notifying about malformed log entry %d in %s (%q): %w", entry.Index, entry.Log.GetMonitoringURL(), parseError, err)
|
|
}
|
|
return nil
|
|
}
|