From 22cc89fc7ea2994d4d2717e5dcc5ad17a444fee7 Mon Sep 17 00:00:00 2001 From: Rico Date: Sat, 11 Jan 2025 04:09:03 +0100 Subject: [PATCH] feat: implement CLI tool for fetching certificates from CT log fixes #47 Build instructions: go build ./cmd/certpicker/ --- cmd/certpicker/main.go | 101 ++++++++++++++++++ internal/certificatetransparency/ct-parser.go | 4 +- .../certificatetransparency/ct-watcher.go | 4 +- 3 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 cmd/certpicker/main.go diff --git a/cmd/certpicker/main.go b/cmd/certpicker/main.go new file mode 100644 index 0000000..e4e15c9 --- /dev/null +++ b/cmd/certpicker/main.go @@ -0,0 +1,101 @@ +package main + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "log" + "net/http" + "strings" + "time" + + ct "github.com/google/certificate-transparency-go" + "github.com/google/certificate-transparency-go/client" + "github.com/google/certificate-transparency-go/jsonclient" + "github.com/google/certificate-transparency-go/scanner" + + "github.com/d-Rickyy-b/certstream-server-go/internal/certificatetransparency" + "github.com/d-Rickyy-b/certstream-server-go/internal/config" +) + +var userAgent = fmt.Sprintf("Certstream v%s (github.com/d-Rickyy-b/certstream-server-go)", config.Version) + +func main() { + ctLogFlag := flag.String("log", "", "URL of the CT log - e.g. ct.googleapis.com/logs/eu1/xenon2025h2") + certIDFlag := flag.Int64("cert", 0, "ID of the certificate to fetch from the CT log") + chainFlag := flag.Bool("chain", false, "Include full chain for the certificate") + asDERFlag := flag.Bool("asder", false, "Include DER encoding of the certificate") + flag.Parse() + + ctLog := *ctLogFlag + certID := *certIDFlag + + if ctLog == "" { + log.Fatalln("CT log URL is required") + } + if !strings.HasPrefix(ctLog, "https://") { + ctLog = "https://" + ctLog + } + + // Initialize the http client and json client provided by the ct library + hc := http.Client{Timeout: 30 * time.Second} + jsonClient, e := client.New(ctLog, &hc, jsonclient.Options{UserAgent: userAgent}) + if e != nil { + log.Fatalln("Error creating JSON client:", e) + } + + // Get entries from CT log + c, _ := context.WithTimeout(context.Background(), 10*time.Second) + entries, getEntriesErr := jsonClient.GetRawEntries(c, certID, certID) + if getEntriesErr != nil { + log.Fatalln("Error getting entries from CT log: ", getEntriesErr) + } + + // Loop over entries and pars each one. + for _, leafEntry := range entries.Entries { + rawLogEntry, err := ct.RawLogEntryFromLeaf(certID, &leafEntry) + if err != nil { + log.Fatalln("Error creating raw log entry: ", err) + } + + entry, parseErr := certificatetransparency.ParseCertstreamEntry(rawLogEntry, "N/A", "N/A", ctLog) + if parseErr != nil { + log.Fatalln("Error parsing certstream entry: ", parseErr) + } + + // Check if the entry is a certificate or precertificate + if logEntry, toLogEntryErr := rawLogEntry.ToLogEntry(); toLogEntryErr != nil { + log.Println("Error converting rawLogEntry to logEntry: ", toLogEntryErr) + } else { + matcher := scanner.MatchAll{} + if logEntry.X509Cert != nil && matcher.CertificateMatches(logEntry.X509Cert) { + entry.Data.UpdateType = "X509LogEntry" + } + if logEntry.Precert != nil && matcher.PrecertificateMatches(logEntry.Precert) { + entry.Data.UpdateType = "PrecertLogEntry" + } + } + + // Remove DER encoding and chain if not requested + if !*asDERFlag { + entry.Data.LeafCert.AsDER = "" + for i := range entry.Data.Chain { + entry.Data.Chain[i].AsDER = "" + } + } + + // Remove chain if not requested + if !*chainFlag { + entry.Data.Chain = nil + } + + // Marshal the certificate entry to JSON and pretty print it + result, marshalErr := json.MarshalIndent(entry, "", " ") + if marshalErr != nil { + return + } + + fmt.Println(string(result)) + } +} diff --git a/internal/certificatetransparency/ct-parser.go b/internal/certificatetransparency/ct-parser.go index d516109..7a2aa8f 100644 --- a/internal/certificatetransparency/ct-parser.go +++ b/internal/certificatetransparency/ct-parser.go @@ -401,8 +401,8 @@ func keyUsageToString(k x509.KeyUsage) string { return buf.String() } -// parseCertstreamEntry creates an Entry from a ct.RawLogEntry. -func parseCertstreamEntry(rawEntry *ct.RawLogEntry, operatorName, logname, ctURL string) (certstream.Entry, error) { +// ParseCertstreamEntry creates an Entry from a ct.RawLogEntry. +func ParseCertstreamEntry(rawEntry *ct.RawLogEntry, operatorName, logname, ctURL string) (certstream.Entry, error) { if rawEntry == nil { return certstream.Entry{}, errors.New("certstream entry is nil") } diff --git a/internal/certificatetransparency/ct-watcher.go b/internal/certificatetransparency/ct-watcher.go index 1ac49f4..6af73b3 100644 --- a/internal/certificatetransparency/ct-watcher.go +++ b/internal/certificatetransparency/ct-watcher.go @@ -253,7 +253,7 @@ func (w *worker) runWorker(ctx context.Context) error { // foundCertCallback is the callback that handles cases where new regular certs are found. func (w *worker) foundCertCallback(rawEntry *ct.RawLogEntry) { - entry, parseErr := parseCertstreamEntry(rawEntry, w.operatorName, w.name, w.ctURL) + entry, parseErr := ParseCertstreamEntry(rawEntry, w.operatorName, w.name, w.ctURL) if parseErr != nil { log.Println("Error parsing certstream entry: ", parseErr) return @@ -267,7 +267,7 @@ func (w *worker) foundCertCallback(rawEntry *ct.RawLogEntry) { // foundPrecertCallback is the callback that handles cases where new precerts are found. func (w *worker) foundPrecertCallback(rawEntry *ct.RawLogEntry) { - entry, parseErr := parseCertstreamEntry(rawEntry, w.operatorName, w.name, w.ctURL) + entry, parseErr := ParseCertstreamEntry(rawEntry, w.operatorName, w.name, w.ctURL) if parseErr != nil { log.Println("Error parsing certstream entry: ", parseErr) return