Skip to content

Commit

Permalink
Refactor CT log monitor to support monitoring certificate identities (#…
Browse files Browse the repository at this point in the history
…534)

* add functionality for scanning OID matchers in a CT log entry

Signed-off-by: linus-sun <[email protected]>

* refactor extension functions to support x509 and google_x509

Signed-off-by: linus-sun <[email protected]>

* refactor extension functions to support x509 and google_x509

Signed-off-by: linus-sun <[email protected]>

* extend certMatchesPolicy to support google_x509

Signed-off-by: linus-sun <[email protected]>

---------

Signed-off-by: linus-sun <[email protected]>
  • Loading branch information
linus-sun authored Nov 20, 2024
1 parent 6c2f5c8 commit 4a90a10
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 23 deletions.
18 changes: 7 additions & 11 deletions pkg/ct/monitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ package ct
import (
"context"
"fmt"
"regexp"

ct "github.com/google/certificate-transparency-go"
ctclient "github.com/google/certificate-transparency-go/client"
Expand All @@ -33,23 +32,20 @@ func GetCTLogEntries(logClient *ctclient.LogClient, startIndex int, endIndex int
return entries, nil
}

func ScanEntrySubject(logEntry ct.LogEntry, monitoredSubjects []string) ([]*identity.LogEntry, error) {
subject := logEntry.X509Cert.Subject.String()
func ScanEntryCertSubject(logEntry ct.LogEntry, monitoredCertIDs []identity.CertificateIdentity) ([]*identity.LogEntry, error) {
matchedEntries := []*identity.LogEntry{}
for _, monitoredSub := range monitoredSubjects {
regex, err := regexp.Compile(monitoredSub)
for _, monitoredCertID := range monitoredCertIDs {
match, sub, iss, err := identity.CertMatchesPolicy(logEntry.X509Cert, monitoredCertID.CertSubject, monitoredCertID.Issuers)
if err != nil {
return nil, fmt.Errorf("error compiling regex: %v", err)
}
matches := regex.FindAllString(subject, -1)
for _, match := range matches {
return nil, fmt.Errorf("error with policy matching at index %d: %w", logEntry.Index, err)
} else if match {
matchedEntries = append(matchedEntries, &identity.LogEntry{
CertSubject: sub,
Issuer: iss,
Index: logEntry.Index,
CertSubject: match,
})
}
}

return matchedEntries, nil
}

Expand Down
36 changes: 26 additions & 10 deletions pkg/ct/monitor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,14 @@ import (

const (
subjectName = "test-subject"
issuerName = "test-issuer"
organizationName = "test-org"
)

func TestScanEntrySubject(t *testing.T) {
func TestScanEntryCertSubject(t *testing.T) {
testCases := map[string]struct {
inputEntry ct.LogEntry
inputSubjects []string
inputSubjects []identity.CertificateIdentity
expected []*identity.LogEntry
}{
"no matching subject": {
Expand All @@ -48,31 +49,46 @@ func TestScanEntrySubject(t *testing.T) {
},
},
},
inputSubjects: []string{},
inputSubjects: []identity.CertificateIdentity{},
expected: []*identity.LogEntry{},
},
"matching subject": {
inputEntry: ct.LogEntry{
Index: 1,
X509Cert: &x509.Certificate{
Subject: pkix.Name{
CommonName: subjectName,
Organization: []string{organizationName},
DNSNames: []string{subjectName},
EmailAddresses: []string{organizationName},
Extensions: []pkix.Extension{
{
Id: google_asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1},
Value: []byte(issuerName),
},
},
},
},
inputSubjects: []string{subjectName, organizationName},
inputSubjects: []identity.CertificateIdentity{
{
CertSubject: subjectName,
Issuers: []string{issuerName},
},
{
CertSubject: organizationName,
Issuers: []string{},
},
},
expected: []*identity.LogEntry{
{Index: 1,
CertSubject: subjectName},
CertSubject: subjectName,
Issuer: issuerName},
{Index: 1,
CertSubject: organizationName},
CertSubject: organizationName,
Issuer: issuerName},
},
},
}

for _, tc := range testCases {
logEntries, err := ScanEntrySubject(tc.inputEntry, tc.inputSubjects)
logEntries, err := ScanEntryCertSubject(tc.inputEntry, tc.inputSubjects)
if err != nil {
t.Errorf("received error scanning entry for subjects: %v", err)
}
Expand Down
54 changes: 52 additions & 2 deletions pkg/identity/identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package identity

import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/json"
"errors"
Expand Down Expand Up @@ -274,11 +275,60 @@ func OIDMatchesPolicy[Certificate *x509.Certificate | *google_x509.Certificate](
return false, nil, "", nil
}

// getSubjectAlternateNames extracts all subject alternative names from
// the certificate, including email addresses, DNS, IP addresses, URIs, and OtherName SANs
// duplicate of cryptoutils function GetSubjectAlternateNames to match in case of google_x509 fork certificate
func getSubjectAlternateNames[Certificate *x509.Certificate | *google_x509.Certificate](certificate Certificate) []string {
sans := []string{}
switch cert := any(certificate).(type) {
case *x509.Certificate:
sans = append(sans, cert.DNSNames...)
sans = append(sans, cert.EmailAddresses...)
for _, ip := range cert.IPAddresses {
sans = append(sans, ip.String())
}
for _, uri := range cert.URIs {
sans = append(sans, uri.String())
}
// ignore error if there's no OtherName SAN
otherName, _ := cryptoutils.UnmarshalOtherNameSAN(cert.Extensions)
if len(otherName) > 0 {
sans = append(sans, otherName)
}
return sans
case *google_x509.Certificate:
sans = append(sans, cert.DNSNames...)
sans = append(sans, cert.EmailAddresses...)
for _, ip := range cert.IPAddresses {
sans = append(sans, ip.String())
}
for _, uri := range cert.URIs {
sans = append(sans, uri.String())
}
// ignore error if there's no OtherName SAN
pkixExts := []pkix.Extension{}
for _, googleExt := range cert.Extensions {
pkixExt := pkix.Extension{
Id: (asn1.ObjectIdentifier)(googleExt.Id),
Critical: googleExt.Critical,
Value: googleExt.Value,
}
pkixExts = append(pkixExts, pkixExt)
}
otherName, _ := cryptoutils.UnmarshalOtherNameSAN(pkixExts)
if len(otherName) > 0 {
sans = append(sans, otherName)
}
return sans
}
return sans
}

// CertMatchesPolicy returns true if a certificate contains a given subject and optionally a given issuer
// expectedSub and expectedIssuers can be regular expressions
// CertMatchesPolicy also returns the matched subject and issuer on success
func CertMatchesPolicy(cert *x509.Certificate, expectedSub string, expectedIssuers []string) (bool, string, string, error) {
sans := cryptoutils.GetSubjectAlternateNames(cert)
func CertMatchesPolicy[Certificate *x509.Certificate | *google_x509.Certificate](cert Certificate, expectedSub string, expectedIssuers []string) (bool, string, string, error) {
sans := getSubjectAlternateNames(cert)
var issuer string
var err error
issuer, err = getExtension(cert, certExtensionOIDCIssuerV2)
Expand Down
21 changes: 21 additions & 0 deletions pkg/identity/identity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -447,3 +447,24 @@ func TestCertMatches(t *testing.T) {
t.Errorf("expected subject %s and issuer %s, received subject %s and issuer %s", emailAddr, issuer, receivedSub, receivedIssuer)
}
}

func TestGoogleCertMatches(t *testing.T) {
emailAddr := "[email protected]"
issuer := "test-issuer"
cert := &google_x509.Certificate{
EmailAddresses: []string{emailAddr},
Extensions: []google_pkix.Extension{
{
Id: (google_asn1.ObjectIdentifier)(certExtensionOIDCIssuer),
Value: []byte(issuer),
},
},
}
matches, receivedSub, receivedIssuer, err := CertMatchesPolicy(cert, emailAddr, []string{issuer})
if !matches || err != nil {
t.Errorf("Expected true without error, got %v, error %v", matches, err)
}
if receivedSub != emailAddr || receivedIssuer != issuer {
t.Errorf("expected subject %s and issuer %s, received subject %s and issuer %s", emailAddr, issuer, receivedSub, receivedIssuer)
}
}

0 comments on commit 4a90a10

Please sign in to comment.