Skip to content

Commit

Permalink
Track certificate inclusion (#40)
Browse files Browse the repository at this point in the history
This commit updates `ct-woodpecker` to track certificate inclusion with the monitored log.
The `certSubmitter` now stores the certs it submits into a sqlite backed DB. 
A separate `inclusionChecker` component periodically verifies that all of the certs 
submitted have been incorporated into the log.
  • Loading branch information
Roland Bracewell Shoemaker authored and cpu committed Jul 21, 2018
1 parent eb7eeb3 commit 6313966
Show file tree
Hide file tree
Showing 8 changed files with 955 additions and 7 deletions.
35 changes: 32 additions & 3 deletions monitor/cert_submitter.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,22 @@ import (
"strconv"
"time"

"github.com/letsencrypt/ct-woodpecker/pki"
"github.com/letsencrypt/ct-woodpecker/storage"

ct "github.com/google/certificate-transparency-go"
cttls "github.com/google/certificate-transparency-go/tls"
"github.com/jmhodges/clock"
"github.com/letsencrypt/ct-woodpecker/pki"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)

// certSubmitterStats is a type to hold the prometheus metrics used by
// a certSubmitter
type certSubmitterStats struct {
certSubmitLatency *prometheus.HistogramVec
certSubmitResults *prometheus.CounterVec
certSubmitLatency *prometheus.HistogramVec
certSubmitResults *prometheus.CounterVec
certStorageFailures *prometheus.CounterVec
}

var (
Expand All @@ -41,6 +45,10 @@ var (
Name: "cert_submit_results",
Help: "Count of results from submitting certificate chains to CT logs, sliced by status",
}, []string{"uri", "status", "precert"}),
certStorageFailures: promauto.NewCounterVec(prometheus.CounterOpts{
Name: "cert_storage_failures",
Help: "Count of failures to store submitted certificates and their SCTs",
}, []string{"type"}),
}
)

Expand Down Expand Up @@ -94,7 +102,9 @@ type certSubmitter struct {
clk clock.Clock
client monitorCTClient
logURI string
logID int64
stats *certSubmitterStats
db storage.Storage

stopChannel chan bool

Expand Down Expand Up @@ -205,6 +215,25 @@ func (c certSubmitter) submitCertificate(cert *x509.Certificate, isPreCert bool)
return
}

if c.db != nil {
sctBytes, err := cttls.Marshal(sct)
if err != nil {
c.logger.Printf("!!! Error serializing SCT: %s", err)
c.stats.certStorageFailures.WithLabelValues("marshalling").Inc()
return
}
err = c.db.AddCert(c.logID, &storage.SubmittedCert{
Cert: cert.Raw,
SCT: sctBytes,
Timestamp: sct.Timestamp,
})
if err != nil {
c.logger.Printf("!!! Error saving submitted cert: %s", err)
c.stats.certStorageFailures.WithLabelValues("storing").Inc()
return
}
}

ts := time.Unix(0, int64(sct.Timestamp)*int64(time.Millisecond))
sctAge := c.clk.Since(ts)

Expand Down
196 changes: 196 additions & 0 deletions monitor/inclusion_checker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package monitor

import (
"context"
"crypto/sha256"
"encoding/binary"
"fmt"
"log"
"time"

"github.com/google/certificate-transparency-go"
"github.com/google/certificate-transparency-go/tls"
"github.com/jmhodges/clock"
"github.com/letsencrypt/ct-woodpecker/storage"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)

var oldestUnseen = promauto.NewGauge(prometheus.GaugeOpts{
Name: "oldest_unincorporated_cert",
Help: "Number of seconds since the oldest SCT that we haven't matched to a log entry was received",
})

var inclusionErrors = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "inclusion_checker_errors",
Help: "Number of errors encountered while attempting to check for certificate inclusion",
}, []string{"type"})

type InclusionOptions struct {
Interval time.Duration
FetchBatchSize int64
MaxGetEntries int64
}

type inclusionClient interface {
GetSTH(context.Context) (*ct.SignedTreeHead, error)
GetEntries(ctx context.Context, start, end int64) ([]ct.LogEntry, error)
}

type inclusionChecker struct {
logger *log.Logger
client inclusionClient
logURI string
db storage.Storage
signatureChecker *ct.SignatureVerifier
clk clock.Clock
logID int64
stopChan chan bool

interval time.Duration
batchSize int64
maxGetEntries int64
}

func (ic *inclusionChecker) run() {
go func() {
ticker := time.NewTicker(ic.interval)
for {
select {
case <-ic.stopChan:
return
case <-ticker.C:
err := ic.checkInclusion()
if err != nil {
ic.logger.Printf("!!! Checking certificate inclusion failed: %s", err)
}
}
}
}()
}

