Skip to content

Commit

Permalink
Removed the Verifier from Tessera
Browse files Browse the repository at this point in the history
This has instead been replaced with unsafe parsing of the checkpoints that skips the signature verification. This is ONLY safe becase all usages are inside the same trust boundary that signed the checkpoint.

This fixes #191.
  • Loading branch information
mhutchinson committed Nov 12, 2024
1 parent afdb129 commit 77471c9
Show file tree
Hide file tree
Showing 14 changed files with 172 additions and 78 deletions.
16 changes: 3 additions & 13 deletions await.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,14 @@
package tessera

import (
"bytes"
"context"
"fmt"
"strconv"
"sync"
"time"

"container/list"

"github.com/transparency-dev/trillian-tessera/internal/parse"
"k8s.io/klog/v2"
)

Expand Down Expand Up @@ -116,18 +115,9 @@ func (a *IntegrationAwaiter) pollLoop(ctx context.Context, readCheckpoint func(c
a.releaseClientsErr(fmt.Errorf("readCheckpoint: %v", err))
continue
}
// Parsing a checkpoint like this is only acceptable because we're in the same binary as the
// log implementation that generated it and thus we can safely assume it's a well formed and
// validly signed checkpoint. Anyone copying similar logic into client code will get hurt.
parts := bytes.SplitN(rawCp, []byte{'\n'}, 3)
if want, got := 3, len(parts); want != got {
a.releaseClientsErr(fmt.Errorf("invalid checkpoint: %q", rawCp))
continue
}
sizeStr := string(parts[1])
size, err := strconv.ParseUint(sizeStr, 10, 64)
_, size, err := parse.CheckpointUnsafe(rawCp)
if err != nil {
a.releaseClientsErr(fmt.Errorf("failed to turn checkpoint size of %q into uint64: %v", sizeStr, err))
a.releaseClientsErr(err)
continue
}
a.releaseClients(size, rawCp)
Expand Down
4 changes: 2 additions & 2 deletions await_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func TestAwait(t *testing.T) {
fIndex: 2,
fErr: nil,
fDelay: 0,
cpBody: []byte("origin\n3\nthisisdefinitelyahash\n"),
cpBody: []byte("origin\n3\nqINS1GRFhWHwdkUeqLEoP4yEMkTBBzxBkGwGQlVlVcs=\n"),
cpErr: nil,
wantErr: false,
},
Expand Down Expand Up @@ -90,7 +90,7 @@ func TestAwait(t *testing.T) {
fIndex: 2,
fErr: nil,
fDelay: 0,
cpBody: []byte("origin\n3\nthisisdefinitelyahash\n"),
cpBody: []byte("origin\n3\nqINS1GRFhWHwdkUeqLEoP4yEMkTBBzxBkGwGQlVlVcs=\n"),
cpErr: nil,
cpDelay: 40 * time.Millisecond,
wantErr: false,
Expand Down
4 changes: 2 additions & 2 deletions cmd/conformance/gcp/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,12 @@ func main() {
flag.Parse()
ctx := context.Background()

s, v, a := signerFromFlags()
s, _, a := signerFromFlags()

// Create our Tessera storage backend:
gcpCfg := storageConfigFromFlags()
storage, err := gcp.New(ctx, gcpCfg,
tessera.WithCheckpointSignerVerifier(s, v, a...),
tessera.WithCheckpointSigner(s, a...),
tessera.WithBatching(1024, time.Second),
tessera.WithPushback(10*4096),
)
Expand Down
6 changes: 2 additions & 4 deletions cmd/conformance/mysql/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,9 @@ func main() {

db := createDatabaseOrDie(ctx)
noteSigner, additionalSigners := createSignersOrDie()
vkey, noteVerifier := createVerifierOrDie()

// Initialise the Tessera MySQL storage
storage, err := mysql.New(ctx, db, tessera.WithCheckpointSignerVerifier(noteSigner, noteVerifier, additionalSigners...))
storage, err := mysql.New(ctx, db, tessera.WithCheckpointSigner(noteSigner, additionalSigners...))
if err != nil {
klog.Exitf("Failed to create new MySQL storage: %v", err)
}
Expand Down Expand Up @@ -89,8 +88,7 @@ func main() {
// TODO(mhutchinson): Change the listen flag to just a port, or fix up this address formatting
klog.Infof("Environment variables useful for accessing this log:\n"+
"export WRITE_URL=http://localhost%s/ \n"+
"export READ_URL=http://localhost%s/ \n"+
"export LOG_PUBLIC_KEY=%s", *listen, *listen, vkey)
"export READ_URL=http://localhost%s/ \n", *listen, *listen)
// Serve HTTP requests until the process is terminated
if err := http.ListenAndServe(*listen, http.DefaultServeMux); err != nil {
klog.Exitf("ListenAndServe: %v", err)
Expand Down
6 changes: 2 additions & 4 deletions cmd/conformance/posix/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,10 @@ func main() {
ctx := context.Background()

// Gather the info needed for reading/writing checkpoints
vkey, v := getVerifierOrDie()
s, a := getSignersOrDie()

// Create the Tessera POSIX storage, using the directory from the --storage_dir flag
storage, err := posix.New(ctx, *storageDir, *initialise, tessera.WithCheckpointSignerVerifier(s, v, a...), tessera.WithBatching(256, time.Second))
storage, err := posix.New(ctx, *storageDir, *initialise, tessera.WithCheckpointSigner(s, a...), tessera.WithBatching(256, time.Second))
if err != nil {
klog.Exitf("Failed to construct storage: %v", err)
}
Expand Down Expand Up @@ -90,8 +89,7 @@ func main() {
// TODO(mhutchinson): Change the listen flag to just a port, or fix up this address formatting
klog.Infof("Environment variables useful for accessing this log:\n"+
"export WRITE_URL=http://localhost%s/ \n"+
"export READ_URL=http://localhost%s/ \n"+
"export LOG_PUBLIC_KEY=%s", *listen, *listen, vkey)
"export READ_URL=http://localhost%s/ \n", *listen, *listen)
// Run the HTTP server with the single handler and block until this is terminated
if err := http.ListenAndServe(*listen, http.DefaultServeMux); err != nil {
klog.Exitf("ListenAndServe: %v", err)
Expand Down
5 changes: 2 additions & 3 deletions cmd/examples/posix-oneshot/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,12 @@ func main() {
ctx := context.Background()

// Gather the info needed for reading/writing checkpoints
v := getVerifierOrDie()
s := getSignerOrDie()

// Handle the case where no entries are to be added.
if len(*entries) == 0 {
if *initialise {
_, err := posix.New(ctx, *storageDir, *initialise, tessera.WithCheckpointSignerVerifier(s, v))
_, err := posix.New(ctx, *storageDir, *initialise, tessera.WithCheckpointSigner(s))
if err != nil {
klog.Exitf("Failed to initialise storage: %v", err)
}
Expand All @@ -79,7 +78,7 @@ func main() {
// The options provide the checkpoint signer & verifier, and batch options.
// In this case, we want to create a single batch containing all of the leaves being added in order to
// add all of these leaves without creating any intermediate checkpoints.
st, err := posix.New(ctx, *storageDir, *initialise, tessera.WithCheckpointSignerVerifier(s, v), tessera.WithBatching(uint(len(filesToAdd)), time.Second))
st, err := posix.New(ctx, *storageDir, *initialise, tessera.WithCheckpointSigner(s), tessera.WithBatching(uint(len(filesToAdd)), time.Second))
if err != nil {
klog.Exitf("Failed to construct storage: %v", err)
}
Expand Down
3 changes: 1 addition & 2 deletions internal/options/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ type EntriesPathFunc func(n, logSize uint64) string

// StorageOptions holds optional settings for all storage implementations.
type StorageOptions struct {
NewCP NewCPFunc
ParseCP ParseCPFunc
NewCP NewCPFunc

BatchMaxAge time.Duration
BatchMaxSize uint
Expand Down
47 changes: 47 additions & 0 deletions internal/parse/parse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright 2024 The Tessera authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package parse contains internal methods for parsing data structures quickly,
// if unsafely. This is a bit of a utility package which is an anti-pattern, but
// this code is critical enough that it should be reused, tested, and benchmarked
// rather than copied around willy nilly.
// If a better home becomes available, feel free to move the contents elsewhere.
package parse

import (
"bytes"
"fmt"
"strconv"
)

// CheckpointUnsafe parses a checkpoint without performing any signature verification.
// This is intended to be as fast as possible, but sacrifices safety because it skips verifying
// the note signature.
//
// Parsing a checkpoint like this is only acceptable in the same binary as the
// log implementation that generated it and thus we can safely assume it's a well formed and
// validly signed checkpoint. Anyone copying similar logic into client code will get hurt.
func CheckpointUnsafe(rawCp []byte) (string, uint64, error) {
parts := bytes.SplitN(rawCp, []byte{'\n'}, 3)
if want, got := 3, len(parts); want != got {
return "", 0, fmt.Errorf("invalid checkpoint: %q", rawCp)
}
origin := string(parts[0])
sizeStr := string(parts[1])
size, err := strconv.ParseUint(sizeStr, 10, 64)
if err != nil {
return "", 0, fmt.Errorf("failed to turn checkpoint size of %q into uint64: %v", sizeStr, err)
}
return origin, size, nil
}
83 changes: 83 additions & 0 deletions internal/parse/parse_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright 2024 The Tessera authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package parse_test

import (
"testing"

"github.com/transparency-dev/trillian-tessera/internal/parse"
)

func TestCheckpointUnsafe(t *testing.T) {
testCases := []struct {
desc string
cp string
wantOrigin string
wantSize uint64
wantErr bool
}{
{
desc: "happy checkpoint",
cp: "original.example.com\n42\nqINS1GRFhWHwdkUeqLEoP4yEMkTBBzxBkGwGQlVlVcs=\n",
wantOrigin: "original.example.com",
wantSize: 42,
},
{
desc: "Negative size",
cp: "original.example.com\n-42\nqINS1GRFhWHwdkUeqLEoP4yEMkTBBzxBkGwGQlVlVcs=\n",
wantErr: true,
},
{
desc: "Bad hash (passes because hashes are not checked)",
cp: "original.example.com\n42\nthisisnotright\n",
wantOrigin: "original.example.com",
wantSize: 42,
},
{
desc: "Empty origin",
cp: "\n42\nthisisnotright\n",
wantOrigin: "",
wantSize: 42,
},
{
desc: "No origin",
cp: "42\nthisisnotright\n",
wantErr: true,
},
}
for _, tC := range testCases {
t.Run(tC.desc, func(t *testing.T) {
origin, size, err := parse.CheckpointUnsafe([]byte(tC.cp))
if gotErr := err != nil; gotErr != tC.wantErr {
t.Fatalf("gotErr != wantErr (%t != %t): %v", gotErr, tC.wantErr, err)
}
if tC.wantErr {
return
}
if tC.wantOrigin != origin {
t.Errorf("origin: got != want (%v != %v)", origin, tC.wantOrigin)
}
if tC.wantSize != size {
t.Errorf("size : got != want (%v != %v)", size, tC.wantSize)
}
})
}
}

func BenchmarkCheckpointUnsafe(b *testing.B) {
cpRaw := []byte("go.sum database tree\n31700353\nqINS1GRFhWHwdkUeqLEoP4yEMkTBBzxBkGwGQlVlVcs=\n\n— sum.golang.org Az3grnmrIUEDFqHzAElIQCPNoRFRAAdFo47fooyWKMHb89k11GJh5zHIfNCOBmwn/C3YI8oW9/C8DJ87F61QqspBYwM=")
for i := 0; i < b.N; i++ {
parse.CheckpointUnsafe(cpRaw)

Check failure on line 81 in internal/parse/parse_test.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `parse.CheckpointUnsafe` is not checked (errcheck)
}
}
20 changes: 5 additions & 15 deletions log.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,10 @@ var ErrPushback = errors.New("too many unintegrated entries")
// some point in the future, and as such will block when called if the data isn't yet available.
type IndexFuture func() (uint64, error)

// WithCheckpointSignerVerifier is an option for setting the note signer and verifier to use when creating and parsing checkpoints.
// WithCheckpointSigner is an option for setting the note signer and verifier to use when creating and parsing checkpoints.
//
// A primary signer and verifier must be provided:
// A primary signer must be provided:
// - the primary signer is the "canonical" signing identity which should be used when creating new checkpoints.
// - the primary verifier is the verifier for the "canonical" identity which signed the _latest_ checkpoint.
// Note that while for the most-part this signer and verifier will relate to the same key, this would not be the case when rolling keys.
//
// Zero or more dditional signers may also be provided.
// This enables cases like:
Expand All @@ -54,12 +52,12 @@ type IndexFuture func() (uint64, error)
// When providing additional signers, their names MUST be identical to the primary signer name, and this name will be used
// as the checkpoint Origin line.
//
// Checkpoints signed by these signer(s) and verified by the provided verifier will be standard checkpoints as defined by https://c2sp.org/tlog-checkpoint.
func WithCheckpointSignerVerifier(s note.Signer, v note.Verifier, additionalSigners ...note.Signer) func(*options.StorageOptions) {
// Checkpoints signed by these signer(s) will be standard checkpoints as defined by https://c2sp.org/tlog-checkpoint.
func WithCheckpointSigner(s note.Signer, additionalSigners ...note.Signer) func(*options.StorageOptions) {
origin := s.Name()
for _, signer := range additionalSigners {
if origin != signer.Name() {
klog.Exitf("WithCheckpointSignerVerifier: additional signer name (%q) does not match primary signer name (%q)", signer.Name(), origin)
klog.Exitf("WithCheckpointSigner: additional signer name (%q) does not match primary signer name (%q)", signer.Name(), origin)
}

}
Expand All @@ -83,14 +81,6 @@ func WithCheckpointSignerVerifier(s note.Signer, v note.Verifier, additionalSign
}
return n, nil
}

o.ParseCP = func(raw []byte) (*f_log.Checkpoint, error) {
cp, _, _, err := f_log.ParseCheckpoint(raw, v.Name(), v)
if err != nil {
return nil, fmt.Errorf("f_log.ParseCheckpoint: %w", err)
}
return cp, nil
}
}
}

Expand Down
7 changes: 1 addition & 6 deletions storage/gcp/gcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ const (
// Storage is a GCP based storage implementation for Tessera.
type Storage struct {
newCP options.NewCPFunc
parseCP options.ParseCPFunc
entriesPath options.EntriesPathFunc

sequencer sequencer
Expand Down Expand Up @@ -130,7 +129,6 @@ func New(ctx context.Context, cfg Config, opts ...func(*options.StorageOptions))
},
sequencer: seq,
newCP: opt.NewCP,
parseCP: opt.ParseCP,
entriesPath: opt.EntriesPath,
}
r.queue = storage.NewQueue(ctx, opt.BatchMaxAge, opt.BatchMaxSize, r.sequencer.assignEntries)
Expand Down Expand Up @@ -191,7 +189,7 @@ func (s *Storage) get(ctx context.Context, path string) ([]byte, error) {

// init ensures that the storage represents a log in a valid state.
func (s *Storage) init(ctx context.Context) error {
cpRaw, err := s.get(ctx, layout.CheckpointPath)
_, err := s.get(ctx, layout.CheckpointPath)
if err != nil {
if errors.Is(err, gcs.ErrObjectNotExist) {
// No checkpoint exists, do a forced (possibly empty) integration to create one in a safe
Expand All @@ -206,9 +204,6 @@ func (s *Storage) init(ctx context.Context) error {
}
return fmt.Errorf("failed to read checkpoint: %v", err)
}
if _, err = s.parseCP(cpRaw); err != nil {
return fmt.Errorf("Found invalid existing checpoint file: %v\ncheckpoint contents:\n%v", err, string(cpRaw))
}

return nil
}
Expand Down
Loading

0 comments on commit 77471c9

Please sign in to comment.