...

Source file src/github.com/google/certificate-transparency-go/ctutil/sctcheck/sctcheck.go

Documentation: github.com/google/certificate-transparency-go/ctutil/sctcheck

     1  // Copyright 2018 Google LLC. All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // sctcheck is a utility to show and check embedded SCTs (Signed Certificate
    16  // Timestamps) in certificates.
    17  package main
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"crypto/tls"
    23  	"errors"
    24  	"flag"
    25  	"fmt"
    26  	"net"
    27  	"net/http"
    28  	"net/url"
    29  	"os"
    30  	"strings"
    31  	"time"
    32  
    33  	"github.com/google/certificate-transparency-go/ctutil"
    34  	"github.com/google/certificate-transparency-go/loglist3"
    35  	"github.com/google/certificate-transparency-go/x509"
    36  	"github.com/google/certificate-transparency-go/x509util"
    37  	"k8s.io/klog/v2"
    38  
    39  	ct "github.com/google/certificate-transparency-go"
    40  )
    41  
    42  var (
    43  	logList        = flag.String("log_list", loglist3.AllLogListURL, "Location of master CT log list (URL or filename)")
    44  	deadline       = flag.Duration("deadline", 30*time.Second, "Timeout deadline for HTTP requests")
    45  	checkInclusion = flag.Bool("check_inclusion", true, "Whether to check SCT inclusion in issuing CT log")
    46  )
    47  
    48  type logInfoFactory func(*loglist3.Log, *http.Client) (*ctutil.LogInfo, error)
    49  
    50  func main() {
    51  	klog.InitFlags(nil)
    52  	flag.Parse()
    53  	ctx := context.Background()
    54  	hc := &http.Client{Timeout: *deadline}
    55  
    56  	llData, err := x509util.ReadFileOrURL(*logList, hc)
    57  	if err != nil {
    58  		klog.Exitf("Failed to read log list: %v", err)
    59  	}
    60  	ll, err := loglist3.NewFromJSON(llData)
    61  	if err != nil {
    62  		klog.Exitf("Failed to parse log list: %v", err)
    63  	}
    64  
    65  	lf := ctutil.NewLogInfo
    66  
    67  	totalInvalid := 0
    68  	for _, arg := range flag.Args() {
    69  		var chain []*x509.Certificate
    70  		var valid, invalid int
    71  		if strings.HasPrefix(arg, "https://") {
    72  			// Get chain served online for TLS connection to site, and check any SCTs
    73  			// provided alongside on the connection along the way.
    74  			chain, valid, invalid, err = getAndCheckSiteChain(ctx, lf, arg, ll, hc)
    75  			if err != nil {
    76  				klog.Errorf("%s: failed to get cert chain: %v", arg, err)
    77  				continue
    78  			}
    79  			klog.Errorf("Found %d external SCTs for %q, of which %d were validated", valid+invalid, arg, valid)
    80  			totalInvalid += invalid
    81  		} else {
    82  			// Treat the argument as a certificate file to load.
    83  			data, err := os.ReadFile(arg)
    84  			if err != nil {
    85  				klog.Errorf("%s: failed to read data: %v", arg, err)
    86  				continue
    87  			}
    88  			chain, err = x509util.CertificatesFromPEM(data)
    89  			if err != nil {
    90  				klog.Errorf("%s: failed to read cert data: %v", arg, err)
    91  				continue
    92  			}
    93  		}
    94  		if len(chain) == 0 {
    95  			klog.Errorf("%s: no certificates found", arg)
    96  			continue
    97  		}
    98  		// Check the chain for embedded SCTs.
    99  		valid, invalid = checkChain(ctx, lf, chain, ll, hc)
   100  		klog.Errorf("Found %d embedded SCTs for %q, of which %d were validated", valid+invalid, arg, valid)
   101  		totalInvalid += invalid
   102  	}
   103  	if totalInvalid > 0 {
   104  		os.Exit(1)
   105  	}
   106  }
   107  
   108  // checkChain iterates over any embedded SCTs in the leaf certificate of the chain
   109  // and checks those SCTs.  Returns the counts of valid and invalid embedded SCTs found.
   110  func checkChain(ctx context.Context, lf logInfoFactory, chain []*x509.Certificate, ll *loglist3.LogList, hc *http.Client) (int, int) {
   111  	leaf := chain[0]
   112  	if len(leaf.SCTList.SCTList) == 0 {
   113  		return 0, 0
   114  	}
   115  
   116  	var issuer *x509.Certificate
   117  	for i := 1; i < len(chain); i++ {
   118  		c := chain[i]
   119  		if bytes.Equal(c.RawSubject, leaf.RawIssuer) && c.CheckSignature(leaf.SignatureAlgorithm, leaf.RawTBSCertificate, leaf.Signature) == nil {
   120  			issuer = c
   121  			if i > 1 {
   122  				klog.Warningf("Certificate chain out of order; issuer cert found at index %d", i)
   123  			}
   124  			break
   125  		}
   126  	}
   127  
   128  	if issuer == nil {
   129  		klog.Info("No issuer in chain; attempting online retrieval")
   130  		var err error
   131  		issuer, err = x509util.GetIssuer(leaf, hc)
   132  		if err != nil {
   133  			klog.Errorf("Failed to get issuer online: %v", err)
   134  		}
   135  	}
   136  
   137  	// Build a Merkle leaf that corresponds to the embedded SCTs.  We can use the same
   138  	// leaf for all of the SCTs, as long as the timestamp field gets updated.
   139  	merkleLeaf, err := ct.MerkleTreeLeafForEmbeddedSCT([]*x509.Certificate{leaf, issuer}, 0)
   140  	if err != nil {
   141  		klog.Errorf("Failed to build Merkle leaf: %v", err)
   142  		return 0, len(leaf.SCTList.SCTList)
   143  	}
   144  
   145  	var valid, invalid int
   146  	for i, sctData := range leaf.SCTList.SCTList {
   147  		subject := fmt.Sprintf("embedded SCT[%d]", i)
   148  		if checkSCT(ctx, lf, subject, merkleLeaf, &sctData, ll, hc) {
   149  			valid++
   150  		} else {
   151  			invalid++
   152  		}
   153  	}
   154  	return valid, invalid
   155  }
   156  
   157  // getAndCheckSiteChain retrieves and returns the chain of certificates presented
   158  // for an HTTPS site.  Along the way it checks any external SCTs that are served
   159  // up on the connection alongside the chain.  Returns the chain and counts of
   160  // valid and invalid external SCTs found.
   161  func getAndCheckSiteChain(ctx context.Context, lf logInfoFactory, target string, ll *loglist3.LogList, hc *http.Client) ([]*x509.Certificate, int, int, error) {
   162  	u, err := url.Parse(target)
   163  	if err != nil {
   164  		return nil, 0, 0, fmt.Errorf("failed to parse URL: %v", err)
   165  	}
   166  	if u.Scheme != "https" {
   167  		return nil, 0, 0, errors.New("non-https URL provided")
   168  	}
   169  	host := u.Host
   170  	if !strings.Contains(host, ":") {
   171  		host += ":443"
   172  	}
   173  
   174  	klog.Infof("Retrieve certificate chain from TLS connection to %q", host)
   175  	dialer := net.Dialer{Timeout: hc.Timeout}
   176  	// Insecure TLS connection here so we can always proceed.
   177  	conn, err := tls.DialWithDialer(&dialer, "tcp", host, &tls.Config{InsecureSkipVerify: true})
   178  	if err != nil {
   179  		return nil, 0, 0, fmt.Errorf("failed to dial %q: %v", host, err)
   180  	}
   181  	defer func() {
   182  		if err := conn.Close(); err != nil {
   183  			klog.Errorf("conn.Close()=%q", err)
   184  		}
   185  	}()
   186  
   187  	goChain := conn.ConnectionState().PeerCertificates
   188  	klog.Infof("Found chain of length %d", len(goChain))
   189  
   190  	// Convert base crypto/x509.Certificates to our forked x509.Certificate type.
   191  	chain := make([]*x509.Certificate, len(goChain))
   192  	for i, goCert := range goChain {
   193  		cert, err := x509.ParseCertificate(goCert.Raw)
   194  		if err != nil {
   195  			return nil, 0, 0, fmt.Errorf("failed to convert Go Certificate [%d]: %v", i, err)
   196  		}
   197  		chain[i] = cert
   198  	}
   199  
   200  	// Check externally-provided SCTs.
   201  	var valid, invalid int
   202  	scts := conn.ConnectionState().SignedCertificateTimestamps
   203  	if len(scts) > 0 {
   204  		merkleLeaf, err := ct.MerkleTreeLeafFromChain(chain, ct.X509LogEntryType, 0 /* timestamp added later */)
   205  		if err != nil {
   206  			klog.Errorf("Failed to build Merkle tree leaf: %v", err)
   207  			return chain, 0, len(scts), nil
   208  		}
   209  		for i, sctData := range scts {
   210  			subject := fmt.Sprintf("external SCT[%d]", i)
   211  			if checkSCT(ctx, lf, subject, merkleLeaf, &x509.SerializedSCT{Val: sctData}, ll, hc) {
   212  				valid++
   213  			} else {
   214  				invalid++
   215  			}
   216  
   217  		}
   218  	}
   219  
   220  	return chain, valid, invalid, nil
   221  }
   222  
   223  // checkSCT performs checks on an SCT and Merkle tree leaf, performing both
   224  // signature validation and online log inclusion checking.  Returns whether
   225  // the SCT is valid.
   226  func checkSCT(ctx context.Context, liFactory logInfoFactory, subject string, merkleLeaf *ct.MerkleTreeLeaf, sctData *x509.SerializedSCT, ll *loglist3.LogList, hc *http.Client) bool {
   227  	sct, err := x509util.ExtractSCT(sctData)
   228  	if err != nil {
   229  		klog.Errorf("Failed to deserialize %s data: %v", subject, err)
   230  		klog.Errorf("Data: %x", sctData.Val)
   231  		return false
   232  	}
   233  	klog.Infof("Examine %s with timestamp: %d (%v) from logID: %x", subject, sct.Timestamp, ct.TimestampToTime(sct.Timestamp), sct.LogID.KeyID[:])
   234  	log := ll.FindLogByKeyHash(sct.LogID.KeyID)
   235  	if log == nil {
   236  		klog.Warningf("Unknown logID: %x, cannot validate %s", sct.LogID, subject)
   237  		return false
   238  	}
   239  	logInfo, err := liFactory(log, hc)
   240  	if err != nil {
   241  		klog.Errorf("Failed to build log info for %q log: %v", log.Description, err)
   242  		return false
   243  	}
   244  
   245  	result := true
   246  	klog.Infof("Validate %s against log %q...", subject, logInfo.Description)
   247  	if err := logInfo.VerifySCTSignature(*sct, *merkleLeaf); err != nil {
   248  		klog.Errorf("Failed to verify %s signature from log %q: %v", subject, log.Description, err)
   249  		result = false
   250  	} else {
   251  		klog.Infof("Validate %s against log %q... validated", subject, log.Description)
   252  	}
   253  
   254  	if *checkInclusion {
   255  		klog.Infof("Check %s inclusion against log %q...", subject, log.Description)
   256  		index, err := logInfo.VerifyInclusion(ctx, *merkleLeaf, sct.Timestamp)
   257  		if err != nil {
   258  			age := time.Since(ct.TimestampToTime(sct.Timestamp))
   259  			if age < logInfo.MMD {
   260  				klog.Warningf("Failed to verify inclusion proof (%v) but %s timestamp is only %v old, less than log's MMD of %d seconds", err, subject, age, log.MMD)
   261  			} else {
   262  				klog.Errorf("Failed to verify inclusion proof for %s: %v", subject, err)
   263  			}
   264  			return false
   265  		}
   266  		klog.Infof("Check %s inclusion against log %q... included at %d", subject, log.Description, index)
   267  	}
   268  	return result
   269  }
   270  

View as plain text