diff options
50 files changed, 3682 insertions, 1654 deletions
| @@ -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 @@ -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 @@ -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 -} | 
