Skip to content

Commit 22cc89f

Browse files
committed
feat: implement CLI tool for fetching certificates from CT log
fixes #47 Build instructions: go build ./cmd/certpicker/
1 parent 28c85b3 commit 22cc89f

File tree

3 files changed

+105
-4
lines changed

3 files changed

+105
-4
lines changed

cmd/certpicker/main.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"flag"
7+
"fmt"
8+
"log"
9+
"net/http"
10+
"strings"
11+
"time"
12+
13+
ct "github.com/google/certificate-transparency-go"
14+
"github.com/google/certificate-transparency-go/client"
15+
"github.com/google/certificate-transparency-go/jsonclient"
16+
"github.com/google/certificate-transparency-go/scanner"
17+
18+
"github.com/d-Rickyy-b/certstream-server-go/internal/certificatetransparency"
19+
"github.com/d-Rickyy-b/certstream-server-go/internal/config"
20+
)
21+
22+
var userAgent = fmt.Sprintf("Certstream v%s (github.com/d-Rickyy-b/certstream-server-go)", config.Version)
23+
24+
func main() {
25+
ctLogFlag := flag.String("log", "", "URL of the CT log - e.g. ct.googleapis.com/logs/eu1/xenon2025h2")
26+
certIDFlag := flag.Int64("cert", 0, "ID of the certificate to fetch from the CT log")
27+
chainFlag := flag.Bool("chain", false, "Include full chain for the certificate")
28+
asDERFlag := flag.Bool("asder", false, "Include DER encoding of the certificate")
29+
flag.Parse()
30+
31+
ctLog := *ctLogFlag
32+
certID := *certIDFlag
33+
34+
if ctLog == "" {
35+
log.Fatalln("CT log URL is required")
36+
}
37+
if !strings.HasPrefix(ctLog, "https://") {
38+
ctLog = "https://" + ctLog
39+
}
40+
41+
// Initialize the http client and json client provided by the ct library
42+
hc := http.Client{Timeout: 30 * time.Second}
43+
jsonClient, e := client.New(ctLog, &hc, jsonclient.Options{UserAgent: userAgent})
44+
if e != nil {
45+
log.Fatalln("Error creating JSON client:", e)
46+
}
47+
48+
// Get entries from CT log
49+
c, _ := context.WithTimeout(context.Background(), 10*time.Second)
50+
entries, getEntriesErr := jsonClient.GetRawEntries(c, certID, certID)
51+
if getEntriesErr != nil {
52+
log.Fatalln("Error getting entries from CT log: ", getEntriesErr)
53+
}
54+
55+
// Loop over entries and pars each one.
56+
for _, leafEntry := range entries.Entries {
57+
rawLogEntry, err := ct.RawLogEntryFromLeaf(certID, &leafEntry)
58+
if err != nil {
59+
log.Fatalln("Error creating raw log entry: ", err)
60+
}
61+
62+
entry, parseErr := certificatetransparency.ParseCertstreamEntry(rawLogEntry, "N/A", "N/A", ctLog)
63+
if parseErr != nil {
64+
log.Fatalln("Error parsing certstream entry: ", parseErr)
65+
}
66+
67+
// Check if the entry is a certificate or precertificate
68+
if logEntry, toLogEntryErr := rawLogEntry.ToLogEntry(); toLogEntryErr != nil {
69+
log.Println("Error converting rawLogEntry to logEntry: ", toLogEntryErr)
70+
} else {
71+
matcher := scanner.MatchAll{}
72+
if logEntry.X509Cert != nil && matcher.CertificateMatches(logEntry.X509Cert) {
73+
entry.Data.UpdateType = "X509LogEntry"
74+
}
75+
if logEntry.Precert != nil && matcher.PrecertificateMatches(logEntry.Precert) {
76+
entry.Data.UpdateType = "PrecertLogEntry"
77+
}
78+
}
79+
80+
// Remove DER encoding and chain if not requested
81+
if !*asDERFlag {
82+
entry.Data.LeafCert.AsDER = ""
83+
for i := range entry.Data.Chain {
84+
entry.Data.Chain[i].AsDER = ""
85+
}
86+
}
87+
88+
// Remove chain if not requested
89+
if !*chainFlag {
90+
entry.Data.Chain = nil
91+
}
92+
93+
// Marshal the certificate entry to JSON and pretty print it
94+
result, marshalErr := json.MarshalIndent(entry, "", " ")
95+
if marshalErr != nil {
96+
return
97+
}
98+
99+
fmt.Println(string(result))
100+
}
101+
}

internal/certificatetransparency/ct-parser.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -401,8 +401,8 @@ func keyUsageToString(k x509.KeyUsage) string {
401401
return buf.String()
402402
}
403403

404-
// parseCertstreamEntry creates an Entry from a ct.RawLogEntry.
405-
func parseCertstreamEntry(rawEntry *ct.RawLogEntry, operatorName, logname, ctURL string) (certstream.Entry, error) {
404+
// ParseCertstreamEntry creates an Entry from a ct.RawLogEntry.
405+
func ParseCertstreamEntry(rawEntry *ct.RawLogEntry, operatorName, logname, ctURL string) (certstream.Entry, error) {
406406
if rawEntry == nil {
407407
return certstream.Entry{}, errors.New("certstream entry is nil")
408408
}

internal/certificatetransparency/ct-watcher.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ func (w *worker) runWorker(ctx context.Context) error {
253253

254254
// foundCertCallback is the callback that handles cases where new regular certs are found.
255255
func (w *worker) foundCertCallback(rawEntry *ct.RawLogEntry) {
256-
entry, parseErr := parseCertstreamEntry(rawEntry, w.operatorName, w.name, w.ctURL)
256+
entry, parseErr := ParseCertstreamEntry(rawEntry, w.operatorName, w.name, w.ctURL)
257257
if parseErr != nil {
258258
log.Println("Error parsing certstream entry: ", parseErr)
259259
return
@@ -267,7 +267,7 @@ func (w *worker) foundCertCallback(rawEntry *ct.RawLogEntry) {
267267

268268
// foundPrecertCallback is the callback that handles cases where new precerts are found.
269269
func (w *worker) foundPrecertCallback(rawEntry *ct.RawLogEntry) {
270-
entry, parseErr := parseCertstreamEntry(rawEntry, w.operatorName, w.name, w.ctURL)
270+
entry, parseErr := ParseCertstreamEntry(rawEntry, w.operatorName, w.name, w.ctURL)
271271
if parseErr != nil {
272272
log.Println("Error parsing certstream entry: ", parseErr)
273273
return

0 commit comments

Comments
 (0)