Skip to content

Commit

Permalink
Removed the Verifier from Tessera (#308)
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 authored Nov 12, 2024
1 parent afdb129 commit 498d39b
Show file tree
Hide file tree
Showing 17 changed files with 178 additions and 145 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
6 changes: 3 additions & 3 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 Expand Up @@ -211,7 +211,7 @@ func TestAwait_multiClient(t *testing.T) {
t.Errorf("function for %d failed: %v", i, err)
}
if i != index {
t.Errorf(fmt.Sprintf("got %d but expected %d", i, index))
t.Errorf("got %d but expected %d", i, index)
}
cp, _, _, err := log.ParseCheckpoint(cpRaw, "example.com/log/testdata", v)
if err != nil {
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
2 changes: 1 addition & 1 deletion cmd/conformance/mysql/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ docker run --name test-mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root -e MYSQL_D
### Starting

```sh
go run ./cmd/conformance/mysql --mysql_uri="root:root@tcp(localhost:3306)/test_tessera" --init_schema_path="./storage/mysql/schema.sql" --private_key_path="./cmd/conformance/mysql/docker/testdata/key" --public_key_path="./cmd/conformance/mysql/docker/testdata/key.pub"
go run ./cmd/conformance/mysql --mysql_uri="root:root@tcp(localhost:3306)/test_tessera" --init_schema_path="./storage/mysql/schema.sql" --private_key_path="./cmd/conformance/mysql/docker/testdata/key"
```

### Stopping
Expand Down
1 change: 0 additions & 1 deletion cmd/conformance/mysql/docker/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ services:
"--mysql_uri=tessera:tessera@tcp(tessera-conformance-mysql-db:3306)/tessera",
"--init_schema_path=/schema.sql",
"--private_key_path=/key",
"--public_key_path=/key.pub",
"--alsologtostderr",
"--v=1",
]
Expand Down
19 changes: 2 additions & 17 deletions cmd/conformance/mysql/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ var (
initSchemaPath = flag.String("init_schema_path", "", "Location of the schema file if database initialization is needed")
listen = flag.String("listen", ":2024", "Address:port to listen on")
privateKeyPath = flag.String("private_key_path", "", "Location of private key file")
publicKeyPath = flag.String("public_key_path", "", "Location of public key file")
additionalPrivateKeyPaths = []string{}
)

Expand All @@ -58,10 +57,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 +87,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 Expand Up @@ -131,18 +128,6 @@ func createSignerOrDie(s string) note.Signer {
return noteSigner
}

func createVerifierOrDie() (string, note.Verifier) {
rawPublicKey, err := os.ReadFile(*publicKeyPath)
if err != nil {
klog.Exitf("Failed to read public key file %q: %v", *publicKeyPath, err)
}
noteVerifier, err := note.NewVerifier(string(rawPublicKey))
if err != nil {
klog.Exitf("Failed to create new verifier: %v", err)
}
return string(rawPublicKey), noteVerifier
}

// configureTilesReadAPI adds the API methods from https://c2sp.org/tlog-tiles to the mux,
// routing the requests to the mysql storage.
// This method could be moved into the storage API as it's likely this will be
Expand Down
31 changes: 2 additions & 29 deletions cmd/conformance/posix/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ var (
storageDir = flag.String("storage_dir", "", "Root directory to store log data.")
initialise = flag.Bool("initialise", false, "Set when creating a new log to initialise the structure.")
listen = flag.String("listen", ":2025", "Address:port to listen on")
pubKeyFile = flag.String("public_key", "", "Location of public key file. If unset, uses the contents of the LOG_PUBLIC_KEY environment variable.")
privKeyFile = flag.String("private_key", "", "Location of private key file. If unset, uses the contents of the LOG_PRIVATE_KEY environment variable.")
additionalPrivateKeyFiles = []string{}
)
Expand All @@ -56,11 +55,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,38 +88,13 @@ 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)
}
}

// Read log public key from file or environment variable
func getVerifierOrDie() (string, note.Verifier) {
var pubKey string
var err error
if len(*pubKeyFile) > 0 {
pubKey, err = getKeyFile(*pubKeyFile)
if err != nil {
klog.Exitf("Unable to get public key: %q", err)
}
} else {
pubKey = os.Getenv("LOG_PUBLIC_KEY")
if len(pubKey) == 0 {
klog.Exit("Supply public key file path using --public_key or set LOG_PUBLIC_KEY environment variable")
}
}
// Check signatures
v, err := note.NewVerifier(pubKey)
if err != nil {
klog.Exitf("Failed to instantiate Verifier: %q", err)
}

return pubKey, v
}

func getSignersOrDie() (note.Signer, []note.Signer) {
s := getSignerOrDie()
a := []note.Signer{}
Expand Down
30 changes: 2 additions & 28 deletions cmd/examples/posix-oneshot/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ var (
storageDir = flag.String("storage_dir", "", "Root directory to store log data.")
initialise = flag.Bool("initialise", false, "Set when creating a new log to initialise the structure.")
entries = flag.String("entries", "", "File path glob of entries to add to the log.")
pubKeyFile = flag.String("public_key", "", "Location of public key file. If unset, uses the contents of the LOG_PUBLIC_KEY environment variable.")
privKeyFile = flag.String("private_key", "", "Location of private key file. If unset, uses the contents of the LOG_PRIVATE_KEY environment variable.")
)

Expand All @@ -57,13 +56,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 +77,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 Expand Up @@ -109,30 +107,6 @@ func main() {
// All futures have been resolved, which means the log is built and we can allow the process to terminate. Goodbye!
}

// Read log public key from file or environment variable
func getVerifierOrDie() note.Verifier {
var pubKey string
var err error
if len(*pubKeyFile) > 0 {
pubKey, err = getKeyFile(*pubKeyFile)
if err != nil {
klog.Exitf("Unable to get public key: %q", err)
}
} else {
pubKey = os.Getenv("LOG_PUBLIC_KEY")
if len(pubKey) == 0 {
klog.Exit("Supply public key file path using --public_key or set LOG_PUBLIC_KEY environment variable")
}
}
// Check signatures
v, err := note.NewVerifier(pubKey)
if err != nil {
klog.Exitf("Failed to instantiate Verifier: %q", err)
}

return v
}

// Read log private key from file or environment variable
func getSignerOrDie() note.Signer {
var privKey string
Expand Down
2 changes: 1 addition & 1 deletion docs/performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ The bottleneck comes from CPU usage of the `cmd/conformance/mysql` binary on the
1. Run `cmd/conformance/mysql`

```sh
instance:~/trillian-tessera$ go run ./cmd/conformance/mysql --mysql_uri="root:root@tcp(127.0.0.1:3306)/test_tessera" --init_schema_path="./storage/mysql/schema.sql" --private_key_path="./cmd/conformance/mysql/docker/testdata/key" --public_key_path="./cmd/conformance/mysql/docker/testdata/key.pub" --db_max_open_conns=1024 --db_max_idle_conns=512
instance:~/trillian-tessera$ go run ./cmd/conformance/mysql --mysql_uri="root:root@tcp(127.0.0.1:3306)/test_tessera" --init_schema_path="./storage/mysql/schema.sql" --private_key_path="./cmd/conformance/mysql/docker/testdata/key" --db_max_open_conns=1024 --db_max_idle_conns=512
```

1. Run `hammer` and get performance metrics
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
}
Loading

0 comments on commit 498d39b

Please sign in to comment.