...

Source file src/github.com/google/certificate-transparency-go/client/ctclient/cmd/get_inclusion_proof.go

Documentation: github.com/google/certificate-transparency-go/client/ctclient/cmd

     1  // Copyright 2022 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  package cmd
    16  
    17  import (
    18  	"context"
    19  	"crypto/sha256"
    20  	"encoding/pem"
    21  	"fmt"
    22  	"os"
    23  	"regexp"
    24  	"strconv"
    25  	"strings"
    26  	"time"
    27  
    28  	ct "github.com/google/certificate-transparency-go"
    29  	"github.com/google/certificate-transparency-go/client"
    30  	"github.com/google/certificate-transparency-go/x509"
    31  	"github.com/spf13/cobra"
    32  	"github.com/transparency-dev/merkle/proof"
    33  	"github.com/transparency-dev/merkle/rfc6962"
    34  	"k8s.io/klog/v2"
    35  )
    36  
    37  var (
    38  	leafHash  string
    39  	certChain string
    40  	timestamp int64
    41  	treeSize  uint64
    42  )
    43  
    44  func init() {
    45  	cmd := cobra.Command{
    46  		Use:     fmt.Sprintf("get-inclusion-proof %s {--leaf_hash=hash | --cert_chain=file} [--timestamp=ts] [--size=N]", connectionFlags),
    47  		Aliases: []string{"getinclusionproof", "inclusion-proof", "inclusion"},
    48  		Short:   "Fetch and verify the inclusion proof for an entry",
    49  		Args:    cobra.MaximumNArgs(0),
    50  		Run: func(cmd *cobra.Command, _ []string) {
    51  			runGetInclusionProof(cmd.Context())
    52  		},
    53  	}
    54  	cmd.Flags().StringVar(&leafHash, "leaf_hash", "", "Leaf hash to retrieve (as hex string or base64)")
    55  	cmd.Flags().StringVar(&certChain, "cert_chain", "", "Name of file containing certificate chain as concatenated PEM files")
    56  	cmd.Flags().Int64Var(&timestamp, "timestamp", 0, "Timestamp to use for inclusion checking")
    57  	cmd.Flags().Uint64Var(&treeSize, "size", 0, "Tree size to query at")
    58  	rootCmd.AddCommand(&cmd)
    59  }
    60  
    61  // runGetInclusionProof runs the get-inclusion-proof command.
    62  func runGetInclusionProof(ctx context.Context) {
    63  	logClient := connect(ctx)
    64  	var hash []byte
    65  	if len(leafHash) > 0 {
    66  		var err error
    67  		hash, err = hashFromString(leafHash)
    68  		if err != nil {
    69  			klog.Exitf("Invalid --leaf_hash supplied: %v", err)
    70  		}
    71  	} else if len(certChain) > 0 {
    72  		// Build a leaf hash from the chain and a timestamp.
    73  		chain, entryTimestamp := chainFromFile(certChain)
    74  		if timestamp != 0 {
    75  			entryTimestamp = timestamp // Use user-specified timestamp.
    76  		}
    77  		if entryTimestamp == 0 {
    78  			klog.Exit("No timestamp available to accompany certificate")
    79  		}
    80  
    81  		var leafEntry *ct.MerkleTreeLeaf
    82  		cert, err := x509.ParseCertificate(chain[0].Data)
    83  		if x509.IsFatal(err) {
    84  			klog.Warningf("Failed to parse leaf certificate: %v", err)
    85  			leafEntry = ct.CreateX509MerkleTreeLeaf(chain[0], uint64(entryTimestamp))
    86  		} else if cert.IsPrecertificate() {
    87  			leafEntry, err = ct.MerkleTreeLeafFromRawChain(chain, ct.PrecertLogEntryType, uint64(entryTimestamp))
    88  			if err != nil {
    89  				klog.Exitf("Failed to build pre-certificate leaf entry: %v", err)
    90  			}
    91  		} else {
    92  			leafEntry = ct.CreateX509MerkleTreeLeaf(chain[0], uint64(entryTimestamp))
    93  		}
    94  
    95  		leafHash, err := ct.LeafHashForLeaf(leafEntry)
    96  		if err != nil {
    97  			klog.Exitf("Failed to create hash of leaf: %v", err)
    98  		}
    99  		hash = leafHash[:]
   100  
   101  		// Print a warning if this timestamp is still within the MMD window.
   102  		when := ct.TimestampToTime(uint64(entryTimestamp))
   103  		if age := time.Since(when); age < logMMD {
   104  			klog.Warningf("WARNING: Timestamp (%v) is with MMD window (%v), log may not have incorporated this entry yet.", when, logMMD)
   105  		}
   106  	}
   107  	if len(hash) != sha256.Size {
   108  		klog.Exit("No leaf hash available")
   109  	}
   110  	getInclusionProofForHash(ctx, logClient, hash)
   111  }
   112  
   113  func getInclusionProofForHash(ctx context.Context, logClient client.CheckLogClient, hash []byte) {
   114  	var sth *ct.SignedTreeHead
   115  	size := treeSize
   116  	if size <= 0 {
   117  		var err error
   118  		sth, err = logClient.GetSTH(ctx)
   119  		if err != nil {
   120  			exitWithDetails(err)
   121  		}
   122  		size = sth.TreeSize
   123  	}
   124  	// Display the inclusion proof.
   125  	rsp, err := logClient.GetProofByHash(ctx, hash, size)
   126  	if err != nil {
   127  		exitWithDetails(err)
   128  	}
   129  	fmt.Printf("Inclusion proof for index %d in tree of size %d:\n", rsp.LeafIndex, size)
   130  	for _, e := range rsp.AuditPath {
   131  		fmt.Printf("  %x\n", e)
   132  	}
   133  	if sth != nil {
   134  		// If we retrieved an STH we can verify the proof.
   135  		if err := proof.VerifyInclusion(rfc6962.DefaultHasher, uint64(rsp.LeafIndex), sth.TreeSize, hash, rsp.AuditPath, sth.SHA256RootHash[:]); err != nil {
   136  			klog.Exitf("Failed to VerifyInclusion(%d, %d)=%v", rsp.LeafIndex, sth.TreeSize, err)
   137  		}
   138  		fmt.Printf("Verified that hash %x + proof = root hash %x\n", hash, sth.SHA256RootHash)
   139  	}
   140  }
   141  
   142  func chainFromFile(filename string) ([]ct.ASN1Cert, int64) {
   143  	contents, err := os.ReadFile(filename)
   144  	if err != nil {
   145  		klog.Exitf("Failed to read certificate file: %v", err)
   146  	}
   147  	rest := contents
   148  	var chain []ct.ASN1Cert
   149  	for {
   150  		var block *pem.Block
   151  		block, rest = pem.Decode(rest)
   152  		if block == nil {
   153  			break
   154  		}
   155  		if block.Type == "CERTIFICATE" {
   156  			chain = append(chain, ct.ASN1Cert{Data: block.Bytes})
   157  		}
   158  	}
   159  	if len(chain) == 0 {
   160  		klog.Exitf("No certificates found in %s", certChain)
   161  	}
   162  
   163  	// Also look for something like a text timestamp for convenience.
   164  	var timestamp int64
   165  	tsRE := regexp.MustCompile(`Timestamp[:=](\d+)`)
   166  	for _, line := range strings.Split(string(contents), "\n") {
   167  		x := tsRE.FindStringSubmatch(line)
   168  		if len(x) > 1 {
   169  			timestamp, err = strconv.ParseInt(x[1], 10, 64)
   170  			if err != nil {
   171  				break
   172  			}
   173  		}
   174  	}
   175  	return chain, timestamp
   176  }
   177  

View as plain text