github.com/letsencrypt/boulder@v0.20251208.0/publisher/publisher_test.go (about) 1 package publisher 2 3 import ( 4 "context" 5 "crypto/ecdsa" 6 "crypto/elliptic" 7 "crypto/rand" 8 "crypto/x509" 9 "crypto/x509/pkix" 10 "encoding/asn1" 11 "encoding/base64" 12 "encoding/json" 13 "fmt" 14 "math/big" 15 "net" 16 "net/http" 17 "net/http/httptest" 18 "net/url" 19 "strconv" 20 "strings" 21 "sync/atomic" 22 "testing" 23 "time" 24 25 ct "github.com/google/certificate-transparency-go" 26 "github.com/prometheus/client_golang/prometheus" 27 28 "github.com/letsencrypt/boulder/core" 29 "github.com/letsencrypt/boulder/issuance" 30 blog "github.com/letsencrypt/boulder/log" 31 "github.com/letsencrypt/boulder/metrics" 32 pubpb "github.com/letsencrypt/boulder/publisher/proto" 33 "github.com/letsencrypt/boulder/test" 34 ) 35 36 var log = blog.UseMock() 37 var ctx = context.Background() 38 39 func getPort(srvURL string) (int, error) { 40 url, err := url.Parse(srvURL) 41 if err != nil { 42 return 0, err 43 } 44 _, portString, err := net.SplitHostPort(url.Host) 45 if err != nil { 46 return 0, err 47 } 48 port, err := strconv.ParseInt(portString, 10, 64) 49 if err != nil { 50 return 0, err 51 } 52 return int(port), nil 53 } 54 55 type testLogSrv struct { 56 *httptest.Server 57 submissions int64 58 } 59 60 func logSrv(k *ecdsa.PrivateKey) *testLogSrv { 61 testLog := &testLogSrv{} 62 m := http.NewServeMux() 63 m.HandleFunc("/ct/", func(w http.ResponseWriter, r *http.Request) { 64 decoder := json.NewDecoder(r.Body) 65 var jsonReq ctSubmissionRequest 66 err := decoder.Decode(&jsonReq) 67 if err != nil { 68 return 69 } 70 precert := false 71 if r.URL.Path == "/ct/v1/add-pre-chain" { 72 precert = true 73 } 74 sct := CreateTestingSignedSCT(jsonReq.Chain, k, precert, time.Now()) 75 fmt.Fprint(w, string(sct)) 76 atomic.AddInt64(&testLog.submissions, 1) 77 }) 78 79 testLog.Server = httptest.NewUnstartedServer(m) 80 testLog.Server.Start() 81 return testLog 82 } 83 84 // lyingLogSrv always signs SCTs with the timestamp it was given. 85 func lyingLogSrv(k *ecdsa.PrivateKey, timestamp time.Time) *testLogSrv { 86 testLog := &testLogSrv{} 87 m := http.NewServeMux() 88 m.HandleFunc("/ct/", func(w http.ResponseWriter, r *http.Request) { 89 decoder := json.NewDecoder(r.Body) 90 var jsonReq ctSubmissionRequest 91 err := decoder.Decode(&jsonReq) 92 if err != nil { 93 return 94 } 95 precert := false 96 if r.URL.Path == "/ct/v1/add-pre-chain" { 97 precert = true 98 } 99 sct := CreateTestingSignedSCT(jsonReq.Chain, k, precert, timestamp) 100 fmt.Fprint(w, string(sct)) 101 atomic.AddInt64(&testLog.submissions, 1) 102 }) 103 104 testLog.Server = httptest.NewUnstartedServer(m) 105 testLog.Server.Start() 106 return testLog 107 } 108 109 func errorBodyLogSrv() *httptest.Server { 110 m := http.NewServeMux() 111 m.HandleFunc("/ct/", func(w http.ResponseWriter, r *http.Request) { 112 w.WriteHeader(http.StatusBadRequest) 113 w.Write([]byte("well this isn't good now is it.")) 114 }) 115 116 server := httptest.NewUnstartedServer(m) 117 server.Start() 118 return server 119 } 120 121 func setup(t *testing.T) (*Impl, *x509.Certificate, *ecdsa.PrivateKey) { 122 // Load chain: R3 <- Root DST 123 chain1, err := issuance.LoadChain([]string{ 124 "../test/hierarchy/int-r3-cross.cert.pem", 125 "../test/hierarchy/root-dst.cert.pem", 126 }) 127 test.AssertNotError(t, err, "failed to load chain1.") 128 129 // Load chain: R3 <- Root X1 130 chain2, err := issuance.LoadChain([]string{ 131 "../test/hierarchy/int-r3.cert.pem", 132 "../test/hierarchy/root-x1.cert.pem", 133 }) 134 test.AssertNotError(t, err, "failed to load chain2.") 135 136 // Load chain: E1 <- Root X2 137 chain3, err := issuance.LoadChain([]string{ 138 "../test/hierarchy/int-e1.cert.pem", 139 "../test/hierarchy/root-x2.cert.pem", 140 }) 141 test.AssertNotError(t, err, "failed to load chain3.") 142 143 // Create an example issuerNameID to CT bundle mapping 144 issuerBundles := map[issuance.NameID][]ct.ASN1Cert{ 145 chain1[0].NameID(): GetCTBundleForChain(chain1), 146 chain2[0].NameID(): GetCTBundleForChain(chain2), 147 chain3[0].NameID(): GetCTBundleForChain(chain3), 148 } 149 pub := New( 150 issuerBundles, 151 "test-user-agent/1.0", 152 log, 153 metrics.NoopRegisterer) 154 155 // Load leaf certificate 156 leaf, err := core.LoadCert("../test/hierarchy/ee-r3.cert.pem") 157 test.AssertNotError(t, err, "unable to load leaf certificate.") 158 159 k, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 160 test.AssertNotError(t, err, "Couldn't generate test key") 161 162 return pub, leaf, k 163 } 164 165 func addLog(t *testing.T, port int, pubKey *ecdsa.PublicKey) *Log { 166 uri := fmt.Sprintf("http://localhost:%d", port) 167 der, err := x509.MarshalPKIXPublicKey(pubKey) 168 test.AssertNotError(t, err, "Failed to marshal key") 169 newLog, err := NewLog(uri, base64.StdEncoding.EncodeToString(der), "test-user-agent/1.0", log) 170 test.AssertNotError(t, err, "Couldn't create log") 171 test.AssertEquals(t, newLog.uri, fmt.Sprintf("http://localhost:%d", port)) 172 return newLog 173 } 174 175 func makePrecert(k *ecdsa.PrivateKey) (map[issuance.NameID][]ct.ASN1Cert, []byte, error) { 176 rootTmpl := x509.Certificate{ 177 SerialNumber: big.NewInt(0), 178 Subject: pkix.Name{CommonName: "root"}, 179 BasicConstraintsValid: true, 180 IsCA: true, 181 } 182 rootBytes, err := x509.CreateCertificate(rand.Reader, &rootTmpl, &rootTmpl, k.Public(), k) 183 if err != nil { 184 return nil, nil, err 185 } 186 root, err := x509.ParseCertificate(rootBytes) 187 if err != nil { 188 return nil, nil, err 189 } 190 precertTmpl := x509.Certificate{ 191 SerialNumber: big.NewInt(0), 192 ExtraExtensions: []pkix.Extension{ 193 {Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 3}, Critical: true, Value: []byte{0x05, 0x00}}, 194 }, 195 } 196 precert, err := x509.CreateCertificate(rand.Reader, &precertTmpl, root, k.Public(), k) 197 if err != nil { 198 return nil, nil, err 199 } 200 precertX509, err := x509.ParseCertificate(precert) 201 if err != nil { 202 return nil, nil, err 203 } 204 precertIssuerNameID := issuance.IssuerNameID(precertX509) 205 bundles := map[issuance.NameID][]ct.ASN1Cert{ 206 precertIssuerNameID: { 207 ct.ASN1Cert{Data: rootBytes}, 208 }, 209 } 210 return bundles, precert, err 211 } 212 213 func TestTimestampVerificationFuture(t *testing.T) { 214 pub, _, k := setup(t) 215 216 server := lyingLogSrv(k, time.Now().Add(time.Hour)) 217 defer server.Close() 218 port, err := getPort(server.URL) 219 test.AssertNotError(t, err, "Failed to get test server port") 220 testLog := addLog(t, port, &k.PublicKey) 221 222 // Precert 223 issuerBundles, precert, err := makePrecert(k) 224 test.AssertNotError(t, err, "Failed to create test leaf") 225 pub.issuerBundles = issuerBundles 226 227 _, err = pub.SubmitToSingleCTWithResult(ctx, &pubpb.Request{ 228 LogURL: testLog.uri, 229 LogPublicKey: testLog.logID, 230 Der: precert, 231 Kind: pubpb.SubmissionType_sct, 232 }) 233 if err == nil { 234 t.Fatal("Expected error for lying log server, got none") 235 } 236 if !strings.HasPrefix(err.Error(), "SCT Timestamp was too far in the future") { 237 t.Fatalf("Got wrong error: %s", err) 238 } 239 } 240 241 func TestTimestampVerificationPast(t *testing.T) { 242 pub, _, k := setup(t) 243 244 server := lyingLogSrv(k, time.Now().Add(-time.Hour)) 245 defer server.Close() 246 port, err := getPort(server.URL) 247 test.AssertNotError(t, err, "Failed to get test server port") 248 testLog := addLog(t, port, &k.PublicKey) 249 250 // Precert 251 issuerBundles, precert, err := makePrecert(k) 252 test.AssertNotError(t, err, "Failed to create test leaf") 253 254 pub.issuerBundles = issuerBundles 255 256 _, err = pub.SubmitToSingleCTWithResult(ctx, &pubpb.Request{ 257 LogURL: testLog.uri, 258 LogPublicKey: testLog.logID, 259 Der: precert, 260 Kind: pubpb.SubmissionType_sct, 261 }) 262 if err == nil { 263 t.Fatal("Expected error for lying log server, got none") 264 } 265 if !strings.HasPrefix(err.Error(), "SCT Timestamp was too far in the past") { 266 t.Fatalf("Got wrong error: %s", err) 267 } 268 } 269 270 func TestLogCache(t *testing.T) { 271 cache := logCache{ 272 logs: make(map[cacheKey]*Log), 273 } 274 275 // Adding a log with an invalid base64 public key should error 276 _, err := cache.AddLog("www.test.com", "1234", "test-user-agent/1.0", log) 277 test.AssertError(t, err, "AddLog() with invalid base64 pk didn't error") 278 279 // Adding a log with an invalid URI should error 280 _, err = cache.AddLog(":", "", "test-user-agent/1.0", log) 281 test.AssertError(t, err, "AddLog() with an invalid log URI didn't error") 282 283 // Create one keypair & base 64 public key 284 k1, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 285 test.AssertNotError(t, err, "ecdsa.GenerateKey() failed for k1") 286 der1, err := x509.MarshalPKIXPublicKey(&k1.PublicKey) 287 test.AssertNotError(t, err, "x509.MarshalPKIXPublicKey(der1) failed") 288 k1b64 := base64.StdEncoding.EncodeToString(der1) 289 290 // Create a second keypair & base64 public key 291 k2, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 292 test.AssertNotError(t, err, "ecdsa.GenerateKey() failed for k2") 293 der2, err := x509.MarshalPKIXPublicKey(&k2.PublicKey) 294 test.AssertNotError(t, err, "x509.MarshalPKIXPublicKey(der2) failed") 295 k2b64 := base64.StdEncoding.EncodeToString(der2) 296 297 // Adding the first log should not produce an error 298 l1, err := cache.AddLog("http://log.one.example.com", k1b64, "test-user-agent/1.0", log) 299 test.AssertNotError(t, err, "cache.AddLog() failed for log 1") 300 test.AssertEquals(t, cache.Len(), 1) 301 test.AssertEquals(t, l1.uri, "http://log.one.example.com") 302 test.AssertEquals(t, l1.logID, k1b64) 303 304 // Adding it again should not produce any errors, or increase the Len() 305 l1, err = cache.AddLog("http://log.one.example.com", k1b64, "test-user-agent/1.0", log) 306 test.AssertNotError(t, err, "cache.AddLog() failed for second add of log 1") 307 test.AssertEquals(t, cache.Len(), 1) 308 test.AssertEquals(t, l1.uri, "http://log.one.example.com") 309 test.AssertEquals(t, l1.logID, k1b64) 310 311 // Adding a second log should not error and should increase the Len() 312 l2, err := cache.AddLog("http://log.two.example.com", k2b64, "test-user-agent/1.0", log) 313 test.AssertNotError(t, err, "cache.AddLog() failed for log 2") 314 test.AssertEquals(t, cache.Len(), 2) 315 test.AssertEquals(t, l2.uri, "http://log.two.example.com") 316 test.AssertEquals(t, l2.logID, k2b64) 317 } 318 319 func TestLogErrorBody(t *testing.T) { 320 pub, leaf, k := setup(t) 321 322 srv := errorBodyLogSrv() 323 defer srv.Close() 324 port, err := getPort(srv.URL) 325 test.AssertNotError(t, err, "Failed to get test server port") 326 327 log.Clear() 328 logURI := fmt.Sprintf("http://localhost:%d", port) 329 pkDER, err := x509.MarshalPKIXPublicKey(&k.PublicKey) 330 test.AssertNotError(t, err, "Failed to marshal key") 331 pkB64 := base64.StdEncoding.EncodeToString(pkDER) 332 _, err = pub.SubmitToSingleCTWithResult(context.Background(), &pubpb.Request{ 333 LogURL: logURI, 334 LogPublicKey: pkB64, 335 Der: leaf.Raw, 336 Kind: pubpb.SubmissionType_final, 337 }) 338 test.AssertError(t, err, "SubmitToSingleCTWithResult didn't fail") 339 test.AssertEquals(t, len(log.GetAllMatching("well this isn't good now is it")), 1) 340 } 341 342 // TestErrorMetrics checks that the ct_errors_count and 343 // ct_submission_time_seconds metrics are updated with the correct labels when 344 // the publisher encounters errors. 345 func TestErrorMetrics(t *testing.T) { 346 pub, leaf, k := setup(t) 347 348 pkDER, err := x509.MarshalPKIXPublicKey(&k.PublicKey) 349 test.AssertNotError(t, err, "Failed to marshal key") 350 pkB64 := base64.StdEncoding.EncodeToString(pkDER) 351 352 // Set up a bad server that will always produce errors. 353 badSrv := errorBodyLogSrv() 354 defer badSrv.Close() 355 port, err := getPort(badSrv.URL) 356 test.AssertNotError(t, err, "Failed to get test server port") 357 logURI := fmt.Sprintf("http://localhost:%d", port) 358 359 _, err = pub.SubmitToSingleCTWithResult(context.Background(), &pubpb.Request{ 360 LogURL: logURI, 361 LogPublicKey: pkB64, 362 Der: leaf.Raw, 363 Kind: pubpb.SubmissionType_sct, 364 }) 365 test.AssertError(t, err, "SubmitToSingleCTWithResult didn't fail") 366 test.AssertMetricWithLabelsEquals(t, pub.metrics.submissionLatency, prometheus.Labels{ 367 "log": logURI, 368 "type": "sct", 369 "status": "error", 370 "http_status": "400", 371 }, 1) 372 test.AssertMetricWithLabelsEquals(t, pub.metrics.errorCount, prometheus.Labels{ 373 "log": logURI, 374 "type": "sct", 375 }, 1) 376 377 _, err = pub.SubmitToSingleCTWithResult(context.Background(), &pubpb.Request{ 378 LogURL: logURI, 379 LogPublicKey: pkB64, 380 Der: leaf.Raw, 381 Kind: pubpb.SubmissionType_final, 382 }) 383 test.AssertError(t, err, "SubmitToSingleCTWithResult didn't fail") 384 test.AssertMetricWithLabelsEquals(t, pub.metrics.submissionLatency, prometheus.Labels{ 385 "log": logURI, 386 "type": "final", 387 "status": "error", 388 "http_status": "400", 389 }, 1) 390 test.AssertMetricWithLabelsEquals(t, pub.metrics.errorCount, prometheus.Labels{ 391 "log": logURI, 392 "type": "final", 393 }, 1) 394 395 _, err = pub.SubmitToSingleCTWithResult(context.Background(), &pubpb.Request{ 396 LogURL: logURI, 397 LogPublicKey: pkB64, 398 Der: leaf.Raw, 399 Kind: pubpb.SubmissionType_info, 400 }) 401 test.AssertError(t, err, "SubmitToSingleCTWithResult didn't fail") 402 test.AssertMetricWithLabelsEquals(t, pub.metrics.submissionLatency, prometheus.Labels{ 403 "log": logURI, 404 "type": "info", 405 "status": "error", 406 "http_status": "400", 407 }, 1) 408 test.AssertMetricWithLabelsEquals(t, pub.metrics.errorCount, prometheus.Labels{ 409 "log": logURI, 410 "type": "info", 411 }, 1) 412 } 413 414 // TestSuccessMetrics checks that the ct_errors_count and 415 // ct_submission_time_seconds metrics are updated with the correct labels when 416 // the publisher succeeds. 417 func TestSuccessMetrics(t *testing.T) { 418 pub, leaf, k := setup(t) 419 420 pkDER, err := x509.MarshalPKIXPublicKey(&k.PublicKey) 421 test.AssertNotError(t, err, "Failed to marshal key") 422 pkB64 := base64.StdEncoding.EncodeToString(pkDER) 423 424 // Set up a working server that will succeed. 425 workingSrv := logSrv(k) 426 defer workingSrv.Close() 427 port, err := getPort(workingSrv.URL) 428 test.AssertNotError(t, err, "Failed to get test server port") 429 logURI := fmt.Sprintf("http://localhost:%d", port) 430 431 // Only the latency metric should be updated on a success. 432 _, err = pub.SubmitToSingleCTWithResult(context.Background(), &pubpb.Request{ 433 LogURL: logURI, 434 LogPublicKey: pkB64, 435 Der: leaf.Raw, 436 Kind: pubpb.SubmissionType_final, 437 }) 438 test.AssertNotError(t, err, "SubmitToSingleCTWithResult failed") 439 test.AssertMetricWithLabelsEquals(t, pub.metrics.submissionLatency, prometheus.Labels{ 440 "log": logURI, 441 "type": "final", 442 "status": "success", 443 "http_status": "", 444 }, 1) 445 test.AssertMetricWithLabelsEquals(t, pub.metrics.errorCount, prometheus.Labels{ 446 "log": logURI, 447 "type": "final", 448 }, 0) 449 } 450 451 func Test_GetCTBundleForChain(t *testing.T) { 452 chain, err := issuance.LoadChain([]string{ 453 "../test/hierarchy/int-r3.cert.pem", 454 "../test/hierarchy/root-x1.cert.pem", 455 }) 456 test.AssertNotError(t, err, "Failed to load chain.") 457 expect := []ct.ASN1Cert{{Data: chain[0].Raw}} 458 type args struct { 459 chain []*issuance.Certificate 460 } 461 tests := []struct { 462 name string 463 args args 464 want []ct.ASN1Cert 465 }{ 466 {"Create a ct bundle with a single intermediate", args{chain}, expect}, 467 } 468 for _, tt := range tests { 469 t.Run(tt.name, func(t *testing.T) { 470 bundle := GetCTBundleForChain(tt.args.chain) 471 test.AssertDeepEquals(t, bundle, tt.want) 472 }) 473 } 474 }