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  }