func (ic *inclusionChecker) stop() {
ic.logger.Printf("Stopping %s inclusionChecker", ic.logURI)
ic.stopChan <- true
}

func (ic *inclusionChecker) checkInclusion() error {
current, err := ic.db.GetIndex(ic.logID)
if err != nil {
inclusionErrors.WithLabelValues("getIndex").Inc()
return fmt.Errorf("error getting current log index for %q: %s", ic.logURI, err)
}

certs, err := ic.db.GetUnseen(ic.logID)
if err != nil {
inclusionErrors.WithLabelValues("getUnseen").Inc()
return fmt.Errorf("error getting unseen certificates from %q: %s", ic.logURI, err)
}
if len(certs) == 0 {
// nothing to do, don't advance the index
return nil
}

sth, err := ic.client.GetSTH(context.Background())
if err != nil {
inclusionErrors.WithLabelValues("getSTH").Inc()
return fmt.Errorf("error getting STH from %q: %s", ic.logURI, err)
}
newHead, entries, err := ic.getEntries(current, int64(sth.TreeSize))
if err != nil {
inclusionErrors.WithLabelValues("getEntries").Inc()
return fmt.Errorf("error retrieving entries from %q: %s", ic.logURI, err)
}

err = ic.checkEntries(certs, entries)
if err != nil {
inclusionErrors.WithLabelValues("checkEntries").Inc()
return fmt.Errorf("error checking retrieved entries for %q: %s", ic.logURI, err)
}

err = ic.db.UpdateIndex(ic.logID, newHead)
if err != nil {
inclusionErrors.WithLabelValues("updateIndex").Inc()
return fmt.Errorf("error updating current index for %q: %s", ic.logURI, err)
}

return nil
}

func min(a, b int64) int64 {
if a < b {
return a
}
return b
}

func (ic *inclusionChecker) getEntries(start, end int64) (int64, []ct.LogEntry, error) {
if ic.maxGetEntries > 0 && end-start > ic.maxGetEntries {
end = start + ic.maxGetEntries
}
var allEntries []ct.LogEntry
for start <= end {
batchEnd := min(start+ic.batchSize, end)
entries, err := ic.client.GetEntries(context.Background(), start, batchEnd)
if err != nil {
return 0, nil, err
}
allEntries = append(allEntries, entries...)
start += int64(len(entries))
}
return start, allEntries, nil
}

func mapKey(cert []byte, timestamp uint64) [32]byte {
content := make([]byte, len(cert)+binary.MaxVarintLen64)
copy(content, cert)
binary.PutUvarint(content[len(cert):], timestamp)
return sha256.Sum256(content)
}

func (ic *inclusionChecker) checkEntries(certs []storage.SubmittedCert, entries []ct.LogEntry) error {
// Key structure for our lookup map is as follows: SHA256 hash of the certificate
// body concatenated with the byte encoding of the SCT timestamp. This prevents
// from having duplicate keys for duplicate submissions with differing SCTs.
lookup := make(map[[32]byte]storage.SubmittedCert)
for _, cert := range certs {
lookup[mapKey(cert.Cert, cert.Timestamp)] = cert
}
for _, entry := range entries {
var certData []byte
switch entry.Leaf.TimestampedEntry.EntryType {
case ct.X509LogEntryType:
certData = entry.X509Cert.Raw
case ct.PrecertLogEntryType:
certData = entry.Precert.Submitted.Data
}
h := mapKey(certData, entry.Leaf.TimestampedEntry.Timestamp)
if matching, found := lookup[h]; found {
var sct ct.SignedCertificateTimestamp
_, err := tls.Unmarshal(matching.SCT, &sct)
if err != nil {
return fmt.Errorf("error unmarshalling SCT: %s", err)
}
err = ic.signatureChecker.VerifySCTSignature(sct, entry)
if err != nil {
return fmt.Errorf("error verifying SCT signature: %s", err)
}
err = ic.db.MarkCertSeen(matching.ID, ic.clk.Now())
if err != nil {
return fmt.Errorf("error marking certificate as seen: %s", err)
}
delete(lookup, h)
}
}

var oldest uint64
for _, unseen := range lookup {
if oldest == 0 || unseen.Timestamp < oldest {
oldest = unseen.Timestamp
}
}
oldestTime := time.Unix(int64(oldest/1000), 0)
oldestUnseen.Set(ic.clk.Since(oldestTime).Seconds())

return nil
}

0 comments on commit 6313966

Please sign in to comment.