aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLinus Nordberg <linus@nordberg.se>2022-05-24 23:33:38 +0200
committerRasmus Dahlberg <rasmus@mullvad.net>2022-06-23 11:33:17 +0200
commit559bccccd40d028e412d9f11709ded0250ba6dcd (patch)
tree50f3193dbe70fec21357963c11e5f663013f4b4c
parent4b20ef0c1732bcef633c0ed7104501898aa84e2c (diff)
implement primary and secondary role, for replicationv0.5.0
-rw-r--r--README.md16
-rw-r--r--cmd/sigsum-log-primary/README.md (renamed from cmd/sigsum_log_go/README.md)18
-rw-r--r--cmd/sigsum-log-primary/main.go233
-rw-r--r--cmd/sigsum-log-secondary/main.go175
-rw-r--r--cmd/sigsum_log_go/.gitignore1
-rw-r--r--cmd/sigsum_log_go/main.go223
-rw-r--r--doc/design.md96
-rw-r--r--go.mod4
-rw-r--r--go.sum26
-rw-r--r--integration/conf/logc.config14
-rw-r--r--integration/conf/primary.config14
-rw-r--r--integration/conf/secondary.config14
-rw-r--r--integration/conf/sigsum.config6
-rw-r--r--integration/conf/trillian.config7
-rwxr-xr-xintegration/test.sh769
-rw-r--r--internal/db/client.go (renamed from pkg/db/client.go)3
-rw-r--r--internal/db/trillian.go (renamed from pkg/db/trillian.go)78
-rw-r--r--internal/db/trillian_test.go (renamed from pkg/db/trillian_test.go)207
-rw-r--r--internal/mocks/client/client.go170
-rw-r--r--internal/mocks/crypto/crypto.go65
-rw-r--r--internal/mocks/db/db.go (renamed from pkg/db/mocks/client.go)31
-rw-r--r--internal/mocks/node/handler/handler.go77
-rw-r--r--internal/mocks/state/state.go (renamed from pkg/state/mocks/state_manager.go)17
-rw-r--r--internal/mocks/trillian/trillian.go (renamed from pkg/db/mocks/trillian.go)4
-rw-r--r--internal/node/handler/handler.go91
-rw-r--r--internal/node/handler/handler_test.go113
-rw-r--r--internal/node/handler/metric.go (renamed from pkg/instance/metric.go)2
-rw-r--r--internal/node/primary/endpoint_external.go147
-rw-r--r--internal/node/primary/endpoint_external_test.go (renamed from pkg/instance/handler_test.go)328
-rw-r--r--internal/node/primary/endpoint_internal.go30
-rw-r--r--internal/node/primary/endpoint_internal_test.go82
-rw-r--r--internal/node/primary/primary.go74
-rw-r--r--internal/node/primary/primary_test.go75
-rw-r--r--internal/node/secondary/endpoint_internal.go44
-rw-r--r--internal/node/secondary/endpoint_internal_test.go111
-rw-r--r--internal/node/secondary/secondary.go112
-rw-r--r--internal/node/secondary/secondary_test.go138
-rw-r--r--internal/requests/requests.go91
-rw-r--r--internal/requests/requests_test.go218
-rw-r--r--internal/state/single.go265
-rw-r--r--internal/state/single_test.go233
-rw-r--r--internal/state/state_manager.go (renamed from pkg/state/state_manager.go)13
-rw-r--r--internal/utils/utils.go69
-rw-r--r--pkg/instance/experimental.go85
-rw-r--r--pkg/instance/handler.go184
-rw-r--r--pkg/instance/instance.go135
-rw-r--r--pkg/instance/instance_test.go23
-rw-r--r--pkg/state/mocks/signer.go23
-rw-r--r--pkg/state/single.go165
-rw-r--r--pkg/state/single_test.go217
50 files changed, 3682 insertions, 1654 deletions
diff --git a/README.md b/README.md
index 9644e51..7030805 100644
--- a/README.md
+++ b/README.md
@@ -1,27 +1,27 @@
-# sigsum-log-go
+# Sigsum log-go
This repository provides a
[Trillian](https://transparency.dev/#trillian)
[personality](https://github.com/google/trillian/blob/master/docs/Personalities.md)
-that implements the sigsum/v0
+implementing the sigsum/v0
[design](https://git.sigsum.org/sigsum/tree/doc/design.md)
and
[API](https://git.sigsum.org/sigsum/tree/doc/api.md).
## Public prototype
-There is a public prototype that is up and running with zero promises of uptime,
+There is a public prototype running with zero promises of uptime,
stability, etc. Relevant log information:
- Base URL: https://poc.sigsum.org/crocodile-icefish/
- Public key: `4791eff3bfc17f352bcc76d4752b38c07882093a5935a84577c63de224b0f6b3`
- Shard start: 1651494520 (Mon 02 May 2022 12:28:40 PM UTC)
-A [witness](https://github.com/sigsum/sigsum-witness-py) is also up and running
-with zero-promises of uptime, stability, etc. Relevant witness information:
+A [witness](https://github.com/sigsum/sigsum-witness-py) is running as well,
+this too with zero promises of uptime, stability, etc. Relevant witness information:
- Public key: `812dbef0156b079e2d048747b2189cbfa64f96e2204a17cb23cb528080871503`.
-As described in our design and API documentation, you can talk to the
-log by passing ASCII key-value pairs. For example, fetching a cosigned
+As described in the design and API documentation, you can talk to the
+log by sending it ASCII key-value pairs. For example, fetching a co-signed
tree head and two log entries:
```
@@ -48,4 +48,4 @@ key_hash=c522d929b261241eef174b51b8472fa5d5f961892089a7b85fd25ce73271abca
Go tooling that makes it easier to interact with sigsum logs will appear in a
separate repository in the near future, see
- [sigsum-lib-go](https://git.sigsum.org/sigsum-lib-go/).
+ [sigsum-go](https://git.sigsum.org/sigsum-go/).
diff --git a/cmd/sigsum_log_go/README.md b/cmd/sigsum-log-primary/README.md
index 5c363f7..a824173 100644
--- a/cmd/sigsum_log_go/README.md
+++ b/cmd/sigsum-log-primary/README.md
@@ -1,11 +1,11 @@
-# Run Trillian + sigsum-log-go locally
+# Run Trillian + sigsum-log-primary locally
Trillian uses a database. So, we will need to set that up. It is documented
[here](https://github.com/google/trillian#mysql-setup), and how to check that it
is setup properly
[here](https://github.com/google/certificate-transparency-go/blob/master/trillian/docs/ManualDeployment.md#data-storage).
Other than the database we need Trillian log signer, Trillian log server, and
-sigsum-log-go. sigsum-log-go has been tested with trillian v.1.3.13.
+sigsum-log-primary. sigsum-log-primary has been tested with trillian v.1.3.13.
```
$ go install github.com/google/trillian/cmd/trillian_log_signer@v1.3.13
$ go install github.com/google/trillian/cmd/trillian_log_server@v1.3.13
@@ -30,25 +30,23 @@ $ createtree --admin_server localhost:6962
<tree id>
```
-Hang on to `<tree id>`. Our sigsum-log-go instance will use it when talking to
+Hang on to `<tree id>`. Our sigsum-log-primary instance will use it when talking to
the Trillian log server to specify which Merkle tree we are working against.
(If you take a look in the `Trees` table you will see that the tree has been
provisioned.)
-We will also need a public key-pair for sigsum-log-go.
+We will also need a public key-pair for sigsum-log-primary.
```
-$ go install git.sigsum.org/sigsum-log-go/cmd/tmp/keygen
-$ ./keygen
-sk: <sk>
-vk: <vk>
+$ go install git.sigsum.org/sigsum-go/cmd/sigsum-debug@latest
+$ sigsum-debug key private | tee sk | sigsum-debug key public > vk
```
-Start sigsum-log-go:
+Start sigsum-log-primary:
```
$ tree_id=<tree_id>
$ sk=<sk>
-$ sigsum_log_go --logtostderr -v 9 --http_endpoint localhost:6965 --log_rpc_server localhost:6962 --trillian_id $tree_id --key $sk
+$ sigsum-log-primary --logtostderr -v 9 --http_endpoint localhost:6965 --log_rpc_server localhost:6962 --trillian_id $tree_id --key <(echo sk)
```
Quick test:
diff --git a/cmd/sigsum-log-primary/main.go b/cmd/sigsum-log-primary/main.go
new file mode 100644
index 0000000..f64643a
--- /dev/null
+++ b/cmd/sigsum-log-primary/main.go
@@ -0,0 +1,233 @@
+// Package main provides a sigsum-log-primary binary
+package main
+
+import (
+ "context"
+ "encoding/hex"
+ "flag"
+ "fmt"
+ "net/http"
+ "os"
+ "os/signal"
+ "strings"
+ "sync"
+ "syscall"
+ "time"
+
+ "github.com/google/trillian"
+ "github.com/prometheus/client_golang/prometheus/promhttp"
+ "google.golang.org/grpc"
+
+ "git.sigsum.org/log-go/internal/db"
+ "git.sigsum.org/log-go/internal/node/primary"
+ "git.sigsum.org/log-go/internal/state"
+ "git.sigsum.org/log-go/internal/utils"
+ "git.sigsum.org/sigsum-go/pkg/client"
+ "git.sigsum.org/sigsum-go/pkg/dns"
+ "git.sigsum.org/sigsum-go/pkg/log"
+ "git.sigsum.org/sigsum-go/pkg/merkle"
+ "git.sigsum.org/sigsum-go/pkg/types"
+)
+
+var (
+ externalEndpoint = flag.String("external-endpoint", "localhost:6965", "host:port specification of where sigsum-log-primary serves clients")
+ internalEndpoint = flag.String("internal-endpoint", "localhost:6967", "host:port specification of where sigsum-log-primary serves other log nodes")
+ rpcBackend = flag.String("trillian-rpc-server", "localhost:6962", "host:port specification of where Trillian serves clients")
+ prefix = flag.String("url-prefix", "", "a prefix that precedes /sigsum/v0/<endpoint>")
+ trillianID = flag.Int64("tree-id", 0, "tree identifier in the Trillian database")
+ deadline = flag.Duration("deadline", time.Second*10, "deadline for backend requests")
+ key = flag.String("key", "", "path to file with hex-encoded Ed25519 private key")
+ witnesses = flag.String("witnesses", "", "comma-separated list of trusted witness public keys in hex")
+ maxRange = flag.Int64("max-range", 10, "maximum number of entries that can be retrived in a single request")
+ interval = flag.Duration("interval", time.Second*30, "interval used to rotate the log's cosigned STH")
+ shardStart = flag.Int64("shard-interval-start", 0, "start of shard interval since the UNIX epoch in seconds")
+ testMode = flag.Bool("test-mode", false, "run in test mode (Default: false)")
+ logFile = flag.String("log-file", "", "file to write logs to (Default: stderr)")
+ logLevel = flag.String("log-level", "info", "log level (Available options: debug, info, warning, error. Default: info)")
+ logColor = flag.Bool("log-color", false, "colored logging output (Default: false)")
+ secondaryURL = flag.String("secondary-url", "", "secondary node endpoint for fetching latest replicated tree head")
+ secondaryPubkey = flag.String("secondary-pubkey", "", "hex-encoded Ed25519 public key for secondary node")
+
+ gitCommit = "unknown"
+)
+
+func main() {
+ flag.Parse()
+
+ if err := utils.SetupLogging(*logFile, *logLevel, *logColor); err != nil {
+ log.Fatal("setup logging: %v", err)
+ }
+ log.Info("log-go git-commit %s", gitCommit)
+
+ // wait for clean-up before exit
+ var wg sync.WaitGroup
+ defer wg.Wait()
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ log.Debug("configuring log-go-primary")
+ node, err := setupPrimaryFromFlags()
+ if err != nil {
+ log.Fatal("setup primary: %v", err)
+ }
+
+ log.Debug("starting primary state manager routine")
+ go func() {
+ wg.Add(1)
+ defer wg.Done()
+ node.Stateman.Run(ctx)
+ log.Debug("state manager shutdown")
+ cancel() // must have state manager running
+ }()
+
+ server := &http.Server{Addr: *externalEndpoint, Handler: node.PublicHTTPMux}
+ intserver := &http.Server{Addr: *internalEndpoint, Handler: node.InternalHTTPMux}
+ log.Debug("starting await routine")
+ go await(ctx, func() {
+ wg.Add(1)
+ defer wg.Done()
+ ctxInner, _ := context.WithTimeout(ctx, time.Second*60)
+ log.Info("stopping http server, please wait...")
+ server.Shutdown(ctxInner)
+ log.Info("... done")
+ log.Info("stopping internal api server, please wait...")
+ intserver.Shutdown(ctxInner)
+ log.Info("... done")
+ log.Info("stopping go routines, please wait...")
+ cancel()
+ log.Info("... done")
+ })
+
+ go func() {
+ wg.Add(1)
+ defer wg.Done()
+ log.Info("serving log nodes on %v/%v", *internalEndpoint, *prefix)
+ if err = intserver.ListenAndServe(); err != http.ErrServerClosed {
+ log.Error("serve(intserver): %v", err)
+ }
+ log.Debug("internal endpoints server shut down")
+ cancel()
+ }()
+
+ log.Info("serving clients on %v/%v", *externalEndpoint, *prefix)
+ if err = server.ListenAndServe(); err != http.ErrServerClosed {
+ log.Error("serve(server): %v", err)
+ }
+
+}
+
+// setupPrimaryFromFlags() sets up a new sigsum primary node from flags.
+func setupPrimaryFromFlags() (*primary.Primary, error) {
+ var p primary.Primary
+ var err error
+
+ // Setup logging configuration.
+ p.Signer, p.Config.LogID, err = utils.NewLogIdentity(*key)
+ if err != nil {
+ return nil, fmt.Errorf("newLogIdentity: %v", err)
+ }
+
+ p.Config.TreeID = *trillianID
+ p.Config.Prefix = *prefix
+ p.Config.MaxRange = *maxRange
+ p.Config.Deadline = *deadline
+ p.Config.Interval = *interval
+ p.Config.ShardStart = uint64(*shardStart)
+ if *shardStart < 0 {
+ return nil, fmt.Errorf("shard start must be larger than zero")
+ }
+ p.Config.Witnesses, err = newWitnessMap(*witnesses)
+ if err != nil {
+ return nil, fmt.Errorf("newWitnessMap: %v", err)
+ }
+
+ // Setup trillian client.
+ dialOpts := []grpc.DialOption{grpc.WithInsecure(), grpc.WithBlock(), grpc.WithTimeout(p.Config.Deadline)}
+ conn, err := grpc.Dial(*rpcBackend, dialOpts...)
+ if err != nil {
+ return nil, fmt.Errorf("Dial: %v", err)
+ }
+ p.TrillianClient = &db.TrillianClient{
+ TreeID: p.TreeID,
+ GRPC: trillian.NewTrillianLogClient(conn),
+ }
+
+ // Setup secondary node configuration.
+ if *secondaryURL != "" && *secondaryPubkey != "" {
+ pubkey, err := utils.PubkeyFromHexString(*secondaryPubkey)
+ if err != nil {
+ return nil, fmt.Errorf("invalid secondary node pubkey: %v", err)
+ }
+ p.Secondary = client.New(client.Config{
+ LogURL: *secondaryURL,
+ LogPub: *pubkey,
+ })
+ } else {
+ p.Secondary = client.New(client.Config{})
+ }
+
+ // Setup state manager.
+ p.Stateman, err = state.NewStateManagerSingle(p.TrillianClient, p.Signer, p.Config.Interval, p.Config.Deadline, p.Secondary)
+ if err != nil {
+ return nil, fmt.Errorf("NewStateManagerSingle: %v", err)
+ }
+ if *testMode == false {
+ p.DNS = dns.NewDefaultResolver()
+ } else {
+ p.DNS = dns.NewDummyResolver()
+ }
+
+ // TODO: verify that GRPC.TreeType() == LOG.
+
+ // Register HTTP endpoints.
+ mux := http.NewServeMux()
+ for _, h := range p.PublicHTTPHandlers() {
+ log.Debug("adding external handler: %s", h.Path())
+ mux.Handle(h.Path(), h)
+ }
+ p.PublicHTTPMux = mux
+
+ mux = http.NewServeMux()
+ for _, h := range p.InternalHTTPHandlers() {
+ log.Debug("adding internal handler: %s", h.Path())
+ mux.Handle(h.Path(), h)
+ }
+ p.InternalHTTPMux = mux
+
+ log.Debug("adding prometheus handler to internal mux, on path: /metrics")
+ http.Handle("/metrics", promhttp.Handler())
+
+ return &p, nil
+}
+
+// newWitnessMap creates a new map of trusted witnesses
+func newWitnessMap(witnesses string) (map[merkle.Hash]types.PublicKey, error) {
+ w := make(map[merkle.Hash]types.PublicKey)
+ if len(witnesses) > 0 {
+ for _, witness := range strings.Split(witnesses, ",") {
+ b, err := hex.DecodeString(witness)
+ if err != nil {
+ return nil, fmt.Errorf("DecodeString: %v", err)
+ }
+
+ var vk types.PublicKey
+ if n := copy(vk[:], b); n != types.PublicKeySize {
+ return nil, fmt.Errorf("Invalid public key size: %v", n)
+ }
+ w[*merkle.HashFn(vk[:])] = vk
+ }
+ }
+ return w, nil
+}
+
+// await waits for a shutdown signal and then runs a clean-up function
+func await(ctx context.Context, done func()) {
+ sigs := make(chan os.Signal, 1)
+ signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
+ select {
+ case <-sigs:
+ case <-ctx.Done():
+ }
+ log.Debug("received shutdown signal")
+ done()
+}
diff --git a/cmd/sigsum-log-secondary/main.go b/cmd/sigsum-log-secondary/main.go
new file mode 100644
index 0000000..7306d4c
--- /dev/null
+++ b/cmd/sigsum-log-secondary/main.go
@@ -0,0 +1,175 @@
+// Package main provides a sigsum-log-secondary binary
+package main
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "net/http"
+ "os"
+ "os/signal"
+ "sync"
+ "syscall"
+ "time"
+
+ "github.com/google/trillian"
+ "github.com/prometheus/client_golang/prometheus/promhttp"
+ "google.golang.org/grpc"
+
+ "git.sigsum.org/log-go/internal/db"
+ "git.sigsum.org/log-go/internal/node/secondary"
+ "git.sigsum.org/log-go/internal/utils"
+ "git.sigsum.org/sigsum-go/pkg/client"
+ "git.sigsum.org/sigsum-go/pkg/log"
+)
+
+var (
+ externalEndpoint = flag.String("external-endpoint", "localhost:6965", "host:port specification of where sigsum-log-secondary serves clients")
+ internalEndpoint = flag.String("internal-endpoint", "localhost:6967", "host:port specification of where sigsum-log-secondary serves other log nodes")
+ rpcBackend = flag.String("trillian-rpc-server", "localhost:6962", "host:port specification of where Trillian serves clients")
+ prefix = flag.String("url-prefix", "", "a prefix that proceeds /sigsum/v0/<endpoint>")
+ trillianID = flag.Int64("tree-id", 0, "log identifier in the Trillian database")
+ deadline = flag.Duration("deadline", time.Second*10, "deadline for backend requests")
+ key = flag.String("key", "", "path to file with hex-encoded Ed25519 private key")
+ interval = flag.Duration("interval", time.Second*30, "interval used to rotate the node's STH")
+ testMode = flag.Bool("test-mode", false, "run in test mode (Default: false)")
+ logFile = flag.String("log-file", "", "file to write logs to (Default: stderr)")
+ logLevel = flag.String("log-level", "info", "log level (Available options: debug, info, warning, error. Default: info)")
+ logColor = flag.Bool("log-color", false, "colored logging output (Default: false)")
+ primaryURL = flag.String("primary-url", "", "primary node endpoint for fetching leaves")
+ primaryPubkey = flag.String("primary-pubkey", "", "hex-encoded Ed25519 public key for primary node")
+
+ gitCommit = "unknown"
+)
+
+func main() {
+ flag.Parse()
+
+ if err := utils.SetupLogging(*logFile, *logLevel, *logColor); err != nil {
+ log.Fatal("setup logging: %v", err)
+ }
+ log.Info("log-go git-commit %s", gitCommit)
+
+ // wait for clean-up before exit
+ var wg sync.WaitGroup
+ defer wg.Wait()
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ log.Debug("configuring log-go-secondary")
+ node, err := setupSecondaryFromFlags()
+ if err != nil {
+ log.Fatal("setup secondary: %v", err)
+ }
+
+ log.Debug("starting periodic routine")
+ go func() {
+ wg.Add(1)
+ defer wg.Done()
+ node.Run(ctx)
+ log.Debug("periodic routine shutdown")
+ cancel() // must have periodic running
+ }()
+
+ server := &http.Server{Addr: *externalEndpoint, Handler: node.PublicHTTPMux}
+ intserver := &http.Server{Addr: *internalEndpoint, Handler: node.InternalHTTPMux}
+ log.Debug("starting await routine")
+ go await(ctx, func() {
+ wg.Add(1)
+ defer wg.Done()
+ ctxInner, _ := context.WithTimeout(ctx, time.Second*60)
+ log.Info("stopping http server, please wait...")
+ server.Shutdown(ctxInner)
+ log.Info("... done")
+ log.Info("stopping internal api server, please wait...")
+ intserver.Shutdown(ctxInner)
+ log.Info("... done")
+ log.Info("stopping go routines, please wait...")
+ cancel()
+ log.Info("... done")
+ })
+
+ go func() {
+ wg.Add(1)
+ defer wg.Done()
+ log.Info("serving log nodes on %v/%v", *internalEndpoint, *prefix)
+ if err = intserver.ListenAndServe(); err != http.ErrServerClosed {
+ log.Error("serve(intserver): %v", err)
+ }
+ log.Debug("internal endpoints server shut down")
+ cancel()
+ }()
+
+ log.Info("serving clients on %v/%v", *externalEndpoint, *prefix)
+ if err = server.ListenAndServe(); err != http.ErrServerClosed {
+ log.Error("serve(server): %v", err)
+ }
+
+}
+
+// setupSecondaryFromFlags() sets up a new sigsum secondary node from flags.
+func setupSecondaryFromFlags() (*secondary.Secondary, error) {
+ var s secondary.Secondary
+ var err error
+
+ // Setup logging configuration.
+ s.Signer, s.Config.LogID, err = utils.NewLogIdentity(*key)
+ if err != nil {
+ return nil, fmt.Errorf("newLogIdentity: %v", err)
+ }
+ s.Config.TreeID = *trillianID
+ s.Config.Prefix = *prefix
+ s.Config.Deadline = *deadline
+ s.Config.Interval = *interval
+
+ // Setup trillian client.
+ dialOpts := []grpc.DialOption{grpc.WithInsecure(), grpc.WithBlock(), grpc.WithTimeout(s.Config.Deadline)}
+ conn, err := grpc.Dial(*rpcBackend, dialOpts...)
+ if err != nil {
+ return nil, fmt.Errorf("Dial: %v", err)
+ }
+ s.TrillianClient = &db.TrillianClient{
+ TreeID: s.TreeID,
+ GRPC: trillian.NewTrillianLogClient(conn),
+ }
+
+ // Setup primary node configuration.
+ pubkey, err := utils.PubkeyFromHexString(*primaryPubkey)
+ if err != nil {
+ return nil, fmt.Errorf("invalid primary node pubkey: %v", err)
+ }
+ s.Primary = client.New(client.Config{
+ LogURL: *primaryURL,
+ LogPub: *pubkey,
+ })
+
+ // TODO: verify that GRPC.TreeType() == PREORDERED_LOG.
+
+ // Register HTTP endpoints.
+ mux := http.NewServeMux()
+ s.PublicHTTPMux = mux // No external endpoints but we want to return 404.
+
+ mux = http.NewServeMux()
+ for _, h := range s.InternalHTTPHandlers() {
+ log.Debug("adding internal handler: %s", h.Path())
+ mux.Handle(h.Path(), h)
+ }
+ s.InternalHTTPMux = mux
+
+ log.Debug("adding prometheus handler to internal mux, on path: /metrics")
+ http.Handle("/metrics", promhttp.Handler())
+
+ return &s, nil
+}
+
+// await waits for a shutdown signal and then runs a clean-up function
+func await(ctx context.Context, done func()) {
+ sigs := make(chan os.Signal, 1)
+ signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
+ select {
+ case <-sigs:
+ case <-ctx.Done():
+ }
+ log.Debug("received shutdown signal")
+ done()
+}
diff --git a/cmd/sigsum_log_go/.gitignore b/cmd/sigsum_log_go/.gitignore
deleted file mode 100644
index 254defd..0000000
--- a/cmd/sigsum_log_go/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-server
diff --git a/cmd/sigsum_log_go/main.go b/cmd/sigsum_log_go/main.go
deleted file mode 100644
index c8e1692..0000000
--- a/cmd/sigsum_log_go/main.go
+++ /dev/null
@@ -1,223 +0,0 @@
-// Package main provides a sigsum-log-go binary
-package main
-
-import (
- "context"
- "crypto"
- "crypto/ed25519"
- "encoding/hex"
- "flag"
- "fmt"
- "io/ioutil"
- "net/http"
- "os"
- "os/signal"
- "strings"
- "sync"
- "syscall"
- "time"
-
- "github.com/google/trillian"
- "github.com/prometheus/client_golang/prometheus/promhttp"
- "google.golang.org/grpc"
-
- "git.sigsum.org/sigsum-go/pkg/log"
- "git.sigsum.org/sigsum-go/pkg/types"
- "git.sigsum.org/sigsum-go/pkg/dns"
- "git.sigsum.org/log-go/pkg/db"
- "git.sigsum.org/log-go/pkg/instance"
- "git.sigsum.org/log-go/pkg/state"
-)
-
-var (
- httpEndpoint = flag.String("http_endpoint", "localhost:6965", "host:port specification of where sigsum-log-go serves clients")
- rpcBackend = flag.String("log_rpc_server", "localhost:6962", "host:port specification of where Trillian serves clients")
- prefix = flag.String("prefix", "", "a prefix that proceeds /sigsum/v0/<endpoint>")
- trillianID = flag.Int64("trillian_id", 0, "log identifier in the Trillian database")
- deadline = flag.Duration("deadline", time.Second*10, "deadline for backend requests")
- key = flag.String("key", "", "path to file with hex-encoded Ed25519 private key")
- witnesses = flag.String("witnesses", "", "comma-separated list of trusted witness public keys in hex")
- maxRange = flag.Int64("max_range", 10, "maximum number of entries that can be retrived in a single request")
- interval = flag.Duration("interval", time.Second*30, "interval used to rotate the log's cosigned STH")
- shardStart = flag.Int64("shard_interval_start", 0, "start of shard interval since the UNIX epoch in seconds")
- logFile = flag.String("log-file", "", "file to write logs to (Default: stderr)")
- logLevel = flag.String("log-level", "info", "log level (Available options: debug, info, warning, error. Default: info)")
- logColor = flag.Bool("log-color", false, "colored logging output (Default: off)")
-
- gitCommit = "unknown"
-)
-
-func main() {
- flag.Parse()
-
- if err := setupLogging(*logFile, *logLevel, *logColor); err != nil {
- log.Fatal("setup logging: %v", err)
- }
- log.Info("log-go git-commit %s", gitCommit)
-
- // wait for clean-up before exit
- var wg sync.WaitGroup
- defer wg.Wait()
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
-
- log.Debug("configuring log-go instance")
- instance, err := setupInstanceFromFlags()
- if err != nil {
- log.Fatal("setup instance: %v", err)
- }
-
- log.Debug("starting state manager routine")
- go func() {
- wg.Add(1)
- defer wg.Done()
- instance.Stateman.Run(ctx)
- log.Debug("state manager shutdown")
- cancel() // must have state manager running
- }()
-
- log.Debug("starting await routine")
- server := http.Server{Addr: *httpEndpoint}
- go await(ctx, func() {
- wg.Add(1)
- defer wg.Done()
- ctxInner, _ := context.WithTimeout(ctx, time.Second*60)
- log.Info("stopping http server, please wait...")
- server.Shutdown(ctxInner)
- log.Info("stopping go routines, please wait...")
- cancel()
- })
-
- log.Info("serving on %v/%v", *httpEndpoint, *prefix)
- if err = server.ListenAndServe(); err != http.ErrServerClosed {
- log.Error("serve: %v", err)
- }
-}
-
-func setupLogging(logFile, logLevel string, logColor bool) error {
- if len(logFile) != 0 {
- f, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
- if err != nil {
- return err
- }
- log.SetOutput(f)
- }
-
- switch logLevel {
- case "debug":
- log.SetLevel(log.DebugLevel)
- case "info":
- log.SetLevel(log.InfoLevel)
- case "warning":
- log.SetLevel(log.WarningLevel)
- case "error":
- log.SetLevel(log.ErrorLevel)
- default:
- return fmt.Errorf("invalid logging level %s", logLevel)
- }
-
- log.SetColor(logColor)
- return nil
-}
-
-// SetupInstance sets up a new sigsum-log-go instance from flags
-func setupInstanceFromFlags() (*instance.Instance, error) {
- var i instance.Instance
- var err error
-
- // Setup log configuration
- i.Signer, i.LogID, err = newLogIdentity(*key)
- if err != nil {
- return nil, fmt.Errorf("newLogIdentity: %v", err)
- }
- i.TreeID = *trillianID
- i.Prefix = *prefix
- i.MaxRange = *maxRange
- i.Deadline = *deadline
- i.Interval = *interval
- i.ShardStart = uint64(*shardStart)
- if *shardStart < 0 {
- return nil, fmt.Errorf("shard start must be larger than zero")
- }
- i.Witnesses, err = newWitnessMap(*witnesses)
- if err != nil {
- return nil, fmt.Errorf("newWitnessMap: %v", err)
- }
-
- // Setup log client
- dialOpts := []grpc.DialOption{grpc.WithInsecure(), grpc.WithBlock(), grpc.WithTimeout(i.Deadline)}
- conn, err := grpc.Dial(*rpcBackend, dialOpts...)
- if err != nil {
- return nil, fmt.Errorf("Dial: %v", err)
- }
- i.Client = &db.TrillianClient{
- TreeID: i.TreeID,
- GRPC: trillian.NewTrillianLogClient(conn),
- }
-
- // Setup state manager
- i.Stateman, err = state.NewStateManagerSingle(i.Client, i.Signer, i.Interval, i.Deadline)
- if err != nil {
- return nil, fmt.Errorf("NewStateManagerSingle: %v", err)
- }
-
- // Setup DNS verifier
- i.DNS = dns.NewDefaultResolver()
-
- // Register HTTP endpoints
- mux := http.NewServeMux()
- http.Handle("/", mux)
- for _, handler := range i.Handlers() {
- log.Debug("adding handler: %s", handler.Path())
- mux.Handle(handler.Path(), handler)
- }
- log.Debug("adding prometheus handler on path: /metrics")
- http.Handle("/metrics", promhttp.Handler())
-
- return &i, nil
-}
-
-func newLogIdentity(keyFile string) (crypto.Signer, string, error) {
- buf, err := ioutil.ReadFile(keyFile)
- if err != nil {
- return nil, "", err
- }
- if buf, err = hex.DecodeString(strings.TrimSpace(string(buf))); err != nil {
- return nil, "", fmt.Errorf("DecodeString: %v", err)
- }
- sk := crypto.Signer(ed25519.NewKeyFromSeed(buf))
- vk := sk.Public().(ed25519.PublicKey)
- return sk, hex.EncodeToString([]byte(vk[:])), nil
-}
-
-// newWitnessMap creates a new map of trusted witnesses
-func newWitnessMap(witnesses string) (map[types.Hash]types.PublicKey, error) {
- w := make(map[types.Hash]types.PublicKey)
- if len(witnesses) > 0 {
- for _, witness := range strings.Split(witnesses, ",") {
- b, err := hex.DecodeString(witness)
- if err != nil {
- return nil, fmt.Errorf("DecodeString: %v", err)
- }
-
- var vk types.PublicKey
- if n := copy(vk[:], b); n != types.PublicKeySize {
- return nil, fmt.Errorf("Invalid public key size: %v", n)
- }
- w[*types.HashFn(vk[:])] = vk
- }
- }
- return w, nil
-}
-
-// await waits for a shutdown signal and then runs a clean-up function
-func await(ctx context.Context, done func()) {
- sigs := make(chan os.Signal, 1)
- signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
- select {
- case <-sigs:
- case <-ctx.Done():
- }
- log.Debug("received shutdown signal")
- done()
-}
diff --git a/doc/design.md b/doc/design.md
new file mode 100644
index 0000000..5478d80
--- /dev/null
+++ b/doc/design.md
@@ -0,0 +1,96 @@
+# sigsum_log_go design
+
+This document describes the design of `sigsum_log_go`, an
+implementation of
+[Sigsum](https://git.sigsum.org/sigsum/tree/doc/design.md).
+
+## General
+
+TODO: add general design info
+
+A log instance
+
+- has one signing key,
+
+- is made up of one or more log nodes -- primary and secondary,
+
+- has at any given time exactly one primary and zero or more,
+ secondaries
+
+- should really have at least one secondary node, to not risk losing
+ data,
+
+- confirms new leaves (add-leaf returning HTTP code 200) once they
+ have been incorporated in the tree and sequenced but not before.
+
+Log nodes
+
+- publish two API:s, one public and one for use by other nodes of the
+ same log instance.
+
+## Roles -- primary and secondary
+
+A log node is configured to act as the `primary` node, or to act as a
+`secondary` node. A primary is configured to know the base URL and
+pubkey of zero or more secondaries. A secondary is configured to know
+the base URL and pubkey of one primary.
+
+### Interaction
+
+A primary node that has no secondary nodes configured is the single
+node in a test instance and lacks all means of recovering from storage
+failures. This configuration is only recommended for testing of the
+software.
+
+A primary node that has at least one secondary node configured
+
+- fetches and verifies the tree head from all its secondaries using
+ the internal API endpoint `getTreeHeadToCosign` (TBD: rename
+ endpoint to be uniquely named across both API:s?)
+
+- considers a secondary node that can not be reached to have a tree
+ size of zero,
+
+- advances its tree head no further than to the lowest tree size of
+ all its secondary nodes.
+
+A secondary node:
+
+- runs a Trillian server configured with a `PREORDERED_LOG` tree and
+ without a sequencer,
+
+- periodically fetches all leaves from the primary using the internal
+ API endpoints `getTreeHeadUnsigned` and `getLeaves`,
+
+- populates Trillian with the leaves fetched from its primary, in the
+ order that they are delivered,
+
+- should advance its tree head more often than its primary node,
+ typically every few seconds.
+
+### Promoting a secondary to become the primary
+
+In order to promote a secondary node to become the primary node of a
+log instance, the following things need to be done:
+
+1. Shutting down the secondary. This effectively stops the primary
+ from advancing its tree head, regardless of its current status.
+
+1. Converting the Trillian tree from type `PREORDERED_LOG` to type
+ `LOG`, using `updatetree`. Note that the tree needs to be `FROZEN`
+ before changing the tree type and unfrozen (`ACTIVE`) afterwards.
+
+1. Configuring the secondary to use the signing key of the log instance.
+
+1. Starting the secondary with `-role primary` and at least one
+ secondary node.
+
+In order for clients to reach the new primary rather than the old one,
+DNS record changes are usually needed as well.
+
+
+### Open questions
+
+- should secondaries publish the public API as well, but reply with
+ "404 not primary"? clients ending up at a secondary might benefit
+ from this
diff --git a/go.mod b/go.mod
index fdb3f09..74f2e3d 100644
--- a/go.mod
+++ b/go.mod
@@ -3,8 +3,8 @@ module git.sigsum.org/log-go
go 1.15
require (
- git.sigsum.org/sigsum-go v0.0.8
- github.com/golang/mock v1.4.4
+ git.sigsum.org/sigsum-go v0.0.9
+ github.com/golang/mock v1.6.0
github.com/google/certificate-transparency-go v1.1.1 // indirect
github.com/google/trillian v1.3.13
github.com/prometheus/client_golang v1.9.0
diff --git a/go.sum b/go.sum
index d6b6e1f..b6a8df7 100644
--- a/go.sum
+++ b/go.sum
@@ -33,8 +33,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
contrib.go.opencensus.io/exporter/stackdriver v0.13.4/go.mod h1:aXENhDJ1Y4lIg4EUaVTwzvYETVNZk10Pu26tevFKLUc=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
-git.sigsum.org/sigsum-go v0.0.8 h1:YJ00f6QyW/6zgMly/LYCsn+sGgsl3sdJhUIbXNXjRrg=
-git.sigsum.org/sigsum-go v0.0.8/go.mod h1:atThndG2UQarpAMDfAWmgm9YYIvuYwbeTDx12kztD0w=
+git.sigsum.org/sigsum-go v0.0.9 h1:BdbJdVmH0UYP4c3nSwUdVfUdLIkLVS0CsgPIKIbgGVg=
+git.sigsum.org/sigsum-go v0.0.9/go.mod h1:atThndG2UQarpAMDfAWmgm9YYIvuYwbeTDx12kztD0w=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
@@ -153,8 +153,9 @@ github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFU
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
+github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
+github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -434,6 +435,7 @@ github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
@@ -497,6 +499,7 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -530,8 +533,9 @@ golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -546,6 +550,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -587,13 +592,18 @@ golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e h1:AyodaIpKjppX+cBfTASF2E1US3H2JFBj920Ot3rtDjs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -647,10 +657,12 @@ golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20200626171337-aa94e735be7f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200630154851-b2d8b0336632/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200706234117-b22de6825cf7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
diff --git a/integration/conf/logc.config b/integration/conf/logc.config
new file mode 100644
index 0000000..3cc31f3
--- /dev/null
+++ b/integration/conf/logc.config
@@ -0,0 +1,14 @@
+node_name=logc
+
+tsrv_rpc=localhost:7162
+tseq_rpc=localhost:7163
+
+tsrv_http=localhost:7164
+tseq_http=localhost:7165
+
+ssrv_role=secondary
+ssrv_interval_sec=2
+ssrv_endpoint=localhost:7166
+ssrv_internal=localhost:7167
+ssrv_prefix=testonly
+ssrv_shard_start=2009
diff --git a/integration/conf/primary.config b/integration/conf/primary.config
new file mode 100644
index 0000000..4126651
--- /dev/null
+++ b/integration/conf/primary.config
@@ -0,0 +1,14 @@
+node_name=loga
+
+tsrv_rpc=localhost:6962
+tseq_rpc=localhost:6963
+
+tsrv_http=localhost:6964
+tseq_http=localhost:6965
+
+ssrv_role=primary
+ssrv_interval_sec=5
+ssrv_endpoint=localhost:6966
+ssrv_internal=localhost:6967
+ssrv_prefix=testonly
+ssrv_shard_start=2009
diff --git a/integration/conf/secondary.config b/integration/conf/secondary.config
new file mode 100644
index 0000000..d00d11e
--- /dev/null
+++ b/integration/conf/secondary.config
@@ -0,0 +1,14 @@
+node_name=logb
+
+tsrv_rpc=localhost:7062
+tseq_rpc=localhost:7063
+
+tsrv_http=localhost:7064
+tseq_http=localhost:7065
+
+ssrv_role=secondary
+ssrv_interval_sec=2
+ssrv_endpoint=localhost:7066
+ssrv_internal=localhost:7067
+ssrv_prefix=testonly
+ssrv_shard_start=2009
diff --git a/integration/conf/sigsum.config b/integration/conf/sigsum.config
deleted file mode 100644
index a28e854..0000000
--- a/integration/conf/sigsum.config
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/bin/bash
-
-ssrv_endpoint=localhost:6966
-ssrv_prefix=testonly
-ssrv_shard_start=2009
-ssrv_interval=5s
diff --git a/integration/conf/trillian.config b/integration/conf/trillian.config
deleted file mode 100644
index eaa6f6d..0000000
--- a/integration/conf/trillian.config
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/bin/bash
-
-tsrv_rpc=localhost:6962
-tseq_rpc=localhost:6963
-
-tsrv_http=localhost:6964
-tseq_http=localhost:6965
diff --git a/integration/test.sh b/integration/test.sh
index 25de7a6..6442704 100755
--- a/integration/test.sh
+++ b/integration/test.sh
@@ -4,50 +4,108 @@
# Requirements to run
#
# - Install required dependencies, see check_go_deps()
-# - Add the empty values in conf/client.config
+# - Fill in the empty values in conf/client.config
#
-# Usage:
+# Example usage:
#
# $ ./test.sh
#
set -eu
+shopt -s nullglob
trap cleanup EXIT
+declare g_offline_mode=1
+
+declare -A nvars
+declare nodes="loga logb"
+declare -r loga=conf/primary.config
+declare -r logb=conf/secondary.config
+declare -r logc=conf/logc.config
+declare -r client=conf/client.config
+
function main() {
- log_dir=$(mktemp -d)
+ local testflavour=basic
+ [[ $# > 0 ]] && { testflavour=$1; shift; }
check_go_deps
- trillian_setup conf/trillian.config
- sigsum_setup conf/sigsum.config
- client_setup conf/client.config
- check_setup
- run_tests
+ node_setup $loga $logb
+
+ # Primary
+ nvars[$loga:ssrv_extra_args]="-secondary-url=http://${nvars[$logb:int_url]}"
+ nvars[$loga:ssrv_extra_args]+=" -secondary-pubkey=${nvars[$logb:ssrv_pub]}"
+ node_start $loga
+
+ # Secondary
+ nvars[$logb:ssrv_extra_args]="-primary-url=http://${nvars[$loga:int_url]}"
+ nvars[$logb:ssrv_extra_args]+=" -primary-pubkey=${nvars[$loga:ssrv_pub]}"
+ node_start $logb
+
+ client_setup $client
+ check_setup $loga $logb
+ run_tests $loga $logb 0 5
+ run_tests $loga $logb 5 1
+
+ if [[ $testflavour == extended ]]; then
+ # for tree equality tests later on; FIXME: remove
+ test_signed_tree_head $loga 6
+ cp ${nvars[$loga:log_dir]}/rsp ${nvars[$loga:log_dir]}/last_sth
+
+ node_stop_fe $loga $logb
+ node_destroy $loga; node_stop_be $loga
+ node_setup $logc
+
+ node_promote $logb $loga
+ nvars[$logb:ssrv_extra_args]="-secondary-url=http://${nvars[$logc:int_url]}"
+ nvars[$logb:ssrv_extra_args]+=" -secondary-pubkey=${nvars[$logc:ssrv_pub]}"
+ node_start_fe $logb
+
+ nvars[$logc:ssrv_extra_args]="-primary-url=http://${nvars[$logb:int_url]}"
+ nvars[$logc:ssrv_extra_args]+=" -primary-pubkey=${nvars[$logb:ssrv_pub]}"
+ nodes+=" logc"
+ node_start $logc
+
+ check_setup $logb $logc
+ run_tests_extended $logb $logc 6 ${nvars[$loga:log_dir]}/last_sth
+ fi
}
function check_go_deps() {
- [[ $(command -v trillian_log_signer) ]] || die "Hint: go install github.com/google/trillian/cmd/trillian_log_signer@v1.3.13"
- [[ $(command -v trillian_log_server) ]] || die "Hint: go install github.com/google/trillian/cmd/trillian_log_server@v1.3.13"
- [[ $(command -v createtree) ]] || die "Hint: go install github.com/google/trillian/cmd/createtree@v1.3.13"
- [[ $(command -v deletetree) ]] || die "Hint: go install github.com/google/trillian/cmd/deletetree@v1.3.13"
- [[ $(command -v sigsum_log_go) ]] || die "Hint: go install git.sigsum.org/log-go/cmd/sigsum_log_go@latest"
- [[ $(command -v sigsum-debug) ]] || die "Hint: install sigsum-debug from sigsum-go, branch merge/sigsum-debug"
+ [[ $(command -v trillian_log_signer) ]] || die "Hint: go install github.com/google/trillian/cmd/trillian_log_signer@v1.3.13"
+ [[ $(command -v trillian_log_server) ]] || die "Hint: go install github.com/google/trillian/cmd/trillian_log_server@v1.3.13"
+ [[ $(command -v createtree) ]] || die "Hint: go install github.com/google/trillian/cmd/createtree@v1.3.13"
+ [[ $(command -v deletetree) ]] || die "Hint: go install github.com/google/trillian/cmd/deletetree@v1.3.13"
+ [[ $(command -v updatetree) ]] || die "Hint: go install github.com/google/trillian/cmd/updatetree@v1.3.13"
+ [[ $(command -v sigsum-log-primary) ]] || die "Hint: go install git.sigsum.org/log-go/cmd/sigsum-log-primary@latest"
+ [[ $(command -v sigsum-log-secondary) ]] || die "Hint: go install git.sigsum.org/log-go/cmd/sigsum-log-secondary@latest"
+ [[ $(command -v sigsum-debug) ]] || die "Hint: go install git.sigsum.org/sigsum-go/cmd/sigsum-debug@latest"
}
function client_setup() {
- info "setting up client"
- source $1
+ for i in $@; do
+ info "setting up client ($i)"
+ source $1 # NOTE: not ready for multiple clients -- stomping on everything
+
+ cli_pub=$(echo $cli_priv | sigsum-debug key public)
+ cli_key_hash=$(echo $cli_pub | sigsum-debug key hash)
- cli_pub=$(echo $cli_priv | sigsum-debug key public)
- cli_key_hash=$(echo $cli_pub | sigsum-debug key hash)
+ [[ $cli_domain_hint =~ ^_sigsum_v0..+ ]] ||
+ die "must have a valid domain hint"
+
+ if [[ $g_offline_mode -ne 1 ]]; then
+ verify_domain_hint_in_dns $cli_domain_hint $cli_key_hash
+ fi
+ done
+}
- [[ $cli_domain_hint =~ ^_sigsum_v0..+ ]] ||
- die "must have a valid domain hint"
+function verify_domain_hint_in_dns() {
+ local domain_hint=$1; shift
+ local key_hash=$1; shift
- for line in $(dig +short -t txt $cli_domain_hint); do
+ for line in $(dig +short -t txt $domain_hint); do
key_hash=${line:1:${#line}-2}
- if [[ $key_hash == $cli_key_hash ]]; then
+ if [[ $key_hash == $key_hash ]]; then
return
fi
done
@@ -55,179 +113,358 @@ function client_setup() {
die "must have a properly configured domain hint"
}
+function node_setup() {
+ for i in $@; do
+ local dir=$(mktemp -d /tmp/sigsum-log-test.XXXXXXXXXX)
+ info "$i: logging to $dir"
+ nvars[$i:log_dir]=$dir
+ trillian_setup $i
+ sigsum_setup $i
+ done
+}
+
+# node_start starts trillian and sigsum and creates new trees
+function node_start() {
+ for i in $@; do
+ trillian_start $i
+ sigsum_start $i
+ done
+}
+
+# node_start_* starts sequencer and sigsum but does not create new trees
+function node_start_fe() {
+ trillian_start_sequencer $@
+ sigsum_start $@
+}
+
+function node_start_be() {
+ trillian_start_server $@
+}
+
+function node_promote() {
+ local new_primary=$1; shift
+ local prev_primary=$1; shift
+ [[ ${nvars[$new_primary:ssrv_role]} == secondary ]] || die "$new_primary: not a secondary node"
+ [[ ${nvars[$prev_primary:ssrv_role]} == primary ]] || die "$prev_primary: not the primary node"
+
+ info "promoting secondary node to primary ($new_primary)"
+ local srv=${nvars[$new_primary:tsrv_rpc]}
+ local tree_id=${nvars[$new_primary:ssrv_tree_id]}
+
+ # NOTE: updatetree doesn't seem to exit with !=0 when failing
+ # TODO: try combining the first two invocations into one
+ [[ $(updatetree --admin_server $srv -tree_id $tree_id -tree_state FROZEN -logtostderr 2>/dev/null) == FROZEN ]] || \
+ die "unable to freeze tree $tree_id"
+ [[ $(updatetree --admin_server $srv -tree_id $tree_id -tree_type LOG -logtostderr 2>/dev/null) == FROZEN ]] || \
+ die "unable to change tree type to LOG for tree $tree_id"
+ [[ $(updatetree --admin_server $srv -tree_id $tree_id -tree_state ACTIVE -logtostderr 2>/dev/null) == ACTIVE ]] || \
+ die "unable to unfreeze tree $tree_id"
+ info "tree $tree_id type changed from PREORDERED_LOG to LOG"
+
+ nvars[$new_primary:ssrv_role]=primary
+ nvars[$new_primary:ssrv_interval]=5 # FIXME: parameterize
+ nvars[$new_primary:ssrv_priv]=${nvars[$prev_primary:ssrv_priv]}
+ nvars[$new_primary:ssrv_pub]=${nvars[$prev_primary:ssrv_pub]}
+ nvars[$new_primary:ssrv_key_hash]=${nvars[$prev_primary:ssrv_key_hash]}
+}
+
function trillian_setup() {
- info "setting up Trillian"
- source $1
+ for i in $@; do
+ info "setting up Trillian ($i)"
+
+ source $i
+ nvars[$i:tsrv_rpc]=$tsrv_rpc
+ nvars[$i:tsrv_http]=$tsrv_http
+ nvars[$i:tseq_rpc]=$tseq_rpc
+ nvars[$i:tseq_http]=$tseq_http
+ done
+}
- trillian_log_server\
- -rpc_endpoint=$tsrv_rpc\
- -http_endpoint=$tsrv_http\
- -log_dir=$log_dir 2>/dev/null &
- tsrv_pid=$!
- info "started Trillian log server (pid $tsrv_pid)"
+# trillian_start starts trillian components and creates new trees
+function trillian_start() {
+ trillian_start_server $@
+ trillian_start_sequencer $@
+ trillian_createtree $@
+}
- trillian_log_signer\
- -force_master\
- -rpc_endpoint=$tseq_rpc\
- -http_endpoint=$tseq_http\
- -log_dir=$log_dir 2>/dev/null &
+function trillian_start_server() {
+ for i in $@; do
+ info "starting up Trillian server ($i)"
- tseq_pid=$!
- info "started Trillian log sequencer (pid $tseq_pid)"
+ trillian_log_server\
+ -rpc_endpoint=${nvars[$i:tsrv_rpc]}\
+ -http_endpoint=${nvars[$i:tsrv_http]}\
+ -log_dir=${nvars[$i:log_dir]} 2>/dev/null &
+ nvars[$i:tsrv_pid]=$!
+ info "started Trillian log server (pid ${nvars[$i:tsrv_pid]})"
+ done
+}
+
+function trillian_start_sequencer() {
+ for i in $@; do
+ # no sequencer needed for secondaries
+ [[ ${nvars[$i:ssrv_role]} == secondary ]] && continue
+
+ info "starting up Trillian sequencer ($i)"
+ trillian_log_signer\
+ -force_master\
+ -rpc_endpoint=${nvars[$i:tseq_rpc]}\
+ -http_endpoint=${nvars[$i:tseq_http]}\
+ -log_dir=${nvars[$i:log_dir]} 2>/dev/null &
+ nvars[$i:tseq_pid]=$!
+ info "started Trillian log sequencer (pid ${nvars[$i:tseq_pid]})"
+ done
+}
+
+function trillian_createtree() {
+ for i in $@; do
+ local createtree_extra_args=""
- ssrv_tree_id=$(createtree --admin_server $tsrv_rpc 2>/dev/null)
- [[ $? -eq 0 ]] ||
- die "must provision a new Merkle tree"
+ [[ ${nvars[$i:ssrv_role]} == secondary ]] && createtree_extra_args=" -tree_type PREORDERED_LOG"
+ nvars[$i:ssrv_tree_id]=$(createtree --admin_server ${nvars[$i:tsrv_rpc]} $createtree_extra_args -logtostderr 2>/dev/null)
+ [[ $? -eq 0 ]] || die "must provision a new Merkle tree"
- info "provisioned Merkle tree with id $ssrv_tree_id"
+ info "provisioned Merkle tree with id ${nvars[$i:ssrv_tree_id]}"
+ done
}
function sigsum_setup() {
- info "setting up Sigsum server"
- source $1
-
- wit1_priv=$(sigsum-debug key private)
- wit1_pub=$(echo $wit1_priv | sigsum-debug key public)
- wit1_key_hash=$(echo $wit1_pub | sigsum-debug key hash)
-
- wit2_priv=$(sigsum-debug key private)
- wit2_pub=$(echo $wit2_priv | sigsum-debug key public)
- wit2_key_hash=$(echo $wit2_pub | sigsum-debug key hash)
-
- ssrv_witnesses=$wit1_pub,$wit2_pub
- ssrv_priv=$(sigsum-debug key private)
- ssrv_pub=$(echo $ssrv_priv | sigsum-debug key public)
- ssrv_key_hash=$(echo $ssrv_pub | sigsum-debug key hash)
-
- sigsum_log_go\
- -prefix=$ssrv_prefix\
- -trillian_id=$ssrv_tree_id\
- -shard_interval_start=$ssrv_shard_start\
- -key=<(echo $ssrv_priv)\
- -witnesses=$ssrv_witnesses\
- -interval=$ssrv_interval\
- -http_endpoint=$ssrv_endpoint\
- -log-color="true"\
- -log-level="debug"\
- -log-file=$log_dir/sigsum-log.log 2>/dev/null &
- ssrv_pid=$!
-
- log_url=$ssrv_endpoint/$ssrv_prefix/sigsum/v0
- info "started Sigsum log server on $ssrv_endpoint (pid $ssrv_pid)"
+ for i in $@; do
+ info "setting up Sigsum server ($i)"
+ source $i
+
+ nvars[$i:ssrv_role]=$ssrv_role
+ nvars[$i:ssrv_endpoint]=$ssrv_endpoint
+ nvars[$i:ssrv_internal]=$ssrv_internal
+ nvars[$i:ssrv_prefix]=$ssrv_prefix
+ nvars[$i:ssrv_shard_start]=$ssrv_shard_start
+ nvars[$i:ssrv_interval]=$ssrv_interval_sec
+
+
+ nvars[$i:log_url]=${nvars[$i:ssrv_endpoint]}/${nvars[$i:ssrv_prefix]}/sigsum/v0
+ nvars[$i:int_url]=${nvars[$i:ssrv_internal]}/${nvars[$i:ssrv_prefix]}/sigsum/v0
+
+ nvars[$i:wit1_priv]=$(sigsum-debug key private)
+ nvars[$i:wit1_pub]=$(echo ${nvars[$i:wit1_priv]} | sigsum-debug key public)
+ nvars[$i:wit1_key_hash]=$(echo ${nvars[$i:wit1_pub]} | sigsum-debug key hash)
+ nvars[$i:wit2_priv]=$(sigsum-debug key private)
+ nvars[$i:wit2_pub]=$(echo ${nvars[$i:wit2_priv]} | sigsum-debug key public)
+ nvars[$i:wit2_key_hash]=$(echo ${nvars[$i:wit2_pub]} | sigsum-debug key hash)
+ nvars[$i:ssrv_witnesses]=${nvars[$i:wit1_pub]},${nvars[$i:wit2_pub]}
+
+ nvars[$i:ssrv_priv]=$(sigsum-debug key private)
+ nvars[$i:ssrv_pub]=$(echo ${nvars[$i:ssrv_priv]} | sigsum-debug key public)
+ nvars[$i:ssrv_key_hash]=$(echo ${nvars[$i:ssrv_pub]} | sigsum-debug key hash)
+ done
}
-function cleanup() {
- set +e
+function sigsum_start() {
+ for i in $@; do
+ local role=${nvars[$i:ssrv_role]}
+ local binary=sigsum-log-primary;
+ local extra_args="${nvars[$i:ssrv_extra_args]}"
+
+ if [[ $role = primary ]]; then
+ extra_args+=" -witnesses=${nvars[$i:ssrv_witnesses]}"
+ extra_args+=" -shard-interval-start=${nvars[$i:ssrv_shard_start]}"
+ else
+ binary=sigsum-log-secondary
+ fi
+ info "starting Sigsum log $role node ($i)"
+
+ args="$extra_args \
+ -url-prefix=${nvars[$i:ssrv_prefix]} \
+ -tree-id=${nvars[$i:ssrv_tree_id]} \
+ -trillian-rpc-server=${nvars[$i:tsrv_rpc]} \
+ -interval=${nvars[$i:ssrv_interval]}s \
+ -external-endpoint=${nvars[$i:ssrv_endpoint]} \
+ -internal-endpoint=${nvars[$i:ssrv_internal]} \
+ -test-mode=true \
+ -log-color=false \
+ -log-level=debug \
+ -log-file=${nvars[$i:log_dir]}/sigsum-log.log"
+ $binary $args -key=<(echo ${nvars[$i:ssrv_priv]}) \
+ 2>${nvars[$i:log_dir]}/sigsum-log.$(date +%s).stderr &
+ nvars[$i:ssrv_pid]=$!
+
+ info "started Sigsum log server on ${nvars[$i:ssrv_endpoint]} / ${nvars[$i:ssrv_internal]} (pid ${nvars[$i:ssrv_pid]})"
+ done
+}
- info "cleaning up, please wait..."
- sleep 1
+function node_stop() {
+ node_stop_fe $@
+ node_stop_be $@
+}
- kill -2 $ssrv_pid
- kill -2 $tseq_pid
- while :; do
- sleep 1
+# Delete log tree for, requires trillian server ("backend") to be running
+function node_destroy() {
+ for i in $@; do
+ if ! deletetree -admin_server=$tsrv_rpc -log_id=${nvars[$i:ssrv_tree_id]} -logtostderr 2>/dev/null; then
+ warn "failed deleting provisioned Merkle tree ${nvars[$i:ssrv_tree_id]}"
+ else
+ info "deleted provisioned Merkle tree ${nvars[$i:ssrv_tree_id]}"
+ fi
+ done
+}
- ps -p $tseq_pid >/dev/null && continue
- ps -p $ssrv_pid >/dev/null && continue
+function node_stop_fe() {
+ for i in $@; do
+
+ [[ -v nvars[$i:ssrv_pid] ]] && pp ${nvars[$i:ssrv_pid]} && kill ${nvars[$i:ssrv_pid]} # FIXME: why is SIGINT (often) not enough?
+ [[ -v nvars[$i:tseq_pid] ]] && pp ${nvars[$i:tseq_pid]} && kill -2 ${nvars[$i:tseq_pid]}
+ while :; do
+ sleep 1
+
+ [[ -v nvars[$i:tseq_pid] ]] && pp ${nvars[$i:tseq_pid]} && continue
+ [[ -v nvars[$i:ssrv_pid] ]] && pp ${nvars[$i:ssrv_pid]} && continue
+
+ break
+ done
+ info "stopped Trillian log sequencer ($i)"
+ info "stopped Sigsum log server ($i)"
- break
done
+}
- info "stopped Trillian log sequencer"
- info "stopped Sigsum log server"
+function node_stop_be() {
+ for i in $@; do
+ pp ${nvars[$i:tsrv_pid]} && kill -2 ${nvars[$i:tsrv_pid]}
+ while :; do
+ sleep 1
- if ! deletetree -admin_server=$tsrv_rpc -log_id=$ssrv_tree_id; then
- warn "failed deleting provisioned Merkle tree"
- else
- info "deleteted provisioned Merkle tree"
- fi
+ pp ${nvars[$i:tsrv_pid]} && continue
- kill -2 $tsrv_pid
- while :; do
- sleep 1
+ break
+ done
+ info "stopped Trillian log server ($i)"
+ done
+}
+
+function cleanup() {
+ set +e
+
+ info "cleaning up, please wait..."
- ps -p $tsrv_pid >/dev/null && continue
+ for var in $nodes; do
+ declare -n cleanup_i=$var # Using unique iterator name, bc leaking
+ node_stop_fe $cleanup_i
+ done
- break
+ for var in $nodes; do
+ declare -n cleanup_i=$var # Using unique iterator name, bc leaking
+ node_destroy $cleanup_i
done
- info "stopped Trillian log server"
+ for var in $nodes; do
+ declare -n cleanup_i=$var # Using unique iterator name, bc leaking
+ node_stop_be $cleanup_i
+ done
- printf "\n Press any key to delete logs in $log_dir"
- read dummy
+ for var in $nodes; do
+ declare -n cleanup_i=$var # Using unique iterator name, bc leaking
+ printf "\n Press enter to delete logs in ${nvars[$cleanup_i:log_dir]}"
+ read dummy
- rm -rf $log_dir
+ rm -rf ${nvars[$cleanup_i:log_dir]}
+ done
}
function check_setup() {
sleep 3
-
- ps -p $tseq_pid >/dev/null || die "must have Trillian log sequencer"
- ps -p $tsrv_pid >/dev/null || die "must have Trillian log server"
- ps -p $ssrv_pid >/dev/null || die "must have Sigsum log server"
+ for i in $@; do
+ info "checking setup for $i"
+ if [[ ${nvars[$i:ssrv_role]} == primary ]]; then
+ [[ -v nvars[$i:tseq_pid] ]] && pp ${nvars[$i:tseq_pid]} || die "must have Trillian log sequencer ($i)"
+ fi
+ [[ -v nvars[$i:tsrv_pid] ]] && pp ${nvars[$i:tsrv_pid]} || die "must have Trillian log server ($i)"
+ [[ -v nvars[$i:ssrv_pid] ]] && pp ${nvars[$i:ssrv_pid]} || die "must have Sigsum log server ($i)"
+ done
}
function run_tests() {
- num_leaf=5
+ local pri=$1; shift
+ local sec=$1; shift
+ local start_leaf=$1; shift # 0-based
+ local num_leaf=$1; shift
- test_signed_tree_head 0
- for i in $(seq 1 $num_leaf); do
- test_add_leaf $i
- done
+ info "running ordinary tests, pri=$pri, start_leaf=$start_leaf, num_leaf=$num_leaf"
+
+ test_signed_tree_head $pri $start_leaf
- info "waiting for $num_leaf leaves to be merged..."
- sleep ${ssrv_interval::-1}
+ info "adding $num_leaf leaves"
+ test_add_leaves $pri $(( $start_leaf + 1 )) $num_leaf
+ num_leaf=$(( $num_leaf + $start_leaf ))
- test_signed_tree_head $num_leaf
- for i in $(seq 1 $(( $num_leaf - 1 ))); do
- test_consistency_proof $i $num_leaf
+ test_signed_tree_head $pri $num_leaf
+ for i in $(seq $(( $start_leaf + 1 )) $(( $num_leaf - 1 ))); do
+ test_consistency_proof $pri $i $num_leaf
done
- test_cosignature $wit1_key_hash $wit1_priv
- test_cosignature $wit2_key_hash $wit2_priv
+ test_cosignature $pri ${nvars[$pri:wit1_key_hash]} ${nvars[$pri:wit1_priv]}
+ test_cosignature $pri ${nvars[$pri:wit2_key_hash]} ${nvars[$pri:wit2_priv]}
- info "waiting for cosignature to be available..."
- sleep ${ssrv_interval::-1}
+ info "waiting for cosignature(s) to be available..."
+ sleep ${nvars[$pri:ssrv_interval]}
- test_cosigned_tree_head $num_leaf
- for i in $(seq 1 $num_leaf); do
- test_inclusion_proof $num_leaf $i $(( $i - 1 ))
+ test_cosigned_tree_head $pri $num_leaf
+ for i in $(seq $(( $start_leaf + 1 )) $num_leaf); do
+ test_inclusion_proof $pri $num_leaf $i $(( $i - 1 ))
done
- for i in $(seq 1 $num_leaf); do
- test_get_leaf $i $(( $i - 1 ))
+ for i in $(seq $(( $start_leaf + 1 )) $num_leaf); do
+ test_get_leaf $pri $i $(( $i - 1 ))
done
warn "no signatures and merkle proofs were verified"
}
+run_tests_extended() {
+ local pri=$1; shift
+ local sec=$1; shift
+ local current_tree_size=$1; shift
+ local old_pri_sth_rsp=$1; shift
+ info "running extended tests"
+
+ info "wait for new primary and secondary to catch up and merge"
+ sleep $(( ${nvars[$pri:ssrv_interval]} + ${nvars[$sec:ssrv_interval]} + 1 ))
+
+ test_signed_tree_head $pri $current_tree_size
+ test_tree_heads_equal ${nvars[$pri:log_dir]}/rsp $old_pri_sth_rsp
+
+ run_tests $pri $sec $current_tree_size 5
+}
+
function test_signed_tree_head() {
- desc="GET tree-head-to-cosign (tree size $1)"
- curl -s -w "%{http_code}" $log_url/get-tree-head-to-cosign \
- >$log_dir/rsp
+ local pri=$1; shift
+ local tree_size=$1; shift
+ local log_dir=${nvars[$pri:log_dir]}
+ local desc="GET get-tree-head-to-cosign (tree size $tree_size)"
+
+ curl -s -w "%{http_code}" ${nvars[$pri:log_url]}/get-tree-head-to-cosign \
+ >$log_dir/rsp
- if [[ $(status_code) != 200 ]]; then
- fail "$desc: http status code $(status_code)"
+ if [[ $(status_code $pri) != 200 ]]; then
+ fail "$desc: http status code $(status_code $pri)"
return
fi
- if ! keys "timestamp" "tree_size" "root_hash" "signature"; then
- fail "$desc: ascii keys in response $(debug_response)"
+ if ! keys $pri "timestamp" "tree_size" "root_hash" "signature"; then
+ fail "$desc: ascii keys in response $(debug_response $pri)"
return
fi
now=$(date +%s)
- if [[ $(value_of "timestamp") -gt $now ]]; then
- fail "$desc: timestamp $(value_of "timestamp") is too large"
+ if [[ $(value_of $pri "timestamp") -gt $now ]]; then
+ fail "$desc: timestamp $(value_of $pri "timestamp") is too high"
return
fi
- if [[ $(value_of "timestamp") -lt $(( $now - ${ssrv_interval::-1} )) ]]; then
- fail "$desc: timestamp $(value_of "timestamp") is too small"
+ if [[ $(value_of $pri "timestamp") -lt $(( $now - ${nvars[$pri:ssrv_interval]} - 1 )) ]]; then
+ fail "$desc: timestamp $(value_of $pri "timestamp") is too low"
return
fi
- if [[ $(value_of "tree_size") != $1 ]]; then
- fail "$desc: tree size $(value_of "tree_size")"
+ if [[ $(value_of $pri "tree_size") != $tree_size ]]; then
+ fail "$desc: tree size $(value_of $pri "tree_size")"
return
fi
@@ -235,39 +472,65 @@ function test_signed_tree_head() {
pass $desc
}
+function test_tree_heads_equal() {
+ local rsp1=$1; shift
+ local rsp2=$1; shift
+ local desc="comparing tree heads ($rsp1, $rsp2)"
+
+ n1_tree_size=$(value_of_file $rsp1 "tree_size")
+ n2_tree_size=$(value_of_file $rsp2 "tree_size")
+ if [[ $n1_tree_size -ne $n2_tree_size ]]; then
+ fail "$desc: tree_size: $n1_tree_size != $n2_tree_size"
+ return
+ fi
+
+ n1_root_hash=$(value_of_file $rsp1 "root_hash")
+ n2_root_hash=$(value_of_file $rsp2 "root_hash")
+ if [[ $n1_root_hash != $n2_root_hash ]]; then
+ fail "$desc: root_hash: $n1_root_hash != $n2_root_hash"
+ return
+ fi
+
+ pass $desc
+}
+
function test_cosigned_tree_head() {
- desc="GET get-tree-head-cosigned (all witnesses)"
- curl -s -w "%{http_code}" $log_url/get-tree-head-cosigned \
- >$log_dir/rsp
+ local pri=$1; shift
+ local tree_size=$1; shift
+ local log_dir=${nvars[$pri:log_dir]}
+ local desc="GET get-tree-head-cosigned (all witnesses), tree_size $tree_size"
- if [[ $(status_code) != 200 ]]; then
- fail "$desc: http status code $(status_code)"
+ curl -s -w "%{http_code}" ${nvars[$pri:log_url]}/get-tree-head-cosigned \
+ >$log_dir/rsp
+
+ if [[ $(status_code $pri) != 200 ]]; then
+ fail "$desc: http status code $(status_code $pri)"
return
fi
- if ! keys "timestamp" "tree_size" "root_hash" "signature" "cosignature" "key_hash"; then
- fail "$desc: ascii keys in response $(debug_response)"
+ if ! keys $pri "timestamp" "tree_size" "root_hash" "signature" "cosignature" "key_hash"; then
+ fail "$desc: ascii keys in response $(debug_response $pri)"
return
fi
now=$(date +%s)
- if [[ $(value_of "timestamp") -gt $now ]]; then
- fail "$desc: timestamp $(value_of "timestamp") is too large"
+ if [[ $(value_of $pri "timestamp") -gt $now ]]; then
+ fail "$desc: timestamp $(value_of $pri "timestamp") is too large"
return
fi
- if [[ $(value_of "timestamp") -lt $(( $now - ${ssrv_interval::-1} * 2 )) ]]; then
- fail "$desc: timestamp $(value_of "timestamp") is too small"
+ if [[ $(value_of $pri "timestamp") -lt $(( $now - ${nvars[$pri:ssrv_interval]} * 2 )) ]]; then
+ fail "$desc: timestamp $(value_of $pri "timestamp") is too small"
return
fi
- if [[ $(value_of "tree_size") != $1 ]]; then
- fail "$desc: tree size $(value_of "tree_size")"
+ if [[ $(value_of $pri "tree_size") != $tree_size ]]; then
+ fail "$desc: tree size $(value_of $pri "tree_size")"
return
fi
- for got in $(value_of key_hash); do
+ for got in $(value_of $pri key_hash); do
found=""
- for want in $wit1_key_hash $wit2_key_hash; do
+ for want in ${nvars[$pri:wit1_key_hash]} ${nvars[$pri:wit2_key_hash]}; do
if [[ $got == $want ]]; then
found=true
fi
@@ -285,23 +548,29 @@ function test_cosigned_tree_head() {
}
function test_inclusion_proof() {
- desc="GET get-inclusion-proof (tree_size $1, data \"$2\", index $3)"
- signature=$(echo $2 | sigsum-debug leaf sign -k $cli_priv -h $ssrv_shard_start)
- leaf_hash=$(echo $2 | sigsum-debug leaf hash -k $cli_key_hash -s $signature -h $ssrv_shard_start)
- curl -s -w "%{http_code}" $log_url/get-inclusion-proof/$1/$leaf_hash >$log_dir/rsp
-
- if [[ $(status_code) != 200 ]]; then
- fail "$desc: http status code $(status_code)"
+ local pri=$1; shift
+ local tree_size=$1; shift
+ local data=$1; shift
+ local index=$1; shift
+ local log_dir=${nvars[$pri:log_dir]}
+ local desc="GET get-inclusion-proof (tree_size $tree_size, data \"$data\", index $index)"
+
+ local signature=$(echo ${data} | sigsum-debug leaf sign -k $cli_priv -h ${nvars[$pri:ssrv_shard_start]})
+ local leaf_hash=$(echo ${data} | sigsum-debug leaf hash -k $cli_key_hash -s $signature -h ${nvars[$pri:ssrv_shard_start]})
+ curl -s -w "%{http_code}" ${nvars[$pri:log_url]}/get-inclusion-proof/${tree_size}/${leaf_hash} >${log_dir}/rsp
+
+ if [[ $(status_code $pri) != 200 ]]; then
+ fail "$desc: http status code $(status_code $pri)"
return
fi
- if ! keys "leaf_index" "inclusion_path"; then
- fail "$desc: ascii keys in response $(debug_response)"
+ if ! keys $pri "leaf_index" "inclusion_path"; then
+ fail "$desc: ascii keys in response $(debug_response $pri)"
return
fi
- if [[ $(value_of leaf_index) != $3 ]]; then
- fail "$desc: wrong leaf index $(value_of leaf_index)"
+ if [[ $(value_of $pri leaf_index) != ${index} ]]; then
+ fail "$desc: wrong leaf index $(value_of $pri leaf_index)"
return
fi
@@ -310,16 +579,19 @@ function test_inclusion_proof() {
}
function test_consistency_proof() {
- desc="GET get-consistency-proof (old_size $1, new_size $2)"
- curl -s -w "%{http_code}" $log_url/get-consistency-proof/$1/$2 >$log_dir/rsp
+ local pri=$1; shift
+ local log_dir=${nvars[$pri:log_dir]}
+ local desc="GET get-consistency-proof (old_size $1, new_size $2)"
+
+ curl -s -w "%{http_code}" ${nvars[$pri:log_url]}/get-consistency-proof/$1/$2 >$log_dir/rsp
- if [[ $(status_code) != 200 ]]; then
- fail "$desc: http status code $(status_code)"
+ if [[ $(status_code $pri) != 200 ]]; then
+ fail "$desc: http status code $(status_code $pri)"
return
fi
- if ! keys "consistency_path"; then
- fail "$desc: ascii keys in response $(debug_response)"
+ if ! keys $pri "consistency_path"; then
+ fail "$desc: ascii keys in response $(debug_response $pri)"
return
fi
@@ -328,80 +600,130 @@ function test_consistency_proof() {
}
function test_get_leaf() {
- desc="GET get-leaves (data \"$1\", index $2)"
- curl -s -w "%{http_code}" $log_url/get-leaves/$2/$2 >$log_dir/rsp
+ local pri=$1; shift
+ local data="$1"; shift
+ local index="$1"; shift
+ local log_dir=${nvars[$pri:log_dir]}
+ local desc="GET get-leaves (data \"$data\", index $index)"
- if [[ $(status_code) != 200 ]]; then
- fail "$desc: http status code $(status_code)"
+ curl -s -w "%{http_code}" ${nvars[$pri:log_url]}/get-leaves/$index/$index >$log_dir/rsp
+
+ if [[ $(status_code $pri) != 200 ]]; then
+ fail "$desc: http status code $(status_code $pri)"
return
fi
- if ! keys "shard_hint" "checksum" "signature" "key_hash"; then
- fail "$desc: ascii keys in response $(debug_response)"
+ if ! keys $pri "shard_hint" "checksum" "signature" "key_hash"; then
+ fail "$desc: ascii keys in response $(debug_response $pri)"
return
fi
- if [[ $(value_of shard_hint) != $ssrv_shard_start ]]; then
- fail "$desc: wrong shard hint $(value_of shard_hint)"
+ if [[ $(value_of $pri shard_hint) != ${nvars[$pri:ssrv_shard_start]} ]]; then
+ fail "$desc: wrong shard hint $(value_of $pri shard_hint)"
return
fi
- message=$(openssl dgst -binary <(echo $1) | base16)
- checksum=$(openssl dgst -binary <(echo $message | base16 -d) | base16)
- if [[ $(value_of checksum) != $checksum ]]; then
- fail "$desc: wrong checksum $(value_of checksum)"
+ local message=$(openssl dgst -binary <(echo $data) | base16)
+ local checksum=$(openssl dgst -binary <(echo $message | base16 -d) | base16)
+ if [[ $(value_of $pri checksum) != $checksum ]]; then
+ fail "$desc: wrong checksum $(value_of $pri checksum)"
return
fi
- if [[ $(value_of key_hash) != $cli_key_hash ]]; then
- fail "$desc: wrong key hash $(value_of key_hash)"
+ if [[ $(value_of $pri key_hash) != $cli_key_hash ]]; then
+ fail "$desc: wrong key hash $(value_of $pri key_hash)"
fi
# TODO: check leaf signature
pass $desc
}
-function test_add_leaf() {
- desc="POST add-leaf (data \"$1\")"
- echo "shard_hint=$ssrv_shard_start" > $log_dir/req
- echo "message=$(openssl dgst -binary <(echo $1) | base16)" >> $log_dir/req
- echo "signature=$(echo $1 |
- sigsum-debug leaf sign -k $cli_priv -h $ssrv_shard_start)" >> $log_dir/req
+function test_add_leaves() {
+ local s=$1; shift
+ local start=$1; shift # integer, used as data and filename under subs/
+ local end=$(( $start + $1 - 1 )); shift # number of leaves to add
+ local desc="add leaves"
+ local log_dir=${nvars[$s:log_dir]}
+ [[ -d $log_dir/subs/$s ]] || mkdir -p $log_dir/subs/$s
+
+ local -a rc
+ for i in $(seq $start $end); do
+ rc[$i]=$(add_leaf $s $i)
+ done
+
+ # TODO: bail out and fail after $timeout seconds
+ while true; do
+ local keep_going=0
+ for i in $(seq $start $end); do
+ if [[ ${rc[$i]} -eq 202 ]]; then
+ keep_going=1
+ break
+ fi
+ done
+ [[ $keep_going -eq 0 ]] && break
+
+ sleep 1
+ for i in $(seq $start $end); do
+ if [[ ${rc[$i]} -eq 202 ]]; then
+ rc[$i]=$(add_leaf $s $i)
+ if [[ ${rc[$i]} -eq 200 ]]; then
+ if ! keys $s; then
+ fail "$desc (data \"$i\"): ascii keys in response $(debug_response $s)"
+ fi
+ fi
+ fi
+ done
+ done
+
+ local all_good=1
+ for i in $(seq $start $end); do
+ if [[ ${rc[$i]} -ne 200 ]]; then
+ fail "$desc (data \"$i\") HTTP status code: ${rc[$i]}"
+ all_good=0
+ fi
+ echo ${rc[$i]} > "$log_dir/subs/$s/$i"
+ done
+ [[ $all_good -eq 1 ]] && pass $desc
+}
+
+function add_leaf() {
+ local s=$1; shift
+ local data="$1"; shift
+ local log_dir=${nvars[$s:log_dir]}
+
+ echo "shard_hint=${nvars[$s:ssrv_shard_start]}" > $log_dir/req
+ echo "message=$(openssl dgst -binary <(echo $data) | base16)" >> $log_dir/req
+ echo "signature=$(echo $data |
+ sigsum-debug leaf sign -k $cli_priv -h ${nvars[$s:ssrv_shard_start]})" >> $log_dir/req
echo "public_key=$cli_pub" >> $log_dir/req
echo "domain_hint=$cli_domain_hint" >> $log_dir/req
- cat $log_dir/req |
- curl -s -w "%{http_code}" --data-binary @- $log_url/add-leaf \
- >$log_dir/rsp
-
- if [[ $(status_code) != 200 ]]; then
- fail "$desc: http status code $(status_code)"
- return
- fi
- if ! keys; then
- fail "$desc: ascii keys in response $(debug_response)"
- return
- fi
+ cat $log_dir/req |
+ curl -s -w "%{http_code}" --data-binary @- ${nvars[$s:log_url]}/add-leaf \
+ >$log_dir/rsp
- pass $desc
+ echo $(status_code $s)
}
function test_cosignature() {
- desc="POST add-cosignature (witness $1)"
+ local pri=$1; shift
+ local log_dir=${nvars[$pri:log_dir]}
+ local desc="POST add-cosignature (witness $1)"
+
echo "key_hash=$1" > $log_dir/req
- echo "cosignature=$(curl -s $log_url/get-tree-head-to-cosign |
- sigsum-debug head sign -k $2 -h $ssrv_key_hash)" >> $log_dir/req
+ echo "cosignature=$(curl -s ${nvars[$pri:log_url]}/get-tree-head-to-cosign |
+ sigsum-debug head sign -k $2 -h ${nvars[$pri:ssrv_key_hash]})" >> $log_dir/req
cat $log_dir/req |
- curl -s -w "%{http_code}" --data-binary @- $log_url/add-cosignature \
- >$log_dir/rsp
+ curl -s -w "%{http_code}" --data-binary @- ${nvars[$pri:log_url]}/add-cosignature \
+ >$log_dir/rsp
- if [[ $(status_code) != 200 ]]; then
- fail "$desc: http status code $(status_code)"
+ if [[ $(status_code $pri) != 200 ]]; then
+ fail "$desc: http status code $(status_code $pri)"
return
fi
- if ! keys; then
- fail "$desc: ascii keys in response $(debug_response)"
+ if ! keys $pri; then
+ fail "$desc: ascii keys in response $(debug_response $pri)"
return
fi
@@ -409,15 +731,23 @@ function test_cosignature() {
}
function debug_response() {
+ local i=$1; shift
echo ""
- cat $log_dir/rsp
+ cat ${nvars[$i:log_dir]}/rsp
}
function status_code() {
- tail -n1 $log_dir/rsp
+ local i=$1; shift
+ tail -n1 ${nvars[$i:log_dir]}/rsp
}
function value_of() {
+ local s=$1; shift
+ value_of_file ${nvars[$s:log_dir]}/rsp $@
+}
+
+function value_of_file() {
+ local rsp=$1; shift
while read line; do
key=$(echo $line | cut -d"=" -f1)
if [[ $key != $1 ]]; then
@@ -426,16 +756,17 @@ function value_of() {
value=$(echo $line | cut -d"=" -f2)
echo $value
- done < <(head --lines=-1 $log_dir/rsp)
+ done < <(head --lines=-1 $rsp)
}
function keys() {
+ local s=$1; shift
declare -A map
map[thedummystring]=to_avoid_error_on_size_zero
while read line; do
key=$(echo $line | cut -d"=" -f1)
map[$key]=ok
- done < <(head --lines=-1 $log_dir/rsp)
+ done < <(head --lines=-1 ${nvars[$s:log_dir]}/rsp)
if [[ $# != $(( ${#map[@]} - 1 )) ]]; then
return 1
@@ -448,6 +779,12 @@ function keys() {
return 0
}
+# Is proces with PID $1 running or not?
+function pp() {
+ [[ $1 == -p ]] && shift
+ [[ -d /proc/$1 ]]
+}
+
function die() {
echo -e "\e[37m$(date +"%y-%m-%d %H:%M:%S %Z")\e[0m [\e[31mFATA\e[0m] $@" >&2
exit 1
@@ -469,4 +806,8 @@ function fail() {
echo -e "\e[37m$(date +"%y-%m-%d %H:%M:%S %Z")\e[0m [\e[91mFAIL\e[0m] $@" >&2
}
-main
+main $@
+
+# Local Variables:
+# sh-basic-offset: 8
+# End:
diff --git a/pkg/db/client.go b/internal/db/client.go
index 09b8bfb..ce3bb2b 100644
--- a/pkg/db/client.go
+++ b/internal/db/client.go
@@ -9,7 +9,8 @@ import (
// Client is an interface that interacts with a log's database backend
type Client interface {
- AddLeaf(context.Context, *requests.Leaf) error
+ AddLeaf(context.Context, *requests.Leaf, uint64) (bool, error)
+ AddSequencedLeaves(ctx context.Context, leaves types.Leaves, index int64) error
GetTreeHead(context.Context) (*types.TreeHead, error)
GetConsistencyProof(context.Context, *requests.ConsistencyProof) (*types.ConsistencyProof, error)
GetInclusionProof(context.Context, *requests.InclusionProof) (*types.InclusionProof, error)
diff --git a/pkg/db/trillian.go b/internal/db/trillian.go
index 3147c8d..e8a9945 100644
--- a/pkg/db/trillian.go
+++ b/internal/db/trillian.go
@@ -6,11 +6,13 @@ import (
"time"
"git.sigsum.org/sigsum-go/pkg/log"
+ "git.sigsum.org/sigsum-go/pkg/merkle"
"git.sigsum.org/sigsum-go/pkg/requests"
"git.sigsum.org/sigsum-go/pkg/types"
"github.com/google/trillian"
trillianTypes "github.com/google/trillian/types"
"google.golang.org/grpc/codes"
+ "google.golang.org/grpc/status"
)
// TrillianClient implements the Client interface for Trillian's gRPC backend
@@ -22,37 +24,73 @@ type TrillianClient struct {
GRPC trillian.TrillianLogClient
}
-func (c *TrillianClient) AddLeaf(ctx context.Context, req *requests.Leaf) error {
+// AddLeaf adds a leaf to the tree and returns true if the leaf has
+// been sequenced into the tree of size treeSize.
+func (c *TrillianClient) AddLeaf(ctx context.Context, req *requests.Leaf, treeSize uint64) (bool, error) {
leaf := types.Leaf{
Statement: types.Statement{
ShardHint: req.ShardHint,
- Checksum: *types.HashFn(req.Message[:]),
+ Checksum: *merkle.HashFn(req.Message[:]),
},
Signature: req.Signature,
- KeyHash: *types.HashFn(req.PublicKey[:]),
+ KeyHash: *merkle.HashFn(req.PublicKey[:]),
}
serialized := leaf.ToBinary()
- log.Debug("queueing leaf request: %x", types.LeafHash(serialized))
- rsp, err := c.GRPC.QueueLeaf(ctx, &trillian.QueueLeafRequest{
+ log.Debug("queueing leaf request: %x", merkle.HashLeafNode(serialized))
+ _, err := c.GRPC.QueueLeaf(ctx, &trillian.QueueLeafRequest{
LogId: c.TreeID,
Leaf: &trillian.LogLeaf{
LeafValue: serialized,
},
})
- if err != nil {
- return fmt.Errorf("backend failure: %v", err)
- }
- if rsp == nil {
- return fmt.Errorf("no response")
- }
- if rsp.QueuedLeaf == nil {
- return fmt.Errorf("no queued leaf")
+ switch status.Code(err) {
+ case codes.OK:
+ case codes.AlreadyExists:
+ default:
+ log.Warning("gRPC error: %v", err)
+ return false, fmt.Errorf("back-end failure")
+ }
+ _, err = c.GetInclusionProof(ctx, &requests.InclusionProof{treeSize, *merkle.HashLeafNode(serialized)})
+ return err == nil, nil
+}
+
+// AddSequencedLeaves adds a set of already sequenced leaves to the tree.
+func (c *TrillianClient) AddSequencedLeaves(ctx context.Context, leaves types.Leaves, index int64) error {
+ trilLeaves := make([]*trillian.LogLeaf, len(leaves))
+ for i, leaf := range leaves {
+ trilLeaves[i] = &trillian.LogLeaf{
+ LeafValue: leaf.ToBinary(),
+ LeafIndex: index + int64(i),
+ }
}
- if codes.Code(rsp.QueuedLeaf.GetStatus().GetCode()) == codes.AlreadyExists {
- return fmt.Errorf("leaf is already queued or included")
+
+ req := trillian.AddSequencedLeavesRequest{
+ LogId: c.TreeID,
+ Leaves: trilLeaves,
+ }
+ log.Debug("adding sequenced leaves: count %d", len(trilLeaves))
+ var err error
+ for wait := 1; wait < 30; wait *= 2 {
+ var rsp *trillian.AddSequencedLeavesResponse
+ rsp, err = c.GRPC.AddSequencedLeaves(ctx, &req)
+ switch status.Code(err) {
+ case codes.ResourceExhausted:
+ log.Info("waiting %d seconds before retrying to add %d leaves, reason: %v", wait, len(trilLeaves), err)
+ time.Sleep(time.Second * time.Duration(wait))
+ continue
+ case codes.OK:
+ if rsp == nil {
+ return fmt.Errorf("GRPC.AddSequencedLeaves no response")
+ }
+ // FIXME: check rsp.Results.QueuedLogLeaf
+ return nil
+ default:
+ return fmt.Errorf("GRPC.AddSequencedLeaves error: %v", err)
+ }
}
- return nil
+
+ return fmt.Errorf("giving up on adding %d leaves", len(trilLeaves))
}
func (c *TrillianClient) GetTreeHead(ctx context.Context) (*types.TreeHead, error) {
@@ -75,7 +113,7 @@ func (c *TrillianClient) GetTreeHead(ctx context.Context) (*types.TreeHead, erro
if err := r.UnmarshalBinary(rsp.SignedLogRoot.LogRoot); err != nil {
return nil, fmt.Errorf("no log root: unmarshal failed: %v", err)
}
- if len(r.RootHash) != types.HashSize {
+ if len(r.RootHash) != merkle.HashSize {
return nil, fmt.Errorf("unexpected hash length: %d", len(r.RootHash))
}
return treeHeadFromLogRoot(&r), nil
@@ -181,10 +219,10 @@ func treeHeadFromLogRoot(lr *trillianTypes.LogRootV1) *types.TreeHead {
return &th
}
-func nodePathFromHashes(hashes [][]byte) ([]types.Hash, error) {
- path := make([]types.Hash, len(hashes))
+func nodePathFromHashes(hashes [][]byte) ([]merkle.Hash, error) {
+ path := make([]merkle.Hash, len(hashes))
for i := 0; i < len(hashes); i++ {
- if len(hashes[i]) != types.HashSize {
+ if len(hashes[i]) != merkle.HashSize {
return nil, fmt.Errorf("unexpected hash length: %v", len(hashes[i]))
}
diff --git a/pkg/db/trillian_test.go b/internal/db/trillian_test.go
index 2b19096..9ae682e 100644
--- a/pkg/db/trillian_test.go
+++ b/internal/db/trillian_test.go
@@ -8,95 +8,98 @@ import (
"testing"
"time"
- "git.sigsum.org/log-go/pkg/db/mocks"
+ mocksTrillian "git.sigsum.org/log-go/internal/mocks/trillian"
+ "git.sigsum.org/sigsum-go/pkg/merkle"
"git.sigsum.org/sigsum-go/pkg/requests"
"git.sigsum.org/sigsum-go/pkg/types"
"github.com/golang/mock/gomock"
"github.com/google/trillian"
ttypes "github.com/google/trillian/types"
- "google.golang.org/grpc/codes"
- "google.golang.org/grpc/status"
+ //"google.golang.org/grpc/codes"
+ //"google.golang.org/grpc/status"
)
-func TestAddLeaf(t *testing.T) {
- req := &requests.Leaf{
- ShardHint: 0,
- Message: types.Hash{},
- Signature: types.Signature{},
- PublicKey: types.PublicKey{},
- DomainHint: "example.com",
- }
- for _, table := range []struct {
- description string
- req *requests.Leaf
- rsp *trillian.QueueLeafResponse
- err error
- wantErr bool
- }{
- {
- description: "invalid: backend failure",
- req: req,
- err: fmt.Errorf("something went wrong"),
- wantErr: true,
- },
- {
- description: "invalid: no response",
- req: req,
- wantErr: true,
- },
- {
- description: "invalid: no queued leaf",
- req: req,
- rsp: &trillian.QueueLeafResponse{},
- wantErr: true,
- },
- {
- description: "invalid: leaf is already queued or included",
- req: req,
- rsp: &trillian.QueueLeafResponse{
- QueuedLeaf: &trillian.QueuedLogLeaf{
- Leaf: &trillian.LogLeaf{
- LeafValue: []byte{0}, // does not matter for test
- },
- Status: status.New(codes.AlreadyExists, "duplicate").Proto(),
- },
- },
- wantErr: true,
- },
- {
- description: "valid",
- req: req,
- rsp: &trillian.QueueLeafResponse{
- QueuedLeaf: &trillian.QueuedLogLeaf{
- Leaf: &trillian.LogLeaf{
- LeafValue: []byte{0}, // does not matter for test
- },
- Status: status.New(codes.OK, "ok").Proto(),
- },
- },
- },
- } {
- // Run deferred functions at the end of each iteration
- func() {
- ctrl := gomock.NewController(t)
- defer ctrl.Finish()
- grpc := mocks.NewMockTrillianLogClient(ctrl)
- grpc.EXPECT().QueueLeaf(gomock.Any(), gomock.Any()).Return(table.rsp, table.err)
- client := TrillianClient{GRPC: grpc}
-
- err := client.AddLeaf(context.Background(), table.req)
- if got, want := err != nil, table.wantErr; got != want {
- t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err)
- }
- }()
- }
-}
+// TODO: Add TestAddSequencedLeaves
+// TODO: Update TestAddLeaf
+//func TestAddLeaf(t *testing.T) {
+// req := &requests.Leaf{
+// ShardHint: 0,
+// Message: merkle.Hash{},
+// Signature: types.Signature{},
+// PublicKey: types.PublicKey{},
+// DomainHint: "example.com",
+// }
+// for _, table := range []struct {
+// description string
+// req *requests.Leaf
+// rsp *trillian.QueueLeafResponse
+// err error
+// wantErr bool
+// }{
+// {
+// description: "invalid: backend failure",
+// req: req,
+// err: fmt.Errorf("something went wrong"),
+// wantErr: true,
+// },
+// {
+// description: "invalid: no response",
+// req: req,
+// wantErr: true,
+// },
+// {
+// description: "invalid: no queued leaf",
+// req: req,
+// rsp: &trillian.QueueLeafResponse{},
+// wantErr: true,
+// },
+// {
+// description: "invalid: leaf is already queued or included",
+// req: req,
+// rsp: &trillian.QueueLeafResponse{
+// QueuedLeaf: &trillian.QueuedLogLeaf{
+// Leaf: &trillian.LogLeaf{
+// LeafValue: []byte{0}, // does not matter for test
+// },
+// Status: status.New(codes.AlreadyExists, "duplicate").Proto(),
+// },
+// },
+// wantErr: true,
+// },
+// {
+// description: "valid",
+// req: req,
+// rsp: &trillian.QueueLeafResponse{
+// QueuedLeaf: &trillian.QueuedLogLeaf{
+// Leaf: &trillian.LogLeaf{
+// LeafValue: []byte{0}, // does not matter for test
+// },
+// Status: status.New(codes.OK, "ok").Proto(),
+// },
+// },
+// },
+// } {
+// // Run deferred functions at the end of each iteration
+// func() {
+// ctrl := gomock.NewController(t)
+// defer ctrl.Finish()
+// grpc := mocksTrillian.NewMockTrillianLogClient(ctrl)
+// grpc.EXPECT().QueueLeaf(gomock.Any(), gomock.Any()).Return(table.rsp, table.err)
+// client := TrillianClient{GRPC: grpc}
+//
+// _, err := client.AddLeaf(context.Background(), table.req, 0)
+// if got, want := err != nil, table.wantErr; got != want {
+// t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err)
+// }
+// }()
+// }
+//}
func TestGetTreeHead(t *testing.T) {
// valid root
root := &ttypes.LogRootV1{
TreeSize: 0,
- RootHash: make([]byte, types.HashSize),
+ RootHash: make([]byte, merkle.HashSize),
TimestampNanos: 1622585623133599429,
}
buf, err := root.MarshalBinary()
@@ -104,7 +107,7 @@ func TestGetTreeHead(t *testing.T) {
t.Fatalf("must marshal log root: %v", err)
}
// invalid root
- root.RootHash = make([]byte, types.HashSize+1)
+ root.RootHash = make([]byte, merkle.HashSize+1)
bufBadHash, err := root.MarshalBinary()
if err != nil {
t.Fatalf("must marshal log root: %v", err)
@@ -166,7 +169,7 @@ func TestGetTreeHead(t *testing.T) {
wantTh: &types.TreeHead{
Timestamp: 1622585623,
TreeSize: 0,
- RootHash: types.Hash{},
+ RootHash: merkle.Hash{},
},
},
} {
@@ -174,7 +177,7 @@ func TestGetTreeHead(t *testing.T) {
func() {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
- grpc := mocks.NewMockTrillianLogClient(ctrl)
+ grpc := mocksTrillian.NewMockTrillianLogClient(ctrl)
grpc.EXPECT().GetLatestSignedLogRoot(gomock.Any(), gomock.Any()).Return(table.rsp, table.err)
client := TrillianClient{GRPC: grpc}
@@ -248,8 +251,8 @@ func TestGetConsistencyProof(t *testing.T) {
rsp: &trillian.GetConsistencyProofResponse{
Proof: &trillian.Proof{
Hashes: [][]byte{
- make([]byte, types.HashSize),
- make([]byte, types.HashSize+1),
+ make([]byte, merkle.HashSize),
+ make([]byte, merkle.HashSize+1),
},
},
},
@@ -261,17 +264,17 @@ func TestGetConsistencyProof(t *testing.T) {
rsp: &trillian.GetConsistencyProofResponse{
Proof: &trillian.Proof{
Hashes: [][]byte{
- make([]byte, types.HashSize),
- make([]byte, types.HashSize),
+ make([]byte, merkle.HashSize),
+ make([]byte, merkle.HashSize),
},
},
},
wantProof: &types.ConsistencyProof{
OldSize: 1,
NewSize: 3,
- Path: []types.Hash{
- types.Hash{},
- types.Hash{},
+ Path: []merkle.Hash{
+ merkle.Hash{},
+ merkle.Hash{},
},
},
},
@@ -280,7 +283,7 @@ func TestGetConsistencyProof(t *testing.T) {
func() {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
- grpc := mocks.NewMockTrillianLogClient(ctrl)
+ grpc := mocksTrillian.NewMockTrillianLogClient(ctrl)
grpc.EXPECT().GetConsistencyProof(gomock.Any(), gomock.Any()).Return(table.rsp, table.err)
client := TrillianClient{GRPC: grpc}
@@ -301,7 +304,7 @@ func TestGetConsistencyProof(t *testing.T) {
func TestGetInclusionProof(t *testing.T) {
req := &requests.InclusionProof{
TreeSize: 4,
- LeafHash: types.Hash{},
+ LeafHash: merkle.Hash{},
}
for _, table := range []struct {
description string
@@ -354,8 +357,8 @@ func TestGetInclusionProof(t *testing.T) {
&trillian.Proof{
LeafIndex: 1,
Hashes: [][]byte{
- make([]byte, types.HashSize),
- make([]byte, types.HashSize+1),
+ make([]byte, merkle.HashSize),
+ make([]byte, merkle.HashSize+1),
},
},
},
@@ -370,8 +373,8 @@ func TestGetInclusionProof(t *testing.T) {
&trillian.Proof{
LeafIndex: 1,
Hashes: [][]byte{
- make([]byte, types.HashSize),
- make([]byte, types.HashSize),
+ make([]byte, merkle.HashSize),
+ make([]byte, merkle.HashSize),
},
},
},
@@ -379,9 +382,9 @@ func TestGetInclusionProof(t *testing.T) {
wantProof: &types.InclusionProof{
TreeSize: 4,
LeafIndex: 1,
- Path: []types.Hash{
- types.Hash{},
- types.Hash{},
+ Path: []merkle.Hash{
+ merkle.Hash{},
+ merkle.Hash{},
},
},
},
@@ -390,7 +393,7 @@ func TestGetInclusionProof(t *testing.T) {
func() {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
- grpc := mocks.NewMockTrillianLogClient(ctrl)
+ grpc := mocksTrillian.NewMockTrillianLogClient(ctrl)
grpc.EXPECT().GetInclusionProofByHash(gomock.Any(), gomock.Any()).Return(table.rsp, table.err)
client := TrillianClient{GRPC: grpc}
@@ -416,18 +419,18 @@ func TestGetLeaves(t *testing.T) {
firstLeaf := &types.Leaf{
Statement: types.Statement{
ShardHint: 0,
- Checksum: types.Hash{},
+ Checksum: merkle.Hash{},
},
Signature: types.Signature{},
- KeyHash: types.Hash{},
+ KeyHash: merkle.Hash{},
}
secondLeaf := &types.Leaf{
Statement: types.Statement{
ShardHint: 0,
- Checksum: types.Hash{},
+ Checksum: merkle.Hash{},
},
Signature: types.Signature{},
- KeyHash: types.Hash{},
+ KeyHash: merkle.Hash{},
}
for _, table := range []struct {
@@ -521,7 +524,7 @@ func TestGetLeaves(t *testing.T) {
func() {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
- grpc := mocks.NewMockTrillianLogClient(ctrl)
+ grpc := mocksTrillian.NewMockTrillianLogClient(ctrl)
grpc.EXPECT().GetLeavesByRange(gomock.Any(), gomock.Any()).Return(table.rsp, table.err)
client := TrillianClient{GRPC: grpc}
diff --git a/internal/mocks/client/client.go b/internal/mocks/client/client.go
new file mode 100644
index 0000000..6cb3b3d
--- /dev/null
+++ b/internal/mocks/client/client.go
@@ -0,0 +1,170 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: git.sigsum.org/sigsum-go/pkg/client (interfaces: Client)
+
+// Package client is a generated GoMock package.
+package client
+
+import (
+ context "context"
+ reflect "reflect"
+
+ requests "git.sigsum.org/sigsum-go/pkg/requests"
+ types "git.sigsum.org/sigsum-go/pkg/types"
+ gomock "github.com/golang/mock/gomock"
+)
+
+// MockClient is a mock of Client interface.
+type MockClient struct {
+ ctrl *gomock.Controller
+ recorder *MockClientMockRecorder
+}
+
+// MockClientMockRecorder is the mock recorder for MockClient.
+type MockClientMockRecorder struct {
+ mock *MockClient
+}
+
+// NewMockClient creates a new mock instance.
+func NewMockClient(ctrl *gomock.Controller) *MockClient {
+ mock := &MockClient{ctrl: ctrl}
+ mock.recorder = &MockClientMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockClient) EXPECT() *MockClientMockRecorder {
+ return m.recorder
+}
+
+// AddCosignature mocks base method.
+func (m *MockClient) AddCosignature(arg0 context.Context, arg1 requests.Cosignature) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "AddCosignature", arg0, arg1)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// AddCosignature indicates an expected call of AddCosignature.
+func (mr *MockClientMockRecorder) AddCosignature(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddCosignature", reflect.TypeOf((*MockClient)(nil).AddCosignature), arg0, arg1)
+}
+
+// AddLeaf mocks base method.
+func (m *MockClient) AddLeaf(arg0 context.Context, arg1 requests.Leaf) (bool, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "AddLeaf", arg0, arg1)
+ ret0, _ := ret[0].(bool)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// AddLeaf indicates an expected call of AddLeaf.
+func (mr *MockClientMockRecorder) AddLeaf(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddLeaf", reflect.TypeOf((*MockClient)(nil).AddLeaf), arg0, arg1)
+}
+
+// GetConsistencyProof mocks base method.
+func (m *MockClient) GetConsistencyProof(arg0 context.Context, arg1 requests.ConsistencyProof) (types.ConsistencyProof, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetConsistencyProof", arg0, arg1)
+ ret0, _ := ret[0].(types.ConsistencyProof)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetConsistencyProof indicates an expected call of GetConsistencyProof.
+func (mr *MockClientMockRecorder) GetConsistencyProof(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConsistencyProof", reflect.TypeOf((*MockClient)(nil).GetConsistencyProof), arg0, arg1)
+}
+
+// GetCosignedTreeHead mocks base method.
+func (m *MockClient) GetCosignedTreeHead(arg0 context.Context) (types.CosignedTreeHead, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetCosignedTreeHead", arg0)
+ ret0, _ := ret[0].(types.CosignedTreeHead)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetCosignedTreeHead indicates an expected call of GetCosignedTreeHead.
+func (mr *MockClientMockRecorder) GetCosignedTreeHead(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCosignedTreeHead", reflect.TypeOf((*MockClient)(nil).GetCosignedTreeHead), arg0)
+}
+
+// GetInclusionProof mocks base method.
+func (m *MockClient) GetInclusionProof(arg0 context.Context, arg1 requests.InclusionProof) (types.InclusionProof, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetInclusionProof", arg0, arg1)
+ ret0, _ := ret[0].(types.InclusionProof)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetInclusionProof indicates an expected call of GetInclusionProof.
+func (mr *MockClientMockRecorder) GetInclusionProof(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInclusionProof", reflect.TypeOf((*MockClient)(nil).GetInclusionProof), arg0, arg1)
+}
+
+// GetLeaves mocks base method.
+func (m *MockClient) GetLeaves(arg0 context.Context, arg1 requests.Leaves) (types.Leaves, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetLeaves", arg0, arg1)
+ ret0, _ := ret[0].(types.Leaves)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetLeaves indicates an expected call of GetLeaves.
+func (mr *MockClientMockRecorder) GetLeaves(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLeaves", reflect.TypeOf((*MockClient)(nil).GetLeaves), arg0, arg1)
+}
+
+// GetToCosignTreeHead mocks base method.
+func (m *MockClient) GetToCosignTreeHead(arg0 context.Context) (types.SignedTreeHead, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetToCosignTreeHead", arg0)
+ ret0, _ := ret[0].(types.SignedTreeHead)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetToCosignTreeHead indicates an expected call of GetToCosignTreeHead.
+func (mr *MockClientMockRecorder) GetToCosignTreeHead(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetToCosignTreeHead", reflect.TypeOf((*MockClient)(nil).GetToCosignTreeHead), arg0)
+}
+
+// GetUnsignedTreeHead mocks base method.
+func (m *MockClient) GetUnsignedTreeHead(arg0 context.Context) (types.TreeHead, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetUnsignedTreeHead", arg0)
+ ret0, _ := ret[0].(types.TreeHead)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetUnsignedTreeHead indicates an expected call of GetUnsignedTreeHead.
+func (mr *MockClientMockRecorder) GetUnsignedTreeHead(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUnsignedTreeHead", reflect.TypeOf((*MockClient)(nil).GetUnsignedTreeHead), arg0)
+}
+
+// Initiated mocks base method.
+func (m *MockClient) Initiated() bool {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Initiated")
+ ret0, _ := ret[0].(bool)
+ return ret0
+}
+
+// Initiated indicates an expected call of Initiated.
+func (mr *MockClientMockRecorder) Initiated() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Initiated", reflect.TypeOf((*MockClient)(nil).Initiated))
+}
diff --git a/internal/mocks/crypto/crypto.go b/internal/mocks/crypto/crypto.go
new file mode 100644
index 0000000..0871e79
--- /dev/null
+++ b/internal/mocks/crypto/crypto.go
@@ -0,0 +1,65 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: crypto (interfaces: Signer)
+
+// Package crypto is a generated GoMock package.
+package crypto
+
+import (
+ crypto "crypto"
+ io "io"
+ reflect "reflect"
+
+ gomock "github.com/golang/mock/gomock"
+)
+
+// MockSigner is a mock of Signer interface.
+type MockSigner struct {
+ ctrl *gomock.Controller
+ recorder *MockSignerMockRecorder
+}
+
+// MockSignerMockRecorder is the mock recorder for MockSigner.
+type MockSignerMockRecorder struct {
+ mock *MockSigner
+}
+
+// NewMockSigner creates a new mock instance.
+func NewMockSigner(ctrl *gomock.Controller) *MockSigner {
+ mock := &MockSigner{ctrl: ctrl}
+ mock.recorder = &MockSignerMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockSigner) EXPECT() *MockSignerMockRecorder {
+ return m.recorder
+}
+
+// Public mocks base method.
+func (m *MockSigner) Public() crypto.PublicKey {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Public")
+ ret0, _ := ret[0].(crypto.PublicKey)
+ return ret0
+}
+
+// Public indicates an expected call of Public.
+func (mr *MockSignerMockRecorder) Public() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Public", reflect.TypeOf((*MockSigner)(nil).Public))
+}
+
+// Sign mocks base method.
+func (m *MockSigner) Sign(arg0 io.Reader, arg1 []byte, arg2 crypto.SignerOpts) ([]byte, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Sign", arg0, arg1, arg2)
+ ret0, _ := ret[0].([]byte)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Sign indicates an expected call of Sign.
+func (mr *MockSignerMockRecorder) Sign(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Sign", reflect.TypeOf((*MockSigner)(nil).Sign), arg0, arg1, arg2)
+}
diff --git a/pkg/db/mocks/client.go b/internal/mocks/db/db.go
index 0313bfb..b96328d 100644
--- a/pkg/db/mocks/client.go
+++ b/internal/mocks/db/db.go
@@ -1,8 +1,8 @@
// Code generated by MockGen. DO NOT EDIT.
-// Source: git.sigsum.org/sigsum-log-go/pkg/db (interfaces: Client)
+// Source: git.sigsum.org/log-go/internal/db (interfaces: Client)
-// Package mocks is a generated GoMock package.
-package mocks
+// Package db is a generated GoMock package.
+package db
import (
context "context"
@@ -37,17 +37,32 @@ func (m *MockClient) EXPECT() *MockClientMockRecorder {
}
// AddLeaf mocks base method.
-func (m *MockClient) AddLeaf(arg0 context.Context, arg1 *requests.Leaf) error {
+func (m *MockClient) AddLeaf(arg0 context.Context, arg1 *requests.Leaf, arg2 uint64) (bool, error) {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "AddLeaf", arg0, arg1)
+ ret := m.ctrl.Call(m, "AddLeaf", arg0, arg1, arg2)
+ ret0, _ := ret[0].(bool)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// AddLeaf indicates an expected call of AddLeaf.
+func (mr *MockClientMockRecorder) AddLeaf(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddLeaf", reflect.TypeOf((*MockClient)(nil).AddLeaf), arg0, arg1, arg2)
+}
+
+// AddSequencedLeaves mocks base method.
+func (m *MockClient) AddSequencedLeaves(arg0 context.Context, arg1 types.Leaves, arg2 int64) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "AddSequencedLeaves", arg0, arg1, arg2)
ret0, _ := ret[0].(error)
return ret0
}
-// AddLeaf indicates an expected call of AddLeaf.
-func (mr *MockClientMockRecorder) AddLeaf(arg0, arg1 interface{}) *gomock.Call {
+// AddSequencedLeaves indicates an expected call of AddSequencedLeaves.
+func (mr *MockClientMockRecorder) AddSequencedLeaves(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddLeaf", reflect.TypeOf((*MockClient)(nil).AddLeaf), arg0, arg1)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddSequencedLeaves", reflect.TypeOf((*MockClient)(nil).AddSequencedLeaves), arg0, arg1, arg2)
}
// GetConsistencyProof mocks base method.
diff --git a/internal/mocks/node/handler/handler.go b/internal/mocks/node/handler/handler.go
new file mode 100644
index 0000000..97cac8e
--- /dev/null
+++ b/internal/mocks/node/handler/handler.go
@@ -0,0 +1,77 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: git.sigsum.org/log-go/internal/node/handler (interfaces: Config)
+
+// Package handler is a generated GoMock package.
+package handler
+
+import (
+ reflect "reflect"
+ time "time"
+
+ gomock "github.com/golang/mock/gomock"
+)
+
+// MockConfig is a mock of Config interface.
+type MockConfig struct {
+ ctrl *gomock.Controller
+ recorder *MockConfigMockRecorder
+}
+
+// MockConfigMockRecorder is the mock recorder for MockConfig.
+type MockConfigMockRecorder struct {
+ mock *MockConfig
+}
+
+// NewMockConfig creates a new mock instance.
+func NewMockConfig(ctrl *gomock.Controller) *MockConfig {
+ mock := &MockConfig{ctrl: ctrl}
+ mock.recorder = &MockConfigMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockConfig) EXPECT() *MockConfigMockRecorder {
+ return m.recorder
+}
+
+// Deadline mocks base method.
+func (m *MockConfig) Deadline() time.Duration {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Deadline")
+ ret0, _ := ret[0].(time.Duration)
+ return ret0
+}
+
+// Deadline indicates an expected call of Deadline.
+func (mr *MockConfigMockRecorder) Deadline() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Deadline", reflect.TypeOf((*MockConfig)(nil).Deadline))
+}
+
+// LogID mocks base method.
+func (m *MockConfig) LogID() string {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "LogID")
+ ret0, _ := ret[0].(string)
+ return ret0
+}
+
+// LogID indicates an expected call of LogID.
+func (mr *MockConfigMockRecorder) LogID() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LogID", reflect.TypeOf((*MockConfig)(nil).LogID))
+}
+
+// Prefix mocks base method.
+func (m *MockConfig) Prefix() string {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Prefix")
+ ret0, _ := ret[0].(string)
+ return ret0
+}
+
+// Prefix indicates an expected call of Prefix.
+func (mr *MockConfigMockRecorder) Prefix() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Prefix", reflect.TypeOf((*MockConfig)(nil).Prefix))
+}
diff --git a/pkg/state/mocks/state_manager.go b/internal/mocks/state/state.go
index 05831c6..52dfb09 100644
--- a/pkg/state/mocks/state_manager.go
+++ b/internal/mocks/state/state.go
@@ -1,8 +1,8 @@
// Code generated by MockGen. DO NOT EDIT.
-// Source: git.sigsum.org/sigsum-log-go/pkg/state (interfaces: StateManager)
+// Source: git.sigsum.org/log-go/internal/state (interfaces: StateManager)
-// Package mocks is a generated GoMock package.
-package mocks
+// Package state is a generated GoMock package.
+package state
import (
context "context"
@@ -77,16 +77,15 @@ func (mr *MockStateManagerMockRecorder) Run(arg0 interface{}) *gomock.Call {
}
// ToCosignTreeHead mocks base method.
-func (m *MockStateManager) ToCosignTreeHead(arg0 context.Context) (*types.SignedTreeHead, error) {
+func (m *MockStateManager) ToCosignTreeHead() *types.SignedTreeHead {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ToCosignTreeHead", arg0)
+ ret := m.ctrl.Call(m, "ToCosignTreeHead")
ret0, _ := ret[0].(*types.SignedTreeHead)
- ret1, _ := ret[1].(error)
- return ret0, ret1
+ return ret0
}
// ToCosignTreeHead indicates an expected call of ToCosignTreeHead.
-func (mr *MockStateManagerMockRecorder) ToCosignTreeHead(arg0 interface{}) *gomock.Call {
+func (mr *MockStateManagerMockRecorder) ToCosignTreeHead() *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ToCosignTreeHead", reflect.TypeOf((*MockStateManager)(nil).ToCosignTreeHead), arg0)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ToCosignTreeHead", reflect.TypeOf((*MockStateManager)(nil).ToCosignTreeHead))
}
diff --git a/pkg/db/mocks/trillian.go b/internal/mocks/trillian/trillian.go
index 8aa3a58..b923e23 100644
--- a/pkg/db/mocks/trillian.go
+++ b/internal/mocks/trillian/trillian.go
@@ -1,8 +1,8 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/google/trillian (interfaces: TrillianLogClient)
-// Package mocks is a generated GoMock package.
-package mocks
+// Package trillian is a generated GoMock package.
+package trillian
import (
context "context"
diff --git a/internal/node/handler/handler.go b/internal/node/handler/handler.go
new file mode 100644
index 0000000..2871c5d
--- /dev/null
+++ b/internal/node/handler/handler.go
@@ -0,0 +1,91 @@
+package handler
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "time"
+
+ "git.sigsum.org/sigsum-go/pkg/log"
+ "git.sigsum.org/sigsum-go/pkg/types"
+)
+
+type Config interface {
+ Prefix() string
+ LogID() string
+ Deadline() time.Duration
+}
+
+// Handler implements the http.Handler interface
+type Handler struct {
+ Config
+ Fun func(context.Context, Config, http.ResponseWriter, *http.Request) (int, error)
+ Endpoint types.Endpoint
+ Method string
+}
+
+// Path returns a path that should be configured for this handler
+func (h Handler) Path() string {
+ if len(h.Prefix()) == 0 {
+ return h.Endpoint.Path("", "sigsum", "v0")
+ }
+ return h.Endpoint.Path("", h.Prefix(), "sigsum", "v0")
+}
+
+// ServeHTTP is part of the http.Handler interface
+func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ start := time.Now()
+ code := 0
+ defer func() {
+ end := time.Now().Sub(start).Seconds()
+ sc := fmt.Sprintf("%d", code)
+
+ rspcnt.Inc(h.LogID(), string(h.Endpoint), sc)
+ latency.Observe(end, h.LogID(), string(h.Endpoint), sc)
+ }()
+ reqcnt.Inc(h.LogID(), string(h.Endpoint))
+
+ code = h.verifyMethod(w, r)
+ if code != 0 {
+ return
+ }
+ h.handle(w, r)
+}
+
+// verifyMethod checks that an appropriate HTTP method is used and
+// returns 0 if so, or an HTTP status code if not. Error handling is
+// based on RFC 7231, see Sections 6.5.5 (Status 405) and 6.5.1
+// (Status 400).
+func (h Handler) verifyMethod(w http.ResponseWriter, r *http.Request) int {
+ checkHTTPMethod := func(m string) bool {
+ return m == http.MethodGet || m == http.MethodPost
+ }
+
+ if h.Method == r.Method {
+ return 0
+ }
+
+ code := http.StatusBadRequest
+ if ok := checkHTTPMethod(r.Method); ok {
+ w.Header().Set("Allow", h.Method)
+ code = http.StatusMethodNotAllowed
+ }
+
+ http.Error(w, fmt.Sprintf("error=%s", http.StatusText(code)), code)
+ return code
+}
+
+// handle handles an HTTP request for which the HTTP method is already verified
+func (h Handler) handle(w http.ResponseWriter, r *http.Request) {
+ deadline := time.Now().Add(h.Deadline())
+ ctx, cancel := context.WithDeadline(r.Context(), deadline)
+ defer cancel()
+
+ code, err := h.Fun(ctx, h.Config, w, r)
+ if err != nil {
+ log.Debug("%s/%s: %v", h.Prefix(), h.Endpoint, err)
+ http.Error(w, fmt.Sprintf("error=%s", err.Error()), code)
+ } else if code != 200 {
+ w.WriteHeader(code)
+ }
+}
diff --git a/internal/node/handler/handler_test.go b/internal/node/handler/handler_test.go
new file mode 100644
index 0000000..dfd27bd
--- /dev/null
+++ b/internal/node/handler/handler_test.go
@@ -0,0 +1,113 @@
+package handler
+
+import (
+ "context"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+
+ "git.sigsum.org/sigsum-go/pkg/types"
+)
+
+type dummyConfig struct {
+ prefix string
+}
+
+func (c dummyConfig) Prefix() string { return c.prefix }
+func (c dummyConfig) LogID() string { return "dummyLogID" }
+func (c dummyConfig) Deadline() time.Duration { return time.Nanosecond }
+
+// TestPath checks that Path works for an endpoint (add-leaf)
+func TestPath(t *testing.T) {
+ testFun := func(_ context.Context, _ Config, _ http.ResponseWriter, _ *http.Request) (int, error) {
+ return 0, nil
+ }
+ for _, table := range []struct {
+ description string
+ prefix string
+ want string
+ }{
+ {
+ description: "no prefix",
+ want: "/sigsum/v0/add-leaf",
+ },
+ {
+ description: "a prefix",
+ prefix: "test-prefix",
+ want: "/test-prefix/sigsum/v0/add-leaf",
+ },
+ } {
+ testConfig := dummyConfig{
+ prefix: table.prefix,
+ }
+ h := Handler{testConfig, testFun, types.EndpointAddLeaf, http.MethodPost}
+ if got, want := h.Path(), table.want; got != want {
+ t.Errorf("got path %v but wanted %v", got, want)
+ }
+ }
+}
+
+// func TestServeHTTP(t *testing.T) {
+// h.ServeHTTP(w http.ResponseWriter, r *http.Request)
+// }
+
+func TestVerifyMethod(t *testing.T) {
+ badMethod := http.MethodHead
+ for _, h := range []Handler{
+ {
+ Endpoint: types.EndpointAddLeaf,
+ Method: http.MethodPost,
+ },
+ {
+ Endpoint: types.EndpointGetTreeHeadToCosign,
+ Method: http.MethodGet,
+ },
+ } {
+ for _, method := range []string{
+ http.MethodGet,
+ http.MethodPost,
+ badMethod,
+ } {
+ url := h.Endpoint.Path("http://log.example.com", "fixme")
+ req, err := http.NewRequest(method, url, nil)
+ if err != nil {
+ t.Fatalf("must create HTTP request: %v", err)
+ }
+
+ w := httptest.NewRecorder()
+ code := h.verifyMethod(w, req)
+ if got, want := code == 0, h.Method == method; got != want {
+ t.Errorf("%s %s: got %v but wanted %v: %v", method, url, got, want, err)
+ continue
+ }
+ if code == 0 {
+ continue
+ }
+
+ if method == badMethod {
+ if got, want := code, http.StatusBadRequest; got != want {
+ t.Errorf("%s %s: got status %d, wanted %d", method, url, got, want)
+ }
+ if _, ok := w.Header()["Allow"]; ok {
+ t.Errorf("%s %s: got Allow header, wanted none", method, url)
+ }
+ continue
+ }
+
+ if got, want := code, http.StatusMethodNotAllowed; got != want {
+ t.Errorf("%s %s: got status %d, wanted %d", method, url, got, want)
+ } else if methods, ok := w.Header()["Allow"]; !ok {
+ t.Errorf("%s %s: got no allow header, expected one", method, url)
+ } else if got, want := len(methods), 1; got != want {
+ t.Errorf("%s %s: got %d allowed method(s), wanted %d", method, url, got, want)
+ } else if got, want := methods[0], h.Method; got != want {
+ t.Errorf("%s %s: got allowed method %s, wanted %s", method, url, got, want)
+ }
+ }
+ }
+}
+
+// func TestHandle(t *testing.T) {
+// h.handle(w http.ResponseWriter, r *http.Request)
+// }
diff --git a/pkg/instance/metric.go b/internal/node/handler/metric.go
index cbd0223..ced0096 100644
--- a/pkg/instance/metric.go
+++ b/internal/node/handler/metric.go
@@ -1,4 +1,4 @@
-package instance
+package handler
import (
"github.com/google/trillian/monitoring"
diff --git a/internal/node/primary/endpoint_external.go b/internal/node/primary/endpoint_external.go
new file mode 100644
index 0000000..42e8fcb
--- /dev/null
+++ b/internal/node/primary/endpoint_external.go
@@ -0,0 +1,147 @@
+package primary
+
+// This file implements external HTTP handler callbacks for primary nodes.
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+
+ "git.sigsum.org/log-go/internal/node/handler"
+ "git.sigsum.org/log-go/internal/requests"
+ "git.sigsum.org/sigsum-go/pkg/log"
+)
+
+func addLeaf(ctx context.Context, c handler.Config, w http.ResponseWriter, r *http.Request) (int, error) {
+ p := c.(Primary)
+ log.Debug("handling add-leaf request")
+ req, err := requests.LeafRequestFromHTTP(r, p.Config.ShardStart, ctx, p.DNS)
+ if err != nil {
+ return http.StatusBadRequest, err
+ }
+
+ sth := p.Stateman.ToCosignTreeHead()
+ sequenced, err := p.TrillianClient.AddLeaf(ctx, req, sth.TreeSize)
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+ if sequenced {
+ return http.StatusOK, nil
+ } else {
+ return http.StatusAccepted, nil
+ }
+}
+
+func addCosignature(ctx context.Context, c handler.Config, w http.ResponseWriter, r *http.Request) (int, error) {
+ p := c.(Primary)
+ log.Debug("handling add-cosignature request")
+ req, err := requests.CosignatureRequestFromHTTP(r, p.Witnesses)
+ if err != nil {
+ return http.StatusBadRequest, err
+ }
+ vk := p.Witnesses[req.KeyHash]
+ if err := p.Stateman.AddCosignature(ctx, &vk, &req.Cosignature); err != nil {
+ return http.StatusBadRequest, err
+ }
+ return http.StatusOK, nil
+}
+
+func getTreeHeadToCosign(ctx context.Context, c handler.Config, w http.ResponseWriter, _ *http.Request) (int, error) {
+ p := c.(Primary)
+ log.Debug("handling get-tree-head-to-cosign request")
+ sth := p.Stateman.ToCosignTreeHead()
+ if err := sth.ToASCII(w); err != nil {
+ return http.StatusInternalServerError, err
+ }
+ return http.StatusOK, nil
+}
+
+func getTreeHeadCosigned(ctx context.Context, c handler.Config, w http.ResponseWriter, _ *http.Request) (int, error) {
+ p := c.(Primary)
+ log.Debug("handling get-tree-head-cosigned request")
+ cth, err := p.Stateman.CosignedTreeHead(ctx)
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+ if err := cth.ToASCII(w); err != nil {
+ return http.StatusInternalServerError, err
+ }
+ return http.StatusOK, nil
+}
+
+func getConsistencyProof(ctx context.Context, c handler.Config, w http.ResponseWriter, r *http.Request) (int, error) {
+ p := c.(Primary)
+ log.Debug("handling get-consistency-proof request")
+ req, err := requests.ConsistencyProofRequestFromHTTP(r)
+ if err != nil {
+ return http.StatusBadRequest, err
+ }
+
+ curTree := p.Stateman.ToCosignTreeHead()
+ if req.NewSize > curTree.TreeHead.TreeSize {
+ return http.StatusBadRequest, fmt.Errorf("new_size outside of current tree")
+ }
+
+ proof, err := p.TrillianClient.GetConsistencyProof(ctx, req)
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+ if err := proof.ToASCII(w); err != nil {
+ return http.StatusInternalServerError, err
+ }
+ return http.StatusOK, nil
+}
+
+func getInclusionProof(ctx context.Context, c handler.Config, w http.ResponseWriter, r *http.Request) (int, error) {
+ p := c.(Primary)
+ log.Debug("handling get-inclusion-proof request")
+ req, err := requests.InclusionProofRequestFromHTTP(r)
+ if err != nil {
+ return http.StatusBadRequest, err
+ }
+
+ curTree := p.Stateman.ToCosignTreeHead()
+ if req.TreeSize > curTree.TreeHead.TreeSize {
+ return http.StatusBadRequest, fmt.Errorf("tree_size outside of current tree")
+ }
+
+ proof, err := p.TrillianClient.GetInclusionProof(ctx, req)
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+ if err := proof.ToASCII(w); err != nil {
+ return http.StatusInternalServerError, err
+ }
+ return http.StatusOK, nil
+}
+
+func getLeavesGeneral(ctx context.Context, c handler.Config, w http.ResponseWriter, r *http.Request, doLimitToCurrentTree bool) (int, error) {
+ p := c.(Primary)
+ log.Debug("handling get-leaves request")
+ req, err := requests.LeavesRequestFromHTTP(r, uint64(p.MaxRange))
+ if err != nil {
+ return http.StatusBadRequest, err
+ }
+
+ if doLimitToCurrentTree {
+ curTree := p.Stateman.ToCosignTreeHead()
+ if req.EndSize >= curTree.TreeHead.TreeSize {
+ return http.StatusBadRequest, fmt.Errorf("end_size outside of current tree")
+ }
+ }
+
+ leaves, err := p.TrillianClient.GetLeaves(ctx, req)
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+ for _, leaf := range *leaves {
+ if err := leaf.ToASCII(w); err != nil {
+ return http.StatusInternalServerError, err
+ }
+ }
+ return http.StatusOK, nil
+}
+
+func getLeavesExternal(ctx context.Context, c handler.Config, w http.ResponseWriter, r *http.Request) (int, error) {
+ return getLeavesGeneral(ctx, c, w, r, true)
+}
diff --git a/pkg/instance/handler_test.go b/internal/node/primary/endpoint_external_test.go
index 50bd3a4..7ee161b 100644
--- a/pkg/instance/handler_test.go
+++ b/internal/node/primary/endpoint_external_test.go
@@ -1,4 +1,4 @@
-package instance
+package primary
import (
"bytes"
@@ -12,151 +12,31 @@ import (
"testing"
"time"
- mocksDB "git.sigsum.org/log-go/pkg/db/mocks"
+ mocksDB "git.sigsum.org/log-go/internal/mocks/db"
mocksDNS "git.sigsum.org/log-go/internal/mocks/dns"
- mocksState "git.sigsum.org/log-go/pkg/state/mocks"
+ mocksState "git.sigsum.org/log-go/internal/mocks/state"
+ "git.sigsum.org/log-go/internal/node/handler"
+ "git.sigsum.org/sigsum-go/pkg/merkle"
"git.sigsum.org/sigsum-go/pkg/types"
"github.com/golang/mock/gomock"
)
var (
- testWitVK = types.PublicKey{}
- testConfig = Config{
- LogID: fmt.Sprintf("%x", types.HashFn([]byte("logid"))[:]),
- TreeID: 0,
- Prefix: "testonly",
- MaxRange: 3,
- Deadline: 10,
- Interval: 10,
- ShardStart: 10,
- Witnesses: map[types.Hash]types.PublicKey{
- *types.HashFn(testWitVK[:]): testWitVK,
- },
- }
testSTH = &types.SignedTreeHead{
- TreeHead: types.TreeHead{
- Timestamp: 0,
- TreeSize: 0,
- RootHash: *types.HashFn([]byte("root hash")),
- },
+ TreeHead: *testTH,
Signature: types.Signature{},
}
testCTH = &types.CosignedTreeHead{
SignedTreeHead: *testSTH,
Cosignature: []types.Signature{types.Signature{}},
- KeyHash: []types.Hash{types.Hash{}},
+ KeyHash: []merkle.Hash{merkle.Hash{}},
}
+ sth1 = types.SignedTreeHead{TreeHead: types.TreeHead{TreeSize: 1}}
+ sth2 = types.SignedTreeHead{TreeHead: types.TreeHead{TreeSize: 2}} // 2 < testConfig.MaxRange
+ sth5 = types.SignedTreeHead{TreeHead: types.TreeHead{TreeSize: 5}} // 5 >= testConfig.MaxRange+1
)
-// TestHandlers check that the expected handlers are configured
-func TestHandlers(t *testing.T) {
- endpoints := map[types.Endpoint]bool{
- types.EndpointAddLeaf: false,
- types.EndpointAddCosignature: false,
- types.EndpointGetTreeHeadToCosign: false,
- types.EndpointGetTreeHeadCosigned: false,
- types.EndpointGetConsistencyProof: false,
- types.EndpointGetInclusionProof: false,
- types.EndpointGetLeaves: false,
- types.Endpoint("get-checkpoint"): false,
- }
- i := &Instance{
- Config: testConfig,
- }
- for _, handler := range i.Handlers() {
- if _, ok := endpoints[handler.Endpoint]; !ok {
- t.Errorf("got unexpected endpoint: %s", handler.Endpoint)
- }
- endpoints[handler.Endpoint] = true
- }
- for endpoint, ok := range endpoints {
- if !ok {
- t.Errorf("endpoint %s is not configured", endpoint)
- }
- }
-}
-
-func TestVerifyMethod(t *testing.T) {
- badMethod := http.MethodHead
- instance := Instance{Config: testConfig}
- for _, handler := range instance.Handlers() {
- for _, method := range []string{
- http.MethodGet,
- http.MethodPost,
- badMethod,
- } {
- url := handler.Endpoint.Path("http://log.example.com", instance.Prefix)
- req, err := http.NewRequest(method, url, nil)
- if err != nil {
- t.Fatalf("must create HTTP request: %v", err)
- }
-
- w := httptest.NewRecorder()
- code := handler.verifyMethod(w, req)
- if got, want := code == 0, handler.Method == method; got != want {
- t.Errorf("%s %s: got %v but wanted %v: %v", method, url, got, want, err)
- continue
- }
- if code == 0 {
- continue
- }
-
- if method == badMethod {
- if got, want := code, http.StatusBadRequest; got != want {
- t.Errorf("%s %s: got status %d, wanted %d", method, url, got, want)
- }
- if _, ok := w.Header()["Allow"]; ok {
- t.Errorf("%s %s: got Allow header, wanted none", method, url)
- }
- continue
- }
-
- if got, want := code, http.StatusMethodNotAllowed; got != want {
- t.Errorf("%s %s: got status %d, wanted %d", method, url, got, want)
- } else if methods, ok := w.Header()["Allow"]; !ok {
- t.Errorf("%s %s: got no allow header, expected one", method, url)
- } else if got, want := len(methods), 1; got != want {
- t.Errorf("%s %s: got %d allowed method(s), wanted %d", method, url, got, want)
- } else if got, want := methods[0], handler.Method; got != want {
- t.Errorf("%s %s: got allowed method %s, wanted %s", method, url, got, want)
- }
- }
- }
-}
-
-// TestPath checks that Path works for an endpoint (add-leaf)
-func TestPath(t *testing.T) {
- for _, table := range []struct {
- description string
- prefix string
- want string
- }{
- {
- description: "no prefix",
- want: "/sigsum/v0/add-leaf",
- },
- {
- description: "a prefix",
- prefix: "test-prefix",
- want: "/test-prefix/sigsum/v0/add-leaf",
- },
- } {
- instance := &Instance{
- Config: Config{
- Prefix: table.prefix,
- },
- }
- handler := Handler{
- Instance: instance,
- Handler: addLeaf,
- Endpoint: types.EndpointAddLeaf,
- Method: http.MethodPost,
- }
- if got, want := handler.Path(), table.want; got != want {
- t.Errorf("got path %v but wanted %v", got, want)
- }
- }
-}
+// TODO: remove tests that are now located in internal/requests instead
func TestAddLeaf(t *testing.T) {
for _, table := range []struct {
@@ -167,6 +47,9 @@ func TestAddLeaf(t *testing.T) {
expectDNS bool // expect DNS verifier code path
errDNS error // error from DNS verifier
wantCode int // HTTP status ok
+ expectStateman bool
+ sequenced bool // return value from db.AddLeaf()
+ sthStateman *types.SignedTreeHead
}{
{
description: "invalid: bad request (parser error)",
@@ -175,39 +58,53 @@ func TestAddLeaf(t *testing.T) {
},
{
description: "invalid: bad request (signature error)",
- ascii: mustLeafBuffer(t, 10, types.Hash{}, false),
+ ascii: mustLeafBuffer(t, 10, merkle.Hash{}, false),
wantCode: http.StatusBadRequest,
},
{
description: "invalid: bad request (shard hint is before shard start)",
- ascii: mustLeafBuffer(t, 9, types.Hash{}, true),
+ ascii: mustLeafBuffer(t, 9, merkle.Hash{}, true),
wantCode: http.StatusBadRequest,
},
{
description: "invalid: bad request (shard hint is after shard end)",
- ascii: mustLeafBuffer(t, uint64(time.Now().Unix())+1024, types.Hash{}, true),
+ ascii: mustLeafBuffer(t, uint64(time.Now().Unix())+1024, merkle.Hash{}, true),
wantCode: http.StatusBadRequest,
},
{
description: "invalid: failed verifying domain hint",
- ascii: mustLeafBuffer(t, 10, types.Hash{}, true),
+ ascii: mustLeafBuffer(t, 10, merkle.Hash{}, true),
expectDNS: true,
errDNS: fmt.Errorf("something went wrong"),
wantCode: http.StatusBadRequest,
},
{
description: "invalid: backend failure",
- ascii: mustLeafBuffer(t, 10, types.Hash{}, true),
+ ascii: mustLeafBuffer(t, 10, merkle.Hash{}, true),
expectDNS: true,
+ expectStateman: true,
+ sthStateman: testSTH,
expectTrillian: true,
errTrillian: fmt.Errorf("something went wrong"),
wantCode: http.StatusInternalServerError,
},
{
- description: "valid",
- ascii: mustLeafBuffer(t, 10, types.Hash{}, true),
+ description: "valid: 202",
+ ascii: mustLeafBuffer(t, 10, merkle.Hash{}, true),
+ expectDNS: true,
+ expectStateman: true,
+ sthStateman: testSTH,
+ expectTrillian: true,
+ wantCode: http.StatusAccepted,
+ },
+ {
+ description: "valid: 200",
+ ascii: mustLeafBuffer(t, 10, merkle.Hash{}, true),
expectDNS: true,
+ expectStateman: true,
+ sthStateman: testSTH,
expectTrillian: true,
+ sequenced: true,
wantCode: http.StatusOK,
},
} {
@@ -221,16 +118,21 @@ func TestAddLeaf(t *testing.T) {
}
client := mocksDB.NewMockClient(ctrl)
if table.expectTrillian {
- client.EXPECT().AddLeaf(gomock.Any(), gomock.Any()).Return(table.errTrillian)
+ client.EXPECT().AddLeaf(gomock.Any(), gomock.Any(), gomock.Any()).Return(table.sequenced, table.errTrillian)
}
- i := Instance{
- Config: testConfig,
- Client: client,
- DNS: dns,
+ stateman := mocksState.NewMockStateManager(ctrl)
+ if table.expectStateman {
+ stateman.EXPECT().ToCosignTreeHead().Return(table.sthStateman)
+ }
+ node := Primary{
+ Config: testConfig,
+ TrillianClient: client,
+ Stateman: stateman,
+ DNS: dns,
}
// Create HTTP request
- url := types.EndpointAddLeaf.Path("http://example.com", i.Prefix)
+ url := types.EndpointAddLeaf.Path("http://example.com", node.Prefix())
req, err := http.NewRequest("POST", url, table.ascii)
if err != nil {
t.Fatalf("must create http request: %v", err)
@@ -238,7 +140,7 @@ func TestAddLeaf(t *testing.T) {
// Run HTTP request
w := httptest.NewRecorder()
- mustHandle(t, i, types.EndpointAddLeaf).ServeHTTP(w, req)
+ mustHandlePublic(t, node, types.EndpointAddLeaf).ServeHTTP(w, req)
if got, want := w.Code, table.wantCode; got != want {
t.Errorf("got HTTP status code %v but wanted %v in test %q", got, want, table.description)
}
@@ -250,7 +152,7 @@ func TestAddCosignature(t *testing.T) {
buf := func() io.Reader {
return bytes.NewBufferString(fmt.Sprintf("%s=%x\n%s=%x\n",
"cosignature", types.Signature{},
- "key_hash", *types.HashFn(testWitVK[:]),
+ "key_hash", *merkle.HashFn(testWitVK[:]),
))
}
for _, table := range []struct {
@@ -269,7 +171,7 @@ func TestAddCosignature(t *testing.T) {
description: "invalid: bad request (unknown witness)",
ascii: bytes.NewBufferString(fmt.Sprintf("%s=%x\n%s=%x\n",
"cosignature", types.Signature{},
- "key_hash", *types.HashFn(testWitVK[1:]),
+ "key_hash", *merkle.HashFn(testWitVK[1:]),
)),
wantCode: http.StatusBadRequest,
},
@@ -295,13 +197,13 @@ func TestAddCosignature(t *testing.T) {
if table.expect {
stateman.EXPECT().AddCosignature(gomock.Any(), gomock.Any(), gomock.Any()).Return(table.err)
}
- i := Instance{
+ node := Primary{
Config: testConfig,
Stateman: stateman,
}
// Create HTTP request
- url := types.EndpointAddCosignature.Path("http://example.com", i.Prefix)
+ url := types.EndpointAddCosignature.Path("http://example.com", node.Prefix())
req, err := http.NewRequest("POST", url, table.ascii)
if err != nil {
t.Fatalf("must create http request: %v", err)
@@ -309,7 +211,7 @@ func TestAddCosignature(t *testing.T) {
// Run HTTP request
w := httptest.NewRecorder()
- mustHandle(t, i, types.EndpointAddCosignature).ServeHTTP(w, req)
+ mustHandlePublic(t, node, types.EndpointAddCosignature).ServeHTTP(w, req)
if got, want := w.Code, table.wantCode; got != want {
t.Errorf("got HTTP status code %v but wanted %v in test %q", got, want, table.description)
}
@@ -317,7 +219,7 @@ func TestAddCosignature(t *testing.T) {
}
}
-func TestGetTreeToSign(t *testing.T) {
+func TestGetTreeToCosign(t *testing.T) {
for _, table := range []struct {
description string
expect bool // set if a mock answer is expected
@@ -326,12 +228,6 @@ func TestGetTreeToSign(t *testing.T) {
wantCode int // HTTP status ok
}{
{
- description: "invalid: backend failure",
- expect: true,
- err: fmt.Errorf("something went wrong"),
- wantCode: http.StatusInternalServerError,
- },
- {
description: "valid",
expect: true,
rsp: testSTH,
@@ -344,15 +240,15 @@ func TestGetTreeToSign(t *testing.T) {
defer ctrl.Finish()
stateman := mocksState.NewMockStateManager(ctrl)
if table.expect {
- stateman.EXPECT().ToCosignTreeHead(gomock.Any()).Return(table.rsp, table.err)
+ stateman.EXPECT().ToCosignTreeHead().Return(table.rsp)
}
- i := Instance{
+ node := Primary{
Config: testConfig,
Stateman: stateman,
}
// Create HTTP request
- url := types.EndpointGetTreeHeadToCosign.Path("http://example.com", i.Prefix)
+ url := types.EndpointGetTreeHeadToCosign.Path("http://example.com", node.Prefix())
req, err := http.NewRequest("GET", url, nil)
if err != nil {
t.Fatalf("must create http request: %v", err)
@@ -360,7 +256,7 @@ func TestGetTreeToSign(t *testing.T) {
// Run HTTP request
w := httptest.NewRecorder()
- mustHandle(t, i, types.EndpointGetTreeHeadToCosign).ServeHTTP(w, req)
+ mustHandlePublic(t, node, types.EndpointGetTreeHeadToCosign).ServeHTTP(w, req)
if got, want := w.Code, table.wantCode; got != want {
t.Errorf("got HTTP status code %v but wanted %v in test %q", got, want, table.description)
}
@@ -377,7 +273,7 @@ func TestGetTreeCosigned(t *testing.T) {
wantCode int // HTTP status ok
}{
{
- description: "invalid: backend failure",
+ description: "invalid: no cosigned STH",
expect: true,
err: fmt.Errorf("something went wrong"),
wantCode: http.StatusInternalServerError,
@@ -397,13 +293,13 @@ func TestGetTreeCosigned(t *testing.T) {
if table.expect {
stateman.EXPECT().CosignedTreeHead(gomock.Any()).Return(table.rsp, table.err)
}
- i := Instance{
+ node := Primary{
Config: testConfig,
Stateman: stateman,
}
// Create HTTP request
- url := types.EndpointGetTreeHeadCosigned.Path("http://example.com", i.Prefix)
+ url := types.EndpointGetTreeHeadCosigned.Path("http://example.com", node.Prefix())
req, err := http.NewRequest("GET", url, nil)
if err != nil {
t.Fatalf("must create http request: %v", err)
@@ -411,7 +307,7 @@ func TestGetTreeCosigned(t *testing.T) {
// Run HTTP request
w := httptest.NewRecorder()
- mustHandle(t, i, types.EndpointGetTreeHeadCosigned).ServeHTTP(w, req)
+ mustHandlePublic(t, node, types.EndpointGetTreeHeadCosigned).ServeHTTP(w, req)
if got, want := w.Code, table.wantCode; got != want {
t.Errorf("got HTTP status code %v but wanted %v in test %q", got, want, table.description)
}
@@ -422,7 +318,8 @@ func TestGetTreeCosigned(t *testing.T) {
func TestGetConsistencyProof(t *testing.T) {
for _, table := range []struct {
description string
- params string // params is the query's url params
+ params string // params is the query's url params
+ sth *types.SignedTreeHead
expect bool // set if a mock answer is expected
rsp *types.ConsistencyProof // consistency proof from Trillian client
err error // error from Trillian client
@@ -444,8 +341,15 @@ func TestGetConsistencyProof(t *testing.T) {
wantCode: http.StatusBadRequest,
},
{
+ description: "invalid: bad request (NewSize > tree size)",
+ params: "1/2",
+ sth: &sth1,
+ wantCode: http.StatusBadRequest,
+ },
+ {
description: "invalid: backend failure",
params: "1/2",
+ sth: &sth2,
expect: true,
err: fmt.Errorf("something went wrong"),
wantCode: http.StatusInternalServerError,
@@ -453,12 +357,13 @@ func TestGetConsistencyProof(t *testing.T) {
{
description: "valid",
params: "1/2",
+ sth: &sth2,
expect: true,
rsp: &types.ConsistencyProof{
OldSize: 1,
NewSize: 2,
- Path: []types.Hash{
- *types.HashFn([]byte{}),
+ Path: []merkle.Hash{
+ *merkle.HashFn([]byte{}),
},
},
wantCode: http.StatusOK,
@@ -472,13 +377,18 @@ func TestGetConsistencyProof(t *testing.T) {
if table.expect {
client.EXPECT().GetConsistencyProof(gomock.Any(), gomock.Any()).Return(table.rsp, table.err)
}
- i := Instance{
- Config: testConfig,
- Client: client,
+ stateman := mocksState.NewMockStateManager(ctrl)
+ if table.sth != nil {
+ stateman.EXPECT().ToCosignTreeHead().Return(table.sth)
+ }
+ node := Primary{
+ Config: testConfig,
+ TrillianClient: client,
+ Stateman: stateman,
}
// Create HTTP request
- url := types.EndpointGetConsistencyProof.Path("http://example.com", i.Prefix)
+ url := types.EndpointGetConsistencyProof.Path("http://example.com", node.Prefix())
req, err := http.NewRequest(http.MethodGet, url+table.params, nil)
if err != nil {
t.Fatalf("must create http request: %v", err)
@@ -486,7 +396,7 @@ func TestGetConsistencyProof(t *testing.T) {
// Run HTTP request
w := httptest.NewRecorder()
- mustHandle(t, i, types.EndpointGetConsistencyProof).ServeHTTP(w, req)
+ mustHandlePublic(t, node, types.EndpointGetConsistencyProof).ServeHTTP(w, req)
if got, want := w.Code, table.wantCode; got != want {
t.Errorf("got HTTP status code %v but wanted %v in test %q", got, want, table.description)
}
@@ -497,7 +407,8 @@ func TestGetConsistencyProof(t *testing.T) {
func TestGetInclusionProof(t *testing.T) {
for _, table := range []struct {
description string
- params string // params is the query's url params
+ params string // params is the query's url params
+ sth *types.SignedTreeHead
expect bool // set if a mock answer is expected
rsp *types.InclusionProof // inclusion proof from Trillian client
err error // error from Trillian client
@@ -509,13 +420,20 @@ func TestGetInclusionProof(t *testing.T) {
wantCode: http.StatusBadRequest,
},
{
- description: "invalid: bad request (no proof for tree size)",
+ description: "invalid: bad request (no proof available for tree size 1)",
params: "1/0000000000000000000000000000000000000000000000000000000000000000",
wantCode: http.StatusBadRequest,
},
{
+ description: "invalid: bad request (request outside current tree size)",
+ params: "2/0000000000000000000000000000000000000000000000000000000000000000",
+ sth: &sth1,
+ wantCode: http.StatusBadRequest,
+ },
+ {
description: "invalid: backend failure",
params: "2/0000000000000000000000000000000000000000000000000000000000000000",
+ sth: &sth2,
expect: true,
err: fmt.Errorf("something went wrong"),
wantCode: http.StatusInternalServerError,
@@ -523,12 +441,13 @@ func TestGetInclusionProof(t *testing.T) {
{
description: "valid",
params: "2/0000000000000000000000000000000000000000000000000000000000000000",
+ sth: &sth2,
expect: true,
rsp: &types.InclusionProof{
TreeSize: 2,
LeafIndex: 0,
- Path: []types.Hash{
- *types.HashFn([]byte{}),
+ Path: []merkle.Hash{
+ *merkle.HashFn([]byte{}),
},
},
wantCode: http.StatusOK,
@@ -542,13 +461,18 @@ func TestGetInclusionProof(t *testing.T) {
if table.expect {
client.EXPECT().GetInclusionProof(gomock.Any(), gomock.Any()).Return(table.rsp, table.err)
}
- i := Instance{
- Config: testConfig,
- Client: client,
+ stateman := mocksState.NewMockStateManager(ctrl)
+ if table.sth != nil {
+ stateman.EXPECT().ToCosignTreeHead().Return(table.sth)
+ }
+ node := Primary{
+ Config: testConfig,
+ TrillianClient: client,
+ Stateman: stateman,
}
// Create HTTP request
- url := types.EndpointGetInclusionProof.Path("http://example.com", i.Prefix)
+ url := types.EndpointGetInclusionProof.Path("http://example.com", node.Prefix())
req, err := http.NewRequest(http.MethodGet, url+table.params, nil)
if err != nil {
t.Fatalf("must create http request: %v", err)
@@ -556,7 +480,7 @@ func TestGetInclusionProof(t *testing.T) {
// Run HTTP request
w := httptest.NewRecorder()
- mustHandle(t, i, types.EndpointGetInclusionProof).ServeHTTP(w, req)
+ mustHandlePublic(t, node, types.EndpointGetInclusionProof).ServeHTTP(w, req)
if got, want := w.Code, table.wantCode; got != want {
t.Errorf("got HTTP status code %v but wanted %v in test %q", got, want, table.description)
}
@@ -567,7 +491,8 @@ func TestGetInclusionProof(t *testing.T) {
func TestGetLeaves(t *testing.T) {
for _, table := range []struct {
description string
- params string // params is the query's url params
+ params string // params is the query's url params
+ sth *types.SignedTreeHead
expect bool // set if a mock answer is expected
rsp *types.Leaves // list of leaves from Trillian client
err error // error from Trillian client
@@ -584,8 +509,15 @@ func TestGetLeaves(t *testing.T) {
wantCode: http.StatusBadRequest,
},
{
+ description: "invalid: bad request (EndSize >= current tree size)",
+ params: "0/2",
+ sth: &sth2,
+ wantCode: http.StatusBadRequest,
+ },
+ {
description: "invalid: backend failure",
params: "0/0",
+ sth: &sth2,
expect: true,
err: fmt.Errorf("something went wrong"),
wantCode: http.StatusInternalServerError,
@@ -593,6 +525,7 @@ func TestGetLeaves(t *testing.T) {
{
description: "valid: one more entry than the configured MaxRange",
params: fmt.Sprintf("%d/%d", 0, testConfig.MaxRange), // query will be pruned
+ sth: &sth5,
expect: true,
rsp: func() *types.Leaves {
var list types.Leaves
@@ -600,10 +533,10 @@ func TestGetLeaves(t *testing.T) {
list = append(list[:], types.Leaf{
Statement: types.Statement{
ShardHint: 0,
- Checksum: types.Hash{},
+ Checksum: merkle.Hash{},
},
Signature: types.Signature{},
- KeyHash: types.Hash{},
+ KeyHash: merkle.Hash{},
})
}
return &list
@@ -619,13 +552,18 @@ func TestGetLeaves(t *testing.T) {
if table.expect {
client.EXPECT().GetLeaves(gomock.Any(), gomock.Any()).Return(table.rsp, table.err)
}
- i := Instance{
- Config: testConfig,
- Client: client,
+ stateman := mocksState.NewMockStateManager(ctrl)
+ if table.sth != nil {
+ stateman.EXPECT().ToCosignTreeHead().Return(table.sth)
+ }
+ node := Primary{
+ Config: testConfig,
+ TrillianClient: client,
+ Stateman: stateman,
}
// Create HTTP request
- url := types.EndpointGetLeaves.Path("http://example.com", i.Prefix)
+ url := types.EndpointGetLeaves.Path("http://example.com", node.Prefix())
req, err := http.NewRequest(http.MethodGet, url+table.params, nil)
if err != nil {
t.Fatalf("must create http request: %v", err)
@@ -633,7 +571,7 @@ func TestGetLeaves(t *testing.T) {
// Run HTTP request
w := httptest.NewRecorder()
- mustHandle(t, i, types.EndpointGetLeaves).ServeHTTP(w, req)
+ mustHandlePublic(t, node, types.EndpointGetLeaves).ServeHTTP(w, req)
if got, want := w.Code, table.wantCode; got != want {
t.Errorf("got HTTP status code %v but wanted %v in test %q", got, want, table.description)
}
@@ -652,17 +590,17 @@ func TestGetLeaves(t *testing.T) {
}
}
-func mustHandle(t *testing.T, i Instance, e types.Endpoint) Handler {
- for _, handler := range i.Handlers() {
+func mustHandlePublic(t *testing.T, p Primary, e types.Endpoint) handler.Handler {
+ for _, handler := range p.PublicHTTPHandlers() {
if handler.Endpoint == e {
return handler
}
}
t.Fatalf("must handle endpoint: %v", e)
- return Handler{}
+ return handler.Handler{}
}
-func mustLeafBuffer(t *testing.T, shardHint uint64, message types.Hash, wantSig bool) io.Reader {
+func mustLeafBuffer(t *testing.T, shardHint uint64, message merkle.Hash, wantSig bool) io.Reader {
t.Helper()
vk, sk, err := ed25519.GenerateKey(rand.Reader)
@@ -671,7 +609,7 @@ func mustLeafBuffer(t *testing.T, shardHint uint64, message types.Hash, wantSig
}
msg := types.Statement{
ShardHint: shardHint,
- Checksum: *types.HashFn(message[:]),
+ Checksum: *merkle.HashFn(message[:]),
}
sig := ed25519.Sign(sk, msg.ToBinary())
if !wantSig {
diff --git a/internal/node/primary/endpoint_internal.go b/internal/node/primary/endpoint_internal.go
new file mode 100644
index 0000000..f7a684f
--- /dev/null
+++ b/internal/node/primary/endpoint_internal.go
@@ -0,0 +1,30 @@
+package primary
+
+// This file implements internal HTTP handler callbacks for primary nodes.
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+
+ "git.sigsum.org/log-go/internal/node/handler"
+ "git.sigsum.org/sigsum-go/pkg/log"
+ "git.sigsum.org/sigsum-go/pkg/types"
+)
+
+func getTreeHeadUnsigned(ctx context.Context, c handler.Config, w http.ResponseWriter, _ *http.Request) (int, error) {
+ log.Debug("handling %s request", types.EndpointGetTreeHeadUnsigned)
+ p := c.(Primary)
+ th, err := p.TrillianClient.GetTreeHead(ctx)
+ if err != nil {
+ return http.StatusInternalServerError, fmt.Errorf("failed getting tree head: %v", err)
+ }
+ if err := th.ToASCII(w); err != nil {
+ return http.StatusInternalServerError, err
+ }
+ return http.StatusOK, nil
+}
+
+func getLeavesInternal(ctx context.Context, c handler.Config, w http.ResponseWriter, r *http.Request) (int, error) {
+ return getLeavesGeneral(ctx, c, w, r, false)
+}
diff --git a/internal/node/primary/endpoint_internal_test.go b/internal/node/primary/endpoint_internal_test.go
new file mode 100644
index 0000000..6c76c33
--- /dev/null
+++ b/internal/node/primary/endpoint_internal_test.go
@@ -0,0 +1,82 @@
+package primary
+
+import (
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ mocksDB "git.sigsum.org/log-go/internal/mocks/db"
+ "git.sigsum.org/log-go/internal/node/handler"
+ "git.sigsum.org/sigsum-go/pkg/merkle"
+ "git.sigsum.org/sigsum-go/pkg/types"
+ "github.com/golang/mock/gomock"
+)
+
+var (
+ testTH = &types.TreeHead{
+ Timestamp: 0,
+ TreeSize: 0,
+ RootHash: *merkle.HashFn([]byte("root hash")),
+ }
+)
+
+func TestGetTreeHeadUnsigned(t *testing.T) {
+ for _, table := range []struct {
+ description string
+ expect bool // set if a mock answer is expected
+ rsp *types.TreeHead // tree head from Trillian client
+ err error // error from Trillian client
+ wantCode int // HTTP status ok
+ }{
+ {
+ description: "invalid: backend failure",
+ expect: true,
+ err: fmt.Errorf("something went wrong"),
+ wantCode: http.StatusInternalServerError,
+ },
+ {
+ description: "valid",
+ expect: true,
+ rsp: testTH,
+ wantCode: http.StatusOK,
+ },
+ } {
+ // Run deferred functions at the end of each iteration
+ func() {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+ trillianClient := mocksDB.NewMockClient(ctrl)
+ trillianClient.EXPECT().GetTreeHead(gomock.Any()).Return(table.rsp, table.err)
+
+ node := Primary{
+ Config: testConfig,
+ TrillianClient: trillianClient,
+ }
+
+ // Create HTTP request
+ url := types.EndpointGetTreeHeadUnsigned.Path("http://example.com", node.Prefix())
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ t.Fatalf("must create http request: %v", err)
+ }
+
+ // Run HTTP request
+ w := httptest.NewRecorder()
+ mustHandleInternal(t, node, types.EndpointGetTreeHeadUnsigned).ServeHTTP(w, req)
+ if got, want := w.Code, table.wantCode; got != want {
+ t.Errorf("got HTTP status code %v but wanted %v in test %q", got, want, table.description)
+ }
+ }()
+ }
+}
+
+func mustHandleInternal(t *testing.T, p Primary, e types.Endpoint) handler.Handler {
+ for _, handler := range p.InternalHTTPHandlers() {
+ if handler.Endpoint == e {
+ return handler
+ }
+ }
+ t.Fatalf("must handle endpoint: %v", e)
+ return handler.Handler{}
+}
diff --git a/internal/node/primary/primary.go b/internal/node/primary/primary.go
new file mode 100644
index 0000000..6128f49
--- /dev/null
+++ b/internal/node/primary/primary.go
@@ -0,0 +1,74 @@
+package primary
+
+import (
+ "crypto"
+ "net/http"
+ "time"
+
+ "git.sigsum.org/log-go/internal/db"
+ "git.sigsum.org/log-go/internal/node/handler"
+ "git.sigsum.org/log-go/internal/state"
+ "git.sigsum.org/sigsum-go/pkg/client"
+ "git.sigsum.org/sigsum-go/pkg/dns"
+ "git.sigsum.org/sigsum-go/pkg/merkle"
+ "git.sigsum.org/sigsum-go/pkg/types"
+)
+
+// Config is a collection of log parameters
+type Config struct {
+ LogID string // H(public key), then hex-encoded
+ TreeID int64 // Merkle tree identifier used by Trillian
+ Prefix string // The portion between base URL and st/v0 (may be "")
+ MaxRange int64 // Maximum number of leaves per get-leaves request
+ Deadline time.Duration // Deadline used for gRPC requests
+ Interval time.Duration // Cosigning frequency
+ ShardStart uint64 // Shard interval start (num seconds since UNIX epoch)
+
+ // Witnesses map trusted witness identifiers to public keys
+ Witnesses map[merkle.Hash]types.PublicKey
+}
+
+// Primary is an instance of the log's primary node
+type Primary struct {
+ Config
+ PublicHTTPMux *http.ServeMux
+ InternalHTTPMux *http.ServeMux
+ TrillianClient db.Client // provides access to the Trillian backend
+ Signer crypto.Signer // provides access to Ed25519 private key
+ Stateman state.StateManager // coordinates access to (co)signed tree heads
+ DNS dns.Verifier // checks if domain name knows a public key
+ Secondary client.Client
+}
+
+// Implementing handler.Config
+func (p Primary) Prefix() string {
+ return p.Config.Prefix
+}
+func (p Primary) LogID() string {
+ return p.Config.LogID
+}
+func (p Primary) Deadline() time.Duration {
+ return p.Config.Deadline
+}
+
+// PublicHTTPHandlers returns all external handlers
+func (p Primary) PublicHTTPHandlers() []handler.Handler {
+ return []handler.Handler{
+ handler.Handler{p, addLeaf, types.EndpointAddLeaf, http.MethodPost},
+ handler.Handler{p, addCosignature, types.EndpointAddCosignature, http.MethodPost},
+ handler.Handler{p, getTreeHeadToCosign, types.EndpointGetTreeHeadToCosign, http.MethodGet},
+ handler.Handler{p, getTreeHeadCosigned, types.EndpointGetTreeHeadCosigned, http.MethodGet},
+ handler.Handler{p, getConsistencyProof, types.EndpointGetConsistencyProof, http.MethodGet},
+ handler.Handler{p, getInclusionProof, types.EndpointGetInclusionProof, http.MethodGet},
+ handler.Handler{p, getLeavesExternal, types.EndpointGetLeaves, http.MethodGet},
+ }
+}
+
+// InternalHTTPHandlers() returns all internal handlers
+func (p Primary) InternalHTTPHandlers() []handler.Handler {
+ return []handler.Handler{
+ handler.Handler{p, getTreeHeadUnsigned, types.EndpointGetTreeHeadUnsigned, http.MethodGet},
+ handler.Handler{p, getConsistencyProof, types.EndpointGetConsistencyProof, http.MethodGet},
+ handler.Handler{p, getLeavesInternal, types.EndpointGetLeaves, http.MethodGet},
+ }
+}
diff --git a/internal/node/primary/primary_test.go b/internal/node/primary/primary_test.go
new file mode 100644
index 0000000..5062955
--- /dev/null
+++ b/internal/node/primary/primary_test.go
@@ -0,0 +1,75 @@
+package primary
+
+import (
+ "fmt"
+ "testing"
+
+ "git.sigsum.org/sigsum-go/pkg/merkle"
+ "git.sigsum.org/sigsum-go/pkg/types"
+)
+
+var (
+ testWitVK = types.PublicKey{}
+ testConfig = Config{
+ LogID: fmt.Sprintf("%x", merkle.HashFn([]byte("logid"))[:]),
+ TreeID: 0,
+ Prefix: "testonly",
+ MaxRange: 3,
+ Deadline: 10,
+ Interval: 10,
+ ShardStart: 10,
+ Witnesses: map[merkle.Hash]types.PublicKey{
+ *merkle.HashFn(testWitVK[:]): testWitVK,
+ },
+ }
+)
+
+// TestPublicHandlers checks that the expected external handlers are configured
+func TestPublicHandlers(t *testing.T) {
+ endpoints := map[types.Endpoint]bool{
+ types.EndpointAddLeaf: false,
+ types.EndpointAddCosignature: false,
+ types.EndpointGetTreeHeadToCosign: false,
+ types.EndpointGetTreeHeadCosigned: false,
+ types.EndpointGetConsistencyProof: false,
+ types.EndpointGetInclusionProof: false,
+ types.EndpointGetLeaves: false,
+ }
+ node := Primary{
+ Config: testConfig,
+ }
+ for _, handler := range node.PublicHTTPHandlers() {
+ if _, ok := endpoints[handler.Endpoint]; !ok {
+ t.Errorf("got unexpected endpoint: %s", handler.Endpoint)
+ }
+ endpoints[handler.Endpoint] = true
+ }
+ for endpoint, ok := range endpoints {
+ if !ok {
+ t.Errorf("endpoint %s is not configured", endpoint)
+ }
+ }
+}
+
+// TestIntHandlers checks that the expected internal handlers are configured
+func TestIntHandlers(t *testing.T) {
+ endpoints := map[types.Endpoint]bool{
+ types.EndpointGetTreeHeadUnsigned: false,
+ types.EndpointGetConsistencyProof: false,
+ types.EndpointGetLeaves: false,
+ }
+ node := Primary{
+ Config: testConfig,
+ }
+ for _, handler := range node.InternalHTTPHandlers() {
+ if _, ok := endpoints[handler.Endpoint]; !ok {
+ t.Errorf("got unexpected endpoint: %s", handler.Endpoint)
+ }
+ endpoints[handler.Endpoint] = true
+ }
+ for endpoint, ok := range endpoints {
+ if !ok {
+ t.Errorf("endpoint %s is not configured", endpoint)
+ }
+ }
+}
diff --git a/internal/node/secondary/endpoint_internal.go b/internal/node/secondary/endpoint_internal.go
new file mode 100644
index 0000000..f60d6d8
--- /dev/null
+++ b/internal/node/secondary/endpoint_internal.go
@@ -0,0 +1,44 @@
+package secondary
+
+// This file implements internal HTTP handler callbacks for secondary nodes.
+
+import (
+ "context"
+ "crypto/ed25519"
+ "fmt"
+ "net/http"
+
+ "git.sigsum.org/log-go/internal/node/handler"
+ "git.sigsum.org/sigsum-go/pkg/log"
+ "git.sigsum.org/sigsum-go/pkg/merkle"
+ "git.sigsum.org/sigsum-go/pkg/types"
+)
+
+func getTreeHeadToCosign(ctx context.Context, c handler.Config, w http.ResponseWriter, _ *http.Request) (int, error) {
+ s := c.(Secondary)
+ log.Debug("handling get-tree-head-to-cosign request")
+
+ signedTreeHead := func() (*types.SignedTreeHead, error) {
+ tctx, cancel := context.WithTimeout(ctx, s.Config.Deadline)
+ defer cancel()
+ th, err := treeHeadFromTrillian(tctx, s.TrillianClient)
+ if err != nil {
+ return nil, fmt.Errorf("getting tree head: %w", err)
+ }
+ namespace := merkle.HashFn(s.Signer.Public().(ed25519.PublicKey))
+ sth, err := th.Sign(s.Signer, namespace)
+ if err != nil {
+ return nil, fmt.Errorf("signing tree head: %w", err)
+ }
+ return sth, nil
+ }
+
+ sth, err := signedTreeHead()
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+ if err := sth.ToASCII(w); err != nil {
+ return http.StatusInternalServerError, err
+ }
+ return http.StatusOK, nil
+}
diff --git a/internal/node/secondary/endpoint_internal_test.go b/internal/node/secondary/endpoint_internal_test.go
new file mode 100644
index 0000000..9637e29
--- /dev/null
+++ b/internal/node/secondary/endpoint_internal_test.go
@@ -0,0 +1,111 @@
+package secondary
+
+import (
+ "crypto"
+ "crypto/ed25519"
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ mocksDB "git.sigsum.org/log-go/internal/mocks/db"
+ "git.sigsum.org/log-go/internal/node/handler"
+ "git.sigsum.org/sigsum-go/pkg/merkle"
+ "git.sigsum.org/sigsum-go/pkg/types"
+ "github.com/golang/mock/gomock"
+)
+
+// TestSigner implements the signer interface. It can be used to mock
+// an Ed25519 signer that always return the same public key,
+// signature, and error.
+// NOTE: Code duplication with internal/state/single_test.go
+type TestSigner struct {
+ PublicKey [ed25519.PublicKeySize]byte
+ Signature [ed25519.SignatureSize]byte
+ Error error
+}
+
+func (ts *TestSigner) Public() crypto.PublicKey {
+ return ed25519.PublicKey(ts.PublicKey[:])
+}
+
+func (ts *TestSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
+ return ts.Signature[:], ts.Error
+}
+
+var (
+ testTH = types.TreeHead{
+ Timestamp: 0,
+ TreeSize: 0,
+ RootHash: *merkle.HashFn([]byte("root hash")),
+ }
+ testSignerFailing = TestSigner{types.PublicKey{}, types.Signature{}, fmt.Errorf("mocked error")}
+ testSignerSucceeding = TestSigner{types.PublicKey{}, types.Signature{}, nil}
+)
+
+func TestGetTreeHeadToCosign(t *testing.T) {
+ for _, tbl := range []struct {
+ desc string
+ trillianTHErr error
+ trillianTHRet *types.TreeHead
+ signer crypto.Signer
+ httpStatus int
+ }{
+ {
+ desc: "trillian GetTreeHead error",
+ trillianTHErr: fmt.Errorf("mocked error"),
+ httpStatus: http.StatusInternalServerError,
+ },
+ {
+ desc: "signer error",
+ trillianTHRet: &testTH,
+ signer: &testSignerFailing,
+ httpStatus: http.StatusInternalServerError,
+ },
+ {
+ desc: "success",
+ trillianTHRet: &testTH,
+ signer: &testSignerSucceeding,
+ httpStatus: http.StatusOK,
+ },
+ } {
+ func() {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ trillianClient := mocksDB.NewMockClient(ctrl)
+ trillianClient.EXPECT().GetTreeHead(gomock.Any()).Return(tbl.trillianTHRet, tbl.trillianTHErr)
+
+ node := Secondary{
+ Config: testConfig,
+ TrillianClient: trillianClient,
+ Signer: tbl.signer,
+ }
+
+ // Create HTTP request
+ url := types.EndpointAddLeaf.Path("http://example.com", node.Prefix())
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ t.Fatalf("must create http request: %v", err)
+ }
+
+ // Run HTTP request
+ w := httptest.NewRecorder()
+ mustHandleInternal(t, node, types.EndpointGetTreeHeadToCosign).ServeHTTP(w, req)
+ if got, want := w.Code, tbl.httpStatus; got != want {
+ t.Errorf("got HTTP status code %v but wanted %v in test %q", got, want, tbl.desc)
+ }
+ }()
+ }
+}
+
+func mustHandleInternal(t *testing.T, s Secondary, e types.Endpoint) handler.Handler {
+ for _, h := range s.InternalHTTPHandlers() {
+ if h.Endpoint == e {
+ return h
+ }
+ }
+ t.Fatalf("must handle endpoint: %v", e)
+ return handler.Handler{}
+}
diff --git a/internal/node/secondary/secondary.go b/internal/node/secondary/secondary.go
new file mode 100644
index 0000000..c181420
--- /dev/null
+++ b/internal/node/secondary/secondary.go
@@ -0,0 +1,112 @@
+package secondary
+
+import (
+ "context"
+ "crypto"
+ "fmt"
+ "net/http"
+ "time"
+
+ "git.sigsum.org/log-go/internal/db"
+ "git.sigsum.org/log-go/internal/node/handler"
+ "git.sigsum.org/sigsum-go/pkg/client"
+ "git.sigsum.org/sigsum-go/pkg/log"
+ "git.sigsum.org/sigsum-go/pkg/requests"
+ "git.sigsum.org/sigsum-go/pkg/types"
+)
+
+// Config is a collection of log parameters
+type Config struct {
+ LogID string // H(public key), then hex-encoded
+ TreeID int64 // Merkle tree identifier used by Trillian
+ Prefix string // The portion between base URL and st/v0 (may be "")
+ Deadline time.Duration // Deadline used for gRPC requests
+ Interval time.Duration // Signing frequency
+}
+
+// Secondary is an instance of a secondary node
+type Secondary struct {
+ Config
+ PublicHTTPMux *http.ServeMux
+ InternalHTTPMux *http.ServeMux
+ TrillianClient db.Client // provides access to the Trillian backend
+ Signer crypto.Signer // provides access to Ed25519 private key
+ Primary client.Client
+}
+
+// Implementing handler.Config
+func (s Secondary) Prefix() string {
+ return s.Config.Prefix
+}
+func (s Secondary) LogID() string {
+ return s.Config.LogID
+}
+func (s Secondary) Deadline() time.Duration {
+ return s.Config.Deadline
+}
+
+func (s Secondary) Run(ctx context.Context) {
+ ticker := time.NewTicker(s.Interval)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case <-ticker.C:
+ s.fetchLeavesFromPrimary(ctx)
+ case <-ctx.Done():
+ return
+ }
+ }
+}
+
+// TODO: nit-pick: the internal endpoint is used by primaries to figure out how much can be signed; not cosigned - update name?
+
+func (s Secondary) InternalHTTPHandlers() []handler.Handler {
+ return []handler.Handler{
+ handler.Handler{s, getTreeHeadToCosign, types.EndpointGetTreeHeadToCosign, http.MethodGet},
+ }
+}
+
+func (s Secondary) fetchLeavesFromPrimary(ctx context.Context) {
+ sctx, cancel := context.WithTimeout(ctx, time.Second*10) // FIXME: parameterize 10
+ defer cancel()
+
+ prim, err := s.Primary.GetUnsignedTreeHead(sctx)
+ if err != nil {
+ log.Warning("unable to get tree head from primary: %v", err)
+ return
+ }
+ log.Debug("got tree head from primary, size %d", prim.TreeSize)
+
+ curTH, err := treeHeadFromTrillian(sctx, s.TrillianClient)
+ if err != nil {
+ log.Warning("unable to get tree head from trillian: %v", err)
+ return
+ }
+ var leaves types.Leaves
+ for index := int64(curTH.TreeSize); index < int64(prim.TreeSize); index += int64(len(leaves)) {
+ req := requests.Leaves{
+ StartSize: uint64(index),
+ EndSize: prim.TreeSize - 1,
+ }
+ leaves, err = s.Primary.GetLeaves(sctx, req)
+ if err != nil {
+ log.Warning("error fetching leaves [%d..%d] from primary: %v", req.StartSize, req.EndSize, err)
+ return
+ }
+ log.Debug("got %d leaves from primary when asking for [%d..%d]", len(leaves), req.StartSize, req.EndSize)
+ if err := s.TrillianClient.AddSequencedLeaves(ctx, leaves, index); err != nil {
+ log.Error("AddSequencedLeaves: %v", err)
+ return
+ }
+ }
+}
+
+func treeHeadFromTrillian(ctx context.Context, trillianClient db.Client) (*types.TreeHead, error) {
+ th, err := trillianClient.GetTreeHead(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("fetching tree head from trillian: %v", err)
+ }
+ log.Debug("got tree head from trillian, size %d", th.TreeSize)
+ return th, nil
+}
diff --git a/internal/node/secondary/secondary_test.go b/internal/node/secondary/secondary_test.go
new file mode 100644
index 0000000..164bdf6
--- /dev/null
+++ b/internal/node/secondary/secondary_test.go
@@ -0,0 +1,138 @@
+package secondary
+
+import (
+ "context"
+ "fmt"
+ "testing"
+
+ mocksClient "git.sigsum.org/log-go/internal/mocks/client"
+ mocksDB "git.sigsum.org/log-go/internal/mocks/db"
+ "git.sigsum.org/sigsum-go/pkg/merkle"
+ "git.sigsum.org/sigsum-go/pkg/types"
+ "github.com/golang/mock/gomock"
+)
+
+var (
+ testConfig = Config{
+ LogID: fmt.Sprintf("%x", merkle.HashFn([]byte("logid"))[:]),
+ TreeID: 0,
+ Prefix: "testonly",
+ Deadline: 10,
+ }
+)
+
+// TestHandlers checks that the expected internal handlers are configured
+func TestIntHandlers(t *testing.T) {
+ endpoints := map[types.Endpoint]bool{
+ types.EndpointGetTreeHeadToCosign: false,
+ }
+ node := Secondary{
+ Config: testConfig,
+ }
+ for _, handler := range node.InternalHTTPHandlers() {
+ if _, ok := endpoints[handler.Endpoint]; !ok {
+ t.Errorf("got unexpected endpoint: %s", handler.Endpoint)
+ }
+ endpoints[handler.Endpoint] = true
+ }
+ for endpoint, ok := range endpoints {
+ if !ok {
+ t.Errorf("endpoint %s is not configured", endpoint)
+ }
+ }
+}
+
+func TestFetchLeavesFromPrimary(t *testing.T) {
+ for _, tbl := range []struct {
+ desc string
+ // client.GetUnsignedTreeHead()
+ primaryTHRet types.TreeHead
+ primaryTHErr error
+ // db.GetTreeHead()
+ trillianTHRet *types.TreeHead
+ trillianTHErr error
+ // client.GetLeaves()
+ primaryGetLeavesRet types.Leaves
+ primaryGetLeavesErr error
+ // db.AddSequencedLeaves()
+ trillianAddLeavesExp bool
+ trillianAddLeavesErr error
+ }{
+ {
+ desc: "no tree head from primary",
+ primaryTHErr: fmt.Errorf("mocked error"),
+ },
+ {
+ desc: "no tree head from trillian",
+ primaryTHRet: types.TreeHead{},
+ trillianTHErr: fmt.Errorf("mocked error"),
+ },
+ {
+ desc: "error fetching leaves",
+ primaryTHRet: types.TreeHead{TreeSize: 6},
+ trillianTHRet: &types.TreeHead{TreeSize: 5}, // 6-5 => 1 expected GetLeaves
+ primaryGetLeavesErr: fmt.Errorf("mocked error"),
+ },
+ {
+ desc: "error adding leaves",
+ primaryTHRet: types.TreeHead{TreeSize: 6},
+ trillianTHRet: &types.TreeHead{TreeSize: 5}, // 6-5 => 1 expected GetLeaves
+ primaryGetLeavesRet: types.Leaves{
+ types.Leaf{},
+ },
+ trillianAddLeavesErr: fmt.Errorf("mocked error"),
+ },
+ {
+ desc: "success",
+ primaryTHRet: types.TreeHead{TreeSize: 10},
+ trillianTHRet: &types.TreeHead{TreeSize: 5},
+ primaryGetLeavesRet: types.Leaves{
+ types.Leaf{},
+ },
+ trillianAddLeavesExp: true,
+ },
+ } {
+ func() {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ primaryClient := mocksClient.NewMockClient(ctrl)
+ primaryClient.EXPECT().GetUnsignedTreeHead(gomock.Any()).Return(tbl.primaryTHRet, tbl.primaryTHErr)
+
+ trillianClient := mocksDB.NewMockClient(ctrl)
+ if tbl.trillianTHErr != nil || tbl.trillianTHRet != nil {
+ trillianClient.EXPECT().GetTreeHead(gomock.Any()).Return(tbl.trillianTHRet, tbl.trillianTHErr)
+ }
+
+ if tbl.primaryGetLeavesErr != nil || tbl.primaryGetLeavesRet != nil {
+ primaryClient.EXPECT().GetLeaves(gomock.Any(), gomock.Any()).Return(tbl.primaryGetLeavesRet, tbl.primaryGetLeavesErr)
+ if tbl.trillianAddLeavesExp {
+ for i := tbl.trillianTHRet.TreeSize; i < tbl.primaryTHRet.TreeSize-1; i++ {
+ primaryClient.EXPECT().GetLeaves(gomock.Any(), gomock.Any()).Return(tbl.primaryGetLeavesRet, tbl.primaryGetLeavesErr)
+ }
+ }
+ }
+
+ if tbl.trillianAddLeavesErr != nil || tbl.trillianAddLeavesExp {
+ trillianClient.EXPECT().AddSequencedLeaves(gomock.Any(), gomock.Any(), gomock.Any()).Return(tbl.trillianAddLeavesErr)
+ if tbl.trillianAddLeavesExp {
+ for i := tbl.trillianTHRet.TreeSize; i < tbl.primaryTHRet.TreeSize-1; i++ {
+ trillianClient.EXPECT().AddSequencedLeaves(gomock.Any(), gomock.Any(), gomock.Any()).Return(tbl.trillianAddLeavesErr)
+ }
+ }
+ }
+
+ node := Secondary{
+ Config: testConfig,
+ Primary: primaryClient,
+ TrillianClient: trillianClient,
+ }
+
+ node.fetchLeavesFromPrimary(context.Background())
+
+ // NOTE: We are not verifying that
+ // AddSequencedLeaves() is being called with
+ // the right data.
+ }()
+ }
+}
diff --git a/internal/requests/requests.go b/internal/requests/requests.go
new file mode 100644
index 0000000..cfd563f
--- /dev/null
+++ b/internal/requests/requests.go
@@ -0,0 +1,91 @@
+package requests
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "time"
+
+ "git.sigsum.org/sigsum-go/pkg/dns"
+ "git.sigsum.org/sigsum-go/pkg/merkle"
+ sigsumreq "git.sigsum.org/sigsum-go/pkg/requests"
+ "git.sigsum.org/sigsum-go/pkg/types"
+)
+
+func LeafRequestFromHTTP(r *http.Request, shardStart uint64, ctx context.Context, vf dns.Verifier) (*sigsumreq.Leaf, error) {
+ var req sigsumreq.Leaf
+ if err := req.FromASCII(r.Body); err != nil {
+ return nil, fmt.Errorf("parse ascii: %w", err)
+ }
+ stmt := types.Statement{
+ ShardHint: req.ShardHint,
+ Checksum: *merkle.HashFn(req.Message[:]),
+ }
+ if !stmt.Verify(&req.PublicKey, &req.Signature) {
+ return nil, fmt.Errorf("invalid signature")
+ }
+ shardEnd := uint64(time.Now().Unix())
+ if req.ShardHint < shardStart {
+ return nil, fmt.Errorf("invalid shard hint: %d not in [%d, %d]", req.ShardHint, shardStart, shardEnd)
+ }
+ if req.ShardHint > shardEnd {
+ return nil, fmt.Errorf("invalid shard hint: %d not in [%d, %d]", req.ShardHint, shardStart, shardEnd)
+ }
+ if err := vf.Verify(ctx, req.DomainHint, &req.PublicKey); err != nil {
+ return nil, fmt.Errorf("invalid domain hint: %v", err)
+ }
+ return &req, nil
+}
+
+func CosignatureRequestFromHTTP(r *http.Request, w map[merkle.Hash]types.PublicKey) (*sigsumreq.Cosignature, error) {
+ var req sigsumreq.Cosignature
+ if err := req.FromASCII(r.Body); err != nil {
+ return nil, fmt.Errorf("parse ascii: %w", err)
+ }
+ if _, ok := w[req.KeyHash]; !ok {
+ return nil, fmt.Errorf("unknown witness: %x", req.KeyHash)
+ }
+ return &req, nil
+}
+
+func ConsistencyProofRequestFromHTTP(r *http.Request) (*sigsumreq.ConsistencyProof, error) {
+ var req sigsumreq.ConsistencyProof
+ if err := req.FromURL(r.URL.Path); err != nil {
+ return nil, fmt.Errorf("parse url: %w", err)
+ }
+ if req.OldSize < 1 {
+ return nil, fmt.Errorf("old_size(%d) must be larger than zero", req.OldSize)
+ }
+ if req.NewSize <= req.OldSize {
+ return nil, fmt.Errorf("new_size(%d) must be larger than old_size(%d)", req.NewSize, req.OldSize)
+ }
+ return &req, nil
+}
+
+func InclusionProofRequestFromHTTP(r *http.Request) (*sigsumreq.InclusionProof, error) {
+ var req sigsumreq.InclusionProof
+ if err := req.FromURL(r.URL.Path); err != nil {
+ return nil, fmt.Errorf("parse url: %w", err)
+ }
+ if req.TreeSize < 2 {
+ // TreeSize:0 => not possible to prove inclusion of anything
+ // TreeSize:1 => you don't need an inclusion proof (it is always empty)
+ return nil, fmt.Errorf("tree_size(%d) must be larger than one", req.TreeSize)
+ }
+ return &req, nil
+}
+
+func LeavesRequestFromHTTP(r *http.Request, maxRange uint64) (*sigsumreq.Leaves, error) {
+ var req sigsumreq.Leaves
+ if err := req.FromURL(r.URL.Path); err != nil {
+ return nil, fmt.Errorf("parse url: %w", err)
+ }
+
+ if req.StartSize > req.EndSize {
+ return nil, fmt.Errorf("start_size(%d) must be less than or equal to end_size(%d)", req.StartSize, req.EndSize)
+ }
+ if req.EndSize-req.StartSize+1 > maxRange {
+ req.EndSize = req.StartSize + maxRange - 1
+ }
+ return &req, nil
+}
diff --git a/internal/requests/requests_test.go b/internal/requests/requests_test.go
new file mode 100644
index 0000000..46d6e15
--- /dev/null
+++ b/internal/requests/requests_test.go
@@ -0,0 +1,218 @@
+package requests
+
+import (
+ "bytes"
+ "context"
+ "crypto/ed25519"
+ "crypto/rand"
+ "fmt"
+ "io"
+ "net/http"
+ "reflect"
+ "testing"
+ "time"
+
+ mocksDNS "git.sigsum.org/log-go/internal/mocks/dns"
+ "git.sigsum.org/sigsum-go/pkg/merkle"
+ sigsumreq "git.sigsum.org/sigsum-go/pkg/requests"
+ "git.sigsum.org/sigsum-go/pkg/types"
+ "github.com/golang/mock/gomock"
+)
+
+func TestLeafRequestFromHTTP(t *testing.T) {
+ st := uint64(10)
+ dh := "_sigsum_v0.example.org"
+ msg := merkle.Hash{}
+ var pub types.PublicKey
+ b, priv, err := ed25519.GenerateKey(rand.Reader)
+ if err != nil {
+ t.Fatalf("must generate key pair: %v", err)
+ }
+ copy(pub[:], b)
+
+ sign := func(sh uint64, msg merkle.Hash) *types.Signature {
+ stm := types.Statement{sh, *merkle.HashFn(msg[:])}
+ sig, err := stm.Sign(priv)
+ if err != nil {
+ t.Fatalf("must sign: %v", err)
+ }
+ return sig
+ }
+ input := func(sh uint64, msg merkle.Hash, badSig bool) io.Reader {
+ sig := sign(sh, msg)[:]
+ if badSig {
+ msg[0] += 1 // use a different message
+ }
+ str := fmt.Sprintf("shard_hint=%d\n", sh)
+ str += fmt.Sprintf("message=%x\n", msg[:])
+ str += fmt.Sprintf("signature=%x\n", sig[:])
+ str += fmt.Sprintf("public_key=%x\n", pub[:])
+ str += fmt.Sprintf("domain_hint=%s\n", dh)
+ return bytes.NewBufferString(str)
+ }
+
+ for _, table := range []struct {
+ desc string
+ params io.Reader
+ dnsExpect bool
+ dnsErr error
+ wantRsp *sigsumreq.Leaf
+ }{
+ {"invalid: parse ascii", bytes.NewBufferString("a=b"), false, nil, nil},
+ {"invalid: signature", input(st, msg, true), false, nil, nil},
+ {"invalid: shard start", input(st-1, msg, false), false, nil, nil},
+ {"invalid: shard end", input(uint64(time.Now().Unix())+1024, msg, false), false, nil, nil},
+ {"invalid: mocked dns error", input(st, msg, false), true, fmt.Errorf("mocked dns error"), nil},
+ {"valid", input(st, msg, false), true, nil, &sigsumreq.Leaf{st, msg, *sign(st, msg), pub, dh}},
+ } {
+ func() {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+ vf := mocksDNS.NewMockVerifier(ctrl)
+ if table.dnsExpect {
+ vf.EXPECT().Verify(gomock.Any(), gomock.Any(), gomock.Any()).Return(table.dnsErr)
+ }
+
+ url := types.EndpointAddLeaf.Path("http://example.org/sigsum/v0")
+ req, err := http.NewRequest(http.MethodPost, url, table.params)
+ if err != nil {
+ t.Fatalf("must create http request: %v", err)
+ }
+
+ parsedReq, err := LeafRequestFromHTTP(req, st, context.Background(), vf)
+ if got, want := err != nil, table.desc != "valid"; got != want {
+ t.Errorf("%s: got error %v but wanted %v: %v", table.desc, got, want, err)
+ }
+ if err != nil {
+ return
+ }
+ if got, want := parsedReq, table.wantRsp; !reflect.DeepEqual(got, want) {
+ t.Errorf("%s: got request %v but wanted %v", table.desc, got, want)
+ }
+ }()
+ }
+}
+
+func TestCosignatureRequestFromHTTP(t *testing.T) {
+ input := func(h merkle.Hash) io.Reader {
+ return bytes.NewBufferString(fmt.Sprintf("cosignature=%x\nkey_hash=%x\n", types.Signature{}, h))
+ }
+ w := map[merkle.Hash]types.PublicKey{
+ *merkle.HashFn([]byte("w1")): types.PublicKey{},
+ }
+ for _, table := range []struct {
+ desc string
+ params io.Reader
+ wantRsp *sigsumreq.Cosignature
+ }{
+ {"invalid: parser error", bytes.NewBufferString("abcd"), nil},
+ {"invalid: unknown witness", input(*merkle.HashFn([]byte("w2"))), nil},
+ {"valid", input(*merkle.HashFn([]byte("w1"))), &sigsumreq.Cosignature{types.Signature{}, *merkle.HashFn([]byte("w1"))}},
+ } {
+ url := types.EndpointAddCosignature.Path("http://example.org/sigsum/v0")
+ req, err := http.NewRequest(http.MethodPost, url, table.params)
+ if err != nil {
+ t.Fatalf("must create http request: %v", err)
+ }
+
+ parsedReq, err := CosignatureRequestFromHTTP(req, w)
+ if got, want := err != nil, table.desc != "valid"; got != want {
+ t.Errorf("%s: got error %v but wanted %v: %v", table.desc, got, want, err)
+ }
+ if err != nil {
+ continue
+ }
+ if got, want := parsedReq, table.wantRsp; !reflect.DeepEqual(got, want) {
+ t.Errorf("%s: got request %v but wanted %v", table.desc, got, want)
+ }
+ }
+}
+
+func TestConsistencyProofRequestFromHTTP(t *testing.T) {
+ for _, table := range []struct {
+ desc string
+ params string
+ wantRsp *sigsumreq.ConsistencyProof
+ }{
+ {"invalid: bad request (parser error)", "a/1", nil},
+ {"invalid: bad request (out of range 1/2)", "0/1", nil},
+ {"invalid: bad request (out of range 2/2)", "1/1", nil},
+ {"valid", "1/2", &sigsumreq.ConsistencyProof{1, 2}},
+ } {
+ url := types.EndpointGetConsistencyProof.Path("http://example.org/sigsum/v0/")
+ req, err := http.NewRequest(http.MethodGet, url+table.params, nil)
+ if err != nil {
+ t.Fatalf("must create http request: %v", err)
+ }
+
+ parsedReq, err := ConsistencyProofRequestFromHTTP(req)
+ if got, want := err != nil, table.desc != "valid"; got != want {
+ t.Errorf("%s: got error %v but wanted %v: %v", table.desc, got, want, err)
+ }
+ if err != nil {
+ continue
+ }
+ if got, want := parsedReq, table.wantRsp; !reflect.DeepEqual(got, want) {
+ t.Errorf("%s: got request %v but wanted %v", table.desc, got, want)
+ }
+ }
+}
+
+func TestInclusionProofRequestFromHTTP(t *testing.T) {
+ for _, table := range []struct {
+ desc string
+ params string
+ wantRsp *sigsumreq.InclusionProof
+ }{
+ {"invalid: bad request (parser error)", "a/0000000000000000000000000000000000000000000000000000000000000000", nil},
+ {"invalid: bad request (out of range)", "1/0000000000000000000000000000000000000000000000000000000000000000", nil},
+ {"valid", "2/0000000000000000000000000000000000000000000000000000000000000000", &sigsumreq.InclusionProof{2, merkle.Hash{}}},
+ } {
+ url := types.EndpointGetInclusionProof.Path("http://example.org/sigsum/v0/")
+ req, err := http.NewRequest(http.MethodGet, url+table.params, nil)
+ if err != nil {
+ t.Fatalf("must create http request: %v", err)
+ }
+
+ parsedReq, err := InclusionProofRequestFromHTTP(req)
+ if got, want := err != nil, table.desc != "valid"; got != want {
+ t.Errorf("%s: got error %v but wanted %v: %v", table.desc, got, want, err)
+ }
+ if err != nil {
+ continue
+ }
+ if got, want := parsedReq, table.wantRsp; !reflect.DeepEqual(got, want) {
+ t.Errorf("%s: got request %v but wanted %v", table.desc, got, want)
+ }
+ }
+}
+
+func TestGetLeaves(t *testing.T) {
+ maxRange := uint64(10)
+ for _, table := range []struct {
+ desc string
+ params string
+ wantRsp *sigsumreq.Leaves
+ }{
+ {"invalid: bad request (parser error)", "a/1", nil},
+ {"invalid: bad request (StartSize > EndSize)", "1/0", nil},
+ {"valid", "0/10", &sigsumreq.Leaves{0, maxRange - 1}},
+ } {
+ url := types.EndpointGetLeaves.Path("http://example.org/sigsum/v0/")
+ req, err := http.NewRequest(http.MethodGet, url+table.params, nil)
+ if err != nil {
+ t.Fatalf("must create http request: %v", err)
+ }
+
+ parsedReq, err := LeavesRequestFromHTTP(req, maxRange)
+ if got, want := err != nil, table.desc != "valid"; got != want {
+ t.Errorf("%s: got error %v but wanted %v: %v", table.desc, got, want, err)
+ }
+ if err != nil {
+ continue
+ }
+ if got, want := parsedReq, table.wantRsp; !reflect.DeepEqual(got, want) {
+ t.Errorf("%s: got request %v but wanted %v", table.desc, got, want)
+ }
+ }
+}
diff --git a/internal/state/single.go b/internal/state/single.go
new file mode 100644
index 0000000..fd73b3f
--- /dev/null
+++ b/internal/state/single.go
@@ -0,0 +1,265 @@
+package state
+
+import (
+ "context"
+ "crypto"
+ "crypto/ed25519"
+ "fmt"
+ "sync"
+ "time"
+
+ "git.sigsum.org/log-go/internal/db"
+ "git.sigsum.org/sigsum-go/pkg/client"
+ "git.sigsum.org/sigsum-go/pkg/log"
+ "git.sigsum.org/sigsum-go/pkg/merkle"
+ "git.sigsum.org/sigsum-go/pkg/requests"
+ "git.sigsum.org/sigsum-go/pkg/types"
+)
+
+// StateManagerSingle implements a single-instance StateManagerPrimary for primary nodes
+type StateManagerSingle struct {
+ client db.Client
+ signer crypto.Signer
+ namespace merkle.Hash
+ interval time.Duration
+ deadline time.Duration
+ secondary client.Client
+
+ // Lock-protected access to pointers. A write lock is only obtained once
+ // per interval when doing pointer rotation. All endpoints are readers.
+ sync.RWMutex
+ signedTreeHead *types.SignedTreeHead
+ cosignedTreeHead *types.CosignedTreeHead
+
+ // Syncronized and deduplicated witness cosignatures for signedTreeHead
+ events chan *event
+ cosignatures map[merkle.Hash]*types.Signature
+}
+
+// NewStateManagerSingle() sets up a new state manager, in particular its
+// signedTreeHead. An optional secondary node can be used to ensure that
+// a newer primary tree is not signed unless it has been replicated.
+func NewStateManagerSingle(dbcli db.Client, signer crypto.Signer, interval, deadline time.Duration, secondary client.Client) (*StateManagerSingle, error) {
+ sm := &StateManagerSingle{
+ client: dbcli,
+ signer: signer,
+ namespace: *merkle.HashFn(signer.Public().(ed25519.PublicKey)),
+ interval: interval,
+ deadline: deadline,
+ secondary: secondary,
+ }
+ sth, err := sm.restoreTreeHead()
+ if err != nil {
+ return nil, fmt.Errorf("restore signed tree head: %v", err)
+ }
+ sm.signedTreeHead = sth
+
+ ictx, cancel := context.WithTimeout(context.Background(), sm.deadline)
+ defer cancel()
+ return sm, sm.tryRotate(ictx)
+}
+
+func (sm *StateManagerSingle) ToCosignTreeHead() *types.SignedTreeHead {
+ sm.RLock()
+ defer sm.RUnlock()
+ return sm.signedTreeHead
+}
+
+func (sm *StateManagerSingle) CosignedTreeHead(_ context.Context) (*types.CosignedTreeHead, error) {
+ sm.RLock()
+ defer sm.RUnlock()
+ if sm.cosignedTreeHead == nil {
+ return nil, fmt.Errorf("no cosignatures available")
+ }
+ return sm.cosignedTreeHead, nil
+}
+
+func (sm *StateManagerSingle) AddCosignature(ctx context.Context, pub *types.PublicKey, sig *types.Signature) error {
+ sm.RLock()
+ defer sm.RUnlock()
+
+ msg := sm.signedTreeHead.TreeHead.ToBinary(&sm.namespace)
+ if !ed25519.Verify(ed25519.PublicKey(pub[:]), msg, sig[:]) {
+ return fmt.Errorf("invalid cosignature")
+ }
+ select {
+ case sm.events <- &event{merkle.HashFn(pub[:]), sig}:
+ return nil
+ case <-ctx.Done():
+ return fmt.Errorf("request timeout")
+ }
+}
+
+func (sm *StateManagerSingle) Run(ctx context.Context) {
+ sm.events = make(chan *event, 4096)
+ defer close(sm.events)
+ ticker := time.NewTicker(sm.interval)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case <-ticker.C:
+ ictx, cancel := context.WithTimeout(ctx, sm.deadline)
+ defer cancel()
+ if err := sm.tryRotate(ictx); err != nil {
+ log.Warning("failed rotating tree heads: %v", err)
+ }
+ case ev := <-sm.events:
+ sm.handleEvent(ev)
+ case <-ctx.Done():
+ return
+ }
+ }
+}
+
+func (sm *StateManagerSingle) tryRotate(ctx context.Context) error {
+ th, err := sm.client.GetTreeHead(ctx)
+ if err != nil {
+ return fmt.Errorf("get tree head: %v", err)
+ }
+ nextSTH, err := sm.chooseTree(ctx, th).Sign(sm.signer, &sm.namespace)
+ if err != nil {
+ return fmt.Errorf("sign tree head: %v", err)
+ }
+ log.Debug("wanted to advance to size %d, chose size %d", th.TreeSize, nextSTH.TreeSize)
+
+ sm.rotate(nextSTH)
+ return nil
+}
+
+// chooseTree picks a tree to publish, taking the state of a possible secondary node into account.
+func (sm *StateManagerSingle) chooseTree(ctx context.Context, proposedTreeHead *types.TreeHead) *types.TreeHead {
+ if !sm.secondary.Initiated() {
+ return proposedTreeHead
+ }
+
+ secSTH, err := sm.secondary.GetToCosignTreeHead(ctx)
+ if err != nil {
+ log.Warning("failed fetching tree head from secondary: %v", err)
+ return refreshTreeHead(sm.signedTreeHead.TreeHead)
+ }
+ if secSTH.TreeSize > proposedTreeHead.TreeSize {
+ log.Error("secondary is ahead of us: %d > %d", secSTH.TreeSize, proposedTreeHead.TreeSize)
+ return refreshTreeHead(sm.signedTreeHead.TreeHead)
+ }
+
+ if secSTH.TreeSize == proposedTreeHead.TreeSize {
+ if secSTH.RootHash != proposedTreeHead.RootHash {
+ log.Error("secondary root hash doesn't match our root hash at tree size %d", secSTH.TreeSize)
+ return refreshTreeHead(sm.signedTreeHead.TreeHead)
+ }
+ log.Debug("secondary is up-to-date with matching tree head, using proposed tree, size %d", proposedTreeHead.TreeSize)
+ return proposedTreeHead
+ }
+ //
+ // Now we know that the proposed tree size is larger than the secondary's tree size.
+ // We also now that the secondary's minimum tree size is 0.
+ // This means that the proposed tree size is at least 1.
+ //
+ // Case 1: secondary tree size is 0, primary tree size is >0 --> return based on what we signed before
+ // Case 2: secondary tree size is 1, primary tree size is >1 --> fetch consistency proof, if ok ->
+ // 2a) secondary tree size is smaller than or equal to what we than signed before -> return whatever we signed before
+ // 2b) secondary tree size is larger than what we signed before -> return secondary tree head
+ //
+ // (If not ok in case 2, return based on what we signed before)
+ //
+ if secSTH.TreeSize == 0 {
+ return refreshTreeHead(sm.signedTreeHead.TreeHead)
+ }
+ if err := sm.verifyConsistencyWithLatest(ctx, secSTH.TreeHead); err != nil {
+ log.Error("secondaries tree not consistent with ours: %v", err)
+ return refreshTreeHead(sm.signedTreeHead.TreeHead)
+ }
+ if secSTH.TreeSize <= sm.signedTreeHead.TreeSize {
+ log.Warning("secondary is behind what primary already signed: %d <= %d", secSTH.TreeSize, sm.signedTreeHead.TreeSize)
+ return refreshTreeHead(sm.signedTreeHead.TreeHead)
+ }
+
+ log.Debug("using latest tree head from secondary: size %d", secSTH.TreeSize)
+ return refreshTreeHead(secSTH.TreeHead)
+}
+
+func (sm *StateManagerSingle) verifyConsistencyWithLatest(ctx context.Context, to types.TreeHead) error {
+ from := sm.signedTreeHead.TreeHead
+ req := &requests.ConsistencyProof{
+ OldSize: from.TreeSize,
+ NewSize: to.TreeSize,
+ }
+ proof, err := sm.client.GetConsistencyProof(ctx, req)
+ if err != nil {
+ return fmt.Errorf("unable to get consistency proof from %d to %d: %w", req.OldSize, req.NewSize, err)
+ }
+ if err := proof.Verify(&from.RootHash, &to.RootHash); err != nil {
+ return fmt.Errorf("invalid consistency proof from %d to %d: %v", req.OldSize, req.NewSize, err)
+ }
+ log.Debug("consistency proof from %d to %d verified", req.OldSize, req.NewSize)
+ return nil
+}
+
+func (sm *StateManagerSingle) rotate(nextSTH *types.SignedTreeHead) {
+ sm.Lock()
+ defer sm.Unlock()
+
+ log.Debug("about to rotate tree heads, next at %d: %s", nextSTH.TreeSize, sm.treeStatusString())
+ sm.handleEvents()
+ sm.setCosignedTreeHead()
+ sm.setToCosignTreeHead(nextSTH)
+ log.Debug("tree heads rotated: %s", sm.treeStatusString())
+}
+
+func (sm *StateManagerSingle) handleEvents() {
+ log.Debug("handling any outstanding events")
+ for i, n := 0, len(sm.events); i < n; i++ {
+ sm.handleEvent(<-sm.events)
+ }
+}
+
+func (sm *StateManagerSingle) handleEvent(ev *event) {
+ log.Debug("handling event from witness %x", ev.keyHash[:])
+ sm.cosignatures[*ev.keyHash] = ev.cosignature
+}
+
+func (sm *StateManagerSingle) setCosignedTreeHead() {
+ n := len(sm.cosignatures)
+ if n == 0 {
+ sm.cosignedTreeHead = nil
+ return
+ }
+
+ var cth types.CosignedTreeHead
+ cth.SignedTreeHead = *sm.signedTreeHead
+ cth.Cosignature = make([]types.Signature, 0, n)
+ cth.KeyHash = make([]merkle.Hash, 0, n)
+ for keyHash, cosignature := range sm.cosignatures {
+ cth.KeyHash = append(cth.KeyHash, keyHash)
+ cth.Cosignature = append(cth.Cosignature, *cosignature)
+ }
+ sm.cosignedTreeHead = &cth
+}
+
+func (sm *StateManagerSingle) setToCosignTreeHead(nextSTH *types.SignedTreeHead) {
+ sm.cosignatures = make(map[merkle.Hash]*types.Signature)
+ sm.signedTreeHead = nextSTH
+}
+
+func (sm *StateManagerSingle) treeStatusString() string {
+ var cosigned uint64
+ if sm.cosignedTreeHead != nil {
+ cosigned = sm.cosignedTreeHead.TreeSize
+ }
+ return fmt.Sprintf("signed at %d, cosigned at %d", sm.signedTreeHead.TreeSize, cosigned)
+}
+
+func (sm *StateManagerSingle) restoreTreeHead() (*types.SignedTreeHead, error) {
+ th := zeroTreeHead() // TODO: restore from disk, stored when advanced the tree; zero tree head if "bootstrap"
+ return refreshTreeHead(*th).Sign(sm.signer, &sm.namespace)
+}
+
+func zeroTreeHead() *types.TreeHead {
+ return refreshTreeHead(types.TreeHead{RootHash: *merkle.HashFn([]byte(""))})
+}
+
+func refreshTreeHead(th types.TreeHead) *types.TreeHead {
+ th.Timestamp = uint64(time.Now().Unix())
+ return &th
+}
diff --git a/internal/state/single_test.go b/internal/state/single_test.go
new file mode 100644
index 0000000..9442fdc
--- /dev/null
+++ b/internal/state/single_test.go
@@ -0,0 +1,233 @@
+package state
+
+import (
+ "bytes"
+ "context"
+ "crypto"
+ "crypto/ed25519"
+ "crypto/rand"
+ "fmt"
+ "io"
+ "reflect"
+ "testing"
+ "time"
+
+ mocksClient "git.sigsum.org/log-go/internal/mocks/client"
+ mocksDB "git.sigsum.org/log-go/internal/mocks/db"
+ "git.sigsum.org/sigsum-go/pkg/hex"
+ "git.sigsum.org/sigsum-go/pkg/merkle"
+ "git.sigsum.org/sigsum-go/pkg/types"
+ "github.com/golang/mock/gomock"
+)
+
+// TestSigner implements the signer interface. It can be used to mock
+// an Ed25519 signer that always return the same public key,
+// signature, and error.
+// NOTE: Code duplication with internal/node/secondary/endpoint_internal_test.go
+type TestSigner struct {
+ PublicKey [ed25519.PublicKeySize]byte
+ Signature [ed25519.SignatureSize]byte
+ Error error
+}
+
+func (ts *TestSigner) Public() crypto.PublicKey {
+ return ed25519.PublicKey(ts.PublicKey[:])
+}
+
+func (ts *TestSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
+ return ts.Signature[:], ts.Error
+}
+
+func TestNewStateManagerSingle(t *testing.T) {
+ signerOk := &TestSigner{types.PublicKey{}, types.Signature{}, nil}
+ signerErr := &TestSigner{types.PublicKey{}, types.Signature{}, fmt.Errorf("err")}
+ for _, table := range []struct {
+ description string
+ signer crypto.Signer
+ thExp bool
+ thErr error
+ th types.TreeHead
+ secExp bool
+ wantErr bool
+ }{
+ {"invalid: signer failure", signerErr, false, nil, types.TreeHead{}, false, true},
+ {"valid", signerOk, true, nil, types.TreeHead{Timestamp: now(t)}, true, false},
+ } {
+ func() {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+ trillianClient := mocksDB.NewMockClient(ctrl)
+ if table.thExp {
+ trillianClient.EXPECT().GetTreeHead(gomock.Any()).Return(&table.th, table.thErr)
+ }
+ secondary := mocksClient.NewMockClient(ctrl)
+ if table.secExp {
+ secondary.EXPECT().Initiated().Return(false)
+ }
+
+ sm, err := NewStateManagerSingle(trillianClient, table.signer, time.Duration(0), time.Duration(0), secondary)
+ if got, want := err != nil, table.description != "valid"; got != want {
+ t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err)
+ }
+ if err != nil {
+ return
+ }
+
+ if got, want := sm.signedTreeHead.TreeSize, table.th.TreeSize; got != want {
+ t.Errorf("%q: got tree size %d but wanted %d", table.description, got, want)
+ }
+ if got, want := sm.signedTreeHead.RootHash[:], table.th.RootHash[:]; !bytes.Equal(got, want) {
+ t.Errorf("%q: got tree size %v but wanted %v", table.description, got, want)
+ }
+ if got, want := sm.signedTreeHead.Timestamp, table.th.Timestamp; got < want {
+ t.Errorf("%q: got timestamp %d but wanted at least %d", table.description, got, want)
+ }
+ if got := sm.cosignedTreeHead; got != nil {
+ t.Errorf("%q: got cosigned tree head but should have none", table.description)
+ }
+ }()
+ }
+}
+
+func TestToCosignTreeHead(t *testing.T) {
+ want := &types.SignedTreeHead{}
+ sm := StateManagerSingle{
+ signedTreeHead: want,
+ }
+ sth := sm.ToCosignTreeHead()
+ if got := sth; !reflect.DeepEqual(got, want) {
+ t.Errorf("got signed tree head\n\t%v\nbut wanted\n\t%v", got, want)
+ }
+}
+
+func TestCosignedTreeHead(t *testing.T) {
+ want := &types.CosignedTreeHead{
+ Cosignature: make([]types.Signature, 1),
+ KeyHash: make([]merkle.Hash, 1),
+ }
+ sm := StateManagerSingle{
+ cosignedTreeHead: want,
+ }
+ cth, err := sm.CosignedTreeHead(context.Background())
+ if err != nil {
+ t.Errorf("should not fail with error: %v", err)
+ return
+ }
+ if got := cth; !reflect.DeepEqual(got, want) {
+ t.Errorf("got cosigned tree head\n\t%v\nbut wanted\n\t%v", got, want)
+ }
+
+ sm.cosignedTreeHead = nil
+ cth, err = sm.CosignedTreeHead(context.Background())
+ if err == nil {
+ t.Errorf("should fail without a cosigned tree head")
+ return
+ }
+}
+
+func TestAddCosignature(t *testing.T) {
+ secret, public := mustKeyPair(t)
+ for _, table := range []struct {
+ desc string
+ signer crypto.Signer
+ vk types.PublicKey
+ wantErr bool
+ }{
+ {
+ desc: "invalid: wrong public key",
+ signer: secret,
+ vk: types.PublicKey{},
+ wantErr: true,
+ },
+ {
+ desc: "valid",
+ signer: secret,
+ vk: public,
+ },
+ } {
+ sm := &StateManagerSingle{
+ namespace: *merkle.HashFn(nil),
+ signedTreeHead: &types.SignedTreeHead{},
+ events: make(chan *event, 1),
+ }
+ defer close(sm.events)
+
+ sth := mustSign(t, table.signer, &sm.signedTreeHead.TreeHead, &sm.namespace)
+ ctx := context.Background()
+ err := sm.AddCosignature(ctx, &table.vk, &sth.Signature)
+ if got, want := err != nil, table.wantErr; got != want {
+ t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.desc, err)
+ }
+ if err != nil {
+ continue
+ }
+
+ ctx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
+ defer cancel()
+ if err := sm.AddCosignature(ctx, &table.vk, &sth.Signature); err == nil {
+ t.Errorf("expected full channel in test %q", table.desc)
+ }
+ if got, want := len(sm.events), 1; got != want {
+ t.Errorf("wanted %d cosignatures but got %d in test %q", want, got, table.desc)
+ }
+ }
+}
+
+func mustKeyPair(t *testing.T) (crypto.Signer, types.PublicKey) {
+ t.Helper()
+ vk, sk, err := ed25519.GenerateKey(rand.Reader)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var pub types.PublicKey
+ copy(pub[:], vk[:])
+ return sk, pub
+}
+
+func mustSign(t *testing.T, s crypto.Signer, th *types.TreeHead, kh *merkle.Hash) *types.SignedTreeHead {
+ t.Helper()
+ sth, err := th.Sign(s, kh)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return sth
+}
+
+func newHashBufferInc(t *testing.T) *merkle.Hash {
+ t.Helper()
+
+ var buf merkle.Hash
+ for i := 0; i < len(buf); i++ {
+ buf[i] = byte(i)
+ }
+ return &buf
+}
+func validConsistencyProof_5_10(t *testing.T) *types.ConsistencyProof {
+ t.Helper()
+ // # old tree head
+ // tree_size=5
+ // root_hash=c8e73a8c09e44c344d515eb717e248c5dbf12420908a6d29568197fae7751803
+ // # new tree head
+ // tree_size=10
+ // root_hash=2a40f11563b45522ca9eccf993c934238a8fbadcf7d7d65be3583ab2584838aa
+ r := bytes.NewReader([]byte("consistency_path=fadca95ab8ca34f17c5f3fa719183fe0e5c194a44c25324745388964a743ecce\nconsistency_path=6366fc0c20f9b8a8c089ed210191e401da6c995592eba78125f0ba0ba142ebaf\nconsistency_path=72b8d4f990b555a72d76fb8da075a65234519070cfa42e082026a8c686160349\nconsistency_path=d92714be792598ff55560298cd3ff099dfe5724646282578531c0d0063437c00\nconsistency_path=4b20d58bbae723755304fb179aef6d5f04d755a601884828c62c07929f6bd84a\n"))
+ var proof types.ConsistencyProof
+ if err := proof.FromASCII(r, 5, 10); err != nil {
+ t.Fatal(err)
+ }
+ return &proof
+}
+
+func hashFromString(t *testing.T, s string) (h merkle.Hash) {
+ b, err := hex.Deserialize(s)
+ if err != nil {
+ t.Fatal(err)
+ }
+ copy(h[:], b)
+ return h
+}
+
+func now(t *testing.T) uint64 {
+ t.Helper()
+ return uint64(time.Now().Unix())
+}
diff --git a/pkg/state/state_manager.go b/internal/state/state_manager.go
index 9533479..60d2af1 100644
--- a/pkg/state/state_manager.go
+++ b/internal/state/state_manager.go
@@ -3,27 +3,28 @@ package state
import (
"context"
+ "git.sigsum.org/sigsum-go/pkg/merkle"
"git.sigsum.org/sigsum-go/pkg/types"
)
-// StateManager coordinates access to a log's tree heads and (co)signatures.
+// StateManager coordinates access to a nodes tree heads and (co)signatures.
type StateManager interface {
- // ToCosignTreeHead returns the log's to-cosign tree head
- ToCosignTreeHead(context.Context) (*types.SignedTreeHead, error)
+ // ToCosignTreeHead returns the node's to-cosign tree head
+ ToCosignTreeHead() *types.SignedTreeHead
- // CosignedTreeHead returns the log's cosigned tree head
+ // CosignedTreeHead returns the node's cosigned tree head
CosignedTreeHead(context.Context) (*types.CosignedTreeHead, error)
// AddCosignature verifies that a cosignature is valid for the to-cosign
// tree head before adding it
AddCosignature(context.Context, *types.PublicKey, *types.Signature) error
- // Run peridically rotates the log's to-cosign and cosigned tree heads
+ // Run peridically rotates the node's to-cosign and cosigned tree heads
Run(context.Context)
}
// event is a verified cosignature request
type event struct {
- keyHash *types.Hash
+ keyHash *merkle.Hash
cosignature *types.Signature
}
diff --git a/internal/utils/utils.go b/internal/utils/utils.go
new file mode 100644
index 0000000..a453107
--- /dev/null
+++ b/internal/utils/utils.go
@@ -0,0 +1,69 @@
+package utils
+
+import (
+ "crypto"
+ "crypto/ed25519"
+ "encoding/hex"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "strings"
+
+ "git.sigsum.org/sigsum-go/pkg/log"
+ "git.sigsum.org/sigsum-go/pkg/types"
+)
+
+// TODO: Move SetupLogging to sigsum-go/pkg/log
+
+func SetupLogging(logFile, logLevel string, logColor bool) error {
+ if len(logFile) != 0 {
+ f, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
+ if err != nil {
+ return err
+ }
+ log.SetOutput(f)
+ }
+
+ switch logLevel {
+ case "debug":
+ log.SetLevel(log.DebugLevel)
+ case "info":
+ log.SetLevel(log.InfoLevel)
+ case "warning":
+ log.SetLevel(log.WarningLevel)
+ case "error":
+ log.SetLevel(log.ErrorLevel)
+ default:
+ return fmt.Errorf("invalid logging level %s", logLevel)
+ }
+
+ log.SetColor(logColor)
+ return nil
+}
+
+func PubkeyFromHexString(pkhex string) (*types.PublicKey, error) {
+ pkbuf, err := hex.DecodeString(pkhex)
+ if err != nil {
+ return nil, fmt.Errorf("DecodeString: %v", err)
+ }
+
+ var pk types.PublicKey
+ if n := copy(pk[:], pkbuf); n != types.PublicKeySize {
+ return nil, fmt.Errorf("invalid pubkey size: %v", n)
+ }
+
+ return &pk, nil
+}
+
+func NewLogIdentity(keyFile string) (crypto.Signer, string, error) {
+ buf, err := ioutil.ReadFile(keyFile)
+ if err != nil {
+ return nil, "", err
+ }
+ if buf, err = hex.DecodeString(strings.TrimSpace(string(buf))); err != nil {
+ return nil, "", fmt.Errorf("DecodeString: %v", err)
+ }
+ sk := crypto.Signer(ed25519.NewKeyFromSeed(buf))
+ vk := sk.Public().(ed25519.PublicKey)
+ return sk, hex.EncodeToString([]byte(vk[:])), nil
+}
diff --git a/pkg/instance/experimental.go b/pkg/instance/experimental.go
deleted file mode 100644
index 24feeaf..0000000
--- a/pkg/instance/experimental.go
+++ /dev/null
@@ -1,85 +0,0 @@
-package instance
-
-import (
- "bytes"
- "context"
- "crypto"
- "crypto/ed25519"
- "crypto/sha256"
- "encoding/base64"
- "encoding/binary"
- "fmt"
- "net/http"
-
- "git.sigsum.org/sigsum-go/pkg/log"
- "git.sigsum.org/sigsum-go/pkg/types"
-)
-
-// algEd25519 identifies a checkpoint signature algorithm
-const algEd25519 byte = 1
-
-// getCheckpoint is an experimental endpoint that is not part of the official
-// Sigsum API. Documentation can be found in the transparency-dev repo.
-func getCheckpoint(ctx context.Context, i *Instance, w http.ResponseWriter, r *http.Request) (int, error) {
- log.Debug("handling get-checkpoint request")
- sth, err := i.Stateman.ToCosignTreeHead(ctx)
- if err != nil {
- return http.StatusInternalServerError, err
- }
- if err := i.signWriteNote(w, sth); err != nil {
- return http.StatusInternalServerError, err
- }
- return http.StatusOK, nil
-}
-
-// signWriteNote signs and writes a checkpoint which uses "sigsum.org:<prefix>"
-// as origin string. Origin string is also used as ID in the note signature.
-// This means that a sigsum log's prefix (say, "glass-frog"), must be unique.
-func (i *Instance) signWriteNote(w http.ResponseWriter, sth *types.SignedTreeHead) error {
- origin := fmt.Sprintf("sigsum.org:%s", i.Prefix)
- msg := fmt.Sprintf("%s\n%d\n%s\n",
- origin,
- sth.TreeSize,
- base64.StdEncoding.EncodeToString(sth.RootHash[:]),
- )
- sig, err := noteSign(i.Signer, origin, msg)
- if err != nil {
- return err
- }
-
- fmt.Fprintf(w, "%s\n\u2014 %s %s\n", msg, origin, sig)
- return nil
-}
-
-// noteSign returns a note signature for the provided origin and message
-func noteSign(signer crypto.Signer, origin, msg string) (string, error) {
- sig, err := signer.Sign(nil, []byte(msg), crypto.Hash(0))
- if err != nil {
- return "", err
- }
-
- var hbuf [4]byte
- binary.BigEndian.PutUint32(hbuf[:], noteKeyHash(origin, notePubKeyEd25519(signer)))
- sig = append(hbuf[:], sig...)
- return base64.StdEncoding.EncodeToString(sig), nil
-}
-
-// See:
-// https://cs.opensource.google/go/x/mod/+/refs/tags/v0.5.1:sumdb/note/note.go;l=336
-func notePubKeyEd25519(signer crypto.Signer) []byte {
- return bytes.Join([][]byte{
- []byte{algEd25519},
- signer.Public().(ed25519.PublicKey),
- }, nil)
-}
-
-// Source:
-// https://cs.opensource.google/go/x/mod/+/refs/tags/v0.5.1:sumdb/note/note.go;l=222
-func noteKeyHash(name string, key []byte) uint32 {
- h := sha256.New()
- h.Write([]byte(name))
- h.Write([]byte("\n"))
- h.Write(key)
- sum := h.Sum(nil)
- return binary.BigEndian.Uint32(sum)
-}
diff --git a/pkg/instance/handler.go b/pkg/instance/handler.go
deleted file mode 100644
index fa465ee..0000000
--- a/pkg/instance/handler.go
+++ /dev/null
@@ -1,184 +0,0 @@
-package instance
-
-import (
- "context"
- "fmt"
- "net/http"
- "time"
-
- "git.sigsum.org/sigsum-go/pkg/log"
- "git.sigsum.org/sigsum-go/pkg/types"
-)
-
-// Handler implements the http.Handler interface, and contains a reference
-// to a sigsum server instance as well as a function that uses it.
-type Handler struct {
- Instance *Instance
- Endpoint types.Endpoint
- Method string
- Handler func(context.Context, *Instance, http.ResponseWriter, *http.Request) (int, error)
-}
-
-// Path returns a path that should be configured for this handler
-func (h Handler) Path() string {
- if len(h.Instance.Prefix) == 0 {
- return h.Endpoint.Path("", "sigsum", "v0")
- }
- return h.Endpoint.Path("", h.Instance.Prefix, "sigsum", "v0")
-}
-
-// ServeHTTP is part of the http.Handler interface
-func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- start := time.Now()
- code := 0
- defer func() {
- end := time.Now().Sub(start).Seconds()
- sc := fmt.Sprintf("%d", code)
-
- rspcnt.Inc(h.Instance.LogID, string(h.Endpoint), sc)
- latency.Observe(end, h.Instance.LogID, string(h.Endpoint), sc)
- }()
- reqcnt.Inc(h.Instance.LogID, string(h.Endpoint))
-
- code = h.verifyMethod(w, r)
- if code != 0 {
- return
- }
- code = h.handle(w, r)
-}
-
-// verifyMethod checks that an appropriate HTTP method is used. Error handling
-// is based on RFC 7231, see Sections 6.5.5 (Status 405) and 6.5.1 (Status 400).
-func (h *Handler) verifyMethod(w http.ResponseWriter, r *http.Request) int {
- if h.Method == r.Method {
- return 0
- }
-
- code := http.StatusBadRequest
- if ok := h.Instance.checkHTTPMethod(r.Method); ok {
- w.Header().Set("Allow", h.Method)
- code = http.StatusMethodNotAllowed
- }
-
- http.Error(w, fmt.Sprintf("error=%s", http.StatusText(code)), code)
- return code
-}
-
-// handle handles an HTTP request for which the HTTP method is already verified
-func (h Handler) handle(w http.ResponseWriter, r *http.Request) int {
- deadline := time.Now().Add(h.Instance.Deadline)
- ctx, cancel := context.WithDeadline(r.Context(), deadline)
- defer cancel()
-
- code, err := h.Handler(ctx, h.Instance, w, r)
- if err != nil {
- log.Debug("%s/%s: %v", h.Instance.Prefix, h.Endpoint, err)
- http.Error(w, fmt.Sprintf("error=%s", err.Error()), code)
- }
- return code
-}
-
-func addLeaf(ctx context.Context, i *Instance, w http.ResponseWriter, r *http.Request) (int, error) {
- log.Debug("handling add-leaf request")
- req, err := i.leafRequestFromHTTP(ctx, r)
- if err != nil {
- return http.StatusBadRequest, err
- }
- if err := i.Client.AddLeaf(ctx, req); err != nil {
- return http.StatusInternalServerError, err
- }
- return http.StatusOK, nil
-}
-
-func addCosignature(ctx context.Context, i *Instance, w http.ResponseWriter, r *http.Request) (int, error) {
- log.Debug("handling add-cosignature request")
- req, err := i.cosignatureRequestFromHTTP(r)
- if err != nil {
- return http.StatusBadRequest, err
- }
- vk := i.Witnesses[req.KeyHash]
- if err := i.Stateman.AddCosignature(ctx, &vk, &req.Cosignature); err != nil {
- return http.StatusBadRequest, err
- }
- return http.StatusOK, nil
-}
-
-func getTreeHeadToCosign(ctx context.Context, i *Instance, w http.ResponseWriter, _ *http.Request) (int, error) {
- log.Debug("handling get-tree-head-to-cosign request")
- sth, err := i.Stateman.ToCosignTreeHead(ctx)
- if err != nil {
- return http.StatusInternalServerError, err
- }
- if err := sth.ToASCII(w); err != nil {
- return http.StatusInternalServerError, err
- }
- return http.StatusOK, nil
-}
-
-func getTreeHeadCosigned(ctx context.Context, i *Instance, w http.ResponseWriter, _ *http.Request) (int, error) {
- log.Debug("handling get-tree-head-cosigned request")
- cth, err := i.Stateman.CosignedTreeHead(ctx)
- if err != nil {
- return http.StatusInternalServerError, err
- }
- if err := cth.ToASCII(w); err != nil {
- return http.StatusInternalServerError, err
- }
- return http.StatusOK, nil
-}
-
-func getConsistencyProof(ctx context.Context, i *Instance, w http.ResponseWriter, r *http.Request) (int, error) {
- log.Debug("handling get-consistency-proof request")
- req, err := i.consistencyProofRequestFromHTTP(r)
- if err != nil {
- return http.StatusBadRequest, err
- }
- // XXX: check tree size of latest thing we signed?
-
- proof, err := i.Client.GetConsistencyProof(ctx, req)
- if err != nil {
- return http.StatusInternalServerError, err
- }
- if err := proof.ToASCII(w); err != nil {
- return http.StatusInternalServerError, err
- }
- return http.StatusOK, nil
-}
-
-func getInclusionProof(ctx context.Context, i *Instance, w http.ResponseWriter, r *http.Request) (int, error) {
- log.Debug("handling get-inclusion-proof request")
- req, err := i.inclusionProofRequestFromHTTP(r)
- if err != nil {
- return http.StatusBadRequest, err
- }
- // XXX: check tree size of latest thing we signed?
-
- proof, err := i.Client.GetInclusionProof(ctx, req)
- if err != nil {
- return http.StatusInternalServerError, err
- }
- if err := proof.ToASCII(w); err != nil {
- return http.StatusInternalServerError, err
- }
- return http.StatusOK, nil
-}
-
-func getLeaves(ctx context.Context, i *Instance, w http.ResponseWriter, r *http.Request) (int, error) {
- log.Debug("handling get-leaves request")
- req, err := i.leavesRequestFromHTTP(r)
- if err != nil {
- return http.StatusBadRequest, err
- }
- // XXX: check tree size of latest thing we signed?
-
- leaves, err := i.Client.GetLeaves(ctx, req)
- if err != nil {
- return http.StatusInternalServerError, err
- }
- for _, leaf := range *leaves {
- if err := leaf.ToASCII(w); err != nil {
- return http.StatusInternalServerError, err
- }
- }
- return http.StatusOK, nil
-}
diff --git a/pkg/instance/instance.go b/pkg/instance/instance.go
deleted file mode 100644
index f4c0089..0000000
--- a/pkg/instance/instance.go
+++ /dev/null
@@ -1,135 +0,0 @@
-package instance
-
-import (
- "context"
- "crypto"
- "fmt"
- "net/http"
- "time"
-
- "git.sigsum.org/log-go/pkg/db"
- "git.sigsum.org/log-go/pkg/state"
- "git.sigsum.org/sigsum-go/pkg/dns"
- "git.sigsum.org/sigsum-go/pkg/requests"
- "git.sigsum.org/sigsum-go/pkg/types"
-)
-
-// Config is a collection of log parameters
-type Config struct {
- LogID string // H(public key), then hex-encoded
- TreeID int64 // Merkle tree identifier used by Trillian
- Prefix string // The portion between base URL and st/v0 (may be "")
- MaxRange int64 // Maximum number of leaves per get-leaves request
- Deadline time.Duration // Deadline used for gRPC requests
- Interval time.Duration // Cosigning frequency
- ShardStart uint64 // Shard interval start (num seconds since UNIX epoch)
-
- // Witnesses map trusted witness identifiers to public keys
- Witnesses map[types.Hash]types.PublicKey
-}
-
-// Instance is an instance of the log's front-end
-type Instance struct {
- Config // configuration parameters
- Client db.Client // provides access to the Trillian backend
- Signer crypto.Signer // provides access to Ed25519 private key
- Stateman state.StateManager // coordinates access to (co)signed tree heads
- DNS dns.Verifier // checks if domain name knows a public key
-}
-
-// Handlers returns a list of sigsum handlers
-func (i *Instance) Handlers() []Handler {
- return []Handler{
- Handler{Instance: i, Handler: addLeaf, Endpoint: types.EndpointAddLeaf, Method: http.MethodPost},
- Handler{Instance: i, Handler: addCosignature, Endpoint: types.EndpointAddCosignature, Method: http.MethodPost},
- Handler{Instance: i, Handler: getTreeHeadToCosign, Endpoint: types.EndpointGetTreeHeadToCosign, Method: http.MethodGet},
- Handler{Instance: i, Handler: getTreeHeadCosigned, Endpoint: types.EndpointGetTreeHeadCosigned, Method: http.MethodGet},
- Handler{Instance: i, Handler: getCheckpoint, Endpoint: types.Endpoint("get-checkpoint"), Method: http.MethodGet},
- Handler{Instance: i, Handler: getConsistencyProof, Endpoint: types.EndpointGetConsistencyProof, Method: http.MethodGet},
- Handler{Instance: i, Handler: getInclusionProof, Endpoint: types.EndpointGetInclusionProof, Method: http.MethodGet},
- Handler{Instance: i, Handler: getLeaves, Endpoint: types.EndpointGetLeaves, Method: http.MethodGet},
- }
-}
-
-// checkHTTPMethod checks if an HTTP method is supported
-func (i *Instance) checkHTTPMethod(m string) bool {
- return m == http.MethodGet || m == http.MethodPost
-}
-
-func (i *Instance) leafRequestFromHTTP(ctx context.Context, r *http.Request) (*requests.Leaf, error) {
- var req requests.Leaf
- if err := req.FromASCII(r.Body); err != nil {
- return nil, fmt.Errorf("FromASCII: %v", err)
- }
- stmt := types.Statement{
- ShardHint: req.ShardHint,
- Checksum: *types.HashFn(req.Message[:]),
- }
- if !stmt.Verify(&req.PublicKey, &req.Signature) {
- return nil, fmt.Errorf("invalid signature")
- }
- shardEnd := uint64(time.Now().Unix())
- if req.ShardHint < i.ShardStart {
- return nil, fmt.Errorf("invalid shard hint: %d not in [%d, %d]", req.ShardHint, i.ShardStart, shardEnd)
- }
- if req.ShardHint > shardEnd {
- return nil, fmt.Errorf("invalid shard hint: %d not in [%d, %d]", req.ShardHint, i.ShardStart, shardEnd)
- }
- if err := i.DNS.Verify(ctx, req.DomainHint, &req.PublicKey); err != nil {
- return nil, fmt.Errorf("invalid domain hint: %v", err)
- }
- return &req, nil
-}
-
-func (i *Instance) cosignatureRequestFromHTTP(r *http.Request) (*requests.Cosignature, error) {
- var req requests.Cosignature
- if err := req.FromASCII(r.Body); err != nil {
- return nil, fmt.Errorf("FromASCII: %v", err)
- }
- if _, ok := i.Witnesses[req.KeyHash]; !ok {
- return nil, fmt.Errorf("Unknown witness: %x", req.KeyHash)
- }
- return &req, nil
-}
-
-func (i *Instance) consistencyProofRequestFromHTTP(r *http.Request) (*requests.ConsistencyProof, error) {
- var req requests.ConsistencyProof
- if err := req.FromURL(r.URL.Path); err != nil {
- return nil, fmt.Errorf("FromASCII: %v", err)
- }
- if req.OldSize < 1 {
- return nil, fmt.Errorf("OldSize(%d) must be larger than zero", req.OldSize)
- }
- if req.NewSize <= req.OldSize {
- return nil, fmt.Errorf("NewSize(%d) must be larger than OldSize(%d)", req.NewSize, req.OldSize)
- }
- return &req, nil
-}
-
-func (i *Instance) inclusionProofRequestFromHTTP(r *http.Request) (*requests.InclusionProof, error) {
- var req requests.InclusionProof
- if err := req.FromURL(r.URL.Path); err != nil {
- return nil, fmt.Errorf("FromASCII: %v", err)
- }
- if req.TreeSize < 2 {
- // TreeSize:0 => not possible to prove inclusion of anything
- // TreeSize:1 => you don't need an inclusion proof (it is always empty)
- return nil, fmt.Errorf("TreeSize(%d) must be larger than one", req.TreeSize)
- }
- return &req, nil
-}
-
-func (i *Instance) leavesRequestFromHTTP(r *http.Request) (*requests.Leaves, error) {
- var req requests.Leaves
- if err := req.FromURL(r.URL.Path); err != nil {
- return nil, fmt.Errorf("FromASCII: %v", err)
- }
-
- if req.StartSize > req.EndSize {
- return nil, fmt.Errorf("StartSize(%d) must be less than or equal to EndSize(%d)", req.StartSize, req.EndSize)
- }
- if req.EndSize-req.StartSize+1 > uint64(i.MaxRange) {
- req.EndSize = req.StartSize + uint64(i.MaxRange) - 1
- }
- return &req, nil
-}
diff --git a/pkg/instance/instance_test.go b/pkg/instance/instance_test.go
deleted file mode 100644
index 00d996d..0000000
--- a/pkg/instance/instance_test.go
+++ /dev/null
@@ -1,23 +0,0 @@
-package instance
-
-import (
- "net/http"
- "testing"
-)
-
-func CheckHTTPMethod(t *testing.T) {
- var instance Instance
- for _, table := range []struct {
- method string
- wantOK bool
- }{
- {wantOK: false, method: http.MethodHead},
- {wantOK: true, method: http.MethodPost},
- {wantOK: true, method: http.MethodGet},
- } {
- ok := instance.checkHTTPMethod(table.method)
- if got, want := ok, table.wantOK; got != want {
- t.Errorf("%s: got %v but wanted %v", table.method, got, want)
- }
- }
-}
diff --git a/pkg/state/mocks/signer.go b/pkg/state/mocks/signer.go
deleted file mode 100644
index 7c699dd..0000000
--- a/pkg/state/mocks/signer.go
+++ /dev/null
@@ -1,23 +0,0 @@
-package mocks
-
-import (
- "crypto"
- "crypto/ed25519"
- "io"
-)
-
-// TestSign implements the signer interface. It can be used to mock an Ed25519
-// signer that always return the same public key, signature, and error.
-type TestSigner struct {
- PublicKey [ed25519.PublicKeySize]byte
- Signature [ed25519.SignatureSize]byte
- Error error
-}
-
-func (ts *TestSigner) Public() crypto.PublicKey {
- return ed25519.PublicKey(ts.PublicKey[:])
-}
-
-func (ts *TestSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
- return ts.Signature[:], ts.Error
-}
diff --git a/pkg/state/single.go b/pkg/state/single.go
deleted file mode 100644
index 695f0e3..0000000
--- a/pkg/state/single.go
+++ /dev/null
@@ -1,165 +0,0 @@
-package state
-
-import (
- "context"
- "crypto"
- "crypto/ed25519"
- "fmt"
- "sync"
- "time"
-
- "git.sigsum.org/log-go/pkg/db"
- "git.sigsum.org/sigsum-go/pkg/log"
- "git.sigsum.org/sigsum-go/pkg/types"
-)
-
-// StateManagerSingle implements a single-instance StateManager
-type StateManagerSingle struct {
- client db.Client
- signer crypto.Signer
- namespace types.Hash
- interval time.Duration
- deadline time.Duration
-
- // Lock-protected access to pointers. A write lock is only obtained once
- // per interval when doing pointer rotation. All endpoints are readers.
- sync.RWMutex
- signedTreeHead *types.SignedTreeHead
- cosignedTreeHead *types.CosignedTreeHead
-
- // Syncronized and deduplicated witness cosignatures for signedTreeHead
- events chan *event
- cosignatures map[types.Hash]*types.Signature
-}
-
-func NewStateManagerSingle(client db.Client, signer crypto.Signer, interval, deadline time.Duration) (*StateManagerSingle, error) {
- sm := &StateManagerSingle{
- client: client,
- signer: signer,
- namespace: *types.HashFn(signer.Public().(ed25519.PublicKey)),
- interval: interval,
- deadline: deadline,
- }
- sth, err := sm.latestSTH(context.Background())
- sm.setCosignedTreeHead()
- sm.setToCosignTreeHead(sth)
- return sm, err
-}
-
-func (sm *StateManagerSingle) Run(ctx context.Context) {
- rotation := func() {
- nextSTH, err := sm.latestSTH(ctx)
- if err != nil {
- log.Warning("cannot rotate without tree head: %v", err)
- return
- }
- sm.rotate(nextSTH)
- }
- sm.events = make(chan *event, 4096)
- defer close(sm.events)
- ticker := time.NewTicker(sm.interval)
- defer ticker.Stop()
-
- rotation()
- for {
- select {
- case <-ticker.C:
- rotation()
- case ev := <-sm.events:
- sm.handleEvent(ev)
- case <-ctx.Done():
- return
- }
- }
-}
-
-func (sm *StateManagerSingle) ToCosignTreeHead(_ context.Context) (*types.SignedTreeHead, error) {
- sm.RLock()
- defer sm.RUnlock()
- return sm.signedTreeHead, nil
-}
-
-func (sm *StateManagerSingle) CosignedTreeHead(_ context.Context) (*types.CosignedTreeHead, error) {
- sm.RLock()
- defer sm.RUnlock()
- if sm.cosignedTreeHead == nil {
- return nil, fmt.Errorf("no cosignatures available")
- }
- return sm.cosignedTreeHead, nil
-}
-
-func (sm *StateManagerSingle) AddCosignature(ctx context.Context, pub *types.PublicKey, sig *types.Signature) error {
- sm.RLock()
- defer sm.RUnlock()
-
- msg := sm.signedTreeHead.TreeHead.ToBinary(&sm.namespace)
- if !ed25519.Verify(ed25519.PublicKey(pub[:]), msg, sig[:]) {
- return fmt.Errorf("invalid cosignature")
- }
- select {
- case sm.events <- &event{types.HashFn(pub[:]), sig}:
- return nil
- case <-ctx.Done():
- return fmt.Errorf("request timeout")
- }
-}
-
-func (sm *StateManagerSingle) rotate(nextSTH *types.SignedTreeHead) {
- sm.Lock()
- defer sm.Unlock()
-
- log.Debug("rotating tree heads")
- sm.handleEvents()
- sm.setCosignedTreeHead()
- sm.setToCosignTreeHead(nextSTH)
-}
-
-func (sm *StateManagerSingle) handleEvents() {
- log.Debug("handling any outstanding events")
- for i, n := 0, len(sm.events); i < n; i++ {
- sm.handleEvent(<-sm.events)
- }
-}
-
-func (sm *StateManagerSingle) handleEvent(ev *event) {
- log.Debug("handling event from witness %x", ev.keyHash[:])
- sm.cosignatures[*ev.keyHash] = ev.cosignature
-}
-
-func (sm *StateManagerSingle) setCosignedTreeHead() {
- n := len(sm.cosignatures)
- if n == 0 {
- sm.cosignedTreeHead = nil
- return
- }
-
- var cth types.CosignedTreeHead
- cth.SignedTreeHead = *sm.signedTreeHead
- cth.Cosignature = make([]types.Signature, 0, n)
- cth.KeyHash = make([]types.Hash, 0, n)
- for keyHash, cosignature := range sm.cosignatures {
- cth.KeyHash = append(cth.KeyHash, keyHash)
- cth.Cosignature = append(cth.Cosignature, *cosignature)
- }
- sm.cosignedTreeHead = &cth
-}
-
-func (sm *StateManagerSingle) setToCosignTreeHead(nextSTH *types.SignedTreeHead) {
- sm.cosignatures = make(map[types.Hash]*types.Signature)
- sm.signedTreeHead = nextSTH
-}
-
-func (sm *StateManagerSingle) latestSTH(ctx context.Context) (*types.SignedTreeHead, error) {
- ictx, cancel := context.WithTimeout(ctx, sm.deadline)
- defer cancel()
-
- th, err := sm.client.GetTreeHead(ictx)
- if err != nil {
- return nil, fmt.Errorf("failed fetching tree head: %v", err)
- }
- sth, err := th.Sign(sm.signer, &sm.namespace)
- if err != nil {
- return nil, fmt.Errorf("failed signing tree head: %v", err)
- }
- return sth, nil
-}
diff --git a/pkg/state/single_test.go b/pkg/state/single_test.go
deleted file mode 100644
index 8e89020..0000000
--- a/pkg/state/single_test.go
+++ /dev/null
@@ -1,217 +0,0 @@
-package state
-
-import (
- "context"
- "crypto"
- "crypto/ed25519"
- "crypto/rand"
- "fmt"
- "reflect"
- "testing"
- "time"
-
- db "git.sigsum.org/log-go/pkg/db/mocks"
- "git.sigsum.org/log-go/pkg/state/mocks"
- "git.sigsum.org/sigsum-go/pkg/types"
- "github.com/golang/mock/gomock"
-)
-
-func TestNewStateManagerSingle(t *testing.T) {
- signerOk := &mocks.TestSigner{types.PublicKey{}, types.Signature{}, nil}
- signerErr := &mocks.TestSigner{types.PublicKey{}, types.Signature{}, fmt.Errorf("err")}
- for _, table := range []struct {
- description string
- signer crypto.Signer
- rsp types.TreeHead
- err error
- wantErr bool
- wantSth types.SignedTreeHead
- }{
- {
- description: "invalid: backend failure",
- signer: signerOk,
- err: fmt.Errorf("something went wrong"),
- wantErr: true,
- },
- {
- description: "invalid: signer failure",
- signer: signerErr,
- rsp: types.TreeHead{},
- wantErr: true,
- },
- {
- description: "valid",
- signer: signerOk,
- rsp: types.TreeHead{},
- wantSth: types.SignedTreeHead{},
- },
- } {
- func() {
- ctrl := gomock.NewController(t)
- defer ctrl.Finish()
- client := db.NewMockClient(ctrl)
- client.EXPECT().GetTreeHead(gomock.Any()).Return(&table.rsp, table.err)
-
- sm, err := NewStateManagerSingle(client, table.signer, time.Duration(0), time.Duration(0))
- if got, want := err != nil, table.wantErr; got != want {
- t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err)
- }
- if err != nil {
- return
- }
- if got, want := sm.signedTreeHead, &table.wantSth; !reflect.DeepEqual(got, want) {
- t.Errorf("got to-cosign tree head\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description)
- }
- if got := sm.cosignedTreeHead; got != nil {
- t.Errorf("got cosigned tree head but should have none in test %q", table.description)
- }
- }()
- }
-}
-
-func TestToCosignTreeHead(t *testing.T) {
- want := &types.SignedTreeHead{}
- sm := StateManagerSingle{
- signedTreeHead: want,
- }
- sth, err := sm.ToCosignTreeHead(context.Background())
- if err != nil {
- t.Errorf("should not fail with error: %v", err)
- return
- }
- if got := sth; !reflect.DeepEqual(got, want) {
- t.Errorf("got signed tree head\n\t%v\nbut wanted\n\t%v", got, want)
- }
-}
-
-func TestCosignedTreeHead(t *testing.T) {
- want := &types.CosignedTreeHead{
- Cosignature: make([]types.Signature, 1),
- KeyHash: make([]types.Hash, 1),
- }
- sm := StateManagerSingle{
- cosignedTreeHead: want,
- }
- cth, err := sm.CosignedTreeHead(context.Background())
- if err != nil {
- t.Errorf("should not fail with error: %v", err)
- return
- }
- if got := cth; !reflect.DeepEqual(got, want) {
- t.Errorf("got cosigned tree head\n\t%v\nbut wanted\n\t%v", got, want)
- }
-
- sm.cosignedTreeHead = nil
- cth, err = sm.CosignedTreeHead(context.Background())
- if err == nil {
- t.Errorf("should fail without a cosigned tree head")
- return
- }
-}
-
-func TestAddCosignature(t *testing.T) {
- secret, public := mustKeyPair(t)
- for _, table := range []struct {
- desc string
- signer crypto.Signer
- vk types.PublicKey
- wantErr bool
- }{
- {
- desc: "invalid: wrong public key",
- signer: secret,
- vk: types.PublicKey{},
- wantErr: true,
- },
- {
- desc: "valid",
- signer: secret,
- vk: public,
- },
- } {
- sm := &StateManagerSingle{
- namespace: *types.HashFn(nil),
- signedTreeHead: &types.SignedTreeHead{},
- events: make(chan *event, 1),
- }
- defer close(sm.events)
-
- sth := mustSign(t, table.signer, &sm.signedTreeHead.TreeHead, &sm.namespace)
- ctx := context.Background()
- err := sm.AddCosignature(ctx, &table.vk, &sth.Signature)
- if got, want := err != nil, table.wantErr; got != want {
- t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.desc, err)
- }
- if err != nil {
- continue
- }
-
- ctx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
- defer cancel()
- if err := sm.AddCosignature(ctx, &table.vk, &sth.Signature); err == nil {
- t.Errorf("expected full channel in test %q", table.desc)
- }
- if got, want := len(sm.events), 1; got != want {
- t.Errorf("wanted %d cosignatures but got %d in test %q", want, got, table.desc)
- }
- }
-}
-
-func TestRotate(t *testing.T) {
- sth := &types.SignedTreeHead{}
- nextSTH := &types.SignedTreeHead{TreeHead: types.TreeHead{Timestamp: 1}}
- ev := &event{
- keyHash: &types.Hash{},
- cosignature: &types.Signature{},
- }
- wantCTH := &types.CosignedTreeHead{
- SignedTreeHead: *sth,
- KeyHash: []types.Hash{*ev.keyHash},
- Cosignature: []types.Signature{*ev.cosignature},
- }
- sm := &StateManagerSingle{
- signedTreeHead: sth,
- cosignatures: make(map[types.Hash]*types.Signature),
- events: make(chan *event, 1),
- }
- defer close(sm.events)
-
- sm.events <- ev
- sm.rotate(nextSTH)
- if got, want := sm.signedTreeHead, nextSTH; !reflect.DeepEqual(got, want) {
- t.Errorf("got to-cosign tree head\n\t%v\nbut wanted\n\t%v", got, want)
- }
- if got, want := sm.cosignedTreeHead, wantCTH; !reflect.DeepEqual(got, want) {
- t.Errorf("got cosigned tree head\n\t%v\nbut wanted\n\t%v", got, want)
- }
-
- sth = nextSTH
- nextSTH = &types.SignedTreeHead{TreeHead: types.TreeHead{Timestamp: 2}}
- sm.rotate(nextSTH)
- if got, want := sm.signedTreeHead, nextSTH; !reflect.DeepEqual(got, want) {
- t.Errorf("got to-cosign tree head\n\t%v\nbut wanted\n\t%v", got, want)
- }
- if got := sm.cosignedTreeHead; got != nil {
- t.Errorf("expected no cosignatures to be available")
- }
-}
-
-func mustKeyPair(t *testing.T) (crypto.Signer, types.PublicKey) {
- t.Helper()
- vk, sk, err := ed25519.GenerateKey(rand.Reader)
- if err != nil {
- t.Fatal(err)
- }
- var pub types.PublicKey
- copy(pub[:], vk[:])
- return sk, pub
-}
-
-func mustSign(t *testing.T, s crypto.Signer, th *types.TreeHead, kh *types.Hash) *types.SignedTreeHead {
- t.Helper()
- sth, err := th.Sign(s, kh)
- if err != nil {
- t.Fatal(err)
- }
- return sth
-}