From ad29d0f3cd3c156921511663f58a3aca913fbafe Mon Sep 17 00:00:00 2001 From: Al Cutter Date: Mon, 16 Feb 2026 13:19:59 +0000 Subject: [PATCH] Add max entry size limit --- README.md | 3 +++ append_lifecycle.go | 28 ++++++++++++++++++++++++++++ append_lifecycle_test.go | 36 ++++++++++++++++++++++++++++++++++++ ct_only.go | 5 +++++ 4 files changed, 72 insertions(+) diff --git a/README.md b/README.md index 2522e1f7b..4d5fbead6 100644 --- a/README.md +++ b/README.md @@ -369,6 +369,9 @@ configured using the [`tessera.NewAppendOptions`](https://pkg.go.dev/github.com/ This is described above in [Constructing the Appender](#constructing-the-appender). +Note that entries are limited to 64KB in size by the [tlog-tiles][] spec, with the exception that when Tessera is configured for use +with Static CT via the `WithCTLayout` option, entries are then limited to 256KB. + See more details in the [Lifecycle Design: Appender](https://github.com/transparency-dev/tessera/blob/main/docs/design/lifecycle.md#appender). ### Migration Target diff --git a/append_lifecycle.go b/append_lifecycle.go index 3bd480509..f3d2aad57 100644 --- a/append_lifecycle.go +++ b/append_lifecycle.go @@ -55,6 +55,9 @@ const ( DefaultAntispamInMemorySize = 256 << 10 // DefaultWitnessTimeout is the default maximum time to wait for responses from configured witnesses. DefaultWitnessTimeout = 5 * time.Second + + // DefaultEntrySizeLimit is the maximum possible size of data for a single entry, as specified by C2SP tlog-tiles. + DefaultEntrySizeLimit = 1<<16 - 1 ) var ( @@ -269,6 +272,7 @@ func NewAppender(ctx context.Context, d Driver, opts *AppendOptions) (*Appender, for i := len(opts.addDecorators) - 1; i >= 0; i-- { a.Add = opts.addDecorators[i](a.Add) } + a.Add = entrySizeLimitDecorator(a.Add, opts.maxEntrySize) sd := &integrationStats{} a.Add = sd.statsDecorator(a.Add) for _, f := range opts.followers { @@ -299,6 +303,19 @@ func NewAppender(ctx context.Context, d Driver, opts *AppendOptions) (*Appender, return a, t.Shutdown, r, nil } +// entrySizeLimitDecorator wraps a delegate AddFn with logic which will return an error if +// it is called with an entry larger than the provided max size. +func entrySizeLimitDecorator(d AddFn, maxSize uint) AddFn { + return func(ctx context.Context, entry *Entry) IndexFuture { + if sz := uint(len(entry.Data())); sz > maxSize { + return func() (Index, error) { + return Index{}, fmt.Errorf("entry data too large (%d > %d)", sz, maxSize) + } + } + return d(ctx, entry) + } +} + // memoizeFuture wraps an AddFn delegate with logic to ensure that the delegate is called at most // once. func memoizeFuture(delegate IndexFuture) IndexFuture { @@ -543,6 +560,7 @@ func NewAppendOptions() *AppendOptions { batchMaxSize: DefaultBatchMaxSize, batchMaxAge: DefaultBatchMaxAge, entriesPath: layout.EntriesPath, + maxEntrySize: DefaultEntrySizeLimit, bundleIDHasher: defaultIDHasher, checkpointInterval: DefaultCheckpointInterval, checkpointRepublishInterval: DefaultCheckpointRepublishInterval, @@ -564,6 +582,16 @@ type AppendOptions struct { // EntriesPath knows how to format entry bundle paths. entriesPath func(n uint64, p uint8) string + + // maxEntrySize is the maximum permitted size of individual entries to be added. + // By default this will correspond with the limit set out in the C2SP tlog-tiles spec, however + // it will be overridden when Tessera is being used to implement static-ct. + // + // Note that storage implementations MAY enforce their own, lower, limits on entry size due to + // storage-level constraints. This should be handled internally by those storage implementations, + // e.g. in their Add() function implementations. + maxEntrySize uint + // bundleIDHasher knows how to create antispam leaf identities for entries in a serialised bundle. bundleIDHasher func([]byte) ([][]byte, error) diff --git a/append_lifecycle_test.go b/append_lifecycle_test.go index c64baf893..fe16b0d86 100644 --- a/append_lifecycle_test.go +++ b/append_lifecycle_test.go @@ -105,6 +105,42 @@ func TestAppendOptionsValid(t *testing.T) { } } +func TestMaxEntrySize(t *testing.T) { + d := func(_ context.Context, e *Entry) IndexFuture { + return func() (Index, error) { + return Index{}, nil + } + } + + const limit = 128 + add := entrySizeLimitDecorator(d, limit) + + for _, test := range []struct { + name string + size uint + wantErr bool + }{ + { + name: "< limit", + size: limit - 1, + }, { + name: "== limit", + size: limit, + }, { + name: "> limit", + size: limit + 1, + wantErr: true, + }, + } { + t.Run(test.name, func(t *testing.T) { + _, err := add(t.Context(), NewEntry(make([]byte, test.size)))() + if gotErr := err != nil; gotErr != test.wantErr { + t.Fatalf("Got err %q, want err? %T", err, test.wantErr) + } + }) + } +} + func mustCreateSigner(t *testing.T, k string) note.Signer { t.Helper() s, err := note.NewSigner(k) diff --git a/ct_only.go b/ct_only.go index 37532bcaf..ca9786fdb 100644 --- a/ct_only.go +++ b/ct_only.go @@ -25,6 +25,10 @@ import ( "golang.org/x/crypto/cryptobyte" ) +// ctEntrySizeLimit is the maximum permitted serialized entry size when Tessera is configured for static-ct logs. +// Note that storage implementations MAY impose a lower limit due to infrastructure limitations. +const ctEntrySizeLimit = 256 << 10 + // NewCertificateTransparencyAppender returns a function which knows how to add a CT-specific entry type to the log. // // This entry point MUST ONLY be used for CT logs participating in the CT ecosystem. @@ -60,6 +64,7 @@ func convertCTEntry(e *ctonly.Entry) *Entry { func (o *AppendOptions) WithCTLayout() *AppendOptions { o.entriesPath = ctEntriesPath o.bundleIDHasher = ctBundleIDHasher + o.maxEntrySize = ctEntrySizeLimit return o }