// Copyright 2018 Google LLC. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // sctscan is a utility to scan a CT log and check embedded SCTs (Signed Certificate // Timestamps) in certificates in the log. package main import ( "context" "crypto/sha256" "flag" "net/http" "time" ct "github.com/google/certificate-transparency-go" "github.com/google/certificate-transparency-go/client" "github.com/google/certificate-transparency-go/ctutil" "github.com/google/certificate-transparency-go/jsonclient" "github.com/google/certificate-transparency-go/loglist3" "github.com/google/certificate-transparency-go/scanner" "github.com/google/certificate-transparency-go/x509" "github.com/google/certificate-transparency-go/x509util" "k8s.io/klog/v2" ) var ( logURI = flag.String("log_uri", "https://ct.googleapis.com/pilot", "CT log base URI") logList = flag.String("log_list", loglist3.AllLogListURL, "Location of master CT log list (URL or filename)") inclusion = flag.Bool("inclusion", false, "Whether to do inclusion checking") deadline = flag.Duration("deadline", 30*time.Second, "Timeout deadline for HTTP requests") batchSize = flag.Int("batch_size", 1000, "Max number of entries to request at per call to get-entries") numWorkers = flag.Int("num_workers", 2, "Number of concurrent matchers") parallelFetch = flag.Int("parallel_fetch", 2, "Number of concurrent GetEntries fetches") startIndex = flag.Int64("start_index", 0, "Log index to start scanning at") ) func main() { klog.InitFlags(nil) flag.Parse() ctx := context.Background() klog.CopyStandardLogTo("WARNING") hc := &http.Client{ Timeout: *deadline, Transport: &http.Transport{ TLSHandshakeTimeout: 30 * time.Second, ResponseHeaderTimeout: 30 * time.Second, MaxIdleConnsPerHost: 10, DisableKeepAlives: false, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, ExpectContinueTimeout: 1 * time.Second, }, } logClient, err := client.New(*logURI, hc, jsonclient.Options{UserAgent: "ct-go-sctscan/1.0"}) if err != nil { klog.Exitf("Failed to create log client: %v", err) } llData, err := x509util.ReadFileOrURL(*logList, hc) if err != nil { klog.Exitf("Failed to read log list: %v", err) } ll, err := loglist3.NewFromJSON(llData) if err != nil { klog.Exitf("Failed to parse log list: %v", err) } klog.Warning("Performing validations via direct log queries") logsByHash, err := ctutil.LogInfoByKeyHash(ll, hc) if err != nil { klog.Exitf("Failed to build log info map: %v", err) } scanOpts := scanner.ScannerOptions{ FetcherOptions: scanner.FetcherOptions{ BatchSize: *batchSize, ParallelFetch: *parallelFetch, StartIndex: *startIndex, }, Matcher: EmbeddedSCTMatcher{}, NumWorkers: *numWorkers, } s := scanner.NewScanner(logClient, scanOpts) if err := s.Scan(ctx, func(entry *ct.RawLogEntry) { checkCertWithEmbeddedSCT(ctx, logsByHash, *inclusion, entry) }, func(entry *ct.RawLogEntry) { klog.Errorf("Internal error: found pre-cert! %+v", entry) }); err != nil { klog.Exitf("Scan failed: %v", err) } } // EmbeddedSCTMatcher implements the scanner.Matcher interface by matching just certificates // that have embedded SCTs. type EmbeddedSCTMatcher struct{} // CertificateMatches identifies certificates in the log that have the SCTList extension. func (e EmbeddedSCTMatcher) CertificateMatches(cert *x509.Certificate) bool { return len(cert.SCTList.SCTList) > 0 } // PrecertificateMatches identifies those precertificates in the log that are of // interest: none. func (e EmbeddedSCTMatcher) PrecertificateMatches(*ct.Precertificate) bool { return false } // checkCertWithEmbeddedSCT is the callback that the scanner invokes for each cert found by the matcher. // Here, we only expect to get certificates that have embedded SCT lists. func checkCertWithEmbeddedSCT(ctx context.Context, logsByKey map[[sha256.Size]byte]*ctutil.LogInfo, checkInclusion bool, rawEntry *ct.RawLogEntry) { entry, err := rawEntry.ToLogEntry() if x509.IsFatal(err) { klog.Errorf("[%d] Internal error: failed to parse cert in entry: %v", rawEntry.Index, err) return } leaf := entry.X509Cert if leaf == nil { klog.Errorf("[%d] Internal error: no cert in entry", entry.Index) return } if len(entry.Chain) == 0 { klog.Errorf("[%d] No issuance chain found", entry.Index) return } issuer, err := x509.ParseCertificate(entry.Chain[0].Data) if err != nil { klog.Errorf("[%d] Failed to parse issuer: %v", entry.Index, err) } // Build a Merkle leaf that corresponds to the embedded SCTs. We can use the same // leaf for all of the SCTs, as long as the timestamp field gets updated. merkleLeaf, err := ct.MerkleTreeLeafForEmbeddedSCT([]*x509.Certificate{leaf, issuer}, 0) if err != nil { klog.Errorf("[%d] Failed to build Merkle leaf: %v", entry.Index, err) return } for i, sctData := range leaf.SCTList.SCTList { sct, err := x509util.ExtractSCT(&sctData) if err != nil { klog.Errorf("[%d] Failed to deserialize SCT[%d] data: %v", entry.Index, i, err) continue } logInfo := logsByKey[sct.LogID.KeyID] if logInfo == nil { klog.Infof("[%d] SCT[%d] for unknown logID: %x, cannot validate SCT", entry.Index, i, sct.LogID) continue } if err := logInfo.VerifySCTSignature(*sct, *merkleLeaf); err != nil { klog.Errorf("[%d] Failed to verify SCT[%d] signature from log %q: %v", entry.Index, i, logInfo.Description, err) } else { klog.V(1).Infof("[%d] Verified SCT[%d] against log %q", entry.Index, i, logInfo.Description) } if !checkInclusion { continue } if index, err := logInfo.VerifyInclusionLatest(ctx, *merkleLeaf, sct.Timestamp); err != nil { // Inclusion failure may be OK if the SCT is within the Log's MMD sth := logInfo.LastSTH() if sth != nil { delta := time.Duration(sth.Timestamp-sct.Timestamp) * time.Millisecond if delta < logInfo.MMD { klog.Warningf("[%d] Failed to verify SCT[%d] inclusion proof (%v), but Log's MMD has not passed %d -> %d < %v", entry.Index, i, err, sct.Timestamp, sth.Timestamp, logInfo.MMD) continue } } klog.Errorf("[%d] Failed to verify SCT[%d] inclusion proof: %v", entry.Index, i, err) } else { klog.V(1).Infof("[%d] Checked SCT[%d] inclusion against log %q, at index %d", entry.Index, i, logInfo.Description, index) } } }