...

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

Documentation: github.com/letsencrypt/boulder/ctpolicy

     1  package ctpolicy
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strings"
     7  	"time"
     8  
     9  	"github.com/letsencrypt/boulder/core"
    10  	"github.com/letsencrypt/boulder/ctpolicy/loglist"
    11  	berrors "github.com/letsencrypt/boulder/errors"
    12  	blog "github.com/letsencrypt/boulder/log"
    13  	pubpb "github.com/letsencrypt/boulder/publisher/proto"
    14  	"github.com/prometheus/client_golang/prometheus"
    15  )
    16  
    17  const (
    18  	succeeded = "succeeded"
    19  	failed    = "failed"
    20  )
    21  
    22  // CTPolicy is used to hold information about SCTs required from various
    23  // groupings
    24  type CTPolicy struct {
    25  	pub                 pubpb.PublisherClient
    26  	sctLogs             loglist.List
    27  	infoLogs            loglist.List
    28  	finalLogs           loglist.List
    29  	stagger             time.Duration
    30  	log                 blog.Logger
    31  	winnerCounter       *prometheus.CounterVec
    32  	operatorGroupsGauge *prometheus.GaugeVec
    33  	shardExpiryGauge    *prometheus.GaugeVec
    34  }
    35  
    36  // New creates a new CTPolicy struct
    37  func New(pub pubpb.PublisherClient, sctLogs loglist.List, infoLogs loglist.List, finalLogs loglist.List, stagger time.Duration, log blog.Logger, stats prometheus.Registerer) *CTPolicy {
    38  	winnerCounter := prometheus.NewCounterVec(
    39  		prometheus.CounterOpts{
    40  			Name: "sct_winner",
    41  			Help: "Counter of logs which are selected for sct submission, by log URL and result (succeeded or failed).",
    42  		},
    43  		[]string{"url", "result"},
    44  	)
    45  	stats.MustRegister(winnerCounter)
    46  
    47  	operatorGroupsGauge := prometheus.NewGaugeVec(
    48  		prometheus.GaugeOpts{
    49  			Name: "ct_operator_group_size_gauge",
    50  			Help: "Gauge for CT operators group size, by operator and log source (capable of providing SCT, informational logs, logs we submit final certs to).",
    51  		},
    52  		[]string{"operator", "source"},
    53  	)
    54  	stats.MustRegister(operatorGroupsGauge)
    55  
    56  	shardExpiryGauge := prometheus.NewGaugeVec(
    57  		prometheus.GaugeOpts{
    58  			Name: "ct_shard_expiration_seconds",
    59  			Help: "CT shard end_exclusive field expressed as Unix epoch time, by operator and logID.",
    60  		},
    61  		[]string{"operator", "logID"},
    62  	)
    63  	stats.MustRegister(shardExpiryGauge)
    64  
    65  	for op, group := range sctLogs {
    66  		operatorGroupsGauge.WithLabelValues(op, "sctLogs").Set(float64(len(group)))
    67  
    68  		for _, log := range group {
    69  			if log.EndExclusive.IsZero() {
    70  				// Handles the case for non-temporally sharded logs too.
    71  				shardExpiryGauge.WithLabelValues(op, log.Name).Set(float64(0))
    72  			} else {
    73  				shardExpiryGauge.WithLabelValues(op, log.Name).Set(float64(log.EndExclusive.Unix()))
    74  			}
    75  		}
    76  	}
    77  
    78  	for op, group := range infoLogs {
    79  		operatorGroupsGauge.WithLabelValues(op, "infoLogs").Set(float64(len(group)))
    80  	}
    81  
    82  	for op, group := range finalLogs {
    83  		operatorGroupsGauge.WithLabelValues(op, "finalLogs").Set(float64(len(group)))
    84  	}
    85  
    86  	return &CTPolicy{
    87  		pub:                 pub,
    88  		sctLogs:             sctLogs,
    89  		infoLogs:            infoLogs,
    90  		finalLogs:           finalLogs,
    91  		stagger:             stagger,
    92  		log:                 log,
    93  		winnerCounter:       winnerCounter,
    94  		operatorGroupsGauge: operatorGroupsGauge,
    95  		shardExpiryGauge:    shardExpiryGauge,
    96  	}
    97  }
    98  
    99  type result struct {
   100  	sct []byte
   101  	url string
   102  	err error
   103  }
   104  
   105  // GetSCTs retrieves exactly two SCTs from the total collection of configured
   106  // log groups, with at most one SCT coming from each group. It expects that all
   107  // logs run by a single operator (e.g. Google) are in the same group, to
   108  // guarantee that SCTs from logs in different groups do not end up coming from
   109  // the same operator. As such, it enforces Google's current CT Policy, which
   110  // requires that certs have two SCTs from logs run by different operators.
   111  func (ctp *CTPolicy) GetSCTs(ctx context.Context, cert core.CertDER, expiration time.Time) (core.SCTDERs, error) {
   112  	// We'll cancel this sub-context when we have the two SCTs we need, to cause
   113  	// any other ongoing submission attempts to quit.
   114  	subCtx, cancel := context.WithCancel(ctx)
   115  	defer cancel()
   116  
   117  	// This closure will be called in parallel once for each operator group.
   118  	getOne := func(i int, g string) ([]byte, string, error) {
   119  		// Sleep a little bit to stagger our requests to the later groups. Use `i-1`
   120  		// to compute the stagger duration so that the first two groups (indices 0
   121  		// and 1) get negative or zero (i.e. instant) sleep durations. If the
   122  		// context gets cancelled (most likely because two logs from other operator
   123  		// groups returned SCTs already) before the sleep is complete, quit instead.
   124  		select {
   125  		case <-subCtx.Done():
   126  			return nil, "", subCtx.Err()
   127  		case <-time.After(time.Duration(i-1) * ctp.stagger):
   128  		}
   129  
   130  		// Pick a random log from among those in the group. In practice, very few
   131  		// operator groups have more than one log, so this loses little flexibility.
   132  		url, key, err := ctp.sctLogs.PickOne(g, expiration)
   133  		if err != nil {
   134  			return nil, "", fmt.Errorf("unable to get log info: %w", err)
   135  		}
   136  
   137  		sct, err := ctp.pub.SubmitToSingleCTWithResult(ctx, &pubpb.Request{
   138  			LogURL:       url,
   139  			LogPublicKey: key,
   140  			Der:          cert,
   141  			Precert:      true,
   142  		})
   143  		if err != nil {
   144  			return nil, url, fmt.Errorf("ct submission to %q (%q) failed: %w", g, url, err)
   145  		}
   146  
   147  		return sct.Sct, url, nil
   148  	}
   149  
   150  	// Ensure that this channel has a buffer equal to the number of goroutines
   151  	// we're kicking off, so that they're all guaranteed to be able to write to
   152  	// it and exit without blocking and leaking.
   153  	results := make(chan result, len(ctp.sctLogs))
   154  
   155  	// Kick off a collection of goroutines to try to submit the precert to each
   156  	// log operator group. Randomize the order of the groups so that we're not
   157  	// always trying to submit to the same two operators.
   158  	for i, group := range ctp.sctLogs.Permute() {
   159  		go func(i int, g string) {
   160  			sctDER, url, err := getOne(i, g)
   161  			results <- result{sct: sctDER, url: url, err: err}
   162  		}(i, group)
   163  	}
   164  
   165  	go ctp.submitPrecertInformational(cert, expiration)
   166  
   167  	// Finally, collect SCTs and/or errors from our results channel. We know that
   168  	// we will collect len(ctp.sctLogs) results from the channel because every
   169  	// goroutine is guaranteed to write one result to the channel.
   170  	scts := make(core.SCTDERs, 0)
   171  	errs := make([]string, 0)
   172  	for i := 0; i < len(ctp.sctLogs); i++ {
   173  		res := <-results
   174  		if res.err != nil {
   175  			errs = append(errs, res.err.Error())
   176  			if res.url != "" {
   177  				ctp.winnerCounter.WithLabelValues(res.url, failed).Inc()
   178  			}
   179  			continue
   180  		}
   181  		scts = append(scts, res.sct)
   182  		ctp.winnerCounter.WithLabelValues(res.url, succeeded).Inc()
   183  		if len(scts) >= 2 {
   184  			return scts, nil
   185  		}
   186  	}
   187  
   188  	// If we made it to the end of that loop, that means we never got two SCTs
   189  	// to return. Error out instead.
   190  	if ctx.Err() != nil {
   191  		// We timed out (the calling function returned and canceled our context),
   192  		// thereby causing all of our getOne sub-goroutines to be cancelled.
   193  		return nil, berrors.MissingSCTsError("failed to get 2 SCTs before ctx finished: %s", ctx.Err())
   194  	}
   195  	return nil, berrors.MissingSCTsError("failed to get 2 SCTs, got %d error(s): %s", len(errs), strings.Join(errs, "; "))
   196  }
   197  
   198  // submitAllBestEffort submits the given certificate or precertificate to every
   199  // log ("informational" for precerts, "final" for certs) configured in the policy.
   200  // It neither waits for these submission to complete, nor tracks their success.
   201  func (ctp *CTPolicy) submitAllBestEffort(blob core.CertDER, isPrecert bool, expiry time.Time) {
   202  	logs := ctp.finalLogs
   203  	if isPrecert {
   204  		logs = ctp.infoLogs
   205  	}
   206  
   207  	for _, group := range logs {
   208  		for _, log := range group {
   209  			if log.StartInclusive.After(expiry) || log.EndExclusive.Equal(expiry) || log.EndExclusive.Before(expiry) {
   210  				continue
   211  			}
   212  
   213  			go func(log loglist.Log) {
   214  				_, err := ctp.pub.SubmitToSingleCTWithResult(
   215  					context.Background(),
   216  					&pubpb.Request{
   217  						LogURL:       log.Url,
   218  						LogPublicKey: log.Key,
   219  						Der:          blob,
   220  						Precert:      isPrecert,
   221  					},
   222  				)
   223  				if err != nil {
   224  					ctp.log.Warningf("ct submission of cert to log %q failed: %s", log.Url, err)
   225  				}
   226  			}(log)
   227  		}
   228  	}
   229  
   230  }
   231  
   232  // submitPrecertInformational submits precertificates to any configured
   233  // "informational" logs, but does not care about success or returned SCTs.
   234  func (ctp *CTPolicy) submitPrecertInformational(cert core.CertDER, expiration time.Time) {
   235  	ctp.submitAllBestEffort(cert, true, expiration)
   236  }
   237  
   238  // SubmitFinalCert submits finalized certificates created from precertificates
   239  // to any configured "final" logs, but does not care about success.
   240  func (ctp *CTPolicy) SubmitFinalCert(cert core.CertDER, expiration time.Time) {
   241  	ctp.submitAllBestEffort(cert, false, expiration)
   242  }
   243  

View as plain text