Skip to content

Commit

Permalink
sunlight,internal/ctlog: add chain fingerprints to data tiles
Browse files Browse the repository at this point in the history
  • Loading branch information
FiloSottile committed Jun 5, 2024
1 parent 143ac0a commit db069f9
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 96 deletions.
9 changes: 2 additions & 7 deletions internal/ctlog/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func (l *Log) CloseCache() error {

func (l *Log) cacheGet(leaf *PendingLogEntry) (*sunlight.LogEntry, error) {
defer prometheus.NewTimer(l.m.CacheGetDuration).ObserveDuration()
h := leaf.cacheHash()
h := computeCacheHash(leaf.Certificate, leaf.IsPrecert, leaf.IssuerKeyHash)
var se *sunlight.LogEntry
err := sqlitex.Exec(l.cacheRead, "SELECT timestamp, leaf_index FROM cache WHERE key = ?",
func(stmt *sqlite.Stmt) error {
Expand All @@ -60,12 +60,7 @@ func (l *Log) cachePut(entries []*sunlight.LogEntry) (err error) {
defer prometheus.NewTimer(l.m.CachePutDuration).ObserveDuration()
defer sqlitex.Save(l.cacheWrite)(&err)
for _, se := range entries {
h := (&PendingLogEntry{
Certificate: se.Certificate,
IsPrecert: se.IsPrecert,
IssuerKeyHash: se.IssuerKeyHash,
PreCertificate: se.PreCertificate,
}).cacheHash()
h := computeCacheHash(se.Certificate, se.IsPrecert, se.IssuerKeyHash)
err := sqlitex.Exec(l.cacheWrite, "INSERT INTO cache (key, timestamp, leaf_index) VALUES (?, ?, ?)",
nil, h[:], se.Timestamp, se.LeafIndex)
if err != nil {
Expand Down
86 changes: 73 additions & 13 deletions internal/ctlog/ctlog.go
Original file line number Diff line number Diff line change
Expand Up @@ -392,34 +392,40 @@ type PendingLogEntry struct {
Certificate []byte
IsPrecert bool
IssuerKeyHash [32]byte
Issuers [][]byte
PreCertificate []byte
}

func (e *PendingLogEntry) asLogEntry(idx, timestamp int64) *sunlight.LogEntry {
fingerprints := make([][32]byte, 0, len(e.Issuers))
for _, i := range e.Issuers {
fingerprints = append(fingerprints, sha256.Sum256(i))
}
return &sunlight.LogEntry{
Certificate: e.Certificate,
IsPrecert: e.IsPrecert,
IssuerKeyHash: e.IssuerKeyHash,
PreCertificate: e.PreCertificate,
LeafIndex: idx,
Timestamp: timestamp,
Certificate: e.Certificate,
IsPrecert: e.IsPrecert,
IssuerKeyHash: e.IssuerKeyHash,
ChainFingerprints: fingerprints,
PreCertificate: e.PreCertificate,
LeafIndex: idx,
Timestamp: timestamp,
}
}

type cacheHash [16]byte // birthday bound of 2⁴⁸ entries with collision chance 2⁻³²

func (e *PendingLogEntry) cacheHash() cacheHash {
func computeCacheHash(Certificate []byte, IsPrecert bool, IssuerKeyHash [32]byte) cacheHash {
b := &cryptobyte.Builder{}
if !e.IsPrecert {
if !IsPrecert {
b.AddUint16(0 /* entry_type = x509_entry */)
b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) {
b.AddBytes(e.Certificate)
b.AddBytes(Certificate)
})
} else {
b.AddUint16(1 /* entry_type = precert_entry */)
b.AddBytes(e.IssuerKeyHash[:])
b.AddBytes(IssuerKeyHash[:])
b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) {
b.AddBytes(e.Certificate)
b.AddBytes(Certificate)
})
}
h := sha256.Sum256(b.BytesOrPanic())
Expand Down Expand Up @@ -459,11 +465,24 @@ var errPoolFull = fmtErrorf("rate limited")
// deduplication cache. It returns a function that will wait until the pool is
// sequenced and return the sequenced leaf, as well as the source of the
// sequenced leaf (pool or cache if deduplicated, sequencer otherwise).
func (l *Log) addLeafToPool(leaf *PendingLogEntry) (f waitEntryFunc, source string) {
func (l *Log) addLeafToPool(ctx context.Context, leaf *PendingLogEntry) (f waitEntryFunc, source string) {
// We could marginally more efficiently do uploadIssuer after checking the
// caches, but it's simpler for the the block below to be under a single
// poolMu lock, and uploadIssuer goes to the network so we don't want to
// cause poolMu contention.
for _, issuer := range leaf.Issuers {
if err := l.uploadIssuer(ctx, issuer); err != nil {
l.c.Log.ErrorContext(ctx, "failed to upload issuer", "err", err)
return func(ctx context.Context) (*sunlight.LogEntry, error) {
return nil, fmtErrorf("failed to upload issuer: %w", err)
}, "issuer"
}
}

l.poolMu.Lock()
defer l.poolMu.Unlock()
p := l.currentPool
h := leaf.cacheHash()
h := computeCacheHash(leaf.Certificate, leaf.IsPrecert, leaf.IssuerKeyHash)
if f, ok := p.byHash[h]; ok {
return f, "pool"
}
Expand Down Expand Up @@ -505,6 +524,47 @@ func (l *Log) addLeafToPool(leaf *PendingLogEntry) (f waitEntryFunc, source stri
return f, "sequencer"
}

func (l *Log) uploadIssuer(ctx context.Context, issuer []byte) error {
fingerprint := sha256.Sum256(issuer)

l.issuersMu.RLock()
found := l.issuers[fingerprint]
l.issuersMu.RUnlock()
if found {
return nil
}

l.issuersMu.Lock()
defer l.issuersMu.Unlock()

if l.issuers[fingerprint] {
return nil
}

path := fmt.Sprintf("issuer/%x", fingerprint)
l.c.Log.InfoContext(ctx, "observed new issuer", "path", path)

// First we try to download and check the issuer from the backend.
// If it's not there, we upload it.

old, err := l.c.Backend.Fetch(ctx, path)
if err != nil {
upErr := l.c.Backend.Upload(ctx, path, issuer, optsIssuer)
l.c.Log.InfoContext(ctx, "uploaded issuer", "path", path, "err", upErr, "fetchErr", err, "size", len(issuer))
if upErr != nil {
return fmtErrorf("upload error: %w; fetch error: %v", upErr, err)
}
} else {
if !bytes.Equal(old, issuer) {
return fmtErrorf("invalid existing issuer: %x", old)
}
}

l.issuers[fingerprint] = true
l.m.Issuers.Set(float64(len(l.issuers)))
return nil
}

func (l *Log) RunSequencer(ctx context.Context, period time.Duration) (err error) {
// If the sequencer stops, return errors for all pending and future leaves.
defer func() {
Expand Down
9 changes: 7 additions & 2 deletions internal/ctlog/ctlog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,11 +158,11 @@ func TestSequenceUploadPaths(t *testing.T) {
tl := NewEmptyTestLog(t)

for i := int64(0); i < tileWidth+5; i++ {
addCertificate(t, tl)
addCertificateWithSeed(t, tl, i)
}
fatalIfErr(t, tl.Log.Sequence())
for i := int64(0); i < tileWidth+10; i++ {
addCertificate(t, tl)
addCertificateWithSeed(t, tl, 1000+i)
}
fatalIfErr(t, tl.Log.Sequence())
tl.CheckLog()
Expand All @@ -176,6 +176,11 @@ func TestSequenceUploadPaths(t *testing.T) {

expected := []string{
"checkpoint",
"issuer/1b48a2acbba79932d3852ccde41197f678256f3c2a280e9edf9aad272d6e9c92",
"issuer/559aead08264d5795d3909718cdd05abd49572e84fe55590eef31a88a08fdffd",
"issuer/6b23c0d5f35d1b11f9b683f0b0a617355deb11277d91ae091d399c655b87940d",
"issuer/81365bbc90b5b3991c762eebada7c6d84d1e39a0a1d648cb4fe5a9890b089da8",
"issuer/df7e70e5021544f4834bbee64a9e3789febc4be81470df629cad6ddb03320a5c",
"tile/0/000",
"tile/0/001",
"tile/0/001.p/5",
Expand Down
12 changes: 10 additions & 2 deletions internal/ctlog/export_test.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
package ctlog

import "context"
import (
"context"

"filippo.io/sunlight"
)

func (l *Log) AddLeafToPool(e *PendingLogEntry) (waitEntryFunc, string) {
return l.addLeafToPool(e)
return l.addLeafToPool(context.Background(), e)
}

func (l *Log) Sequence() error {
return l.sequence(context.Background())
}

func (e *PendingLogEntry) AsLogEntry(idx, timestamp int64) *sunlight.LogEntry {
return e.asLogEntry(idx, timestamp)
}

func SetTimeNowUnixMilli(f func() int64) {
timeNowUnixMilli = f
}
Expand Down
67 changes: 10 additions & 57 deletions internal/ctlog/http.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package ctlog

import (
"bytes"
"context"
"crypto/sha256"
"encoding/base64"
Expand Down Expand Up @@ -145,23 +144,25 @@ func (l *Log) addChainOrPreChain(ctx context.Context, reqBody io.ReadCloser, che
labels["issuer"] = x509util.NameToString(chain[0].Issuer)

e := &PendingLogEntry{Certificate: chain[0].Raw}
issuers := chain[1:]
for _, issuer := range chain[1:] {
e.Issuers = append(e.Issuers, issuer.Raw)
}
if isPrecert, err := ctfe.IsPrecertificate(chain[0]); err != nil {
l.c.Log.WarnContext(ctx, "invalid precertificate", "err", err, "body", body)
return nil, http.StatusBadRequest, fmtErrorf("invalid precertificate: %w", err)
} else if isPrecert {
labels["precert"] = "true"
if len(issuers) == 0 {
if len(chain) < 2 {
l.c.Log.WarnContext(ctx, "missing precertificate issuer", "err", err, "body", body)
return nil, http.StatusBadRequest, fmtErrorf("missing precertificate issuer")
}

var preIssuer *x509.Certificate
if ct.IsPreIssuer(issuers[0]) {
preIssuer = issuers[0]
if ct.IsPreIssuer(chain[1]) {
preIssuer = chain[1]
labels["preissuer"] = "true"
labels["issuer"] = x509util.NameToString(preIssuer.Issuer)
if len(issuers) == 1 {
if len(chain) < 3 {
l.c.Log.WarnContext(ctx, "missing precertificate signing certificate issuer", "err", err, "body", body)
return nil, http.StatusBadRequest, fmtErrorf("missing precertificate signing certificate issuer")
}
Expand All @@ -177,23 +178,16 @@ func (l *Log) addChainOrPreChain(ctx context.Context, reqBody io.ReadCloser, che
e.Certificate = defangedTBS
e.PreCertificate = chain[0].Raw
if preIssuer != nil {
e.IssuerKeyHash = sha256.Sum256(issuers[1].RawSubjectPublicKeyInfo)
e.IssuerKeyHash = sha256.Sum256(chain[2].RawSubjectPublicKeyInfo)
} else {
e.IssuerKeyHash = sha256.Sum256(issuers[0].RawSubjectPublicKeyInfo)
e.IssuerKeyHash = sha256.Sum256(chain[1].RawSubjectPublicKeyInfo)
}
}
if err := checkType(e); err != nil {
return nil, http.StatusBadRequest, err
}

for _, issuer := range issuers {
if err := l.uploadIssuer(ctx, issuer); err != nil {
l.c.Log.ErrorContext(ctx, "failed to upload issuer", "err", err, "body", body)
return nil, http.StatusInternalServerError, fmtErrorf("failed to upload issuer: %w", err)
}
}

waitLeaf, source := l.addLeafToPool(e)
waitLeaf, source := l.addLeafToPool(ctx, e)
labels["source"] = source
waitTimer := prometheus.NewTimer(l.m.AddChainWait)
seq, err := waitLeaf(ctx)
Expand Down Expand Up @@ -236,47 +230,6 @@ func (l *Log) addChainOrPreChain(ctx context.Context, reqBody io.ReadCloser, che
return rsp, http.StatusOK, nil
}

func (l *Log) uploadIssuer(ctx context.Context, issuer *x509.Certificate) error {
fingerprint := sha256.Sum256(issuer.Raw)

l.issuersMu.RLock()
found := l.issuers[fingerprint]
l.issuersMu.RUnlock()
if found {
return nil
}

l.issuersMu.Lock()
defer l.issuersMu.Unlock()

if l.issuers[fingerprint] {
return nil
}

path := fmt.Sprintf("issuer/%x", fingerprint)
l.c.Log.InfoContext(ctx, "observed new issuer", "issuer", x509util.NameToString(issuer.Subject), "path", path)

// First we try to download and check the issuer from the backend.
// If it's not there, we upload it.

old, err := l.c.Backend.Fetch(ctx, path)
if err != nil {
upErr := l.c.Backend.Upload(ctx, path, issuer.Raw, optsIssuer)
l.c.Log.InfoContext(ctx, "uploaded issuer", "path", path, "err", upErr, "fetchErr", err, "size", len(issuer.Raw))
if upErr != nil {
return fmtErrorf("upload error: %w; fetch error: %v", upErr, err)
}
} else {
if !bytes.Equal(old, issuer.Raw) {
return fmtErrorf("invalid existing issuer: %x", old)
}
}

l.issuers[fingerprint] = true
l.m.Issuers.Set(float64(len(l.issuers)))
return nil
}

func (l *Log) getRoots(rw http.ResponseWriter, r *http.Request) {
roots := l.c.Roots.RawCertificates()
var res struct {
Expand Down
47 changes: 41 additions & 6 deletions internal/ctlog/testlog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,37 @@ func (tl *TestLog) CheckLog() (sthTimestamp int64) {
if exp := leafHashes[idx]; got != exp {
t.Errorf("tile leaf entry %d hashes to %v, level 0 hash is %v", idx, got, exp)
}

if len(e.Certificate) == 0 {
t.Errorf("empty certificate at index %d", idx)
}
if e.IsPrecert {
if len(e.PreCertificate) == 0 {
t.Errorf("empty precertificate at index %d", idx)
}
if e.IssuerKeyHash == [32]byte{} {
t.Errorf("empty issuer key hash at index %d", idx)
}
} else {
if e.PreCertificate != nil {
t.Errorf("unexpected precertificate at index %d", idx)
}
if e.IssuerKeyHash != [32]byte{} {
t.Errorf("unexpected issuer key hash at index %d", idx)
}
}
for _, fp := range e.ChainFingerprints {
b, err := tl.Config.Backend.Fetch(context.Background(), fmt.Sprintf("issuer/%x", fp))
if err != nil {
t.Errorf("issuer %x not found", fp)
}
if len(b) == 0 {
t.Errorf("issuer %x is empty", fp)
}
if sha256.Sum256(b) != fp {
t.Errorf("issuer %x does not hash to %x", fp, fp)
}
}
}
if len(b) != 0 {
t.Errorf("invalid data tile %v: trailing data", tile)
Expand Down Expand Up @@ -240,12 +271,7 @@ func waitFuncWrapper(t testing.TB, le *ctlog.PendingLogEntry, expectSuccess bool
}
} else if err != nil {
t.Error(err)
} else if !reflect.DeepEqual(le, &ctlog.PendingLogEntry{
Certificate: se.Certificate,
IsPrecert: se.IsPrecert,
IssuerKeyHash: se.IssuerKeyHash,
PreCertificate: se.PreCertificate,
}) {
} else if !reflect.DeepEqual(se, le.AsLogEntry(se.LeafIndex, se.Timestamp)) {
t.Error("LogEntry is different")
}
return se, err
Expand All @@ -262,11 +288,19 @@ func addCertificate(t *testing.T, tl *TestLog) func(ctx context.Context) (*sunli
return addCertificateWithSeed(t, tl, mathrand.Int63()) // 2⁻³² chance of collision after 2¹⁶ entries
}

var chains = [][][]byte{
{[]byte("A"), []byte("rootX")},
{[]byte("B"), []byte("C"), []byte("rootX")},
{[]byte("A"), []byte("rootY")},
{},
}

func addCertificateWithSeed(t *testing.T, tl *TestLog, seed int64) func(ctx context.Context) (*sunlight.LogEntry, error) {
r := mathrand.New(mathrand.NewSource(seed))
e := &ctlog.PendingLogEntry{}
e.Certificate = make([]byte, r.Intn(4)+8)
r.Read(e.Certificate)
e.Issuers = chains[r.Intn(len(chains))]
f, _ := tl.Log.AddLeafToPool(e)
return waitFuncWrapper(t, e, true, f)
}
Expand Down Expand Up @@ -298,6 +332,7 @@ func addPreCertificateWithSeed(t *testing.T, tl *TestLog, seed int64) func(ctx c
e.PreCertificate = make([]byte, r.Intn(4)+1)
r.Read(e.PreCertificate)
r.Read(e.IssuerKeyHash[:])
e.Issuers = chains[r.Intn(len(chains))]
f, _ := tl.Log.AddLeafToPool(e)
return waitFuncWrapper(t, e, true, f)
}
Expand Down
Loading

0 comments on commit db069f9

Please sign in to comment.