1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package main
17
18 import (
19 "bytes"
20 "compress/gzip"
21 "context"
22 "crypto/tls"
23 "encoding/base64"
24 "flag"
25 "fmt"
26 "io"
27 "net/http"
28 "os"
29 "strings"
30 "sync"
31 "time"
32
33 "github.com/google/certificate-transparency-go/client"
34 "github.com/google/certificate-transparency-go/jsonclient"
35 "github.com/google/certificate-transparency-go/loglist3"
36 "github.com/google/certificate-transparency-go/trillian/ctfe"
37 "github.com/google/certificate-transparency-go/trillian/ctfe/configpb"
38 "github.com/google/certificate-transparency-go/trillian/integration"
39 "github.com/google/certificate-transparency-go/x509util"
40 "github.com/google/trillian/monitoring"
41 "github.com/google/trillian/monitoring/prometheus"
42 "github.com/prometheus/client_golang/prometheus/promhttp"
43 "golang.org/x/time/rate"
44 "k8s.io/klog/v2"
45 )
46
47 var (
48 banner = flag.Bool("banner", true, "Display intro")
49 httpServers = flag.String("ct_http_servers", "localhost:8092", "Comma-separated list of (assumed interchangeable) servers, each as address:port")
50
51
52 testDir = flag.String("testdata_dir", "testdata", "Name of directory with test data")
53 leafNotAfter = flag.String("leaf_not_after", "", "Not-After date to use for leaf certs, RFC3339/ISO-8601 format (e.g. 2017-11-26T12:29:19Z)")
54
55 srcLogURI = flag.String("src_log_uri", "", "URI for source log to copy certificates from")
56 srcPubKey = flag.String("src_pub_key", "", "Name of file containing source log's public key")
57 srcLogName = flag.String("src_log_name", "", "Name of source log to copy certificate from (from --log_list)")
58 logList = flag.String("log_list", loglist3.AllLogListURL, "Location of master log list (URL or filename)")
59 skipHTTPSVerify = flag.Bool("skip_https_verify", false, "Skip verification of HTTPS transport connection to source log")
60 chainBufSize = flag.Int("buffered_chains", 100, "Number of buffered certificate chains to hold")
61 startIndex = flag.Int64("start_index", 0, "Index of start point in source log to scan from (-1 for random start index)")
62 batchSize = flag.Int("batch_size", 500, "Max number of entries to request at per call to get-entries")
63 parallelFetch = flag.Int("parallel_fetch", 2, "Number of concurrent GetEntries fetches")
64
65 metricsEndpoint = flag.String("metrics_endpoint", "", "Endpoint for serving metrics; if left empty, metrics will not be exposed")
66 logConfig = flag.String("log_config", "", "File holding log config in JSON")
67 mmd = flag.Duration("mmd", 2*time.Minute, "Default MMD for logs")
68 operations = flag.Uint64("operations", ^uint64(0), "Number of operations to perform")
69 minGetEntries = flag.Int("min_get_entries", 1, "Minimum get-entries request size")
70 maxGetEntries = flag.Int("max_get_entries", 500, "Maximum get-entries request size")
71 oversizedGetEntries = flag.Bool("oversized_get_entries", false, "Whether get-entries requests can go beyond log size")
72 maxParallelChains = flag.Int("max_parallel_chains", 2, "Maximum number of chains to add in parallel (will always add at least 1 chain)")
73 limit = flag.Int("rate_limit", 0, "Maximum rate of requests to an individual log; 0 for no rate limit")
74 ignoreErrors = flag.Bool("ignore_errors", false, "Whether to ignore errors and retry the operation")
75 maxRetry = flag.Duration("max_retry", 60*time.Second, "How long to keep retrying when ignore_errors is set")
76 reqDeadline = flag.Duration("req_deadline", 10*time.Second, "Deadline to set on individual requests")
77 )
78 var (
79 addChainBias = flag.Int("add_chain", 20, "Bias for add-chain operations")
80 addPreChainBias = flag.Int("add_pre_chain", 20, "Bias for add-pre-chain operations")
81 getSTHBias = flag.Int("get_sth", 2, "Bias for get-sth operations")
82 getSTHConsistencyBias = flag.Int("get_sth_consistency", 2, "Bias for get-sth-consistency operations")
83 getProofByHashBias = flag.Int("get_proof_by_hash", 2, "Bias for get-proof-by-hash operations")
84 getEntriesBias = flag.Int("get_entries", 2, "Bias for get-entries operations")
85 getRootsBias = flag.Int("get_roots", 1, "Bias for get-roots operations")
86 getEntryAndProofBias = flag.Int("get_entry_and_proof", 0, "Bias for get-entry-and-proof operations")
87 invalidChance = flag.Int("invalid_chance", 10, "Chance of generating an invalid operation, as the N in 1-in-N (0 for never)")
88 dupeChance = flag.Int("duplicate_chance", 10, "Chance of generating a duplicate submission, as the N in 1-in-N (0 for never)")
89 strictSTHConsistencySize = flag.Bool("strict_sth_consistency_size", true, "If set to true, hammer will use only tree sizes from STHs it's seen for consistency proofs, otherwise it'll choose a random size for the smaller tree")
90 )
91
92 func newLimiter(limit int) integration.Limiter {
93 if limit <= 0 {
94 return nil
95 }
96 return rate.NewLimiter(rate.Limit(limit), 1)
97 }
98
99
100
101 func copierGeneratorFactory(ctx context.Context) integration.GeneratorFactory {
102 var tlsCfg *tls.Config
103 if *skipHTTPSVerify {
104 klog.Warning("Skipping HTTPS connection verification")
105 tlsCfg = &tls.Config{InsecureSkipVerify: *skipHTTPSVerify}
106 }
107 httpClient := &http.Client{
108 Timeout: 60 * time.Second,
109 Transport: &http.Transport{
110 TLSHandshakeTimeout: 30 * time.Second,
111 ResponseHeaderTimeout: 30 * time.Second,
112 MaxIdleConnsPerHost: 10,
113 DisableKeepAlives: false,
114 MaxIdleConns: 100,
115 IdleConnTimeout: 90 * time.Second,
116 ExpectContinueTimeout: 1 * time.Second,
117 TLSClientConfig: tlsCfg,
118 },
119 }
120 uri := *srcLogURI
121 var opts jsonclient.Options
122 if *srcPubKey != "" {
123 pubkey, err := os.ReadFile(*srcPubKey)
124 if err != nil {
125 klog.Exit(err)
126 }
127 opts.PublicKey = string(pubkey)
128 }
129 if len(*srcLogName) > 0 {
130 llData, err := x509util.ReadFileOrURL(*logList, httpClient)
131 if err != nil {
132 klog.Exitf("Failed to read log list: %v", err)
133 }
134 ll, err := loglist3.NewFromJSON(llData)
135 if err != nil {
136 klog.Exitf("Failed to build log list: %v", err)
137 }
138
139 logs := ll.FindLogByName(*srcLogName)
140 if len(logs) == 0 {
141 klog.Exitf("No log with name like %q found in loglist %q", *srcLogName, *logList)
142 }
143 if len(logs) > 1 {
144 logNames := make([]string, len(logs))
145 for i, log := range logs {
146 logNames[i] = fmt.Sprintf("%q", log.Description)
147 }
148 klog.Exitf("Multiple logs with name like %q found in loglist: %s", *srcLogName, strings.Join(logNames, ","))
149 }
150 uri = "https://" + logs[0].URL
151 if opts.PublicKey == "" {
152 opts.PublicKeyDER = logs[0].Key
153 }
154 }
155
156 logClient, err := client.New(uri, httpClient, opts)
157 if err != nil {
158 klog.Exitf("Failed to create client for %q: %v", uri, err)
159 }
160 klog.Infof("Testing with certs copied from log at %s starting at index %d", uri, *startIndex)
161 genOpts := integration.CopyChainOptions{
162 StartIndex: *startIndex,
163 BufSize: *chainBufSize,
164 BatchSize: *batchSize,
165 ParallelFetch: *parallelFetch,
166 }
167 return func(c *configpb.LogConfig) (integration.ChainGenerator, error) {
168 return integration.NewCopyChainGeneratorFromOpts(ctx, logClient, c, genOpts)
169 }
170 }
171
172 func main() {
173 klog.InitFlags(nil)
174 flag.Parse()
175 if *logConfig == "" {
176 klog.Exit("Test aborted as no log config provided (via --log_config)")
177 }
178
179 cfg, err := ctfe.LogConfigFromFile(*logConfig)
180 if err != nil {
181 klog.Exitf("Failed to read log config: %v", err)
182 }
183 ctx, cancel := context.WithCancel(context.Background())
184 defer cancel()
185
186 var generatorFactory integration.GeneratorFactory
187 if len(*srcLogURI) > 0 || len(*srcLogName) > 0 {
188
189 generatorFactory = copierGeneratorFactory(ctx)
190 } else if *testDir != "" {
191
192
193 klog.Infof("Testing with synthetic certs based on data from %s", *testDir)
194 generatorFactory, err = integration.SyntheticGeneratorFactory(*testDir, *leafNotAfter)
195 if err != nil {
196 klog.Exitf("Failed to make cert generator: %v", err)
197 }
198 }
199
200 if generatorFactory == nil {
201 klog.Warningf("Warning: add-[pre-]chain operations disabled as no cert generation method available")
202 *addChainBias = 0
203 *addPreChainBias = 0
204 generatorFactory = func(c *configpb.LogConfig) (integration.ChainGenerator, error) {
205 return nil, nil
206 }
207 }
208
209 bias := integration.HammerBias{
210 Bias: map[ctfe.EntrypointName]int{
211 ctfe.AddChainName: *addChainBias,
212 ctfe.AddPreChainName: *addPreChainBias,
213 ctfe.GetSTHName: *getSTHBias,
214 ctfe.GetSTHConsistencyName: *getSTHConsistencyBias,
215 ctfe.GetProofByHashName: *getProofByHashBias,
216 ctfe.GetEntriesName: *getEntriesBias,
217 ctfe.GetRootsName: *getRootsBias,
218 ctfe.GetEntryAndProofName: *getEntryAndProofBias,
219 },
220 InvalidChance: map[ctfe.EntrypointName]int{
221 ctfe.AddChainName: *invalidChance,
222 ctfe.AddPreChainName: *invalidChance,
223 ctfe.GetSTHName: 0,
224 ctfe.GetSTHConsistencyName: *invalidChance,
225 ctfe.GetProofByHashName: *invalidChance,
226 ctfe.GetEntriesName: *invalidChance,
227 ctfe.GetRootsName: 0,
228 ctfe.GetEntryAndProofName: 0,
229 },
230 }
231
232 var mf monitoring.MetricFactory
233 if *metricsEndpoint != "" {
234 mf = prometheus.MetricFactory{}
235 http.Handle("/metrics", promhttp.Handler())
236 server := http.Server{Addr: *metricsEndpoint, Handler: nil}
237 klog.Infof("Serving metrics at %v", *metricsEndpoint)
238 go func() {
239 err := server.ListenAndServe()
240 klog.Warningf("Metrics server exited: %v", err)
241 }()
242 } else {
243 mf = monitoring.InertMetricFactory{}
244 }
245
246 if *banner {
247 fmt.Print("\n\nStop")
248 for i := 0; i < 8; i++ {
249 time.Sleep(100 * time.Millisecond)
250 fmt.Print(".")
251 }
252 mc := "H4sIAAAAAAAA/4xVPbLzMAjsv1OkU8FI9LqDOAUFDUNBxe2/QXYSS/HLe5SeXZYfsf73+D1KB8D2B2RxZpGw8gcsSoQYeH1ya0fof1BpnhpuUR+P8ijorESq8Yto6WYWqsrMGh4qSkdI/YFZWu8d3AAAkklEHBGTNAYxbpKltWRgRzQ3A3CImDIjVSVCicThbLK0VjsiAGAGIIKbmUcIq/KkqYo4BNZDqtgZMAPNPSJCRISZZ36d5OiTUbqJZAOYIoCHUreImJsCPMobQ20SqjBbLWWbBGRREhHQU2MMUu9TwB12cC7X3SNrs1yPKvv5gD4yn+kzshOfMg69fVknJNbdcsjuDvgNXWPmTXCuEnuvP4NdlSWymIQjfsFWzbERZ5sz730NpbvoOGMOzu7eeBUaW3w8r4z2iRuD4uY6W9wgZ96+YZvpHW7SabvlH7CviKWQyp81EL2zj7Fcbee7MpSuNHzj2z18LdAvAkAr8pr/3cGFUO+apa2n64TK3XouTBpEch2Rf8GnzajAFY438+SzgURfV7sXT+q1FNTJYdLF9WxJzFheAyNmXfKuiel5/mW2QqSx2umlQ+L2GpTPWZBu5tvpXW5/fy4xTYd2ly+vR052dZbjTIh0u4vzyRDF6kPzoRLRfhp2pqnr5wce5eAGP6onaRv8EYdl7gfd5zIId/gxYvr4pWW7KnbjoU6kRL62e25b44ZQz7Oaf4GrTovnqemNsyOdL40Dls11ocMPn29nYeUvmt3S1v8DAAD//wEAAP//TRo+KHEIAAA="
253 mcData, _ := base64.StdEncoding.DecodeString(mc)
254 b := bytes.NewReader(mcData)
255 r, _ := gzip.NewReader(b)
256 if _, err := io.Copy(os.Stdout, r); err != nil {
257 klog.Exitf("Failed to print banner!")
258 }
259 r.Close()
260 fmt.Print("\n\nHammer Time\n\n")
261 }
262
263 type result struct {
264 prefix string
265 err error
266 }
267 results := make(chan result, len(cfg))
268 var wg sync.WaitGroup
269 for _, c := range cfg {
270 wg.Add(1)
271 pool, err := integration.NewRandomPool(*httpServers, c.PublicKey, c.Prefix)
272 if err != nil {
273 klog.Exitf("Failed to create client pool: %v", err)
274 }
275
276 mmd := *mmd
277
278
279 if emd := c.ExpectedMergeDelaySec; emd != 0 {
280 mmd = time.Second * time.Duration(emd)
281 }
282
283 generator, err := generatorFactory(c)
284 if err != nil {
285 klog.Exitf("Failed to build chain generator: %v", err)
286 }
287
288 cfg := integration.HammerConfig{
289 LogCfg: c,
290 MetricFactory: mf,
291 MMD: mmd,
292 ChainGenerator: generator,
293 ClientPool: pool,
294 EPBias: bias,
295 MinGetEntries: *minGetEntries,
296 MaxGetEntries: *maxGetEntries,
297 OversizedGetEntries: *oversizedGetEntries,
298 Operations: *operations,
299 Limiter: newLimiter(*limit),
300 MaxParallelChains: *maxParallelChains,
301 IgnoreErrors: *ignoreErrors,
302 MaxRetryDuration: *maxRetry,
303 RequestDeadline: *reqDeadline,
304 DuplicateChance: *dupeChance,
305 StrictSTHConsistencySize: *strictSTHConsistencySize,
306 }
307 go func(cfg integration.HammerConfig) {
308 defer wg.Done()
309 err := integration.HammerCTLog(ctx, cfg)
310 results <- result{prefix: cfg.LogCfg.Prefix, err: err}
311 }(cfg)
312 }
313 wg.Wait()
314
315 klog.Infof("completed tests on all %d logs:", len(cfg))
316 close(results)
317 errCount := 0
318 for e := range results {
319 if e.err != nil {
320 errCount++
321 klog.Errorf(" %s: failed with %v", e.prefix, e.err)
322 }
323 }
324 if errCount > 0 {
325 klog.Exitf("non-zero error count (%d), exiting", errCount)
326 }
327 klog.Info(" no errors; done")
328 }
329
View as plain text