1
2
3
4
5
6
7
8
9
10
11
12
13
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(×tamp, "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
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
73 chain, entryTimestamp := chainFromFile(certChain)
74 if timestamp != 0 {
75 entryTimestamp = 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
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
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
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
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