...

Source file src/github.com/letsencrypt/boulder/publisher/publisher.go

Documentation: github.com/letsencrypt/boulder/publisher

     1  package publisher
     2  
     3  import (
     4  	"context"
     5  	"crypto/ecdsa"
     6  	"crypto/rand"
     7  	"crypto/sha256"
     8  	"crypto/tls"
     9  	"crypto/x509"
    10  	"encoding/asn1"
    11  	"encoding/base64"
    12  	"encoding/json"
    13  	"errors"
    14  	"fmt"
    15  	"math/big"
    16  	"net/http"
    17  	"net/url"
    18  	"strings"
    19  	"sync"
    20  	"time"
    21  
    22  	ct "github.com/google/certificate-transparency-go"
    23  	ctClient "github.com/google/certificate-transparency-go/client"
    24  	"github.com/google/certificate-transparency-go/jsonclient"
    25  	cttls "github.com/google/certificate-transparency-go/tls"
    26  	"github.com/prometheus/client_golang/prometheus"
    27  
    28  	"github.com/letsencrypt/boulder/canceled"
    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  )
    34  
    35  // Log contains the CT client for a particular CT log
    36  type Log struct {
    37  	logID  string
    38  	uri    string
    39  	client *ctClient.LogClient
    40  }
    41  
    42  // logCache contains a cache of *Log's that are constructed as required by
    43  // `SubmitToSingleCT`
    44  type logCache struct {
    45  	sync.RWMutex
    46  	logs map[string]*Log
    47  }
    48  
    49  // AddLog adds a *Log to the cache by constructing the statName, client and
    50  // verifier for the given uri & base64 public key.
    51  func (c *logCache) AddLog(uri, b64PK, userAgent string, logger blog.Logger) (*Log, error) {
    52  	// Lock the mutex for reading to check the cache
    53  	c.RLock()
    54  	log, present := c.logs[b64PK]
    55  	c.RUnlock()
    56  
    57  	// If we have already added this log, give it back
    58  	if present {
    59  		return log, nil
    60  	}
    61  
    62  	// Lock the mutex for writing to add to the cache
    63  	c.Lock()
    64  	defer c.Unlock()
    65  
    66  	// Construct a Log, add it to the cache, and return it to the caller
    67  	log, err := NewLog(uri, b64PK, userAgent, logger)
    68  	if err != nil {
    69  		return nil, err
    70  	}
    71  	c.logs[b64PK] = log
    72  	return log, nil
    73  }
    74  
    75  // Len returns the number of logs in the logCache
    76  func (c *logCache) Len() int {
    77  	c.RLock()
    78  	defer c.RUnlock()
    79  	return len(c.logs)
    80  }
    81  
    82  type logAdaptor struct {
    83  	blog.Logger
    84  }
    85  
    86  func (la logAdaptor) Printf(s string, args ...interface{}) {
    87  	la.Logger.Infof(s, args...)
    88  }
    89  
    90  // NewLog returns an initialized Log struct
    91  func NewLog(uri, b64PK, userAgent string, logger blog.Logger) (*Log, error) {
    92  	url, err := url.Parse(uri)
    93  	if err != nil {
    94  		return nil, err
    95  	}
    96  	url.Path = strings.TrimSuffix(url.Path, "/")
    97  
    98  	pemPK := fmt.Sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----",
    99  		b64PK)
   100  	opts := jsonclient.Options{
   101  		Logger:    logAdaptor{logger},
   102  		PublicKey: pemPK,
   103  		UserAgent: userAgent,
   104  	}
   105  	httpClient := &http.Client{
   106  		// We set the HTTP client timeout to about half of what we expect
   107  		// the gRPC timeout to be set to. This allows us to retry the
   108  		// request at least twice in the case where the server we are
   109  		// talking to is simply hanging indefinitely.
   110  		Timeout: time.Minute*2 + time.Second*30,
   111  		// We provide a new Transport for each Client so that different logs don't
   112  		// share a connection pool. This shouldn't matter, but we occasionally see a
   113  		// strange bug where submission to all logs hangs for about fifteen minutes.
   114  		// One possibility is that there is a strange bug in the locking on
   115  		// connection pools (possibly triggered by timed-out TCP connections). If
   116  		// that's the case, separate connection pools should prevent cross-log impact.
   117  		// We set some fields like TLSHandshakeTimeout to the values from
   118  		// DefaultTransport because the zero value for these fields means
   119  		// "unlimited," which would be bad.
   120  		Transport: &http.Transport{
   121  			MaxIdleConns:        http.DefaultTransport.(*http.Transport).MaxIdleConns,
   122  			IdleConnTimeout:     http.DefaultTransport.(*http.Transport).IdleConnTimeout,
   123  			TLSHandshakeTimeout: http.DefaultTransport.(*http.Transport).TLSHandshakeTimeout,
   124  			// In Boulder Issue 3821[0] we found that HTTP/2 support was causing hard
   125  			// to diagnose intermittent freezes in CT submission. Disabling HTTP/2 with
   126  			// an environment variable resolved the freezes but is not a stable fix.
   127  			//
   128  			// Per the Go `http` package docs we can make this change persistent by
   129  			// changing the `http.Transport` config:
   130  			//   "Programs that must disable HTTP/2 can do so by setting
   131  			//   Transport.TLSNextProto (for clients) or Server.TLSNextProto (for
   132  			//   servers) to a non-nil, empty map"
   133  			//
   134  			// [0]: https://github.com/letsencrypt/boulder/issues/3821
   135  			TLSNextProto: map[string]func(string, *tls.Conn) http.RoundTripper{},
   136  		},
   137  	}
   138  	client, err := ctClient.New(url.String(), httpClient, opts)
   139  	if err != nil {
   140  		return nil, fmt.Errorf("making CT client: %s", err)
   141  	}
   142  
   143  	return &Log{
   144  		logID:  b64PK,
   145  		uri:    url.String(),
   146  		client: client,
   147  	}, nil
   148  }
   149  
   150  type ctSubmissionRequest struct {
   151  	Chain []string `json:"chain"`
   152  }
   153  
   154  type pubMetrics struct {
   155  	submissionLatency *prometheus.HistogramVec
   156  	probeLatency      *prometheus.HistogramVec
   157  	errorCount        *prometheus.CounterVec
   158  }
   159  
   160  func initMetrics(stats prometheus.Registerer) *pubMetrics {
   161  	submissionLatency := prometheus.NewHistogramVec(
   162  		prometheus.HistogramOpts{
   163  			Name:    "ct_submission_time_seconds",
   164  			Help:    "Time taken to submit a certificate to a CT log",
   165  			Buckets: metrics.InternetFacingBuckets,
   166  		},
   167  		[]string{"log", "status", "http_status"},
   168  	)
   169  	stats.MustRegister(submissionLatency)
   170  
   171  	probeLatency := prometheus.NewHistogramVec(
   172  		prometheus.HistogramOpts{
   173  			Name:    "ct_probe_time_seconds",
   174  			Help:    "Time taken to probe a CT log",
   175  			Buckets: metrics.InternetFacingBuckets,
   176  		},
   177  		[]string{"log", "status"},
   178  	)
   179  	stats.MustRegister(probeLatency)
   180  
   181  	errorCount := prometheus.NewCounterVec(
   182  		prometheus.CounterOpts{
   183  			Name: "ct_errors_count",
   184  			Help: "Count of errors by type",
   185  		},
   186  		[]string{"type"},
   187  	)
   188  	stats.MustRegister(errorCount)
   189  
   190  	return &pubMetrics{submissionLatency, probeLatency, errorCount}
   191  }
   192  
   193  // Impl defines a Publisher
   194  type Impl struct {
   195  	pubpb.UnimplementedPublisherServer
   196  	log           blog.Logger
   197  	userAgent     string
   198  	issuerBundles map[issuance.IssuerNameID][]ct.ASN1Cert
   199  	ctLogsCache   logCache
   200  	metrics       *pubMetrics
   201  }
   202  
   203  // New creates a Publisher that will submit certificates
   204  // to requested CT logs
   205  func New(
   206  	bundles map[issuance.IssuerNameID][]ct.ASN1Cert,
   207  	userAgent string,
   208  	logger blog.Logger,
   209  	stats prometheus.Registerer,
   210  ) *Impl {
   211  	return &Impl{
   212  		issuerBundles: bundles,
   213  		userAgent:     userAgent,
   214  		ctLogsCache: logCache{
   215  			logs: make(map[string]*Log),
   216  		},
   217  		log:     logger,
   218  		metrics: initMetrics(stats),
   219  	}
   220  }
   221  
   222  // SubmitToSingleCTWithResult will submit the certificate represented by certDER to the CT
   223  // log specified by log URL and public key (base64) and return the SCT to the caller
   224  func (pub *Impl) SubmitToSingleCTWithResult(ctx context.Context, req *pubpb.Request) (*pubpb.Result, error) {
   225  	cert, err := x509.ParseCertificate(req.Der)
   226  	if err != nil {
   227  		pub.log.AuditErrf("Failed to parse certificate: %s", err)
   228  		return nil, err
   229  	}
   230  	chain := []ct.ASN1Cert{{Data: req.Der}}
   231  	id := issuance.GetIssuerNameID(cert)
   232  	issuerBundle, ok := pub.issuerBundles[id]
   233  	if !ok {
   234  		err := fmt.Errorf("No issuerBundle matching issuerNameID: %d", int64(id))
   235  		pub.log.AuditErrf("Failed to submit certificate to CT log: %s", err)
   236  		return nil, err
   237  	}
   238  	chain = append(chain, issuerBundle...)
   239  
   240  	// Add a log URL/pubkey to the cache, if already present the
   241  	// existing *Log will be returned, otherwise one will be constructed, added
   242  	// and returned.
   243  	ctLog, err := pub.ctLogsCache.AddLog(req.LogURL, req.LogPublicKey, pub.userAgent, pub.log)
   244  	if err != nil {
   245  		pub.log.AuditErrf("Making Log: %s", err)
   246  		return nil, err
   247  	}
   248  
   249  	isPrecert := req.Precert
   250  
   251  	sct, err := pub.singleLogSubmit(ctx, chain, isPrecert, ctLog)
   252  	if err != nil {
   253  		if canceled.Is(err) {
   254  			return nil, err
   255  		}
   256  		var body string
   257  		var rspErr jsonclient.RspError
   258  		if errors.As(err, &rspErr) && rspErr.StatusCode < 500 {
   259  			body = string(rspErr.Body)
   260  		}
   261  		pub.log.AuditErrf("Failed to submit certificate to CT log at %s: %s Body=%q",
   262  			ctLog.uri, err, body)
   263  		return nil, err
   264  	}
   265  
   266  	sctBytes, err := cttls.Marshal(*sct)
   267  	if err != nil {
   268  		return nil, err
   269  	}
   270  	return &pubpb.Result{Sct: sctBytes}, nil
   271  }
   272  
   273  func (pub *Impl) singleLogSubmit(
   274  	ctx context.Context,
   275  	chain []ct.ASN1Cert,
   276  	isPrecert bool,
   277  	ctLog *Log,
   278  ) (*ct.SignedCertificateTimestamp, error) {
   279  	var submissionMethod func(context.Context, []ct.ASN1Cert) (*ct.SignedCertificateTimestamp, error)
   280  	submissionMethod = ctLog.client.AddChain
   281  	if isPrecert {
   282  		submissionMethod = ctLog.client.AddPreChain
   283  	}
   284  
   285  	start := time.Now()
   286  	sct, err := submissionMethod(ctx, chain)
   287  	took := time.Since(start).Seconds()
   288  	if err != nil {
   289  		status := "error"
   290  		if canceled.Is(err) {
   291  			status = "canceled"
   292  		}
   293  		httpStatus := ""
   294  		var rspError ctClient.RspError
   295  		if errors.As(err, &rspError) && rspError.StatusCode != 0 {
   296  			httpStatus = fmt.Sprintf("%d", rspError.StatusCode)
   297  		}
   298  		pub.metrics.submissionLatency.With(prometheus.Labels{
   299  			"log":         ctLog.uri,
   300  			"status":      status,
   301  			"http_status": httpStatus,
   302  		}).Observe(took)
   303  		if isPrecert {
   304  			pub.metrics.errorCount.WithLabelValues("precert").Inc()
   305  		} else {
   306  			pub.metrics.errorCount.WithLabelValues("final").Inc()
   307  		}
   308  		return nil, err
   309  	}
   310  	pub.metrics.submissionLatency.With(prometheus.Labels{
   311  		"log":         ctLog.uri,
   312  		"status":      "success",
   313  		"http_status": "",
   314  	}).Observe(took)
   315  
   316  	timestamp := time.Unix(int64(sct.Timestamp)/1000, 0)
   317  	if time.Until(timestamp) > time.Minute {
   318  		return nil, fmt.Errorf("SCT Timestamp was too far in the future (%s)", timestamp)
   319  	}
   320  	// For regular certificates, we could get an old SCT, but that shouldn't
   321  	// happen for precertificates.
   322  	if isPrecert && time.Until(timestamp) < -10*time.Minute {
   323  		return nil, fmt.Errorf("SCT Timestamp was too far in the past (%s)", timestamp)
   324  	}
   325  
   326  	return sct, nil
   327  }
   328  
   329  // CreateTestingSignedSCT is used by both the publisher tests and ct-test-serv, which is
   330  // why it is exported. It creates a signed SCT based on the provided chain.
   331  func CreateTestingSignedSCT(req []string, k *ecdsa.PrivateKey, precert bool, timestamp time.Time) []byte {
   332  	chain := make([]ct.ASN1Cert, len(req))
   333  	for i, str := range req {
   334  		b, err := base64.StdEncoding.DecodeString(str)
   335  		if err != nil {
   336  			panic("cannot decode chain")
   337  		}
   338  		chain[i] = ct.ASN1Cert{Data: b}
   339  	}
   340  
   341  	// Generate the internal leaf entry for the SCT
   342  	etype := ct.X509LogEntryType
   343  	if precert {
   344  		etype = ct.PrecertLogEntryType
   345  	}
   346  	leaf, err := ct.MerkleTreeLeafFromRawChain(chain, etype, 0)
   347  	if err != nil {
   348  		panic(fmt.Sprintf("failed to create leaf: %s", err))
   349  	}
   350  
   351  	// Sign the SCT
   352  	rawKey, _ := x509.MarshalPKIXPublicKey(&k.PublicKey)
   353  	logID := sha256.Sum256(rawKey)
   354  	timestampMillis := uint64(timestamp.UnixNano()) / 1e6
   355  	serialized, _ := ct.SerializeSCTSignatureInput(ct.SignedCertificateTimestamp{
   356  		SCTVersion: ct.V1,
   357  		LogID:      ct.LogID{KeyID: logID},
   358  		Timestamp:  timestampMillis,
   359  	}, ct.LogEntry{Leaf: *leaf})
   360  	hashed := sha256.Sum256(serialized)
   361  	var ecdsaSig struct {
   362  		R, S *big.Int
   363  	}
   364  	ecdsaSig.R, ecdsaSig.S, _ = ecdsa.Sign(rand.Reader, k, hashed[:])
   365  	sig, _ := asn1.Marshal(ecdsaSig)
   366  
   367  	// The ct.SignedCertificateTimestamp object doesn't have the needed
   368  	// `json` tags to properly marshal so we need to transform in into
   369  	// a struct that does before we can send it off
   370  	var jsonSCTObj struct {
   371  		SCTVersion ct.Version `json:"sct_version"`
   372  		ID         string     `json:"id"`
   373  		Timestamp  uint64     `json:"timestamp"`
   374  		Extensions string     `json:"extensions"`
   375  		Signature  string     `json:"signature"`
   376  	}
   377  	jsonSCTObj.SCTVersion = ct.V1
   378  	jsonSCTObj.ID = base64.StdEncoding.EncodeToString(logID[:])
   379  	jsonSCTObj.Timestamp = timestampMillis
   380  	ds := ct.DigitallySigned{
   381  		Algorithm: cttls.SignatureAndHashAlgorithm{
   382  			Hash:      cttls.SHA256,
   383  			Signature: cttls.ECDSA,
   384  		},
   385  		Signature: sig,
   386  	}
   387  	jsonSCTObj.Signature, _ = ds.Base64String()
   388  
   389  	jsonSCT, _ := json.Marshal(jsonSCTObj)
   390  	return jsonSCT
   391  }
   392  
   393  // GetCTBundleForChain takes a slice of *issuance.Certificate(s)
   394  // representing a certificate chain and returns a slice of
   395  // ct.ANS1Cert(s) in the same order
   396  func GetCTBundleForChain(chain []*issuance.Certificate) []ct.ASN1Cert {
   397  	var ctBundle []ct.ASN1Cert
   398  	for _, cert := range chain {
   399  		ctBundle = append(ctBundle, ct.ASN1Cert{Data: cert.Raw})
   400  	}
   401  	return ctBundle
   402  }
   403  

View as plain text