1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
73
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
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
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
109
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
138
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
158
159
160
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
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
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
201 var valid, invalid int
202 scts := conn.ConnectionState().SignedCertificateTimestamps
203 if len(scts) > 0 {
204 merkleLeaf, err := ct.MerkleTreeLeafFromChain(chain, ct.X509LogEntryType, 0 )
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
224
225
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