pax_global_header00006660000000000000000000000064145537445770014537gustar00rootroot0000000000000052 comment=d6bb9add29c86561a1eff711dfc6a3e7add4fff3 sigsum-go-0.7.2/000077500000000000000000000000001455374457700134575ustar00rootroot00000000000000sigsum-go-0.7.2/.gitlab-ci.yml000066400000000000000000000010651455374457700161150ustar00rootroot00000000000000image: docker.io/library/golang:1.19 stages: - build - test - build-log make-all: stage: build script: make all make-check: stage: test script: make check make-check-386: stage: test script: GOARCH=386 make check # Succeeds if no changes are suggested by gofmt -d . gofmt: stage: test script: if gofmt -d . | grep . ; then false ; else true ; fi build-log-go: stage: build-log allow_failure: true script: - git clone https://git.glasklar.is/sigsum/core/log-go.git && cd log-go && go work init && go work use . .. && go build ./... sigsum-go-0.7.2/AUTHORS000066400000000000000000000021121455374457700145230ustar00rootroot00000000000000Authors of the Sigsum project The copyright on the Sigsum implementation is held by the respective authors (or their organizations/employers). Unless file-specific copyright headers say otherwise, Sigsum is permissively licensed according to the BSD 2-Clause License (see the LICENSE file). This file contains only a summary; for more fine-grained information on who authored a particular file or feature, please refer to the version control history at: https://git.glasklar.is/sigsum/core/sigsum-go For contributions where copyrights are held by an organization, e.g., the author's employer, the copyright holder should be identified by the commit, preferably by using an author email address belonging to the organization, or otherwise explained in the commit message. File-specific copyright headers should be used when necessary to document the origin of a file's contents, e.g., for code copied from other sources, or governed by different license requirements. Authors, in chronological order of initial contribution: Rasmus Dahlberg Linus Nordberg Grégoire Détrez Niels Möller sigsum-go-0.7.2/LICENSE000066400000000000000000000024701455374457700144670ustar00rootroot00000000000000BSD 2-Clause License Copyright (c) 2021, The Sigsum Project Authors All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. sigsum-go-0.7.2/MAINTAINERS000066400000000000000000000012341455374457700151540ustar00rootroot00000000000000The current maintainers of sigsum-go are: +-----------------+--------------------+--------+-------------------------------------------------+ | Name | Affiliation | GitLab | IRC/Matrix | Email | +-----------------+--------------------+--------+------------+------------------------------------+ | Niels Möller | Glasklar Teknik AB | nisse | nielsm | nisse (at) glasklarteknik (dot) se | | Rasmus Dahlberg | Glasklar Teknik AB | rgdd | rgdd | rgdd (at) glasklarteknik (dot) se | +-----------------+--------------------+--------+------------+------------------------------------+ sigsum-go-0.7.2/Makefile000066400000000000000000000003661455374457700151240ustar00rootroot00000000000000# Let the go tool manage dependencies. all: go build ./... mocks: cd pkg/mocks && $(MAKE) check: mocks go test ./... cd tests && $(MAKE) check clean: cd tests && $(MAKE) clean cd pkg/mocks && $(MAKE) clean .PHONY: all check clean mocks sigsum-go-0.7.2/README.md000066400000000000000000000003721455374457700147400ustar00rootroot00000000000000# sigsum-go Sigsum is a system for public and transparent logging of signed checksums, see for an overview of the system. This repository contains a Go library and client utilities for interacting with the system servers. sigsum-go-0.7.2/cmd/000077500000000000000000000000001455374457700142225ustar00rootroot00000000000000sigsum-go-0.7.2/cmd/sigsum-key/000077500000000000000000000000001455374457700163175ustar00rootroot00000000000000sigsum-go-0.7.2/cmd/sigsum-key/sigsum-key.go000066400000000000000000000200351455374457700207430ustar00rootroot00000000000000package main import ( "bytes" "fmt" "io" "log" "os" "strings" "github.com/pborman/getopt/v2" "sigsum.org/sigsum-go/internal/ssh" "sigsum.org/sigsum-go/pkg/crypto" "sigsum.org/sigsum-go/pkg/key" ) type GenSettings struct { outputFile string } type VerifySettings struct { keyFile string signatureFile string namespace string } type SignSettings struct { keyFile string outputFile string namespace string } type ExportSettings struct { keyFile string outputFile string } func main() { const usage = `sigsum-key sub commands: sigsum-key help | --help Display this help. All the below sub commands also accept the --help option, to display help for that sub command. sigsum-key gen -o file Generate a new key pair. Private key is stored in the given file, in OpenSSH private key format. Corresponding public key file gets a ".pub" suffix, and is written in OpenSSH public key format. sigsum-key verify [options] < msg Verify a signature. For option details, see sigsum-key verify --help. sigsum-key sign [options] < msg Create a signature. For option details, see sigsum-key sign --help. sigsum-key hash [-k file] [-o output] Reads public key from file (by default, stdin) and writes key hash to output (by default, stdout). sigsum-key hex [-k file] [-o output] Reads public key from file (by default, stdin) and writes hex key to output (by default, stdout). sigsum-key hex-to-pub [-k file] [-o output] Reads hex public key from file (by default, stdin) and writes OpenSSH format public key to output (by default, stdout). ` log.SetFlags(0) if len(os.Args) < 2 { log.Fatal(usage) } switch os.Args[1] { default: log.Fatal(usage) case "help", "--help": fmt.Print(usage) os.Exit(0) case "gen": var settings GenSettings settings.parse(os.Args) pub, signer, err := crypto.NewKeyPair() if err != nil { log.Fatalf("generating key failed: %v\n", err) } writeKeyFiles(settings.outputFile, &pub, signer) case "verify": var settings VerifySettings settings.parse(os.Args) publicKey, err := key.ReadPublicKeyFile(settings.keyFile) if err != nil { log.Fatal(err) } signature := readSignatureFile(settings.signatureFile) msg := readMessage(settings.namespace) if !crypto.Verify(&publicKey, msg, &signature) { log.Fatalf("signature is not valid\n") } case "sign": var settings SignSettings settings.parse(os.Args) signer, err := key.ReadPrivateKeyFile(settings.keyFile) if err != nil { log.Fatal(err) } msg := readMessage(settings.namespace) signature, err := signer.Sign(msg) if err != nil { log.Fatalf("signing failed: %v", err) } writeSignatureFile(settings.outputFile, &signature) // TODO: Change all subcommands hash, hex, hex-to-pub // to take an optional filename arguments for input // and output, and by default read stdin and write to // stdout. case "hash": var settings ExportSettings settings.parse(os.Args, false) publicKey, err := key.ParsePublicKey(readInput(settings.keyFile)) if err != nil { log.Fatal(err) } withOutput(settings.outputFile, 0660, func(f io.Writer) error { _, err := fmt.Fprintf(f, "%x\n", crypto.HashBytes(publicKey[:])) return err }) case "hex": var settings ExportSettings settings.parse(os.Args, false) publicKey, err := key.ParsePublicKey(readInput(settings.keyFile)) if err != nil { log.Fatal(err) } withOutput(settings.outputFile, 0660, func(f io.Writer) error { _, err := fmt.Fprintf(f, "%x\n", publicKey[:]) return err }) case "hex-to-pub": var settings ExportSettings settings.parse(os.Args, true) pub, err := crypto.PublicKeyFromHex(strings.TrimSpace(readInput(settings.keyFile))) if err != nil { log.Fatalf("invalid key: %v", err) } withOutput(settings.outputFile, 0660, func(f io.Writer) error { _, err := fmt.Fprint(f, ssh.FormatPublicEd25519(&pub)) return err }) } } func newOptionSet(args []string, useStdin bool) *getopt.Set { set := getopt.New() set.SetProgram(args[0] + " " + args[1]) if useStdin { set.SetParameters("< msg") } else { set.SetParameters("") } return set } // Also adds and processes the help option. func parseNoArgs(set *getopt.Set, args []string) { help := false set.FlagLong(&help, "help", 0, "Display help") err := set.Getopt(args[1:], nil) // Check help first; if seen, ignore errors about missing mandatory arguments. if help { set.PrintUsage(os.Stdout) fmt.Printf("\nFor general information on this tool, see %s help.\n", args[0]) os.Exit(0) } if err != nil { log.Printf("err: %v\n", err) set.PrintUsage(log.Writer()) os.Exit(1) } if set.NArgs() > 0 { log.Fatal("Too many arguments.") } } func (s *GenSettings) parse(args []string) { set := newOptionSet(args, false) set.Flag(&s.outputFile, 'o', "Output", "file").Mandatory() parseNoArgs(set, args) } func (s *VerifySettings) parse(args []string) { // By default, no namespace. s.namespace = "" set := newOptionSet(args, true) set.FlagLong(&s.keyFile, "key", 'k', "Public key", "file").Mandatory() set.FlagLong(&s.signatureFile, "signature", 's', "Signature", "file").Mandatory() set.FlagLong(&s.namespace, "namespace", 'n', "Signature namespace") parseNoArgs(set, args) } func (s *SignSettings) parse(args []string) { // By default, no namespace. s.namespace = "" set := newOptionSet(args, true) set.FlagLong(&s.keyFile, "signing-key", 'k', "Private key for signing", "file").Mandatory() set.Flag(&s.outputFile, 'o', "Signature output", "file") set.FlagLong(&s.namespace, "namespace", 'n', "Signature namespace") parseNoArgs(set, args) } func (s *ExportSettings) parse(args []string, hex bool) { set := newOptionSet(args, false) if hex { set.FlagLong(&s.keyFile, "key", 'k', "Hex public key", "file") } else { set.FlagLong(&s.keyFile, "key", 'k', "Public key", "file") } set.Flag(&s.outputFile, 'o', "Output", "file") parseNoArgs(set, args) } // If outputFile is non-empty: open file, pass to f, and automatically // close it after f returns. Otherwise, just pass os.Stdout to f. Also // exit program on error from f. func withOutput(outputFile string, mode os.FileMode, f func(io.Writer) error) { file := os.Stdout if len(outputFile) > 0 { var err error file, err = os.OpenFile(outputFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, mode) if err != nil { log.Fatalf("failed to open file '%v': %v", outputFile, err) } defer file.Close() } err := f(file) if err != nil { log.Fatalf("writing output failed: %v", err) } } func writeKeyFiles(outputFile string, pub *crypto.PublicKey, signer *crypto.Ed25519Signer) { withOutput(outputFile, 0600, func(f io.Writer) error { return ssh.WritePrivateKeyFile(f, signer) }) if len(outputFile) > 0 { // Openssh insists that also public key files have // restrictive permissions. withOutput(outputFile+".pub", 0600, func(f io.Writer) error { _, err := io.WriteString(f, ssh.FormatPublicEd25519(pub)) return err }) } } func writeSignatureFile(outputFile string, signature *crypto.Signature) { withOutput(outputFile, 0644, func(f io.Writer) error { _, err := fmt.Fprintf(f, "%x\n", signature[:]) return err }) } func readSignatureFile(fileName string) crypto.Signature { contents, err := os.ReadFile(fileName) if err != nil { log.Fatalf("reading file %q failed: %v", fileName, err) } signature, err := crypto.SignatureFromHex(strings.TrimSpace(string(contents))) if err != nil { log.Fatal(err) } return signature } // Read message being signed from stdin. Prepend namespace if it is nonempty. func readMessage(namespace string) []byte { var buf bytes.Buffer if len(namespace) > 0 { buf.Write(crypto.AttachNamespace(namespace, []byte{})) } _, err := io.Copy(&buf, os.Stdin) if err != nil { log.Fatal(err) } return buf.Bytes() } // Reads given file, or stdin. func readInput(fileName string) string { var contents []byte var err error if len(fileName) > 0 { contents, err = os.ReadFile(fileName) } else { contents, err = io.ReadAll(os.Stdin) } if err != nil { log.Fatalf("Reading input failed: %v", err) } return string(contents) } sigsum-go-0.7.2/cmd/sigsum-monitor/000077500000000000000000000000001455374457700172165ustar00rootroot00000000000000sigsum-go-0.7.2/cmd/sigsum-monitor/sigsum-monitor.go000066400000000000000000000055431455374457700225500ustar00rootroot00000000000000package main import ( "context" "fmt" "os" "os/signal" "syscall" "time" "github.com/pborman/getopt/v2" "sigsum.org/sigsum-go/pkg/crypto" "sigsum.org/sigsum-go/pkg/key" "sigsum.org/sigsum-go/pkg/log" "sigsum.org/sigsum-go/pkg/monitor" "sigsum.org/sigsum-go/pkg/policy" "sigsum.org/sigsum-go/pkg/types" ) type Settings struct { policyFile string keys []string diagnostics string interval time.Duration } type callbacks struct{} func (_ callbacks) NewTreeHead(logKeyHash crypto.Hash, signedTreeHead types.SignedTreeHead) { fmt.Printf("New %x tree, size %d\n", logKeyHash, signedTreeHead.Size) } func (_ callbacks) NewLeaves(logKeyHash crypto.Hash, numberOfProcessedLeaves uint64, indices []uint64, leaves []types.Leaf) { fmt.Printf("New %x leaves, count %d, total processed %d\n", logKeyHash, len(leaves), numberOfProcessedLeaves) for i, l := range leaves { fmt.Printf(" index %d keyhash %x checksum %x\n", indices[i], l.KeyHash, l.Checksum) } } func (_ callbacks) Alert(logKeyHash crypto.Hash, e error) { log.Fatal("Alert log %x: %v\n", logKeyHash, e) } func main() { var settings Settings settings.parse(os.Args) if err := log.SetLevelFromString(settings.diagnostics); err != nil { log.Fatal("%v", err) } policy, err := policy.ReadPolicyFile(settings.policyFile) if err != nil { log.Fatal("failed to create policy: %v", err) } config := monitor.Config{ QueryInterval: settings.interval, Callbacks: callbacks{}, } if len(settings.keys) > 0 { config.SubmitKeys = make(map[crypto.Hash]crypto.PublicKey) for _, f := range settings.keys { pub, err := key.ReadPublicKeyFile(f) if err != nil { log.Fatal("Failed reading key: %v", err) } config.SubmitKeys[crypto.HashBytes(pub[:])] = pub } } // TODO: Read state from disk. Also store the list of submit // keys, and discard state if keys are added, since whenever // new keys are added, the log must be rescanned from the // start. ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer cancel() done := monitor.StartMonitoring(ctx, policy, &config, nil) <-done } func (s *Settings) parse(args []string) { set := getopt.New() set.SetParameters("submit-keys") help := false s.diagnostics = "info" s.interval = 10 * time.Minute set.FlagLong(&s.policyFile, "policy", 'p', "Sigsum policy", "file").Mandatory() set.FlagLong(&s.interval, "interval", 0, "Monitoring interval") set.FlagLong(&s.diagnostics, "diagnostics", 0, "One of \"fatal\", \"error\", \"warning\", \"info\", or \"debug\"", "level") set.FlagLong(&help, "help", 0, "Display help") err := set.Getopt(args, nil) // Check help first; if seen, ignore errors about missing mandatory arguments. if help { set.PrintUsage(os.Stdout) os.Exit(0) } if err != nil { fmt.Printf("err: %v\n", err) set.PrintUsage(os.Stderr) os.Exit(1) } s.keys = set.Args() } sigsum-go-0.7.2/cmd/sigsum-submit/000077500000000000000000000000001455374457700170325ustar00rootroot00000000000000sigsum-go-0.7.2/cmd/sigsum-submit/sigsum-submit.go000066400000000000000000000315251455374457700221770ustar00rootroot00000000000000package main import ( "context" "errors" "fmt" "io" "io/fs" "net" "os" "path/filepath" "strings" "time" "github.com/dchest/safefile" "github.com/pborman/getopt/v2" "sigsum.org/sigsum-go/pkg/crypto" "sigsum.org/sigsum-go/pkg/key" "sigsum.org/sigsum-go/pkg/log" "sigsum.org/sigsum-go/pkg/policy" "sigsum.org/sigsum-go/pkg/proof" "sigsum.org/sigsum-go/pkg/requests" "sigsum.org/sigsum-go/pkg/submit" "sigsum.org/sigsum-go/pkg/submit-token" "sigsum.org/sigsum-go/pkg/types" ) type Settings struct { rawHash bool keyFile string policyFile string leafHash bool diagnostics string inputFiles []string outputFile string outputDir string tokenDomain string tokenKeyFile string timeout time.Duration } // Empty name for stdin type LeafSink func(name string, leaf *requests.Leaf) type LeafSkip func(name string, msg *crypto.Hash, publicKey *crypto.PublicKey) bool type LeafSource func(skip LeafSkip, sink LeafSink) func main() { var settings Settings settings.parse(os.Args) if err := log.SetLevelFromString(settings.diagnostics); err != nil { log.Fatal("%v", err) } var source LeafSource if len(settings.keyFile) > 0 { signer, err := key.ReadPrivateKeyFile(settings.keyFile) if err != nil { log.Fatal("reading key file failed: %v", err) } publicKey := signer.Public() if len(settings.inputFiles) == 0 { source = func(_ LeafSkip, sink LeafSink) { msg, err := readMessage(os.Stdin, settings.rawHash) if err != nil { log.Fatal("Reading message from stdin failed: %v", err) } signature, err := types.SignLeafMessage(signer, msg[:]) if err != nil { log.Fatal("Signing failed: %v", err) } sink("", &requests.Leaf{Message: msg, Signature: signature, PublicKey: publicKey}) } } else { source = func(skip LeafSkip, sink LeafSink) { for _, inputFile := range settings.inputFiles { msg := readMessageFile(inputFile, settings.rawHash) if skip(inputFile, &msg, &publicKey) { continue } signature, err := types.SignLeafMessage(signer, msg[:]) if err != nil { log.Fatal("signing failed: %v", err) } sink(inputFile, &requests.Leaf{Message: msg, Signature: signature, PublicKey: publicKey}) } } } } else { if len(settings.inputFiles) == 0 { source = func(_ LeafSkip, sink LeafSink) { leaf, err := readLeafRequest(os.Stdin) if err != nil { log.Fatal("Leaf request on stdin not valid: %v", err) } sink("", &leaf) } } else { source = func(skip LeafSkip, sink LeafSink) { for _, inputFile := range settings.inputFiles { leaf, err := readLeafRequestFile(inputFile) if err != nil { log.Fatal("Leaf request %q not valid: %v", inputFile, err) } // Strip suffix. inputFile = strings.TrimSuffix(inputFile, ".req") if len(inputFile) == 0 { log.Fatal("Invalid input file name %q", ".req") } if !skip(inputFile, &leaf.Message, &leaf.PublicKey) { sink(inputFile, &leaf) } } } } } if len(settings.policyFile) > 0 { policy, err := policy.ReadPolicyFile(settings.policyFile) if err != nil { log.Fatal("Invalid policy file: %v", err) } config := submit.Config{Policy: policy, Domain: settings.tokenDomain, PerLogTimeout: settings.timeout, } ctx := context.Background() if len(config.Domain) > 0 { var err error config.RateLimitSigner, err = key.ReadPrivateKeyFile(settings.tokenKeyFile) if err != nil { log.Fatal("reading token key file failed: %v", err) } // Warn if corresponding public key isn't registered for the domain. if err := checkTokenDomain(ctx, config.Domain, config.RateLimitSigner.Public()); err != nil { log.Warning("warn: token domain and signer does not match DNS records: %v", err) } } skip := func(inputName string, msg *crypto.Hash, publicKey *crypto.PublicKey) bool { if len(inputName) == 0 { return false } proofName := settings.getOutputFile(inputName, ".proof") f, err := os.Open(proofName) if errors.Is(err, fs.ErrNotExist) { return false } if err != nil { log.Fatal("Opening proof file %q failed: %v", proofName, err) } defer f.Close() var sigsumProof proof.SigsumProof if err := sigsumProof.FromASCII(f); err != nil { log.Fatal("Parsing proof file %q failed: %v", proofName, err) } if err := sigsumProof.Verify(msg, publicKey, policy); err != nil { log.Fatal("Existing proof file %q is not valid: %v", proofName, err) } return true } // An item to submit. type Item struct { leaf requests.Leaf inputName string // empty for stdin } var items []Item // TODO: Actually do requests in batch. source(skip, func(name string, leaf *requests.Leaf) { items = append(items, Item{ leaf: *leaf, inputName: name, }) }) for _, item := range items { proof, err := submit.SubmitLeafRequest(ctx, &config, &item.leaf) if err != nil { log.Fatal("Submit failed: %v", err) } if err := settings.withOutputFile(item.inputName, ".proof", proof.ToASCII); err != nil { log.Fatal("Writing proof failed: %v", err) } } } else { sink := func(_ string, _ *requests.Leaf) {} if settings.leafHash { sink = func(inputFile string, req *requests.Leaf) { leaf, err := req.Verify() if err != nil { log.Fatal("Internal error; leaf request invalid: %v", err) } settings.withOutputFile(inputFile, ".hash", func(w io.Writer) error { _, err := fmt.Fprintf(w, "%x\n", leaf.ToHash()) return err }) } } else if len(settings.keyFile) > 0 { // Output created add-leaf requests. sink = func(inputFile string, leaf *requests.Leaf) { if err := settings.withOutputFile(inputFile, ".req", leaf.ToASCII); err != nil { log.Fatal("Writing leaf request failed: %v", err) } } } source(func(_ string, _ *crypto.Hash, _ *crypto.PublicKey) bool { return false }, sink) } } func (s *Settings) parse(args []string) { const usage = ` Create and/or submit add-leaf request(s). If no input files are listed on the command line, a single request is processed, reading from standard input, and writing to standard output (or file specified with the -o option). See further below for processing of multiple files. If a signing key (-k option) is specified, a new request is created by signing the the SHA256 hash of the input (or, if --raw-hash is given, input is the hash value, either exactly 32 octets, or a hex string). The key file uses openssh format, it must be either an unencrypted private key, or a public key, in which case the corresponding private key is accessed via ssh-agent. If no signing key is provided, input should instead be the body of an add-leaf request, which is parsed and verified. If a Sigsum policy (-p option) is provided, the request is submitted to the log specified by the policy, and a Sigsum proof is collected and output. If there are multiple logs in the policy, they are tried in randomized order. With -k but without -p, the add-leaf request itself is output. With no -k and no -p, the request syntax and signature of the input request are verified, but there is no output. The --leaf-hash option can be used to output the hash of the resulting leaf, instead of submitting it. If input files are provided on the command line, each file corresponds to one request, and result is written to a corresponding output file, based on these rules: 1. If there's exactly one input file, and the -o option is used, output is written to that file. Any existing file is overwritten. 2. For a request output, the suffix ".req" is added to the input file name. 3. For a proof output, if the input is a request, any ".req" suffix on the input file name is stripped. Then the suffix ".proof" is added. 4. If the --output-dir option is provided, any directory part of the input file name is stripped, and the output is written as a file in the specified output directory. If a corresponding .proof file already exists, that proof is read and verified. If the proof is valid, the input file is skipped. If the proof is not valid, sigsum-submit exits with an error. If a corresponding .req output file already exists, it is overwritten (TODO: Figure out if that is the proper behavior). ` s.diagnostics = "info" set := getopt.New() set.SetParameters("[input files]") set.SetUsage(func() { fmt.Print(usage) }) help := false set.FlagLong(&s.rawHash, "raw-hash", 0, "Input is already hashed") set.FlagLong(&s.keyFile, "signing-key", 'k', "Key for signing the leaf", "file") set.FlagLong(&s.policyFile, "policy", 'p', "Sigsum policy", "file") set.FlagLong(&s.leafHash, "leaf-hash", 0, "Output leaf hash") set.Flag(&s.outputFile, 'o', "Write output to file, instead of stdout", "file") set.FlagLong(&s.outputDir, "output-dir", 0, "Directory for output files", "directory") set.FlagLong(&s.diagnostics, "diagnostics", 0, "One of \"fatal\", \"error\", \"warning\", \"info\", or \"debug\"", "level") set.FlagLong(&s.tokenDomain, "token-domain", 0, "Create a Sigsum-Token: header for this domain") set.FlagLong(&s.tokenKeyFile, "token-signing-key", 0, "Key for signing Sigsum-Token: header", "file") set.FlagLong(&s.timeout, "timeout", 0, "Per-log submission timeout. Zero means library default, currently 45s", "duration") set.FlagLong(&help, "help", 0, "Display help") set.Parse(args) if help { set.PrintUsage(os.Stdout) fmt.Print(usage) os.Exit(0) } s.inputFiles = set.Args() if len(s.inputFiles) > 1 && len(s.outputFile) > 0 { log.Fatal("The -o option is invalid with more than one input file.") } if len(s.inputFiles) == 0 && len(s.outputDir) > 0 { log.Fatal("The --output-dir option is invalid when no input files are provided.") } if len(s.outputFile) > 0 && len(s.outputDir) > 0 { log.Fatal("The -o and the --output-dir options are mutually exclusive.") } if len(s.policyFile) > 0 && s.leafHash { log.Fatal("The -p (--policy) and --leaf-hash options are mutually exclusive.") } for _, f := range s.inputFiles { if len(f) == 0 { log.Fatal("Empty string is not a valid input file name.") } } } // Empty input name means stdin. Empty output name means stdout should be used. func (s *Settings) getOutputFile(name, suffix string) string { if len(s.outputFile) > 0 { return s.outputFile } if len(name) == 0 { return "" } name += suffix if len(s.outputDir) > 0 { return filepath.Join(s.outputDir, filepath.Base(name)) } return name } func (s *Settings) withOutputFile(name, suffix string, writer func(f io.Writer) error) error { outputFile := s.getOutputFile(name, suffix) if len(outputFile) == 0 { return writer(os.Stdout) } return withOutputFile(outputFile, writer) } // Reads the named input file, or stdin if filename is empty. func readMessage(r io.Reader, rawHash bool) (crypto.Hash, error) { if !rawHash { return crypto.HashFile(r) } data, err := io.ReadAll(r) if err != nil { return crypto.Hash{}, err } if len(data) == crypto.HashSize { var msg crypto.Hash copy(msg[:], data) return msg, nil } return crypto.HashFromHex(strings.TrimSpace(string(data))) } func readMessageFile(name string, rawHash bool) crypto.Hash { r, err := os.Open(name) if err != nil { log.Fatal("Opening %q failed: %v", name, err) } defer r.Close() msg, err := readMessage(r, rawHash) if err != nil { log.Fatal("Reading %q failed: %v", name, err) } return msg } func readLeafRequest(r io.Reader) (requests.Leaf, error) { var leaf requests.Leaf if err := leaf.FromASCII(r); err != nil { return requests.Leaf{}, err } if !types.VerifyLeafMessage(&leaf.PublicKey, leaf.Message[:], &leaf.Signature) { return requests.Leaf{}, fmt.Errorf("invalid leaf signature") } return leaf, nil } func readLeafRequestFile(name string) (requests.Leaf, error) { r, err := os.Open(name) if err != nil { return requests.Leaf{}, err } defer r.Close() return readLeafRequest(r) } // Create temporary file, and atomically replace. func withOutputFile(outputFile string, writer func(f io.Writer) error) error { f, err := safefile.Create(outputFile, 0644) if err != nil { return err } defer f.Close() if err := writer(f); err != nil { return fmt.Errorf("writing to (temporary) output file for %q failed: %v", outputFile, err) } return f.Commit() } // Warn if corresponding public key isn't registered for the domain. func checkTokenDomain(ctx context.Context, domain string, pubkey crypto.PublicKey) error { resolver := net.Resolver{} rsps, err := token.LookupDomain(ctx, resolver.LookupTXT, domain) if err != nil { return err } var badKeys int for _, keyHex := range rsps { key, err := crypto.PublicKeyFromHex(keyHex) if err != nil { badKeys++ continue } if key == pubkey { return nil } } return fmt.Errorf("key not registered (%d records found, syntactically bad: %d)", len(rsps), badKeys) } sigsum-go-0.7.2/cmd/sigsum-token/000077500000000000000000000000001455374457700166475ustar00rootroot00000000000000sigsum-go-0.7.2/cmd/sigsum-token/sigsum-token.go000066400000000000000000000153561455374457700216350ustar00rootroot00000000000000package main import ( "context" "fmt" "io" "log" "os" "strings" "github.com/pborman/getopt/v2" "sigsum.org/sigsum-go/pkg/crypto" "sigsum.org/sigsum-go/pkg/key" "sigsum.org/sigsum-go/pkg/submit-token" ) type createSettings struct { keyFile string outputFile string logKeyFile string domain string } type recordSettings struct { keyFile string outputFile string } type verifySettings struct { keyFile string logKeyFile string domain string quiet bool } func main() { const usage = `sigsum-token sub commands: sigsum-token help | --help Display this help. All the below sub commands also accept the --help option, to display help for that sub command. sigsum-token create [options] Create a token for submissions to the the given log, essentially a signature on the log's public key. sigsum-token record [options] Format a public key as a TXT record in zone file format. sigsum-token verify [options] < token Verifies a submit token. The input on stdin is either a raw hex token or a HTTP header. ` log.SetFlags(0) if len(os.Args) < 2 { log.Fatal(usage) } switch os.Args[1] { default: log.Fatal(usage) case "help", "--help": fmt.Print(usage) os.Exit(0) case "create": var settings createSettings settings.parse(os.Args) signer, err := key.ReadPrivateKeyFile(settings.keyFile) if err != nil { log.Fatal(err) } logKey, err := key.ReadPublicKeyFile(settings.logKeyFile) if err != nil { log.Fatal(err) } signature, err := token.MakeToken(signer, &logKey) if err != nil { log.Fatalf("signing failed: %v", err) } withOutput(settings.outputFile, func(w io.Writer) error { if len(settings.domain) > 0 { _, err := fmt.Fprintf(w, "sigsum-token: %s %x\n", settings.domain, signature) return err } _, err := fmt.Fprintf(w, "%x\n", signature) return err }) case "record": var settings recordSettings settings.parse(os.Args) logKey, err := key.ReadPublicKeyFile(settings.keyFile) if err != nil { log.Fatal(err) } withOutput(settings.outputFile, func(w io.Writer) error { _, err := fmt.Fprintf(w, "%s IN TXT \"%x\"\n", token.Label, logKey) return err }) case "verify": var settings verifySettings settings.parse(os.Args) if settings.quiet { log.SetOutput(nil) } logKey, err := key.ReadPublicKeyFile(settings.logKeyFile) if err != nil { log.Fatal(err) } contents, err := io.ReadAll(os.Stdin) if err != nil { log.Fatalf("Reading input failed: %v", err) } input := string(contents) var domain *string var signatureHex string if colon := strings.Index(input, ":"); colon >= 0 { if !strings.EqualFold(input[:colon], token.HeaderName) { log.Fatalf("Invalid header, expected a %s:-line", token.HeaderName) } headerValue := strings.TrimLeft(input[colon+1:], " \t") parts := strings.Split(headerValue, " ") if len(parts) != 2 { log.Fatalf("Invalid Sigsum-Token value: %q", headerValue) } domain = &parts[0] if len(settings.domain) > 0 && !strings.EqualFold(*domain, settings.domain) { log.Fatalf("Unexpected domain: %q", *domain) } signatureHex = strings.TrimSuffix(parts[1], "\n") } else { signatureHex = strings.TrimSpace(input) if len(settings.domain) > 0 { domain = &settings.domain } } signature, err := crypto.SignatureFromHex(signatureHex) if err != nil { log.Fatalf("Invalid hex signature: %v", err) } if domain != nil { if err := token.NewDnsVerifier(&logKey).Verify( context.Background(), &token.SubmitHeader{Domain: *domain, Token: signature}); err != nil { log.Fatalf("Verifying with domain %q failed: %v", *domain, err) } } if len(settings.keyFile) > 0 { key, err := key.ReadPublicKeyFile(settings.keyFile) if err != nil { log.Fatal(err) } if err := token.VerifyToken(&key, &logKey, &signature); err != nil { log.Fatalf("Verifying using given key failed: %v", err) } } } } func newOptionSet(args []string, parameters string) *getopt.Set { set := getopt.New() set.SetProgram(os.Args[0] + " " + os.Args[1]) set.SetParameters(parameters) return set } // Also adds and processes the help option. func parseNoArgs(set *getopt.Set, args []string, usage string) { help := false set.FlagLong(&help, "help", 0, "Display help") err := set.Getopt(args[1:], nil) // Check help first; if seen, ignore errors about missing mandatory arguments. if help { set.PrintUsage(os.Stdout) fmt.Print(usage) os.Exit(0) } if err != nil { log.Printf("err: %v\n", err) set.PrintUsage(log.Writer()) os.Exit(1) } if set.NArgs() > 0 { log.Fatal("Too many arguments.") } } func (s *createSettings) parse(args []string) { set := newOptionSet(args, "") set.FlagLong(&s.keyFile, "signing-key", 'k', "Private key for signing", "file").Mandatory() set.Flag(&s.outputFile, 'o', "Output", "file") set.FlagLong(&s.logKeyFile, "log-key", 0, "Log's public key", "file").Mandatory() set.FlagLong(&s.domain, "domain", 0, "Domain") parseNoArgs(set, args, ` Create a token for submissions to the the given log, essentially a signature on the log's public key. If --domain is given, output a complete HTTP header. `) } func (s *recordSettings) parse(args []string) { set := newOptionSet(args, "") set.FlagLong(&s.keyFile, "key", 'k', "Public key", "file").Mandatory() set.Flag(&s.outputFile, 'o', "Output", "file") parseNoArgs(set, args, ` Format the public key as a TXT record in zone file format. `) } func (s *verifySettings) parse(args []string) { set := newOptionSet(args, "< token") set.FlagLong(&s.keyFile, "key", 'k', "Public key", "file") set.FlagLong(&s.logKeyFile, "log-key", 0, "Log's public key", "file").Mandatory() set.FlagLong(&s.domain, "domain", 0, "Domain") set.FlagLong(&s.quiet, "quiet", 'q', "Quiet mode") parseNoArgs(set, args, ` Verifies a submit token. The input on stdin is either a raw hex token or a HTTP header. For a raw token, one of -k or --domain is required. For a HTTP header, --key and --domain are optional, but validation fails if they are inconsistent with what's looked up from the HTTP header. The -q (quiet) option suppresses output on validation errors, with result only reflected in the exit code. `) } // If outputFile is non-empty: open file, pass to f, and automatically // close it after f returns. Otherwise, just pass os.Stdout to f. Also // exit program on error from f. func withOutput(outputFile string, f func(io.Writer) error) { file := os.Stdout if len(outputFile) > 0 { var err error file, err = os.OpenFile(outputFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) if err != nil { log.Fatalf("failed to open file '%v': %v", outputFile, err) } defer file.Close() } err := f(file) if err != nil { log.Fatalf("writing output failed: %v", err) } } sigsum-go-0.7.2/cmd/sigsum-verify/000077500000000000000000000000001455374457700170335ustar00rootroot00000000000000sigsum-go-0.7.2/cmd/sigsum-verify/sigsum-verify.go000066400000000000000000000047151455374457700222020ustar00rootroot00000000000000package main import ( "fmt" "io" "log" "os" "strings" "github.com/pborman/getopt/v2" "sigsum.org/sigsum-go/pkg/crypto" "sigsum.org/sigsum-go/pkg/key" "sigsum.org/sigsum-go/pkg/policy" "sigsum.org/sigsum-go/pkg/proof" ) type Settings struct { rawHash bool proofFile string submitKey string policyFile string } func main() { log.SetFlags(0) var settings Settings settings.parse(os.Args) submitKey, err := key.ReadPublicKeyFile(settings.submitKey) if err != nil { log.Fatal(err) } msg, err := readMessage(os.Stdin, settings.rawHash) if err != nil { log.Fatal(err) } f, err := os.Open(settings.proofFile) if err != nil { log.Fatalf("opening file %q failed: %v", settings.proofFile, err) } var pr proof.SigsumProof if err := pr.FromASCII(f); err != nil { log.Fatalf("invalid proof: %v", err) } policy, err := policy.ReadPolicyFile(settings.policyFile) if err != nil { log.Fatalf("failed to create policy: %v", err) } if err := pr.Verify(&msg, &submitKey, policy); err != nil { log.Fatalf("sigsum proof failed to verify: %v", err) } } func (s *Settings) parse(args []string) { const usage = ` Verifies a sigsum proof, as produced by sigsum-submit. The proof file is passed on the command line. The message being verified is the hash of the data on stdin (or if --raw-hash is given, input is the hash value, either exactly 32 octets, or a hex string). ` set := getopt.New() set.SetParameters("proof < input") help := false set.FlagLong(&s.rawHash, "raw-hash", 0, "Input is already hashed") set.FlagLong(&s.submitKey, "key", 'k', "Submitter's public key", "file").Mandatory() set.FlagLong(&s.policyFile, "policy", 'p', "Sigsum policy", "file").Mandatory() set.FlagLong(&help, "help", 0, "Display help") err := set.Getopt(args, nil) // Check help first; if seen, ignore errors about missing mandatory arguments. if help { set.PrintUsage(os.Stdout) fmt.Print(usage) os.Exit(0) } if err != nil { fmt.Printf("err: %v\n", err) fmt.Fprint(os.Stderr, usage) os.Exit(1) } if set.NArgs() != 1 { log.Fatalf("no proof given on command line") } s.proofFile = set.Arg(0) } func readMessage(r io.Reader, rawHash bool) (crypto.Hash, error) { if !rawHash { return crypto.HashFile(r) } data, err := io.ReadAll(r) if err != nil { return crypto.Hash{}, err } if len(data) == crypto.HashSize { var msg crypto.Hash copy(msg[:], data) return msg, nil } return crypto.HashFromHex(strings.TrimSpace(string(data))) } sigsum-go-0.7.2/cmd/sigsum-witness/000077500000000000000000000000001455374457700172235ustar00rootroot00000000000000sigsum-go-0.7.2/cmd/sigsum-witness/sigsum-witness.go000066400000000000000000000131771455374457700225640ustar00rootroot00000000000000package main import ( "context" "errors" "fmt" "log" "net/http" "os" "os/signal" "sync" "syscall" "time" "github.com/dchest/safefile" "github.com/pborman/getopt/v2" "sigsum.org/sigsum-go/pkg/api" "sigsum.org/sigsum-go/pkg/crypto" "sigsum.org/sigsum-go/pkg/key" "sigsum.org/sigsum-go/pkg/requests" "sigsum.org/sigsum-go/pkg/server" "sigsum.org/sigsum-go/pkg/types" ) type Settings struct { keyFile string logKey string stateFile string prefix string hostAndPort string } func main() { log.SetFlags(0) var settings Settings settings.parse(os.Args) signer, err := key.ReadPrivateKeyFile(settings.keyFile) if err != nil { log.Fatal(err) } pub := signer.Public() logPub, err := key.ReadPublicKeyFile(settings.logKey) if err != nil { log.Fatal(err) } state := state{fileName: settings.stateFile} if err := state.Load(&pub, &logPub); err != nil { log.Fatal(err) } witness := witness{ signer: signer, logPub: logPub, state: &state, } httpServer := http.Server{ Addr: settings.hostAndPort, Handler: server.NewWitness(&server.Config{Prefix: settings.prefix}, &witness), } var wg sync.WaitGroup defer wg.Wait() wg.Add(1) go func() { defer wg.Done() err := httpServer.ListenAndServe() if err != http.ErrServerClosed { log.Fatal(err) } }() sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) <-sigs shutdownCtx, _ := context.WithTimeout(context.Background(), 10*time.Second) httpServer.Shutdown(shutdownCtx) } func (s *Settings) parse(args []string) { const usage = ` Provides a service for cosigning a sigsum log (currently, only a single log), listening on the given host and port. ` set := getopt.New() set.SetParameters("host:port") set.SetUsage(func() { fmt.Print(usage) }) help := false set.FlagLong(&s.keyFile, "signing-key", 'k', "Witness private key", "file").Mandatory() set.FlagLong(&s.logKey, "log-key", 0, "Log public key", "file").Mandatory() // TODO: Better name? set.FlagLong(&s.stateFile, "state-file", 0, "Name of state file", "file").Mandatory() set.FlagLong(&s.prefix, "url-prefix", 0, "Prefix preceding the endpoint names", "string") set.FlagLong(&help, "help", 0, "Display help") err := set.Getopt(args, nil) // Check help first; if seen, ignore errors about missing mandatory arguments. if help { set.PrintUsage(os.Stdout) fmt.Print(usage) os.Exit(0) } if err != nil { fmt.Printf("err: %v\n", err) fmt.Fprint(os.Stderr, usage) os.Exit(1) } if set.NArgs() != 1 { log.Fatal("Mandatory HOST:PORT argument missing") } s.hostAndPort = set.Arg(0) } type witness struct { signer crypto.Signer logPub crypto.PublicKey state *state } func (s *witness) GetTreeSize(_ context.Context, req requests.GetTreeSize) (uint64, error) { if req.KeyHash != crypto.HashBytes(s.logPub[:]) { return 0, api.ErrNotFound } return s.state.GetSize(), nil } func (s *witness) AddTreeHead(_ context.Context, req requests.AddTreeHead) (types.Cosignature, error) { logKeyHash := crypto.HashBytes(s.logPub[:]) if req.KeyHash != logKeyHash { return types.Cosignature{}, api.ErrNotFound } if !req.TreeHead.Verify(&s.logPub) { return types.Cosignature{}, api.ErrForbidden } return s.state.Update(&req.TreeHead, req.OldSize, &req.Proof, func() (types.Cosignature, error) { return req.TreeHead.Cosign(s.signer, &logKeyHash, uint64(time.Now().Unix())) }) } type state struct { fileName string // Syncronizes all updates to both the size field and the // underlying file. m sync.Mutex th types.TreeHead } func (s *state) Load(pub, logPub *crypto.PublicKey) error { f, err := os.Open(s.fileName) if err != nil { if !errors.Is(err, os.ErrNotExist) { return err } s.th = types.TreeHead{ Size: 0, RootHash: crypto.HashBytes([]byte{}), } return nil } defer f.Close() var cth types.CosignedTreeHead if err := cth.FromASCII(f); err != nil { return err } if !cth.Verify(logPub) { return fmt.Errorf("Invalid log signature on stored tree head") } keyHash := crypto.HashBytes(pub[:]) logKeyHash := crypto.HashBytes(logPub[:]) for _, cs := range cth.Cosignatures { if cs.KeyHash != keyHash { continue } if cs.Verify(pub, &logKeyHash, &cth.TreeHead) { s.th = cth.SignedTreeHead.TreeHead return nil } return fmt.Errorf("Invalid cosignature on stored tree head") } return fmt.Errorf("No matching cosignature on stored tree head") } func (s *state) GetSize() uint64 { s.m.Lock() defer s.m.Unlock() return s.th.Size } // Must be called with lock held. func (s *state) Store(cth *types.CosignedTreeHead) error { if cth.Size < s.th.Size { // TODO: Panic? return fmt.Errorf("cosigning an old tree, internal error") } f, err := safefile.Create(s.fileName, 0644) if err != nil { return err } defer f.Close() if err := cth.ToASCII(f); err != nil { return err } // Atomically replace old file with new. return f.Commit() } // On success, returns stored cosignature. On failure, returns HTTP status code and error. func (s *state) Update(sth *types.SignedTreeHead, oldSize uint64, proof *types.ConsistencyProof, cosign func() (types.Cosignature, error)) (types.Cosignature, error) { s.m.Lock() defer s.m.Unlock() if s.th.Size != oldSize { return types.Cosignature{}, api.ErrConflict } if err := proof.Verify(&s.th, &sth.TreeHead); err != nil { return types.Cosignature{}, api.ErrUnprocessableEntity } cs, err := cosign() if err != nil { return types.Cosignature{}, err } cth := types.CosignedTreeHead{ SignedTreeHead: *sth, Cosignatures: []types.Cosignature{cs}, } if err := s.Store(&cth); err != nil { return types.Cosignature{}, err } s.th = sth.TreeHead return cs, nil } sigsum-go-0.7.2/doc/000077500000000000000000000000001455374457700142245ustar00rootroot00000000000000sigsum-go-0.7.2/doc/monitor.md000066400000000000000000000114051455374457700162360ustar00rootroot00000000000000# Sigsum monitor A Sigsum monitor is required to be able to detect, and hence act, on unexpected or unauthorized signatures appear in a log. This file documents the `sigsum-monitor` program and the corresponding library included in sigsum-go. Using the `sigsum-monitor` as-is provides the monitoring needed for key-usage transparency. Additional processing of leaves of interest could be done either as post-processing the output of the program, or by writing an extended monitor on top of the library. ## Cryptographic operations For each log, the monitor repeatedly fetches the latest tree head, and verifies the log's signature. (It should also verify cosignatures of known witnesses, and use cosignature timestamps for freshness checks, but that is not yet implemented). As the tree grows, the monitor asks for all the new leaves, and corresponding inclusion proofs, to ensure that it gets to see all leaves included in the log. For each new leaf, the monitor compares the submitter's key hash with the monitor's list of configured keys, and for keys that match, the signature is verified, and the leaf is output. As a special case, it is possible to run the monitor with an empty list of submitter keys; in that case, all new leaves are output, but without any verification of leaf signatures. ## The sigsum-monitor program This program can monitor one or several logs. It is configured with a sigsum policy file, listing logs and witnesses, and a list of public keys of interest. It writes one line to standard out for each leaf carrying a signature from one of the listed keys, and one line for each detected problem in the log. This output could be used by non-cryptographic monitoring tools, to file issues or send out notifications. There are a few missing features: Witness cosignatures are not processed at all (it is desirable to log an alert if a witness disappears, or if too many witnesses disappears so that the policy quorum isn't satisfied). The precise format of the output is not yet stable or documented, it may also be useful with a mode with more structured output, e.g., in json format. ### Invocation The `sigsum-monitor` has one mandatory option, `-p`, specifying the sigsum [policy file](./policy.md), and a few optional options. It takes the list of submitters' public key files as non-option command line arguments. The options are: `--interval` for specifying how often to query logs for new tree head, `--diagnostics` for specifying the level of diagnostic output written to standard error, and `--state-directory` for specifying a directory where the monitor's state is stored, so that it can be stopped and restarted without starting over from the start of the log. ## Monitor state For each log, the monitor records the most recently seen tree head, and the number of leaves that the monitor has downloaded and verified (when a monitor is far behind a log, it processes leaves in smaller batches). TODO: Consider if it really makes sense to store the tree head signature; it could be demoted to a write-only log, to be used only for later troubleshooting. When the monitor's state is persisted to disk (using the `--state-directory` option), the directory can hold one file per log, with name being the lowercase hex hash of the log's key. The contents of the file is an ASCII-format signed tree head. Format is the same as returned by the `get-tree-head` request to the log, see [sigsum protocol][], except that there are no cosignature lines. This tree head is followed by an empty line, and a line "next_leaf_index=NUMBER". [sigsum protocol]: https://git.glasklar.is/sigsum/project/documentation/-/blob/log.md-release-v1.0.0/log.md ## Monitor go package The corresponding go library, `sigsum.org/pkg/monitor` is work in progress. The main parts are: ### Config The `monitor.Config` defines the configuration shared between logs. The submit keys to watch, the query interval and the batch size, and most importantly, the application's `monitor.Callbacks` interface, see below. ### Callback interface The `monitor.Callback` interface includes call back functions invoked by the monitor when a new tree head is seen, when new leaves are seen, and when any problems with the log are observed. ### MonitorLog The `monitor.MonitorLog` function monitors a single log. This is a blocking function, intended to be called in its own goroutine. ### StartMonitoring The `monitor.StartMonitoring` takes a sigsum policy as input, and spawns one monitoring goroutine for each log, and returns immediately. It returns a channel, that is closed after monitor has been terminated. To stop the monitoring from the application, first cancel the passed in context, and then wait on that channel. sigsum-go-0.7.2/doc/policy.md000066400000000000000000000130201455374457700160410ustar00rootroot00000000000000# Sigsum policy file Documentation of how to specifying sigsum policy. ## What is a "policy" A sigsum policy includes three pieces of information. * A set of known logs. * A set of known witnesses. * The quorum: the rule that determines whether or not a subset of witnesses that have cosigned a tree head is strong enough to consider the tree head to be valid. The policy can be used and enforced by several of the sigsum roles, but the most important use is for verifying a [sigsum proof](./sigsum-proof.md). The policy says that a tree head is considered valid if it is signed by any one of the listed logs, and it is cosigned according to the defined quorum rule. Both logs and witnesses are identified primarily by their respective public key. Each log or witness may also have an associated URL; this is required for operations interacting with the log or witness, but no URLs are needed if the policy is used only for offline verification. Witnesses also have a name. These names are used only for referencing the witnesses in the definition of the quorum (directly, or indirectly via group definitions); they have no meaning outside of the policy file itself. We will look at an example policy, before specifying the contents of the policy file in detail. ## Example policy This is an example of a policy with a quorum defined using two levels of groups. Actual keys are elided, for brevity, and the optional URLs are omitted: ``` log witness X1 witness X2 witness X3 group X-witnesses 2 X1 X2 X3 witness Y1 witness Y2 witness Y2 group Y-witnesses any Y1 Y2 Y2 group X-and-Y all X-witnesses Y-witnesses quorum X-and-Y ``` This policy file lists a single log and six witnesses. The witnesses are divided into two groups, e.g, because the `X-witnesses` are operated by one organization, and the `Y-witnesses` are operated by a separate organization. The number "2" in the definition of the `X-witnesses` is a threshold. It means that if at least two out of these three witnesses have cosigned (or "witnessed") a tree head, then the group is considered to have witnessed that same tree head. The keywords "all" and "any" can be used instead of a numeric threshold. The quorum definition in this example means that when verifying a cosigned tree head, it is required that there are valid cosignatures from at least two of the `X-witnesses`, and from at least one of the `Y-witnesses`, and this rule is represented by the group `X-and-Y`. ## Policy file syntax and structure The policy file is line based, where each line consist of items separated by white space. Comments are written with "#" and extend to the end of the line. Public keys are written in raw hex representation. (The `sigsum-key hex` command can be used to convert a public key in openssh format to raw hex, and `sigsum-key hex-to-pub` for the opposite conversion.) Lines defining witnesses and logs can appear in any order; the order does not imply any preference or priority. A line defining a group can only reference names of groups and witnesses defined on preceding lines. Similarly, the quorum line must specify a witness or group defined earlier. ### Defining a log A log is defined by a line ``` log [] ``` When the policy is used for verifying a sigsum proof, all of the listed logs are accepted. When the policy is used for submitting a new entry to a log, any of the logs that has an associated URL can be used. (The `sigsum-submit` tool tries them in randomized order, until logging succeeds). ### Defining a witness A witness is defined by a line ``` witness [] ``` Since only logs and possibly monitors interact directly with witnesses, most policy files will not need any witness URLs. The name is used to refer to this witness when defining the quorum, or when defining witness groups. ### Defining the quorum The quorum is defined by a line ``` quorum ``` In the simplest case, the name refers to a witness, and it means that a cosignature from that witness is required for a tree head to be considered valid. To not require any cosignatures a all, one can use the predefined name `none`, like ``` quorum none ``` To define more interesting quorums, the name can also refer to a witness group, the next topic. In either case, the name must be properly defined on a line preceding the quorum definition. A policy file must include exactly one quorum line. TODO: Make quorum line syntactically optional, i.e., don't fail when parsing a policy with no quorum; only fail later if the policy is used to verify a tree head, e.g., the `policy.VerifyCosignedTreeHead` would fail. Potentially useful for log server policy, since a log server needs public keys and urls for witnesses, but has no need for a quorum to verify its own tree heads. ### Defining a witness group Defining a witness group is required for defining a quorum that is not a single witness. A group is defined by a line of one of these forms: ``` group all ... group any ... group ... ``` All these defines a group, where the group is considered to witnes a tree head if and only if at least k of its n members have witnessed that tree head, each group member being either a witness or another group. In this terminology, for a single witness, "witnessing" is the same as cosigning. Like for the quorum definition, a group definition can only refer to names defined on preceding lines. (This also rules out circular group definitions). The `any` variant is a shorthand for k = 1, and the `all` variant is a shorthand for k = n. sigsum-go-0.7.2/doc/sigsum-proof.md000066400000000000000000000113761455374457700172100ustar00rootroot00000000000000# What is a Sigsum proof? In short, a "sigsum proof" is a proof of public logging, which is distributed and verified in a similar way as a detached signature (as created by, e.g., `gpg --detach-sign`). This document describes the contents of such a proof, and the corresponding steps needed to verify it. ## Example use case To take a step back, the idea is that one party, the *submitter*, wants to distribute a message together with proof that the message is publicly logged. See [design](https://git.glasklar.is/sigsum/project/documentation/-/blob/main/design.md) for the bigger picture on why that is useful. The submitter interacts with a sigsum log server to submit the message, and collect related inclusion proof and cosignatures. This is packaged, together with the message and any associated data, to be distributed to a *verifier*. As concrete usecase, consider distribution of software updates. Then the logged message is the hash of a software update. The update and the sigsum proof is distributed to the devices to be updated. The software update client then uses the sigsum proof to determine whether or not the update should be installed. # Syntax/serialization In principle, each application can choose its own representation, e.g., if a sigsum proof is incorporated inside a future version of a binary debian package. The intention of this document is to both describe the parts that must be included in a sigsum proof, regardless of representation, and specify the particular format that is used by the sigsum commandline tools. ## Ascii representation Building on the [ascii format](https://git.glasklar.is/sigsum/project/documentation/-/blob/main/log.md) used on the wire when interacting with a sigsum log, we defined the following ascii format for a sigsum proof. It includes a version number, currently 1, the keyhash identifying the log that was used, the recorded sigsum leaf (but with truncated checksum), a cosigned tree head and an inclusion proof, with an empty line (i.e., double newline character) separating distinct parts. ``` version=1 log=KEYHASH leaf=SHORT-CHECKSUM KEYHASH SIGNATURE tree_size=NUMBER root_hash=HASH signature=SIGNATURE cosignature=VERSION KEYHASH TIMESTAMP SIGNATURE cosignature=... leaf_index=NUMBER node_hash=HASH node_hash=... ``` The version line specifies the version of the proof format, and will be incremented as the format is changed or extended. The `log` line identifies the sigsum log. In the next line, `leaf` is similar to the response to the `get-leaves` request, but the checksum is truncated to only the first 16 bits (4 hex digits); full checksum must be derived from other context. The last two blocks are verbatim responses from the get-tree-head and get-inclusion proof requests (in the corner case that `tree_size` = 1, the last part is omitted, since it is implied that `leaf_index` = 0, and there is no inclusion path). # Verifying a proof To verify a sigsum proof, as defined above, the verifier needs additional information: It needs to know the message being logged (in the software update usecase, `message = H(file)`, where `file` is the update package to possibly be installed). The verifier also needs the submitter's and the log's public keys, as well as public keys for some witnesses. To verify the proof, the following steps are required: 1. Compute `checksum = H(message)`, and check that it matches the truncated checksum on the leaf line. (This check serves to detect accidental mismatch between message and proof). 2. Check that the leaf keyhash equals the hash of the submitter's public key, and that the log keyhash equals the hash of a recognized log's public key. (Requiring bitwise equality defends against attacks involving multiple equivalent representations of public keys). 3. Check that the leaf signature (with `checksum` computed as above) is valid. 4. Check that the log's tree head signature is valid. 5. Verify all cosignatures for witnesses known to the verifier. Which subsets of witnesses are considered strong enough, is determined by application policy. One possible policy is to require k valid cosignatures out of n known witnesses; more complex policies are possible but out of scope for this document. 6. Compute the `leaf_hash` from the `checksum` and the other items on the leaf line, and check that the inclusion proof is valid. In the corner case that `tree_size = 1`, instead check that `leaf_hash = root_hash`. ## Use of timestamps Each cosignature timestamp is covered by the corresponding witness cosignature, and hence are required to be able to verify the cosignature. However, after a cosignature has been verified, the timestamp value is ignored by the above verification procedure. Application policy may apply additional constraints on the timestamps. sigsum-go-0.7.2/doc/tools.md000066400000000000000000000453161455374457700157170ustar00rootroot00000000000000# Sigsum command line tools Documentation of the Sigsum command line tools, including `sigsum-key`, `sigsum-submit` and `sigsum-verify`. ## Table of contents * [General conventions tool](#general-conventions) * [The sigsum-key tool](#the-sigsum-key-tool) * [The sigsum-submit tool](#the-sigsum-submit-tool) * [The sigsum-verify tool](#the-sigsum-verify-tool) * [The sigsum-token tool](#the-sigsum-token-tool) # General conventions There are several tools, some of which have sub commands, e.g., `sigsum-key gen`. The aim is that each command should address one task, e.g., `sigsum-submit` is the tool to use to submit new items to a Sigsum log, and collect proof of public logging, and `sigsum-verify` is the tool to do offline verification of such a proof. ## Configuration Command line options follow GNU conventions, with long and short options, e.g., `-k` or `--key`, and a `--help` option to display usage information. Operation of several tools is controlled by a Sigsum policy, defined by a separate [policy file](./policy.md). The location of the policy file is specified using the `--policy` option. There are no default locations for policy file or keys, and no other configuration files read by default. ## Key handling The Ed25519 digital signature scheme is used for all Sigsum signatures, hence all keys are Ed25519 keys. ### Public keys Public key files use OpenSSH format: A single line of the form ``` ssh-ed25519 [optional comment] ``` where the base64 blob in turn represent [SSH wire format](https://www.rfc-editor.org/rfc/rfc8709#name-public-key-format). In certain places, in particular, in the policy file, public keys are used in "raw" form, without this wrapping. Then an Ed25519 public key is 32 octets in the format defined by [RFC 8032](https://www.rfc-editor.org/rfc/rfc8032.html#section-5.1.2). The `sigsum-key` tool can be used to convert between these two forms. ### Private keys Private keys are stored as unencrypted OpenSSH private key files (i.e., PEM files with a tag OPENSSH PRIVATE KEY, and contents defined by [OpenSSH key format](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key)). Using unencrypted private keys on disk may be adequate for some use cases, e.g., for the key used to sign the submit tokens used for domain-based rate limiting. To support other kinds of key storage, the key can be made available via the [ssh-agent protocol](https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent). Whenever the tools need a signing key, they accept the name of either an unencrypted private key file as above, or the name of a public key file. In the latter case, the tools access the corresponding private key by connecting to the ssh-agent listening on `${SSH_AUTH_SOCK}`. For private keys of high value, it is recommended that keys are stored in a hardware token providing a signing oracle, and made accessible to appropriate users via the ssh-agent protocol. # The `sigsum-key` tool The `sigsum-key` tool can generate new keys, create and verify signatures, and convert between different key formats. ## Key generation To generate a new key pair, run ``` sigsum-key gen -o KEY-FILE ``` This generates a new Ed25519 keypair (with key material provided by the `crypto/rand` module in the golang standard library). The private key is stored to the given output KEY-FILE, in OpenSSH format. The private key is *not* encrypted, but stored with restrictive file permissions. The corresponding public key is written to a file with an added ".pub" suffix, in OpenSSH format. Behavior is similar to the OpenSSH key generation utility, if invoked like ``` ssh-keygen -q -N '' -t ed25519 -f KEY-FILE ``` ## Public key conversion As explained above, OpenSSH format is the main representation for public Sigsum keys, when stored in key files. Such a public key can be converted to a raw form using ``` sigsum-key hex -k KEY-FILE ``` The hex representation is used in the Sigsum policy file, and in messages on the wire. For the opposite conversion, use ``` sigsum-key hex-to-pub -k HEX-FILE ``` Occasionally, also the key hash is needed; it is used in certain messages on the wire, and in the Sigsum log server's [rate limit](https://git.glasklar.is/sigsum/core/log-go/-/blob/main/doc/rate-limit.md) configuration. The key hash can be computed using ``` sigsum-key hash -k KEY-FILE ``` These three conversion tools read standard input and write to standard output by default. It's optional to specify an input file, with `-k`, or output file, with `-o`. ## Sign and verify operations The `sigsum-key` tool can also create and verify signatures. Signing a message is done using ``` sigsum-key sign -k KEY-FILE [-n NAMESPACE] [-o FILE] < MSG ``` The `-k` option is required, and specifies the key to use for signing (either an unencrypted private key, or a public key, if corresponding private key is accessible via ssh-agent). The message to sign is read from standard input. If a non-empty namespace is provided, the namespace string and a NUL character is prepended to the message before it is signed with Ed25519. The created signature, in hex representation, is written to standard output, if no output file is specified with the `-o` option. Signatures can be verified using ``` sigsum-key verify -k KEY-FILE -s SIGNATURE-FILE [-n NAMESPACE] < MSG ``` The `-k `and `-s` options, specifying the public key and the signature, are required. The namespace must match the namespace used when the signature was created. The message signed is read from standard input. ## Examples Create a new private key file "example.key" and corresponding public key file "example.key.pub". ``` $ sigsum-key gen -o example.key ``` Sign a message using that key. ``` $ echo Hello | sigsum-key sign -k example.key -o hello.sign ``` Verify the signature, with exit code indicating success or failure. On success, there is no output. ``` $ echo Hello | sigsum-key verify -s hello.sign -k example.key.pub $ echo Helloo | sigsum-key verify -s hello.sign -k example.key.pub signature is not valid ``` Convert key to raw hex format. ``` $ sigsum-key hex -k example.key.pub e0863b18794d2150f3999590e0e508c09068b9883f05ea65f58cfc0827130e92 ``` # The `sigsum-submit` tool The `sigsum-submit` tool is used to create and/or submit add-leaf requests to a Sigsum log (as defined in the [Sigsum spec](https://git.glasklar.is/sigsum/project/documentation/-/blob/main/log.md). To create and immediately submit one or more requests, pass both of the `-k` (signing key) and `-p` (policy) options, described below. To separate these two parts of the process (e.g., if the machine with access to the private signing key does not have Internet connectivity), first run `sigsum-submit -k` to create and sign the request. Collect the output, which in this case is the body of a Sigsum add-leaf request, and pass that as input input to `sigsum -p` later on, to submit it to a log. ## Inputs Each input to `sigsum-submit` is either a message, a message hash, or a leaf request, depending on other options. Input files are provided on the command line; if no arguments are provided, a single input is read from standard input. ## Outputs If the input is read from standard input, by default, the output of `sigsum-submit`, if any, is written to standard output. The `-o` option can be used to redirect output to the specified file (any existing file is overwritten). For file inputs, there's one output file for each input file. The name of the output file is constructed as follows: 1. If there's exactly one input file, and the -o option is used, output is written to that file. Any existing file is overwritten. 2. For a request output, the suffix ".req" is added to the input file name. 3. For a proof output, if the input is a request, any ".req" suffix on the input file name is stripped. Then the suffix ".proof" is added. 4. If the --output-dir option is provided, any directory part of the input file name is stripped, and the output is written as a file in the specified output directory. When output is written to a named file (i.e., not to standard output), the output is first written to a temporary file, which is atomically renamed to the specified name only on success. The tool can log various diagnostic messages, and the level of verbosity is controlled with the `--diagnostics` option, which takes an argument that can be one of "fatal", "error", "warning", "info", or "debug", the default being "info". ## Creating a request To create, and sign, a new an add-leaf request, use the `-k` option to pass a signing key. The message(s) to sign are listed as file arguments on the command line, or, by default, read from standard input. By default, the message submitted to the log is the SHA256 hash of the input. To use the input as is, without hashing, pass the `--raw-hash` option. In this case, the input data must either be exactly 32 octets, or a hex string representing 32 octets (64 digits, possibly with some leading and trailing whitespace). If the request(s) are not to be submitted right away, as described below, they are written to the respective output file(s), as described above. Any existing output files are overwritten. ## Submitting a request To submit one or more the leaf requests, specify a Sigsum policy file using the `-p` option. If the `-k` option and a signing key was provided, the leaf(s) to be submitted are the newly created ones. If no `-k` option was provided, each input should instead be a the body of an add-leaf request, which is parsed and verified. Separating signing and submission is useful if the machine with access to the signing key is not directly connected to the Internet. The policy file must specify a public key and URL for at least one log. If the policy file specifies a quorum different from "none" and corresponding witness public keys, `sigsum-submit` will not be satisfied until it has retrieved enough valid cosignatures to satisfy the quorum. If the policy file specifies URLs for more than one log, they are tried in random order. If the log(s) used are configured to apply domain-based rate limiting (as publicly accessible logs are expected to do), the `--token-key` option must be used to specify the private key used for signing a submit token, and the `--token-domain` option specifies the domain (without the special "_sigsum_v1" label) where the corresponding public key is registered. An appropriate "sigsum-token:" header is created and attached to each add-leaf request. When the inputs are provided on the command line (i.e., not read from standard input), `sigsum-submit` first checks if the corresponding output ".proof" file already exists. If it does exist, the proof is read and verified; if the proof is valid, the corresponding input is skipped, if it is not valid, `sigsum-submit` exits with an error. This way, if a `sigsum-submit` call to submit a batch of requests fails half-way for any reason, exactly the same command can be rerun and it will process only the requests for which proofs are still missing. When submitting a request, `sigsum-submit` repeats the request until it is acknowledged by the log. It keeps polling the log until it has collected all the pieces for a [Sigsum proof](./sigsum-proof.md), i.e., a cosigned tree head, with cosignatures satisfying quorum requirements, and an inclusion proof for the submitted leaf. If submission to the first log fails, or polling for the required proof material times out, `sigsum-submit` tries the next log. On submission success, the Sigsum proof is written to respective output file, as described above. ## Producing a leaf hash The `--leaf-hash` options can be used to output the hex-encoded leaf hash for the leaf to be created. This option can be used with or without `-k`, but it is mutually exclusive with `-p`. When the output is written to a file (rather than stdout), a file suffix of ".hash" is used. ## Verifying a leaf request If neither a signing key (`-k`), policy file (`-p`) or the `--leaf-hash` option is provided, `sigsum-submit` reads the leaf request(s) on the command line (or from standard input if there are no arguments). Syntax and signature of each leaf request is verified, but there is no output, just the exit code to signal success or failure. ## Examples To submit to the log server at `poc.sigsum.org`, we first need a policy file with the following two lines. ``` log 154f49976b59ff09a123675f58cb3e346e0455753c3c3b15d465dcb4f6512b0b https://poc.sigsum.org/jellyfish quorum none ``` Submit a message to this log. ``` $ echo "Hello old friend" | sigsum-submit -k example.key -p example.policy version=0 log=c9e525b98f412ede185ff2ac5abf70920a2e63a6ae31c88b1138b85de328706b leaf=9c30 5aa7e6233f9f4d2efbeb9eeef766dce8ba2aa5e8cdd3f53da94b5d59e67d92fc 40160c833571c121bfdc6a02006053a80d3e91a8b73abb4dd0e07cc3098d8e58a41921d8f5649e9fb81c9b7c6b458747c4c3b49cc08c869867100a7f7be78902 size=3 root_hash=5b0cc467f86fdd57b371e434843b571a4cb47c6a64dad4bc80d96dd7d15c63a9 signature=f6a87ce27a6df207eaaee6589ab73ac8cb5bead7bd0c0fea65556d847d11f3baea8ebdc686730f64e38000c77f5327048e73e08b7dc4de04b91f65930bedc100 leaf_index=2 node_hash=ede77b77a3bba27ea0af640d37e58281aef4459d71afdf5cf442cee8f9bebf5d ``` We can also do the submission in two steps. First create a requests, saving it to "example.req". ``` $ echo "Hello again" | sigsum-submit -k example.key | tee example.req message=07305a3200629a7b8a04f77008fa1b1f719fec3b60d4fdf2683ba60cf2956381 signature=aa5bd628d88be12d4f09feefe4bf65290b03bdeba8523fa38e396218140d79e0850132082914b08876cdc4a6041be8217402a57bfb8328310ad5407bc440060e public_key=e0863b18794d2150f3999590e0e508c09068b9883f05ea65f58cfc0827130e92 ``` Then submit it to the log. ``` $ sigsum-submit -p example.policy < example.req version=0 log=c9e525b98f412ede185ff2ac5abf70920a2e63a6ae31c88b1138b85de328706b leaf=a2ee 5aa7e6233f9f4d2efbeb9eeef766dce8ba2aa5e8cdd3f53da94b5d59e67d92fc aa5bd628d88be12d4f09feefe4bf65290b03bdeba8523fa38e396218140d79e0850132082914b08876cdc4a6041be8217402a57bfb8328310ad5407bc440060e size=4 root_hash=fd23842c67ba396cbabaa22226f3cd7737a4cc9f36c897f4fce2cc5070925dc2 signature=fb573c4365ddc71110724f40dcbda62324d5c9b8e92d9e7cbda056f4c8e45e17018e72484c9d5af6e7c38b9705ed504375c3a03c7acc5abc3827dd042d1fe100 leaf_index=3 node_hash=4b3f8b78ae7fb7e6f6925d8a6f66af4d30de9b3e3a3f66cd4b0dba2c6b5b8725 node_hash=ede77b77a3bba27ea0af640d37e58281aef4459d71afdf5cf442cee8f9bebf5d ``` We can also verify the signature on the leaf request created above. ``` $ sigsum-submit < example.req ``` # The `sigsum-verify` tool The `sigsum-verify` tool verifies a Sigsum proof, as created by `sigsum-submit`. The message to be verified is read from standard input. Like for `sigsum-submit`, by default the message is the SHA256 hash of the input data. If the `--raw-hash` options is provided, the input is used as is, without hashing, and in this case, it must be either exactly 32 octets, or a hex string representing 32 octets. The submitter's public key (`-k` option) and a policy file (`-p` option) must be provided, and the name of the proof file is the only non-option argument. The proof is considered valid if 1. the submitter's signature on the message is valid, 2. the tree head is signed by one of the logs listed in the policy, 3. there are enough cosignatures to satisfy the policy's quorum requirement, and 4. the inclusion proof ties the leaf to the signed tree head. See [Sigsum proof](./sigsum-proof.md) for more information on the meaning of a sigsum proof, and the validation criteria, ## Example Verify the proof from the first `sigsum-submit` example above, assuming the proof has been saved to the file "example.proof". ``` $ echo "Hello old friend" | sigsum-verify -k example.key.pub -p example.policy example.proof ``` # The `sigsum-token` tool The `sigsum-token` tool is used to manage the Sigsum "submit tokens" that are used for domain based rate limiting (as defined in the [Sigsum spec](https://git.glasklar.is/sigsum/project/documentation/-/blob/main/log.md), see also [rate limit configuration](https://git.glasklar.is/sigsum/core/log-go/-/blob/main/doc/rate-limit.md)). There are three sub commands, `record`, `create` and `verify`. The `record` sub command is useful when setting up the DNS record that is required for submitting to a log server with rate limits. The other two sub commands are more obscure, and are intended for scripts that need to handle submit tokens manually, e.g., to submit an add-leaf request without using the `sigsum-submit` tool. Using submit tokens requires a signing key, and it is recommended to create a separate key used exclusively for this purpose. ## Creating a DNS record for a key To use submit tokens, the corresponding public key must be registered in DNS. The `sigsum-token record` sub command formats an appropriate TXT record, in zone file format. There's one mandatory argument, `-k`, specifying the public key to use. The TXT record is written to standard output, or to the file specified with the `-o` option. ## Creating a submit token A token is a fix string, to be included in the "sigsum-token:" header in `add-leaf` requests sent to a log. One can use the same rate limit key with multiple logs, but tokens will be distinct, since they're essentially a signature on the log's public key. To create a token, use `sigsum-token create`. There are two mandatory options, `-k` to specify the signing key, i.e., the private half of the rate limit keypair, and `--log-key`, to specify the file with the log's public key. If no other options are used, the output is the token in the form of a hex string (representing an Ed25519 signature). If the `--domain` option is used, the argument to this option is the domain where the corresponding public key is registered, and then the command outputs a complete HTTP header line. Note that when using `sigsum-submit`, you don't need `sigsum-token` to create any tokens; `sigsum-submit` creates appropriate tokens for each log if you pass the `--token-key` and `--token-domain` options. ## Verifying a submit token The `sigsum-token verify` sub command reads the token to validate from standard input, and it handles both raw hex tokens, and complete HTTP headers. For a raw token, one of `-k` (public key) or `--domain` is required. For a HTTP header, `--key` and `--domain` are optional, but validation fails if they are inconsistent with what's looked up from the HTTP header. The `-q` (quiet) option suppresses output on validation errors, with result only reflected in the exit code. ## Examples Format a public key as a TXT record. ``` $ sigsum-token record -k example.key.pub _sigsum_v1 IN TXT "e0863b18794d2150f3999590e0e508c09068b9883f05ea65f58cfc0827130e92" ``` Create a token, formatted as a HTTP header. ``` $ sigsum-token create -k example.key --log-key poc.key.pub --domain test.example.org sigsum-token: test.example.org 327b93c116155a9755975a3a1847628e680e9d4fb1e6dc6e938f1b99dcc9333954c9eab1dfaf89643679a47c7a33fa2182c8f8cb8eb1222f90c55355a8b5b300 ``` sigsum-go-0.7.2/go.mod000066400000000000000000000007701455374457700145710ustar00rootroot00000000000000module sigsum.org/sigsum-go // We don't want to depend on golang version later than is available // in debian's stable or backports repos. go 1.19 require ( github.com/dchest/safefile v0.0.0-20151022103144-855e8d98f185 github.com/golang/mock v1.6.0 github.com/pborman/getopt/v2 v2.1.0 golang.org/x/net v0.5.0 golang.org/x/text v0.6.0 ) require ( golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect golang.org/x/sys v0.4.0 // indirect golang.org/x/tools v0.1.12 // indirect ) sigsum-go-0.7.2/go.sum000066400000000000000000000072521455374457700146200ustar00rootroot00000000000000github.com/dchest/safefile v0.0.0-20151022103144-855e8d98f185 h1:3T8ZyTDp5QxTx3NU48JVb2u+75xc040fofcBaN+6jPA= github.com/dchest/safefile v0.0.0-20151022103144-855e8d98f185/go.mod h1:cFRxtTwTOJkz2x3rQUNCYKWC93yP1VKjR8NUhqFxZNU= 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/pborman/getopt/v2 v2.1.0 h1:eNfR+r+dWLdWmV8g5OlpyrTYHkhVNxHBdN2cCrJmOEA= github.com/pborman/getopt/v2 v2.1.0/go.mod h1:4NtW75ny4eBw9fO1bhtNdYTlZKYX5/tBLtsOpwKIKd0= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/sync v0.0.0-20190423024810-112230192c58/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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 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-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= sigsum-go-0.7.2/internal/000077500000000000000000000000001455374457700152735ustar00rootroot00000000000000sigsum-go-0.7.2/internal/mocks/000077500000000000000000000000001455374457700164075ustar00rootroot00000000000000sigsum-go-0.7.2/internal/mocks/signer/000077500000000000000000000000001455374457700176765ustar00rootroot00000000000000sigsum-go-0.7.2/internal/mocks/signer/signer.go000066400000000000000000000006131455374457700215140ustar00rootroot00000000000000package signer import ( "sigsum.org/sigsum-go/pkg/crypto" ) // Signer implements crypto.Signer with fixed outputs. Use for tests only. type Signer struct { PublicKey crypto.PublicKey Signature crypto.Signature Error error } func (s *Signer) Public() crypto.PublicKey { return s.PublicKey } func (s *Signer) Sign(_ []byte) (crypto.Signature, error) { return s.Signature, s.Error } sigsum-go-0.7.2/internal/ssh/000077500000000000000000000000001455374457700160705ustar00rootroot00000000000000sigsum-go-0.7.2/internal/ssh/agent.go000066400000000000000000000055431455374457700175240ustar00rootroot00000000000000package ssh import ( "bytes" "encoding/binary" "fmt" "io" "net" "os" "sigsum.org/sigsum-go/pkg/crypto" ) const ( sshAgentEnv = "SSH_AUTH_SOCK" sshAgentFailure = 5 sshAgentSignRequest = 13 sshAgentSignResponse = 14 ) type Connection struct { conn io.ReadWriter } type Signer struct { publicKey crypto.PublicKey conn *Connection } func ConnectTo(sockName string) (*Connection, error) { conn, err := net.Dial("unix", sockName) return &Connection{conn: conn}, err } func Connect() (*Connection, error) { if sockName := os.Getenv(sshAgentEnv); len(sockName) > 0 { return ConnectTo(sockName) } return nil, fmt.Errorf("no ssh-agent available") } func (c *Connection) request(msg []byte) ([]byte, error) { request := serializeString(msg) _, err := c.conn.Write(request) if err != nil { return nil, err } // Read response length. lenBuf := make([]byte, 4) _, err = io.ReadFull(c.conn, lenBuf) if err != nil { return nil, err } length := binary.BigEndian.Uint32(lenBuf) if length == 0 || length > 1000 { return nil, fmt.Errorf("read from agent gave unexpected length: %d", length) } buffer := make([]byte, length) _, err = io.ReadFull(c.conn, buffer) if err != nil { return nil, err } return buffer, nil } func (c Connection) SignEd25519(publicKey *crypto.PublicKey, msg []byte) (crypto.Signature, error) { buffer, err := c.request(bytes.Join([][]byte{ []byte{sshAgentSignRequest}, serializeString(serializePublicEd25519(publicKey)), serializeString(msg), serializeUint32(0), // flags }, nil)) if err != nil { return crypto.Signature{}, err } switch msgType, body := buffer[0], buffer[1:]; msgType { case sshAgentFailure: return crypto.Signature{}, fmt.Errorf("ssh-agent refused signature request") case sshAgentSignResponse: return parseSignature(body) default: return crypto.Signature{}, fmt.Errorf("unexpected ssh-agent response, type %d", msgType) } } func (c Connection) NewSigner(publicKey *crypto.PublicKey) (*Signer, error) { // TODO: Use SSH_AGENTC_REQUEST_IDENIFIER to list public keys, // and fail if given key is not on the list. return &Signer{publicKey: *publicKey, conn: &c}, nil } func (s *Signer) Sign(message []byte) (crypto.Signature, error) { return s.conn.SignEd25519(&s.publicKey, message) } func (s *Signer) Public() crypto.PublicKey { return s.publicKey } func parseSignature(blob []byte) (crypto.Signature, error) { signature := skipPrefix(blob, bytes.Join([][]byte{ serializeUint32(83), // length of signature serializeString("ssh-ed25519"), serializeUint32(crypto.SignatureSize)}, nil)) if signature == nil { return crypto.Signature{}, fmt.Errorf("invalid signature blob") } if len(signature) != crypto.SignatureSize { return crypto.Signature{}, fmt.Errorf("bad signature length: %d", len(signature)) } var ret crypto.Signature copy(ret[:], signature) return ret, nil } sigsum-go-0.7.2/internal/ssh/agent_test.go000066400000000000000000000110161455374457700205530ustar00rootroot00000000000000package ssh import ( "bytes" "encoding/hex" "fmt" "io" "strings" "testing" "sigsum.org/sigsum-go/pkg/crypto" ) type mockConnection struct { readBuf []byte writeBuf []byte } func (c *mockConnection) Read(buf []byte) (int, error) { if len(buf) == 0 { return 0, nil } if c.readBuf == nil { return 0, fmt.Errorf("mocked read error") } if len(c.readBuf) == 0 { return 0, io.EOF } // Return bytes only one at a time. buf[0] = c.readBuf[0] c.readBuf = c.readBuf[1:] return 1, nil } func (c *mockConnection) Write(buf []byte) (int, error) { if c.writeBuf == nil { return 0, fmt.Errorf("mocked write failure") } c.writeBuf = append(c.writeBuf, buf...) return len(buf), nil } func h(ascii string) []byte { s, err := hex.DecodeString(ascii) if err != nil { panic(fmt.Errorf("invalid hex %q: %v", ascii, err)) } return s } func TestRequest(t *testing.T) { for _, table := range []struct { desc string request []byte expResponse []byte // nil for expected error expWireRequest []byte // nil for write errors wireResponse []byte }{ {"empty", []byte{}, nil, h("00000000"), h("00000000")}, {"empty body", []byte{}, []byte{5}, h("00000000"), h("0000000105")}, {"eof length", []byte{}, nil, h("00000000"), h("000000")}, {"non-empty", []byte("abc"), []byte("defg"), h("00000003616263"), h("0000000464656667")}, {"eof data", []byte("abc"), nil, h("00000003616263"), h("0000004064656667")}, {"write error", []byte("abc"), nil, nil, h("0000000464656667")}, {"read error", []byte("abc"), nil, h("00000003616263"), nil}, } { mockConn := mockConnection{} mockConn.readBuf = table.wireResponse if table.expWireRequest != nil { mockConn.writeBuf = []byte{} } c := Connection{&mockConn} response, err := c.request(table.request) if err != nil { if table.expResponse != nil { t.Errorf("%q: unexpected failure: %v", table.desc, err) } } else { if !bytes.Equal(mockConn.writeBuf, table.expWireRequest) { t.Errorf("%q: unexpected request on the wire, got %x, wanted %x", table.desc, mockConn.writeBuf, table.expWireRequest) } if table.expResponse == nil { t.Errorf("%q: unexpected success, response: %x", table.desc, response) } else if !bytes.Equal(response, table.expResponse) { t.Errorf("%q: bad response, got %x, wanted %x", table.desc, response, table.expResponse) } } } } func TestSignEd25519(t *testing.T) { privateKey := crypto.PrivateKey{17} signer := crypto.NewEd25519Signer(&privateKey) publicKey := signer.Public() msg := []byte("abc") signature, err := signer.Sign(msg) if err != nil { t.Fatalf("signing failed: %v", err) } response := serializeString(bytes.Join([][]byte{ []byte{sshAgentSignResponse}, serializeString(bytes.Join([][]byte{ serializeString("ssh-ed25519"), serializeString(signature[:]), }, nil)), }, nil)) mockConn := mockConnection{readBuf: response, writeBuf: []byte{}} c := Connection{&mockConn} resp, err := c.SignEd25519(&publicKey, msg) if err != nil { t.Errorf("SignEd25519 failed: %v", err) } else if resp != signature { t.Errorf("unexpected signature, got %x, wanted %x", resp, signature) } expRequest := h("000000430d000000330000000b7373682d656432353531390000002066e0b858" + "e462a609e66fe71370c816d8846ff103d5499a22a7fec37fdbc424a70000000361626300000000") if !bytes.Equal(mockConn.writeBuf, expRequest) { t.Errorf("unexpected request on the wire, got %x, wanted %x", mockConn.writeBuf, expRequest) } } func TestSignEd25519Fail(t *testing.T) { // Test a couple of failure cases. for _, table := range []struct { desc string wireResponse []byte expError string }{ {"agent failure message", h("0000000105"), "refused"}, {"top parse failure", h("00000000"), "unexpected length"}, {"unexpected type", h("000000010f"), "unexpected ssh-agent response"}, // Contains algorithm "ssh-ed25518" {"signature parse failure", h("000000580e000000530000000b7373682d656432353531380000004084443b7c0c7fef71eaed5acd742c6cf765b4f2af4cf901adaad0b56dccbe72f42cafe3d3649a352173b7ac38a6f702050b71f5a6212c6d5a26053daca445db0a"), "invalid signature blob"}, } { mockConn := mockConnection{readBuf: table.wireResponse, writeBuf: []byte{}} c := Connection{&mockConn} signature, err := c.SignEd25519(&crypto.PublicKey{}, []byte("msg")) if err == nil { t.Errorf("%q: unexpected success, got signature %x\n", table.desc, signature) } else if !strings.Contains(err.Error(), table.expError) { t.Errorf("%q: expected error containing %q, got: %v", table.desc, table.expError, err) } } } sigsum-go-0.7.2/internal/ssh/private.go000066400000000000000000000101631455374457700200720ustar00rootroot00000000000000package ssh import ( "bytes" "crypto/rand" "encoding/pem" "errors" "fmt" "io" "sigsum.org/sigsum-go/pkg/crypto" ) // For documentation of the openssh private key format, see // https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key // https://coolaj86.com/articles/the-openssh-private-key-format // // This implementation supports only unencrypted ed25519 keys. const pemPrivateKeyTag = "OPENSSH PRIVATE KEY" var NoPEMError = errors.New("not a PEM file") var opensshPrivateKeyPrefix = bytes.Join([][]byte{ []byte("openssh-key-v1"), []byte{0}, // cipher "none", kdf "none" serializeString("none"), serializeString("none"), serializeUint32(0), serializeUint32(1), // empty kdf, and #keys = 1 }, nil) // Deterministic variant with nonce input, for unit testing. func writePrivateKeyFile(w io.Writer, signer *crypto.Ed25519Signer, nonce [4]byte) error { pub := signer.Public() priv := signer.Private() pubBlob := serializePublicEd25519(&pub) blob := bytes.Join([][]byte{ // Prefix + first copy of public key opensshPrivateKeyPrefix, serializeString(pubBlob), // Followed by the data that could be encrypted, but isn't in our case. // Length of below data. serializeUint32(136), // Size of above is // 8 (nonce) // 51 (public part) // 68 (private part) // 4 (comment) // 5 (padding) // ---- // 136 (sum) // Add nonce twice, presumably to check for correct decryption nonce[:], nonce[:], // Private key is public key + additional private parameters. pubBlob, // Finally, the ssh secret key, which includes the raw public // key once more. serializeUint32(64), // Length of private + public key priv[:], pub[:], // Empty comment. serializeUint32(0), // Padding []byte{1, 2, 3, 4, 5}, }, nil) return pem.Encode(w, &pem.Block{Type: pemPrivateKeyTag, Bytes: blob}) } func WritePrivateKeyFile(w io.Writer, signer *crypto.Ed25519Signer) error { var nonce [4]byte _, err := rand.Read(nonce[:]) if err != nil { return err } return writePrivateKeyFile(w, signer, nonce) } func ParsePrivateKeyFile(ascii []byte) (crypto.PublicKey, *crypto.Ed25519Signer, error) { parseBlob := func(blob []byte) (crypto.PublicKey, *crypto.Ed25519Signer, error) { blob = skipPrefix(blob, opensshPrivateKeyPrefix) if blob == nil { return crypto.PublicKey{}, nil, fmt.Errorf("invalid or encrypted private key") } publicKeyBlob, blob := parseString(blob) if blob == nil { return crypto.PublicKey{}, nil, fmt.Errorf("invalid private key, pubkey missing") } pub, err := parsePublicEd25519(publicKeyBlob) if err != nil { return crypto.PublicKey{}, nil, fmt.Errorf("invalid private key, pubkey invalid: %w", err) } length, blob := parseUint32(blob) if blob == nil || int64(length) != int64(len(blob)) || length%8 != 0 { return crypto.PublicKey{}, nil, fmt.Errorf("invalid private key") } n1, blob := parseUint32(blob) n2, blob := parseUint32(blob) if blob == nil || n1 != n2 { return crypto.PublicKey{}, nil, fmt.Errorf("invalid private key, bad nonce") } blob = skipPrefix(blob, publicKeyBlob) if blob == nil { return crypto.PublicKey{}, nil, fmt.Errorf("invalid private key, inconsistent public key") } keys, blob := parseString(blob) if blob == nil { return crypto.PublicKey{}, nil, fmt.Errorf("invalid private key, private key missing") } // The keys blob consists of the 32-byte private key + // 32 byte public key. if len(keys) != 64 { return crypto.PublicKey{}, nil, fmt.Errorf("unexpected private key size: %d", len(keys)) } if !bytes.Equal(pub[:], keys[32:]) { return crypto.PublicKey{}, nil, fmt.Errorf("inconsistent public key") } var privateKey crypto.PrivateKey copy(privateKey[:], keys[:32]) signer := crypto.NewEd25519Signer(&privateKey) if signer.Public() != pub { return crypto.PublicKey{}, nil, fmt.Errorf("inconsistent private key") } return pub, signer, nil } block, _ := pem.Decode(ascii) if block == nil { return crypto.PublicKey{}, nil, NoPEMError } if block.Type != pemPrivateKeyTag { return crypto.PublicKey{}, nil, fmt.Errorf("unexpected PEM tag: %q", block.Type) } return parseBlob(block.Bytes) } sigsum-go-0.7.2/internal/ssh/private_test.go000066400000000000000000000051201455374457700211260ustar00rootroot00000000000000package ssh import ( "bytes" "encoding/hex" "testing" "sigsum.org/sigsum-go/pkg/crypto" ) func TestParsePrivateKeyFile(t *testing.T) { // Generated with ssh-keygen -q -N '' -t ed25519 -f test.key testPriv := []byte( `-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW QyNTUxOQAAACCA7NJS5FcoZ5MTq9ad2sujyYF+KwjHjZRV6Q8maqHQeAAAAJjnOhbl5zoW 5QAAAAtzc2gtZWQyNTUxOQAAACCA7NJS5FcoZ5MTq9ad2sujyYF+KwjHjZRV6Q8maqHQeA AAAEAwD0Vne2KfZCN+zKUSrRai+/6Vz5ivCQrvT1wU47e1SoDs0lLkVyhnkxOr1p3ay6PJ gX4rCMeNlFXpDyZqodB4AAAADm5pc3NlQGJseWdsYW5zAQIDBAUGBw== -----END OPENSSH PRIVATE KEY----- `) testPub := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIDs0lLkVyhnkxOr1p3ay6PJgX4rCMeNlFXpDyZqodB4" pub, signer, err := ParsePrivateKeyFile(testPriv) if err != nil { t.Fatalf("parsing failed: %v", err) } if got, want := signer.Private(), mustDecodeHex(t, "300f45677b629f64237ecca512ad16a2fbfe95cf98af090aef4f5c14e3b7b54a"); got != want { t.Errorf("unexpected private key: %x, expected %x", got, want) } if pub != signer.Public() { t.Errorf("inconsistent public key, doesn't match signer.Public()") } pubFromFile, err := ParsePublicEd25519(testPub) if err != nil { t.Fatalf("failed to parse pubkey file") } if pub != pubFromFile { t.Errorf("inconsistent public key, doesn't match pubkey file") } } func TestWritePrivateKeyFile(t *testing.T) { expFile := []byte( `-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtz c2gtZWQyNTUxOQAAACDGPZYiP3oZYapEsY1zR4NQFx99FB/NNkkAY+1dWkur1gAA AIgwMTIzMDEyMwAAAAtzc2gtZWQyNTUxOQAAACDGPZYiP3oZYapEsY1zR4NQFx99 FB/NNkkAY+1dWkur1gAAAEDerb7vAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AMY9liI/ehlhqkSxjXNHg1AXH30UH802SQBj7V1aS6vWAAAAAAECAwQF -----END OPENSSH PRIVATE KEY----- `) nonce := [4]byte{'0', '1', '2', '3'} priv := crypto.PrivateKey{0xde, 0xad, 0xbe, 0xef} var buf bytes.Buffer if err := writePrivateKeyFile(&buf, crypto.NewEd25519Signer(&priv), nonce); err != nil { t.Fatal(err) } if !bytes.Equal(buf.Bytes(), expFile) { t.Errorf("unexpected file:\n%s", buf.Bytes()) } _, signer, err := ParsePrivateKeyFile(buf.Bytes()) if err != nil { t.Fatalf("failed to parse private key file: %v", err) } if got := signer.Private(); got != priv { t.Errorf("unexpected privatekey %x, wanted %x", got, priv) } } func mustDecodeHex(t *testing.T, s string) (out crypto.PrivateKey) { b, err := hex.DecodeString(s) if err != nil { t.Fatal(err) } if len(b) != len(out) { t.Fatalf("unexpected length of hex data, expected %d, got %d", len(out), len(b)) } copy(out[:], b) return } sigsum-go-0.7.2/internal/ssh/public.go000066400000000000000000000027701455374457700177030ustar00rootroot00000000000000package ssh import ( "bytes" "encoding/base64" "fmt" "strings" "sigsum.org/sigsum-go/pkg/crypto" ) func serializePublicEd25519(pub *crypto.PublicKey) []byte { return bytes.Join([][]byte{ serializeString("ssh-ed25519"), serializeString(pub[:])}, nil) } func parsePublicEd25519(blob []byte) (crypto.PublicKey, error) { pub := skipPrefix(blob, bytes.Join([][]byte{ serializeString("ssh-ed25519"), serializeUint32(crypto.PublicKeySize), }, nil)) if pub == nil { return crypto.PublicKey{}, fmt.Errorf("invalid public key blob prefix") } if len(pub) != crypto.PublicKeySize { return crypto.PublicKey{}, fmt.Errorf("invalid public key length: %v", len(blob)) } var ret crypto.PublicKey copy(ret[:], pub) return ret, nil } func ParsePublicEd25519(asciiKey string) (crypto.PublicKey, error) { // Split into fields, recognizing exclusively ascii space and TAB fields := strings.FieldsFunc(asciiKey, func(c rune) bool { return c == ' ' || c == '\t' }) if len(fields) < 2 { return crypto.PublicKey{}, fmt.Errorf("invalid public key, splitting line failed") } if fields[0] != "ssh-ed25519" { return crypto.PublicKey{}, fmt.Errorf("unsupported public key type: %v", fields[0]) } blob, err := base64.StdEncoding.DecodeString(fields[1]) if err != nil { return crypto.PublicKey{}, err } return parsePublicEd25519(blob) } func FormatPublicEd25519(pub *crypto.PublicKey) string { return "ssh-ed25519 " + base64.StdEncoding.EncodeToString(serializePublicEd25519(pub)) + " sigsum key\n" } sigsum-go-0.7.2/internal/ssh/public_test.go000066400000000000000000000024551455374457700207420ustar00rootroot00000000000000package ssh import ( "testing" "sigsum.org/sigsum-go/pkg/crypto" ) func TestParsePublicEd25519(t *testing.T) { expKey, err := crypto.PublicKeyFromHex("314cb82ac8b5fe90cf18bf190afa4759b80779709f991f736f044d5e13bcbca6") if err != nil { t.Fatalf("parsing test key failed: %v", err) } for _, table := range []struct { desc string ascii string expSuccess bool }{ {"basic", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFMuCrItf6Qzxi/GQr6R1m4B3lwn5kfc28ETV4TvLym", true}, {"with newline", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFMuCrItf6Qzxi/GQr6R1m4B3lwn5kfc28ETV4TvLym\n", true}, {"with comment", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFMuCrItf6Qzxi/GQr6R1m4B3lwn5kfc28ETV4TvLym comment", true}, {"truncated b64", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFMuCrItf6Qzxi/GQr6R1m4B3lwn5kfc28ETV4TvLy comment", false}, {"truncated bin", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFMuCrItf6Qzxi/GQr6R1m4B3lwn5kfc28ETV4T comment", false}, } { key, err := ParsePublicEd25519(table.ascii) if err != nil { if table.expSuccess { t.Errorf("%q: parsing failed: %v", table.desc, err) } } else { if !table.expSuccess { t.Errorf("%q: unexpected success, should have failed", table.desc) } else if key != expKey { t.Errorf("%q: parsing gave wrong key: %x", table.desc, key) } } } } sigsum-go-0.7.2/internal/ssh/ssh.go000066400000000000000000000047711455374457700172250ustar00rootroot00000000000000// The ssh package implements utilities for working with SSH formats. // // The way values are serialized in SSH is documented in // https://www.rfc-editor.org/rfc/rfc4251#section-5. // // Use of ED25519 keys is specified in https://www.rfc-editor.org/rfc/rfc8709 // // There are also a few openssh-specific formats (outside of the IETF standards). // // The SSH signature format adopted by sigsum is documented at // https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig. // // The ssh-agent protocol is documented at // https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent. // // The private key format used by openssh is documented at // https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key package ssh import ( "bytes" "encoding/binary" "log" "math" "sigsum.org/sigsum-go/pkg/crypto" ) type bytesOrString interface{ []byte | string } func serializeUint32(x uint32) []byte { buffer := make([]byte, 4) binary.BigEndian.PutUint32(buffer, x) return buffer } func serializeString[T bytesOrString](s T) []byte { if len(s) > math.MaxInt32 { log.Panicf("string too large for ssh, length %d", len(s)) } buffer := make([]byte, 4+len(s)) binary.BigEndian.PutUint32(buffer, uint32(len(s))) copy(buffer[4:], s) return buffer } func signedDataFromHash(namespace string, hash *crypto.Hash) []byte { return bytes.Join([][]byte{ []byte("SSHSIG"), serializeString(namespace), serializeString(""), // Empty reserved string serializeString("sha256"), serializeString(hash[:])}, nil) } // Deprecated; only for backwards compatibility in SignedTreeHead.VerifyVersion0. func SignedData(namespace string, msg []byte) []byte { hash := crypto.HashBytes(msg) return signedDataFromHash(namespace, &hash) } // Skips prefix, if present, otherwise return nil. func skipPrefix(buffer []byte, prefix []byte) []byte { if !bytes.HasPrefix(buffer, prefix) { return nil } return buffer[len(prefix):] } // Skips an ssh-encoded string, including length field. func skipPrefixString[T bytesOrString](buffer []byte, prefix T) []byte { return skipPrefix(buffer, serializeString(prefix)) } func parseUint32(buffer []byte) (uint32, []byte) { if buffer == nil || len(buffer) < 4 { return 0, nil } return binary.BigEndian.Uint32(buffer[:4]), buffer[4:] } func parseString(buffer []byte) ([]byte, []byte) { length, buffer := parseUint32(buffer) if buffer == nil { return nil, nil } if int64(len(buffer)) < int64(length) { return nil, nil } return buffer[:int(length)], buffer[int(length):] } sigsum-go-0.7.2/internal/ssh/ssh_test.go000066400000000000000000000015611455374457700202560ustar00rootroot00000000000000package ssh import ( "bytes" "encoding/hex" "testing" ) func TestSerializeString(t *testing.T) { for _, tbl := range []struct { desc string in string want []byte }{ {"empty", "", []byte{0, 0, 0, 0}}, {"valid", "ö foo is a bar", bytes.Join([][]byte{{0, 0, 0, 15, 0xc3, 0xb6}, []byte(" foo is a bar")}, nil)}, } { if got, want := serializeString(tbl.in), tbl.want; !bytes.Equal(got, want) { t.Errorf("%q: got %x but wanted %x", tbl.desc, got, want) } } } func TestSignedData(t *testing.T) { msg := []byte("foo\n") namespace := "test" got := SignedData(namespace, msg) want, _ := hex.DecodeString("5353485349470000000474657374000000000000000673686132353600000020" + // echo foo | sha256sum "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c") if !bytes.Equal(got, want) { t.Errorf("got %x but wanted %x", got, want) } } sigsum-go-0.7.2/pkg/000077500000000000000000000000001455374457700142405ustar00rootroot00000000000000sigsum-go-0.7.2/pkg/api/000077500000000000000000000000001455374457700150115ustar00rootroot00000000000000sigsum-go-0.7.2/pkg/api/api.go000066400000000000000000000017571455374457700161230ustar00rootroot00000000000000// The api package defines the abstract api between sigsum servers. package api import ( "context" "sigsum.org/sigsum-go/pkg/requests" "sigsum.org/sigsum-go/pkg/submit-token" "sigsum.org/sigsum-go/pkg/types" ) // Interface for log api. type Log interface { GetTreeHead(context.Context) (types.CosignedTreeHead, error) GetInclusionProof(context.Context, requests.InclusionProof) (types.InclusionProof, error) GetConsistencyProof(context.Context, requests.ConsistencyProof) (types.ConsistencyProof, error) GetLeaves(context.Context, requests.Leaves) ([]types.Leaf, error) AddLeaf(context.Context, requests.Leaf, *token.SubmitHeader) (bool, error) } // Interface for witness api. type Witness interface { GetTreeSize(context.Context, requests.GetTreeSize) (uint64, error) AddTreeHead(context.Context, requests.AddTreeHead) (types.Cosignature, error) } // Interface for the secondary node's api. type Secondary interface { GetSecondaryTreeHead(context.Context) (types.SignedTreeHead, error) } sigsum-go-0.7.2/pkg/api/errors.go000066400000000000000000000040221455374457700166520ustar00rootroot00000000000000package api import ( "errors" "fmt" "net/http" ) // Partial success from AddLeaf, caller should retry. var ErrAccepted error = NewError(http.StatusAccepted, fmt.Errorf("Accepted")) // 202 // E.g., GetInclusionProof fails because leaf isn't included. var ErrNotFound error = NewError(http.StatusNotFound, fmt.Errorf("Not Found")) // 404 // Failure of witness AddTreeHead, caller should retry with correct // tree size. var ErrConflict error = NewError(http.StatusConflict, fmt.Errorf("Conflict")) // Failure of witness AddTreeHead, invalid consistency proof. var ErrUnprocessableEntity error = NewError(422, fmt.Errorf("Unprocessable Entity")) // Unauthorized, typically because signature is invalid, or public key // not recognized. var ErrForbidden error = NewError(http.StatusForbidden, fmt.Errorf("Forbidden")) // 403 // Error due to exceeded rate limit. var ErrTooManyRequests error = NewError(http.StatusTooManyRequests, fmt.Errorf("Too Many Requests")) // 429 // An error with an associated HTTP status code. type Error struct { statusCode int // HTTP status code for this error err error } func (e *Error) StatusCode() int { return e.statusCode } func (e *Error) Error() string { return fmt.Sprintf("(%d) %s", e.statusCode, e.err) } func (e *Error) Unwrap() error { return e.err } // An error is considered matching if the status code is the same. // Example usage: // // if errors.Is(api.ErrNotFound, err) {...} func (e *Error) Is(err error) bool { if err, ok := err.(*Error); ok { return e.statusCode == err.statusCode } return false } func NewError(statusCode int, err error) *Error { // TODO: Allow err == nil, and return nil for that case? if statusCode == 0 || statusCode == http.StatusOK || err == nil { panic(fmt.Sprintf("Invalid call to NewError, status = %d, err = %v", statusCode, err)) } return &Error{statusCode: statusCode, err: err} } func ErrorStatusCode(err error) int { var apiError *Error if errors.As(err, &apiError) { return apiError.StatusCode() } return http.StatusInternalServerError } sigsum-go-0.7.2/pkg/ascii/000077500000000000000000000000001455374457700153305ustar00rootroot00000000000000sigsum-go-0.7.2/pkg/ascii/paragraph.go000066400000000000000000000053301455374457700176250ustar00rootroot00000000000000package ascii import ( "bytes" "fmt" "io" ) const ( // To limit the amount of data we may to store a copy of. maxParagraphBufferSize = 1024 ) // Make a slice copy, reusing b's underlying array, if possible. func copySlice(b []byte, s []byte) []byte { if len(s) > cap(b) { // Allocate a new slice. return append([]byte{}, s...) } b = b[:len(s)] copy(b, s) return b } // Implements io.Reader, wrapping an underlying io.Reader. Returns EOF // when encountering a paragraph separator (double newline), passing // on only the first newline to the reader. type ParagraphReader struct { // Underlying reader. r io.Reader // Encountered end of paragraph, Read returns EOF, and // NextParagraph can be used to continue reading next // paragraph. atEnd bool // Last read character was a newline. atEndOfLine bool // Error (possibly EOF) from underlying reader. err error // Buffered left-over data. buf []byte } // If a double newline is found, return the index of the // second newline character, otherwise returns -1. func findEndOfParagraph(atEndOfLine bool, p []byte) int { if atEndOfLine && len(p) > 0 && p[0] == '\n' { return 0 } end := bytes.Index(p, []byte{'\n', '\n'}) if end >= 0 { return end + 1 } return -1 } func (pr *ParagraphReader) Read(p []byte) (int, error) { if pr.atEnd { return 0, io.EOF } n := len(p) if len(pr.buf) > 0 { if n > len(pr.buf) { n = len(pr.buf) } end := findEndOfParagraph(pr.atEndOfLine, pr.buf[:n]) if end >= 0 { copy(p[:end], pr.buf[:end]) pr.buf = pr.buf[end+1:] pr.atEnd = true return end, io.EOF } pr.atEndOfLine = (pr.buf[n-1] == '\n') copy(p[:n], pr.buf[:n]) pr.buf = pr.buf[n:] return n, nil } if pr.err != nil { return 0, pr.err } if n > maxParagraphBufferSize { n = maxParagraphBufferSize } n, pr.err = pr.r.Read(p[:n]) if n == 0 { return 0, pr.err } end := findEndOfParagraph(pr.atEndOfLine, p[:n]) if end >= 0 { pr.buf = copySlice(pr.buf, p[end+1:n]) pr.atEnd = true return end, io.EOF } pr.atEndOfLine = (p[n-1] == '\n') return n, pr.err } // Advances to next paragraph, if at a paragraph separator. Should be // called only after encountering EOF from Read. If at the end of the // data from the underlying io.Reader, returns the corresponding // error, in particular, io.EOF means that the last paragraph has been // read. func (pr *ParagraphReader) NextParagraph() error { if pr.atEnd { pr.atEnd = false pr.atEndOfLine = false if len(pr.buf) == 0 { return pr.err } return nil } if len(pr.buf) == 0 && pr.err != nil { // At end of underlying reader. return pr.err } return fmt.Errorf("not at end of paragraph") } func NewParagraphReader(r io.Reader) *ParagraphReader { return &ParagraphReader{r: r} } sigsum-go-0.7.2/pkg/ascii/paragraph_test.go000066400000000000000000000025641455374457700206720ustar00rootroot00000000000000package ascii import ( "bytes" "io" "testing" ) func TestParagraphReader(t *testing.T) { for _, table := range []struct { in string want []string }{ {"", []string{""}}, {"ab", []string{"ab"}}, {"ab\n", []string{"ab\n"}}, {"ab\nc", []string{"ab\nc"}}, {"ab\nc\n", []string{"ab\nc\n"}}, {"ab\n\n", []string{"ab\n", ""}}, {"ab\n\nc", []string{"ab\n", "c"}}, {"ab\n\nc\n", []string{"ab\n", "c\n"}}, {"ab\n\n\nc", []string{"ab\n", "\nc"}}, // Abbreviated sigsum proof. { "version=1\nlog=abc\n\nsize=3\nroot_hash=def\n\nleaf_index=2\n", []string{ "version=1\nlog=abc\n", "size=3\nroot_hash=def\n", "leaf_index=2\n", }, }, } { pr := NewParagraphReader(bytes.NewBufferString(table.in)) var nextErr error for i, want := range table.want { if nextErr != nil { t.Errorf("Failed at start of paragraph %d on input %q: %v", i, table.in, nextErr) continue } data, err := io.ReadAll(pr) if err != nil { t.Errorf("Failed for paragraph %d on input %q: %v", i, table.in, err) continue } if got := string(data); got != want { t.Errorf("Failed for paragraph %d on input %q: got %q, want %q", i, table.in, got, want) continue } nextErr = pr.NextParagraph() } if nextErr != io.EOF { t.Errorf("Unexpected result at end of data %q: got %v, want EOF", table.in, nextErr) } } } sigsum-go-0.7.2/pkg/ascii/parser.go000066400000000000000000000050231455374457700171530ustar00rootroot00000000000000// Package ascii implements an ASCII key-value parser and writer. package ascii import ( "bufio" "bytes" "fmt" "io" "strconv" "strings" "sigsum.org/sigsum-go/pkg/crypto" ) func IntFromDecimal(s string) (uint64, error) { // Use ParseUint, to not accept leading +/-. return strconv.ParseUint(s, 10, 63) } type Parser struct { scanner *bufio.Scanner } func NewParser(input io.Reader) Parser { p := Parser{bufio.NewScanner(input)} // This is like bufio.ScanLines but it doesn't strip CRs // and fails on final unterminated lines. p.scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { if i := bytes.IndexByte(data, '\n'); i >= 0 { return i + 1, data[0:i], nil } if atEOF { if len(data) > 0 { return 0, nil, io.ErrUnexpectedEOF } return 0, nil, io.EOF } return 0, nil, nil }) return p } func (p *Parser) GetEOF() error { if p.scanner.Scan() { return fmt.Errorf("garbage at end of message: %q", p.scanner.Text()) } return p.scanner.Err() } // next scans the next line, expecting it to contain a key/value pair separated // by =, where the key is name. It returns the value. func (p *Parser) next(name string) (string, error) { if !p.scanner.Scan() { if err := p.scanner.Err(); err != nil { return "", err } return "", io.EOF } line := p.scanner.Text() key, value, ok := strings.Cut(line, "=") if !ok { return "", fmt.Errorf("invalid input line: %q", line) } if key != name { return "", fmt.Errorf("invalid input line, expected %v, got key: %q", name, key) } return value, nil } func (p *Parser) GetInt(name string) (uint64, error) { v, err := p.next(name) if err != nil { return 0, err } return IntFromDecimal(v) } func (p *Parser) GetHash(name string) (crypto.Hash, error) { v, err := p.next(name) if err != nil { return crypto.Hash{}, err } return crypto.HashFromHex(v) } func (p *Parser) GetPublicKey(name string) (crypto.PublicKey, error) { v, err := p.next(name) if err != nil { return crypto.PublicKey{}, err } return crypto.PublicKeyFromHex(v) } func (p *Parser) GetSignature(name string) (crypto.Signature, error) { v, err := p.next(name) if err != nil { return crypto.Signature{}, err } return crypto.SignatureFromHex(v) } func (p *Parser) GetValues(name string, count int) ([]string, error) { v, err := p.next(name) if err != nil { return nil, err } values := strings.Split(v, " ") if len(values) != count { return nil, fmt.Errorf("bad number of values, got %d, expected %d", len(values), count) } return values, nil } sigsum-go-0.7.2/pkg/ascii/parser_test.go000066400000000000000000000061701455374457700202160ustar00rootroot00000000000000package ascii import ( "testing" "bytes" "fmt" "sigsum.org/sigsum-go/pkg/crypto" ) func TestValidIntFromDecimal(t *testing.T) { for _, table := range []struct { in string want uint64 }{ {"0", 0}, {"1", 1}, {"0123456789", 123456789}, {"9223372036854775807", (1 << 63) - 1}, } { x, err := IntFromDecimal(table.in) if err != nil { t.Errorf("error on valid input %q: %v", table.in, err) } if x != table.want { t.Errorf("failed on %q, wanted %d, got %d", table.in, table.want, x) } } } func TestInvalidIntFromDecimal(t *testing.T) { for _, in := range []string{ "", "-1", "+9", "0123456789x", "9223372036854775808", "99223372036854775808", } { x, err := IntFromDecimal(in) if err == nil { t.Errorf("no error on invalid input %q, got %d", in, x) } } } func TestParser(t *testing.T) { hash := crypto.HashBytes([]byte("x")) input := fmt.Sprintf("hash=%x\nint=12345\nvalues=a b c\n", hash) p := NewParser(bytes.NewBufferString(input)) if got, err := p.GetHash("hash"); err != nil || got != hash { if err != nil { t.Fatal(err) } t.Errorf("bad hash, got %x, wanted %x", got, hash) } if got, err := p.GetInt("int"); err != nil || got != 12345 { if err != nil { t.Fatal(err) } t.Errorf("bad int, got %d, wanted %d", got, 12345) } v, err := p.GetValues("values", 3) if err != nil { t.Fatal(err) } if len(v) != 3 { t.Errorf("unexpected number of values (wanted 3): %#v", v) } if v[0] != "a" || v[1] != "b" || v[2] != "c" { t.Errorf("unexpected values (wanted a, b,c): %#v", v) } if err := p.GetEOF(); err != nil { t.Errorf("GetEOF failure: %v", err) } } func TestParserCRLF(t *testing.T) { input := "foo=bar\r\nfoo=bar\r\n" p := NewParser(bytes.NewBufferString(input)) v, err := p.GetValues("foo", 1) if err != nil { t.Fatal(err) } if len(v) != 1 { t.Errorf("unexpected number of values (wanted 1): %#v", v) } if v[0] != "bar\r" { t.Errorf("unexpected values (wanted bar and a CR): %q", v[0]) } } func TestParserMissingNewline(t *testing.T) { input := "foo=bar\nx=y" p := NewParser(bytes.NewBufferString(input)) if _, err := p.GetValues("foo", 1); err != nil { t.Errorf("GetValues failure: %v", err) } if _, err := p.GetValues("x", 1); err == nil { t.Errorf("expected GetValues failure") } } func TestParserEOF(t *testing.T) { input := "foo=bar\n" p := NewParser(bytes.NewBufferString(input)) if _, err := p.GetValues("foo", 1); err != nil { t.Errorf("GetValues failure: %v", err) } if _, err := p.GetValues("foo", 1); err == nil { t.Errorf("expected GetValues error") } } func TestParserGetEOF(t *testing.T) { input := "foo=bar\nx=y\n" p := NewParser(bytes.NewBufferString(input)) if _, err := p.GetValues("foo", 1); err != nil { t.Errorf("GetValues failure: %v", err) } if err := p.GetEOF(); err == nil { t.Errorf("expected GetEOF failure") } } func TestParserEOFGarbage(t *testing.T) { input := "foo=bar\ngarbage" p := NewParser(bytes.NewBufferString(input)) if _, err := p.GetValues("foo", 1); err != nil { t.Errorf("GetValues failure: %v", err) } if err := p.GetEOF(); err == nil { t.Errorf("expected GetEOF failure") } } sigsum-go-0.7.2/pkg/ascii/writer.go000066400000000000000000000023751455374457700172020ustar00rootroot00000000000000package ascii import ( "fmt" "io" "sigsum.org/sigsum-go/pkg/crypto" ) func writeItem(w io.Writer, item interface{}) error { switch item := item.(type) { case []byte: _, err := fmt.Fprintf(w, "%x", item) return err case uint64: if item >= (1 << 63) { return fmt.Errorf("out of range number: %d", item) } _, err := fmt.Fprintf(w, "%d", item) return err default: return fmt.Errorf("unsupported type: %t", item) } } func WriteLine(w io.Writer, key string, first interface{}, rest ...interface{}) error { _, err := fmt.Fprintf(w, "%s=", key) if err != nil { return err } if err := writeItem(w, first); err != nil { return err } for _, i := range rest { fmt.Fprintf(w, " ") if err := writeItem(w, i); err != nil { return err } } _, err = fmt.Fprintf(w, "\n") return err } // Helpers with better type safety. func WriteInt(w io.Writer, name string, i uint64) error { return WriteLine(w, name, i) } func WriteHash(w io.Writer, name string, h *crypto.Hash) error { return WriteLine(w, name, (*h)[:]) } func WritePublicKey(w io.Writer, name string, k *crypto.PublicKey) error { return WriteLine(w, name, (*k)[:]) } func WriteSignature(w io.Writer, name string, s *crypto.Signature) error { return WriteLine(w, name, (*s)[:]) } sigsum-go-0.7.2/pkg/client/000077500000000000000000000000001455374457700155165ustar00rootroot00000000000000sigsum-go-0.7.2/pkg/client/client.go000066400000000000000000000106331455374457700173260ustar00rootroot00000000000000// The client package implements a low-level client for sigsum's http // api. Verifying appropriate signatures and cosignatures (depending // on policy) is out of scope. package client import ( "bytes" "context" "errors" "fmt" "io" "net/http" "sigsum.org/sigsum-go/pkg/api" "sigsum.org/sigsum-go/pkg/ascii" "sigsum.org/sigsum-go/pkg/requests" token "sigsum.org/sigsum-go/pkg/submit-token" "sigsum.org/sigsum-go/pkg/types" ) type Config struct { UserAgent string URL string } func New(cfg Config) *Client { return &Client{ config: cfg, client: http.Client{}, } } type Client struct { config Config client http.Client } func (cli *Client) GetSecondaryTreeHead(ctx context.Context) (sth types.SignedTreeHead, err error) { err = cli.get(ctx, types.EndpointGetSecondaryTreeHead.Path(cli.config.URL), sth.FromASCII) return } func (cli *Client) GetTreeHead(ctx context.Context) (cth types.CosignedTreeHead, err error) { err = cli.get(ctx, types.EndpointGetTreeHead.Path(cli.config.URL), cth.FromASCII) return } func (cli *Client) GetInclusionProof(ctx context.Context, req requests.InclusionProof) (proof types.InclusionProof, err error) { err = cli.get(ctx, req.ToURL(types.EndpointGetInclusionProof.Path(cli.config.URL)), proof.FromASCII) return } func (cli *Client) GetConsistencyProof(ctx context.Context, req requests.ConsistencyProof) (proof types.ConsistencyProof, err error) { err = cli.get(ctx, req.ToURL(types.EndpointGetConsistencyProof.Path(cli.config.URL)), proof.FromASCII) return } func (cli *Client) GetLeaves(ctx context.Context, req requests.Leaves) (leaves []types.Leaf, err error) { if req.StartIndex >= req.EndIndex { return nil, fmt.Errorf("invalid request, StartIndex (%d) >= EndIndex (%d)", req.StartIndex, req.EndIndex) } err = cli.get(ctx, req.ToURL(types.EndpointGetLeaves.Path(cli.config.URL)), func(r io.Reader) (err error) { leaves, err = types.LeavesFromASCII(r, req.StartIndex-req.EndIndex) return err }) return } func (cli *Client) AddLeaf(ctx context.Context, req requests.Leaf, header *token.SubmitHeader) (bool, error) { buf := bytes.Buffer{} req.ToASCII(&buf) var tokenHeader *string if header != nil { s := header.ToHeader() tokenHeader = &s } if err := cli.post(ctx, types.EndpointAddLeaf.Path(cli.config.URL), tokenHeader, &buf, nil); err != nil { if errors.Is(err, api.ErrAccepted) { return false, nil } return false, err } return true, nil } func (cli *Client) GetTreeSize(ctx context.Context, req requests.GetTreeSize) (uint64, error) { var size uint64 if err := cli.get(ctx, req.ToURL(types.EndpointGetTreeSize.Path(cli.config.URL)), func(body io.Reader) error { p := ascii.NewParser(body) var err error size, err = p.GetInt("size") if err != nil { return err } return p.GetEOF() }); err != nil { return 0, err } return size, nil } func (cli *Client) AddTreeHead(ctx context.Context, req requests.AddTreeHead) (types.Cosignature, error) { buf := bytes.Buffer{} req.ToASCII(&buf) var cs types.Cosignature if err := cli.post(ctx, types.EndpointAddTreeHead.Path(cli.config.URL), nil, &buf, cs.FromASCII); err != nil { return types.Cosignature{}, err } return cs, nil } func (cli *Client) get(ctx context.Context, url string, parseBody func(io.Reader) error) error { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return err } return cli.do(req, parseBody) } func (cli *Client) post(ctx context.Context, url string, tokenHeader *string, requestBody io.Reader, parseResponse func(io.Reader) error) error { req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, requestBody) if err != nil { return err } if tokenHeader != nil { req.Header.Add(token.HeaderName, *tokenHeader) } return cli.do(req, parseResponse) } func (cli *Client) do(req *http.Request, parseBody func(io.Reader) error) error { // TODO: redirects, see go doc http.Client.CheckRedirect req.Header.Set("User-Agent", cli.config.UserAgent) rsp, err := cli.client.Do(req) if err != nil { return fmt.Errorf("send request: %w", err) } defer rsp.Body.Close() if rsp.StatusCode == http.StatusOK && parseBody != nil { return parseBody(rsp.Body) } b, err := io.ReadAll(rsp.Body) if err != nil { return fmt.Errorf("status code %d, no server response: %w", rsp.StatusCode, err) } if rsp.StatusCode != http.StatusOK { return api.NewError(rsp.StatusCode, fmt.Errorf("server: %q", b)) } return nil } sigsum-go-0.7.2/pkg/crypto/000077500000000000000000000000001455374457700155605ustar00rootroot00000000000000sigsum-go-0.7.2/pkg/crypto/crypto.go000066400000000000000000000055521455374457700174360ustar00rootroot00000000000000// package crypto provides lowest-level crypto types and primitives used by sigsum package crypto import ( "bytes" "crypto" "crypto/ed25519" "crypto/rand" "crypto/sha256" "encoding/hex" "fmt" "io" ) const ( HashSize = sha256.Size SignatureSize = ed25519.SignatureSize PublicKeySize = ed25519.PublicKeySize PrivateKeySize = ed25519.SeedSize ) type ( Hash [HashSize]byte Signature [SignatureSize]byte PublicKey [PublicKeySize]byte PrivateKey [PrivateKeySize]byte ) type Signer interface { Sign([]byte) (Signature, error) Public() PublicKey } func HashBytes(b []byte) Hash { return sha256.Sum256(b) } func HashFile(f io.Reader) (digest Hash, err error) { h := sha256.New() if _, err = io.Copy(h, f); err != nil { return } copy(digest[:], h.Sum(nil)) return } func Verify(pub *PublicKey, msg []byte, sig *Signature) bool { return ed25519.Verify(ed25519.PublicKey(pub[:]), msg, sig[:]) } type Ed25519Signer struct { secret ed25519.PrivateKey } func NewEd25519Signer(key *PrivateKey) *Ed25519Signer { return &Ed25519Signer{secret: ed25519.NewKeyFromSeed((*key)[:])} } func (s *Ed25519Signer) Sign(msg []byte) (Signature, error) { sig, err := s.secret.Sign(nil, msg, crypto.Hash(0)) if err != nil { return Signature{}, err } if len(sig) != SignatureSize { return Signature{}, fmt.Errorf("internal error, unexpected signature size %d: ", len(sig)) } var ret Signature copy(ret[:], sig) return ret, nil } func (s *Ed25519Signer) Public() (ret PublicKey) { copy(ret[:], s.secret.Public().(ed25519.PublicKey)) return } func (s *Ed25519Signer) Private() (ret PrivateKey) { copy(ret[:], s.secret.Seed()) return } func NewKeyPair() (PublicKey, *Ed25519Signer, error) { var secret PrivateKey n, err := rand.Read(secret[:]) if err != nil { return PublicKey{}, nil, err } if n != PrivateKeySize { return PublicKey{}, nil, fmt.Errorf("key generation failed, got only %d out of %d random bytes", n, PrivateKeySize) } signer := NewEd25519Signer(&secret) return signer.Public(), signer, nil } func decodeHex(out []byte, s string) error { b, err := hex.DecodeString(s) if err != nil { return err } if len(b) != len(out) { return fmt.Errorf("unexpected length of hex data, expected %d, got %d", len(out), len(b)) } copy(out, b) return nil } func HashFromHex(s string) (h Hash, err error) { err = decodeHex(h[:], s) return } func PublicKeyFromHex(s string) (pub PublicKey, err error) { err = decodeHex(pub[:], s) return } func SignatureFromHex(s string) (sig Signature, err error) { err = decodeHex(sig[:], s) return } func SignerFromHex(s string) (*Ed25519Signer, error) { var secret PrivateKey err := decodeHex(secret[:], s) if err != nil { return nil, err } return NewEd25519Signer(&secret), nil } func AttachNamespace(namespace string, msg []byte) []byte { return bytes.Join([][]byte{[]byte(namespace), msg}, []byte{0}) } sigsum-go-0.7.2/pkg/crypto/crypto_test.go000066400000000000000000000117041455374457700204710ustar00rootroot00000000000000package crypto import ( "bytes" "encoding/hex" "strings" "testing" ) func incBytes(n int) []byte { b := make([]byte, n) for i := 0; i < len(b); i++ { b[i] = byte(i) } return b } func TestValidHashFromHex(t *testing.T) { b := incBytes(32) s := hex.EncodeToString(b) for _, in := range []string{ s, strings.ToUpper(s), } { hash, err := HashFromHex(in) if err != nil { t.Errorf("error on input %q: %v", in, err) } if !bytes.Equal(b, hash[:]) { t.Errorf("fail on input %q, wanted %x, got %x", in, b, hash) } } } func TestInvalidHashFromHex(t *testing.T) { b := incBytes(33) s := hex.EncodeToString(b) for _, in := range []string{ "", "0x11", "123z", s[:63], s[:65], s[:66], } { hash, err := HashFromHex(in) if err == nil { t.Errorf("no error on invalid input %q, got %x", in, hash) } } } func TestValidPublicKeyFromHex(t *testing.T) { b := incBytes(32) s := hex.EncodeToString(b) for _, in := range []string{ s, strings.ToUpper(s), } { hash, err := PublicKeyFromHex(in) if err != nil { t.Errorf("error on input %q: %v", in, err) } if !bytes.Equal(b, hash[:]) { t.Errorf("fail on input %q, wanted %x, got %x", in, b, hash) } } } func TestInvalidPublicKeyFromHex(t *testing.T) { b := incBytes(33) s := hex.EncodeToString(b) for _, in := range []string{ "", "0x11", "123z", s[:63], s[:65], s[:66], } { hash, err := PublicKeyFromHex(in) if err == nil { t.Errorf("no error on invalid input %q, got %x", in, hash) } } } func TestValidSignatureFromHex(t *testing.T) { b := incBytes(64) s := hex.EncodeToString(b) for _, in := range []string{ s, strings.ToUpper(s), } { hash, err := SignatureFromHex(in) if err != nil { t.Errorf("error on input %q: %v", in, err) } if !bytes.Equal(b, hash[:]) { t.Errorf("fail on input %q, wanted %x, got %x", in, b, hash) } } } func TestInvalidSignatureFromHex(t *testing.T) { b := incBytes(65) s := hex.EncodeToString(b) for _, in := range []string{ "", "0x11", "123z", s[:127], s[:129], s[:130], } { hash, err := SignatureFromHex(in) if err == nil { t.Errorf("no error on invalid input %q, got %x", in, hash) } } } func mustHashFromHex(t *testing.T, s string) Hash { hash, err := HashFromHex(s) if err != nil { t.Fatal(err) } return hash } // Basic sanity check, not intended as thorough SHA256 regression test. func TestHash(t *testing.T) { for _, table := range []struct { in string out string }{ {"", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}, {"abc", "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"}, } { want := mustHashFromHex(t, table.out) in := []byte(table.in) if got := HashBytes(in); got != want { t.Errorf("incorrect HashBytes of %q: got: %x, expected: %x", table.in, got, want) } if got, err := HashFile(bytes.NewBuffer(in)); err != nil || got != want { if err != nil { t.Fatal(err) } t.Errorf("incorrect HashFile of %q: got: %x, expected: %x", table.in, got, want) } } } func mustSignatureFromHex(t *testing.T, s string) Signature { signature, err := SignatureFromHex(s) if err != nil { t.Fatal(err) } return signature } func mustPublicKeyFromHex(t *testing.T, s string) PublicKey { pub, err := PublicKeyFromHex(s) if err != nil { t.Fatal(err) } return pub } func mustSignerFromHex(t *testing.T, s string) Signer { signer, err := SignerFromHex(s) if err != nil { t.Fatal(err) } return signer } // Basic sanity check, not intended as a thorough ed25519 test. Uses // second line from https://ed25519.cr.yp.to/python/sign.input (single // byte message). func TestSign(t *testing.T) { signer := mustSignerFromHex(t, "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb") msg := []byte{0x72} signature, err := signer.Sign(msg) if err != nil { t.Fatalf("sign failed: %v", err) } want := mustSignatureFromHex(t, "92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da"+ "085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00") if signature != want { t.Fatalf("unexpected signature value, got %x, expected %x", signature[:], want[:]) } publicKey := mustPublicKeyFromHex(t, "3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c") if !Verify(&publicKey, msg, &signature) { t.Errorf("verify on valid message and signature failed") } } func TestVerify(t *testing.T) { var secret PrivateKey copy(secret[:], incBytes(PrivateKeySize)) signer := NewEd25519Signer(&secret) pub := signer.Public() message := []byte("squeemish ossifrage") signature, err := signer.Sign(message) if err != nil { t.Fatalf("sign failed: %v", err) } if !Verify(&pub, message, &signature) { t.Errorf("verify on valid message and signature failed") } badSignature := signature badSignature[3]++ if Verify(&pub, message, &badSignature) { t.Errorf("verify on invalid signature succeeded") } message[3]++ if Verify(&pub, message, &signature) { t.Errorf("verify on modified message succeeded") } } sigsum-go-0.7.2/pkg/key/000077500000000000000000000000001455374457700150305ustar00rootroot00000000000000sigsum-go-0.7.2/pkg/key/key.go000066400000000000000000000035411455374457700161520ustar00rootroot00000000000000package key import ( "fmt" "os" "strings" "sigsum.org/sigsum-go/internal/ssh" "sigsum.org/sigsum-go/pkg/crypto" ) // Expects an Openssh public key (single-line format) func ParsePublicKey(ascii string) (crypto.PublicKey, error) { return ssh.ParsePublicEd25519(ascii) } // Supports two formats: // - Openssh private key // - Openssh public key, in which case ssh-agent is used to // access the corresponding private key. // - (Deprecated) Raw hex-encoded private key (RFC 8032) func ParsePrivateKey(ascii string) (crypto.Signer, error) { ascii = strings.TrimSpace(ascii) // Accepts public keys only in openssh format, since with raw // hex-encoded keys, we can't distinguish between public and // private keys. if strings.HasPrefix(ascii, "ssh-ed25519 ") { key, err := ssh.ParsePublicEd25519(ascii) if err != nil { return nil, err } c, err := ssh.Connect() if err != nil { return nil, fmt.Errorf("only public key available, and no ssh-agent: %v", err) } return c.NewSigner(&key) } _, signer, err := ssh.ParsePrivateKeyFile([]byte(ascii)) if err == ssh.NoPEMError { // TODO: Delete support for raw keys. signer, err = crypto.SignerFromHex(ascii) } return signer, err } func ReadPublicKeyFile(fileName string) (crypto.PublicKey, error) { contents, err := os.ReadFile(fileName) if err != nil { return crypto.PublicKey{}, err } key, err := ParsePublicKey(string(contents)) if err != nil { return crypto.PublicKey{}, fmt.Errorf("parsing public key file %q failed: %v", fileName, err) } return key, nil } func ReadPrivateKeyFile(fileName string) (crypto.Signer, error) { contents, err := os.ReadFile(fileName) if err != nil { return nil, err } signer, err := ParsePrivateKey(string(contents)) if err != nil { return nil, fmt.Errorf("parsing private key file %q failed: %v", fileName, err) } return signer, nil } sigsum-go-0.7.2/pkg/log/000077500000000000000000000000001455374457700150215ustar00rootroot00000000000000sigsum-go-0.7.2/pkg/log/log.go000066400000000000000000000043451455374457700161370ustar00rootroot00000000000000// package log provides a simple logger with leveled log messages. // // - DebugLevel (highest verbosity) // - InfoLevel // - WarningLevel // - ErrorLevel // - FatalLevel (lowest verbosity) // // Output is written to the default logger. package log import ( "fmt" "log" "os" "sync/atomic" ) type level int32 const ( DebugLevel level = iota // DebugLevel logs all messages InfoLevel // InfoLevel logs info messages and and above WarningLevel // WarningLevel logs warning messages and above ErrorLevel // ErrorLevel logs error messages and above FatalLevel // FatalLevel only logs fatal messages ) const ( tagDebug = "DEBU" tagInfo = "INFO" tagWarning = "WARN" tagError = "ERRO" tagFatal = "FATA" ) var currentLevel int32 func init() { currentLevel = int32(InfoLevel) } // SetLevel sets the logging level. Available options: DebugLevel, InfoLevel, // WarningLevel, ErrorLevel, FatalLevel. func SetLevel(lv level) { atomic.StoreInt32(¤tLevel, int32(lv)) } func SetLevelFromString(levelName string) error { switch levelName { case "debug": SetLevel(DebugLevel) case "info": SetLevel(InfoLevel) case "warning": SetLevel(WarningLevel) case "error": SetLevel(ErrorLevel) case "fatal": SetLevel(FatalLevel) default: return fmt.Errorf("invalid logging level %s", levelName) } return nil } func SetLogFile(logFile string) error { f, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return err } log.SetOutput(f) return nil } func isEnabled(lv level) bool { return level(atomic.LoadInt32(¤tLevel)) <= lv } func Debug(format string, v ...interface{}) { if isEnabled(DebugLevel) { log.Printf("["+tagDebug+"] "+format, v...) } } func Info(format string, v ...interface{}) { if isEnabled(InfoLevel) { log.Printf("["+tagInfo+"] "+format, v...) } } func Warning(format string, v ...interface{}) { if isEnabled(WarningLevel) { log.Printf("["+tagWarning+"] "+format, v...) } } func Error(format string, v ...interface{}) { if isEnabled(ErrorLevel) { log.Printf("["+tagError+"] "+format, v...) } } func Fatal(format string, v ...interface{}) { log.Fatalf("["+tagFatal+"] "+format, v...) } sigsum-go-0.7.2/pkg/log/log_test.go000066400000000000000000000006731455374457700171760ustar00rootroot00000000000000package log import ( "log" "os" ) func Example() { log.SetOutput(os.Stdout) log.SetFlags(0) // To disable date and time output. err := SetLevelFromString("warning") if err != nil { panic(err) } Debug("some debug number: %d\n", 10) Info("some info number: %d\n", 20) Warning("some warning number: %d\n", 30) Error("some error number: %d\n", 40) // Output: // [WARN] some warning number: 30 // [ERRO] some error number: 40 } sigsum-go-0.7.2/pkg/merkle/000077500000000000000000000000001455374457700155175ustar00rootroot00000000000000sigsum-go-0.7.2/pkg/merkle/merkle.go000066400000000000000000000016471455374457700173350ustar00rootroot00000000000000// package merkle provides hashing operations that can be used to verify a // Sigsum log's Merkle tree. The exact hash strategy is defined in RFC 6962. package merkle import ( "bytes" "sigsum.org/sigsum-go/pkg/crypto" ) type Prefix uint8 const ( PrefixLeafNode Prefix = iota PrefixInteriorNode ) func formatLeafNode(b []byte) []byte { prefixLeafNode := []byte{byte(PrefixLeafNode)} return bytes.Join([][]byte{prefixLeafNode, b}, nil) } func formatInternalNode(left, right *crypto.Hash) []byte { prefixInteriorNode := []byte{byte(PrefixInteriorNode)} return bytes.Join([][]byte{prefixInteriorNode, (*left)[:], (*right)[:]}, nil) } func HashLeafNode(leaf []byte) crypto.Hash { return crypto.HashBytes(formatLeafNode(leaf)) } func HashInteriorNode(left, right *crypto.Hash) crypto.Hash { return crypto.HashBytes(formatInternalNode(left, right)) } func HashEmptyTree() crypto.Hash { return crypto.HashBytes([]byte{}) } sigsum-go-0.7.2/pkg/merkle/tree.go000066400000000000000000000126731455374457700170160ustar00rootroot00000000000000package merkle import ( "fmt" "math/bits" "sigsum.org/sigsum-go/pkg/crypto" ) // Represents a compact range starting at index zero. See // https://github.com/transparency-dev/merkle/blob/main/docs/compact_ranges.md // for the general definition. type compactRange []crypto.Hash // Like append, returns the new range, but may also modify the input. func (cr compactRange) extend(i uint64, h crypto.Hash, makeNode func(left, right *crypto.Hash) crypto.Hash) compactRange { for s := i + 1; len(cr) > 0 && isEven(s); s >>= 1 { h = makeNode(&cr[len(cr)-1], &h) cr = cr[:len(cr)-1] } return append(cr, h) } // Returns a compact range for leaves starting at index zero. func newCompactRange(leaves []crypto.Hash) compactRange { cr := compactRange{} for i, leaf := range leaves { cr = cr.extend(uint64(i), leaf, HashInteriorNode) } return cr } func (cr compactRange) getRootHash() crypto.Hash { if len(cr) == 0 { return HashEmptyTree() } h := cr[len(cr)-1] for i := len(cr) - 1; i > 0; i-- { h = HashInteriorNode(&cr[i-1], &h) } return h } // Represents a tree of leaf hashes. Not concurrency safe; needs // external synchronization. type Tree struct { leafs []crypto.Hash // Maps leaf hash to index. leafIndex map[crypto.Hash]int // Compact range; hash of one power-of-two subtree per one-bit // in current size. cRange compactRange } func NewTree() Tree { return Tree{leafIndex: make(map[crypto.Hash]int)} } func (t *Tree) Size() uint64 { return uint64(len(t.leafs)) } // Returns true if added, false for duplicates. func (t *Tree) AddLeafHash(leafHash *crypto.Hash) bool { if _, ok := t.leafIndex[*leafHash]; ok { return false } h := *leafHash t.leafIndex[h] = len(t.leafs) t.leafs = append(t.leafs, h) t.cRange = t.cRange.extend(uint64(len(t.leafs))-1, h, HashInteriorNode) return true } func (t *Tree) GetLeafIndex(leafHash *crypto.Hash) (uint64, error) { if i, ok := t.leafIndex[*leafHash]; ok { return uint64(i), nil } return 0, fmt.Errorf("leaf hash not present") } func (t *Tree) GetRootHash() crypto.Hash { return t.cRange.getRootHash() } func rootOf(leaves []crypto.Hash) crypto.Hash { return newCompactRange(leaves).getRootHash() } func reversePath(p []crypto.Hash) []crypto.Hash { n := len(p) for i := 0; i < n-1-i; i++ { p[i], p[n-1-i] = p[n-1-i], p[i] } return p } // Produces inclusion path from root down (opposite to rfc 9162 order). // cRange and size represent the larger tree, where leaves is a prefix. func inclusion(leaves []crypto.Hash, m uint64, cRange []crypto.Hash, size uint64) []crypto.Hash { p := []crypto.Hash{} // Try reusing hashes of internal nodes on the cRange; useful // if m and len(leaves) are close to the end of the tree. for len(leaves) > 1 && len(cRange) > 1 { // Size of subtree represented by cRange[0] k := split(size) if m < k { // Could possibly use some other elements of // cRange, but it gets complicated. break } // k gives a valid split also for the subtree // for which we prove inclusion. p = append(p, cRange[0]) cRange = cRange[1:] size -= k leaves = leaves[k:] m -= k } for len(leaves) > 1 { n := uint64(len(leaves)) k := split(n) // We select the subtree which m is in, for further // processing, after adding the hash of the other // subtree to the path. if m < k { p = append(p, rootOf(leaves[k:])) leaves = leaves[:k] } else { p = append(p, rootOf(leaves[:k])) leaves = leaves[k:] m -= k } } return p } func (t *Tree) ProveInclusion(index, size uint64) ([]crypto.Hash, error) { if index >= size || size > t.Size() { return nil, fmt.Errorf("invalid argument index %d, size %d, tree %d", index, size, t.Size()) } return reversePath(inclusion(t.leafs[:size], index, t.cRange, t.Size())), nil } // Based on RFC 9162, 2.1.4.1, but produces path in opposite order. func consistency(leaves []crypto.Hash, m uint64, cRange []crypto.Hash, size uint64) []crypto.Hash { p := []crypto.Hash{} complete := true // Try reusing hashes of internal nodes on the cRange; useful // if m and len(leaves) are close to the end of the tree. for len(cRange) > 1 { n := uint64(len(leaves)) if m == n { break } // Size of subtree represented by cRange[0] k := split(size) if m <= k { // Could possibly use some other elements of // cRange, but it gets complicated. break } // k gives a valid split also for the subtree // for which we prove consistency. p = append(p, cRange[0]) cRange = cRange[1:] size -= k leaves = leaves[k:] m -= k complete = false } for { n := uint64(len(leaves)) if m > n { panic(fmt.Errorf("internal error, m %d, n %d", m, n)) } if m == n { if complete { return p } return append(p, rootOf(leaves)) } k := split(n) if m <= k { p = append(p, rootOf(leaves[k:])) leaves = leaves[:k] } else { p = append(p, rootOf(leaves[:k])) leaves = leaves[k:] m -= k complete = false } } } func (t *Tree) ProveConsistency(m, n uint64) ([]crypto.Hash, error) { if n > t.Size() || m > n { return nil, fmt.Errorf("invalid argument m %d, n %d, tree %d", m, n, t.Size()) } if m == 0 || m == n { return []crypto.Hash{}, nil } return reversePath(consistency(t.leafs[:n], m, t.cRange, t.Size())), nil } // Returns largest power of 2 smaller than n. Requires n >= 2. func split(n uint64) uint64 { if n < 2 { panic(fmt.Errorf("internal error, can't split %d", n)) } return uint64(1) << (bits.Len64(n-1) - 1) } func isEven(num uint64) bool { return (num & 1) == 0 } sigsum-go-0.7.2/pkg/merkle/tree_test.go000066400000000000000000000243661455374457700200570ustar00rootroot00000000000000package merkle import ( "testing" "encoding/binary" "math/bits" "math/rand" "sigsum.org/sigsum-go/pkg/crypto" ) func TestSize(t *testing.T) { hashes := newLeaves(5) tree := NewTree() for i, h := range hashes { if !tree.AddLeafHash(&h) { t.Fatalf("AddLeafHash failed at size %d", tree.Size()) } if got, want := tree.Size(), uint64(i)+1; got != want { t.Errorf("unexepcted size, got %d, want %d", got, want) } } } func TestGetLeafIndex(t *testing.T) { hashes := newLeaves(5) tree := NewTree() for _, h := range hashes { if !tree.AddLeafHash(&h) { t.Fatalf("AddLeafHash failed at size %d", tree.Size()) } } for i, h := range hashes { got, err := tree.GetLeafIndex(&h) if err != nil { t.Errorf("GetLeafIndex failed at index %d: %v", i, err) } else if got != uint64(i) { t.Errorf("incorrect index, got %d, want %d", got, i) } } } func TestInternal(t *testing.T) { tree := NewTree() for _, h := range newLeaves(100) { if !tree.AddLeafHash(&h) { t.Fatalf("AddLeafHash failed at size %d", tree.Size()) } if len(tree.leafs) != len(tree.leafIndex) { t.Fatalf("invalid state: %d leafs, %d index entries", len(tree.leafs), len(tree.leafIndex)) } if popc := bits.OnesCount(uint(len(tree.leafs))); popc != len(tree.cRange) { t.Fatalf("internal error: popc %d, len 0x%x", popc, len(tree.cRange)) } } } func TestGetRootHash(t *testing.T) { hashes := newLeaves(5) h01 := HashInteriorNode(&hashes[0], &hashes[1]) h23 := HashInteriorNode(&hashes[2], &hashes[3]) h0123 := HashInteriorNode(&h01, &h23) tree := NewTree() for i, want := range []crypto.Hash{ mustHashFromHex(t, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"), hashes[0], h01, HashInteriorNode(&h01, &hashes[2]), h0123, HashInteriorNode(&h0123, &hashes[4]), } { if tree.Size() < uint64(i) { if !tree.AddLeafHash(&hashes[tree.Size()]) { t.Fatalf("AddLeafHash failed at size %d", tree.Size()) } } if got := tree.GetRootHash(); got != want { t.Errorf("bad root hash for size %d\n got: %x\n want: %x", i, got, want) } } } func TestInclusion(t *testing.T) { hashes := newLeaves(5) h01 := HashInteriorNode(&hashes[0], &hashes[1]) h23 := HashInteriorNode(&hashes[2], &hashes[3]) h0123 := HashInteriorNode(&h01, &h23) tree := NewTree() for _, h := range hashes { if !tree.AddLeafHash(&h) { t.Fatalf("AddLeafHash failed at size %d", tree.Size()) } } // Inclusion path for index i and size 5. for i, p := range [][]crypto.Hash{ []crypto.Hash{hashes[1], h23, hashes[4]}, []crypto.Hash{hashes[0], h23, hashes[4]}, []crypto.Hash{hashes[3], h01, hashes[4]}, []crypto.Hash{hashes[2], h01, hashes[4]}, []crypto.Hash{h0123}, } { if proof, err := tree.ProveInclusion(uint64(i), 5); err != nil || !pathEqual(proof, p) { if err != nil { t.Fatalf("ProveInclusion %d, 5 failed: %v", i, err) } t.Errorf("unexpected inclusion path\n got: %x\n want: %x\n", proof, p) } } } func TestInclusionValid(t *testing.T) { hashes := newLeaves(100) rootHashes := []crypto.Hash{} tree := NewTree() for _, h := range hashes { if !tree.AddLeafHash(&h) { t.Fatalf("AddLeafHash failed at size %d", tree.Size()) } rootHashes = append(rootHashes, tree.GetRootHash()) } r := rand.New(rand.NewSource(17)) for i := 0; i < len(hashes); i++ { for n := i + 1; n <= len(hashes); n++ { proof, err := tree.ProveInclusion(uint64(i), uint64(n)) if err != nil { t.Fatalf("ProveInclusion %d, %d failed: %v", i, n, err) } leaf := hashes[i] if err := VerifyInclusion(&leaf, uint64(i), uint64(n), &rootHashes[n-1], proof); err != nil { t.Errorf("inclusion proof not valid, i %d, n %d: %v\n proof: %x\n", i, n, err, proof) } bitToFlip := r.Intn(crypto.HashSize * 8) hashToFlip := r.Intn(len(proof) + 1) if hashToFlip > 0 { proof[hashToFlip-1][bitToFlip/8] ^= 1 << (bitToFlip % 8) } else { leaf[bitToFlip/8] ^= 1 << (bitToFlip % 8) } if err := VerifyInclusion(&leaf, uint64(i), uint64(n), &rootHashes[n-1], proof); err == nil { t.Errorf("inclusion proof should have failed, i %d, n %d: flipped bit %d of hash %d\n", i, n, bitToFlip, hashToFlip) } } } } func TestInclusionBatchValid(t *testing.T) { hashes := newLeaves(100) rootHashes := []crypto.Hash{} tree := NewTree() for _, h := range hashes { if !tree.AddLeafHash(&h) { t.Fatalf("AddLeafHash failed at size %d", tree.Size()) } rootHashes = append(rootHashes, tree.GetRootHash()) } r := rand.New(rand.NewSource(17)) for i := 0; i < len(hashes); i++ { for n := i + 1; n <= len(hashes); n++ { sProof, err := tree.ProveInclusion(uint64(i), uint64(n)) if err != nil { t.Fatalf("ProveInclusion %d, %d failed: %v", i, n, err) } batchSize := 1 + r.Intn(n-i) eProof, err := tree.ProveInclusion(uint64(i+batchSize-1), uint64(n)) if err != nil { t.Fatalf("ProveInclusion %d, %d failed: %v", i+batchSize-1, n, err) } leaves := make([]crypto.Hash, batchSize) copy(leaves, hashes[i:]) if err := VerifyInclusionBatch(leaves, uint64(i), uint64(n), &rootHashes[n-1], sProof, eProof); err != nil { t.Errorf("inclusion proof not valid, i %d, n %d, batch %d: %v\n proofs: %x %x\n", i, n, batchSize, err, sProof, eProof) } bitToFlip := r.Intn(crypto.HashSize * 8) hashToFlip := r.Intn(len(sProof) + len(eProof) + len(leaves)) if hashToFlip < len(leaves) { leaves[hashToFlip][bitToFlip/8] ^= 1 << (bitToFlip % 8) } else if hashToFlip < len(leaves)+len(sProof) { sProof[hashToFlip-len(leaves)][bitToFlip/8] ^= 1 << (bitToFlip % 8) } else { eProof[hashToFlip-len(leaves)-len(sProof)][bitToFlip/8] ^= 1 << (bitToFlip % 8) } if err := VerifyInclusionBatch(leaves, uint64(i), uint64(n), &rootHashes[n-1], sProof, eProof); err == nil { t.Errorf("inclusion proof should have failed, i %d, n %d, batch: %d: flipped bit %d of hash %d\n", i, n, batchSize, bitToFlip, hashToFlip) } } } } func TestInclusionTailValid(t *testing.T) { hashes := newLeaves(100) rootHashes := []crypto.Hash{} tree := NewTree() for _, h := range hashes { if !tree.AddLeafHash(&h) { t.Fatalf("AddLeafHash failed at size %d", tree.Size()) } rootHashes = append(rootHashes, tree.GetRootHash()) } r := rand.New(rand.NewSource(17)) for i := 0; i < len(hashes); i++ { for n := i + 1; n <= len(hashes); n++ { proof, err := tree.ProveInclusion(uint64(i), uint64(n)) if err != nil { t.Fatalf("ProveInclusion %d, %d failed: %v", i, n, err) } leaves := make([]crypto.Hash, n-i) copy(leaves, hashes[i:]) if err := VerifyInclusionTail(leaves, uint64(i), &rootHashes[n-1], proof); err != nil { t.Errorf("inclusion proof not valid, i %d, n %d: %v\n proof: %x\n", i, n, err, proof) } bitToFlip := r.Intn(crypto.HashSize * 8) hashToFlip := r.Intn(len(proof) + len(leaves)) if hashToFlip < len(leaves) { leaves[hashToFlip][bitToFlip/8] ^= 1 << (bitToFlip % 8) } else { proof[hashToFlip-len(leaves)][bitToFlip/8] ^= 1 << (bitToFlip % 8) } if err := VerifyInclusionTail(leaves, uint64(i), &rootHashes[n-1], proof); err == nil { t.Errorf("inclusion proof should have failed, i %d, n %d: flipped bit %d of hash %d\n", i, n, bitToFlip, hashToFlip) } } } } func TestConsistency(t *testing.T) { hashes := newLeaves(7) h01 := HashInteriorNode(&hashes[0], &hashes[1]) h23 := HashInteriorNode(&hashes[2], &hashes[3]) h0123 := HashInteriorNode(&h01, &h23) h45 := HashInteriorNode(&hashes[4], &hashes[5]) h456 := HashInteriorNode(&h45, &hashes[6]) tree := NewTree() for _, h := range hashes { if !tree.AddLeafHash(&h) { t.Fatalf("AddLeafHash failed at size %d", tree.Size()) } } for _, table := range []struct { m uint64 n uint64 path []crypto.Hash // nil for expected error }{ {3, 7, []crypto.Hash{hashes[2], hashes[3], h01, h456}}, {4, 7, []crypto.Hash{h456}}, {6, 7, []crypto.Hash{h45, hashes[6], h0123}}, {6, 8, nil}, {7, 6, nil}, {0, 6, []crypto.Hash{}}, {6, 6, []crypto.Hash{}}, } { proof, err := tree.ProveConsistency(table.m, table.n) if table.path == nil { // Expect error if err == nil { t.Errorf("expected error, got consistency path: %x", proof) } } else { if err != nil { t.Errorf("ProveConsistency %d, %d failed: %v", table.m, table.n, err) } else if !pathEqual(proof, table.path) { t.Errorf("unexpected inclusion path m %d, n %d\n got: %x\n want: %x\n", table.m, table.n, proof, table.path) } } } } func TestConsistencyValid(t *testing.T) { leaves := newLeaves(100) tree := NewTree() // rootHash[n] is the root hash at size n. rootHashes := []crypto.Hash{} rootHashes = append(rootHashes, tree.GetRootHash()) for _, h := range leaves { if !tree.AddLeafHash(&h) { t.Fatalf("AddLeafHash failed at size %d", tree.Size()) } rootHashes = append(rootHashes, tree.GetRootHash()) } r := rand.New(rand.NewSource(18)) for m := 0; m < len(rootHashes); m++ { for n := m; n < len(rootHashes); n++ { proof, err := tree.ProveConsistency(uint64(m), uint64(n)) if err != nil { t.Fatalf("ProveConsistency %d, %d failed: %v", m, n, err) } oldRoot := rootHashes[m] newRoot := rootHashes[n] if err := VerifyConsistency( uint64(m), uint64(n), &oldRoot, &newRoot, proof); err != nil { t.Errorf("consistency proof not valid, m %d, n %d: %v\n proof: %x\n", m, n, err, proof) } bitToFlip := r.Intn(crypto.HashSize * 8) hashToFlip := r.Intn(len(proof) + 2) switch hashToFlip { case 0: oldRoot[bitToFlip/8] ^= 1 << (bitToFlip % 8) case 1: if m == 0 { // Any new root is consistent. continue } newRoot[bitToFlip/8] ^= 1 << (bitToFlip % 8) default: proof[hashToFlip-2][bitToFlip/8] ^= 1 << (bitToFlip % 8) } if err := VerifyConsistency( uint64(m), uint64(n), &oldRoot, &newRoot, proof); err == nil { t.Errorf("consistency proof should have failed, m %d, n %d: flipped bit %d of hash %d\n", m, n, bitToFlip, hashToFlip) } } } } func mustHashFromHex(t *testing.T, hex string) crypto.Hash { h, err := crypto.HashFromHex(hex) if err != nil { t.Fatal(err) } return h } func newLeaves(n int) []crypto.Hash { hashes := make([]crypto.Hash, n) for i := 0; i < n; i++ { var blob [8]byte binary.BigEndian.PutUint64(blob[:], uint64(i)) hashes[i] = HashLeafNode(blob[:]) } return hashes } sigsum-go-0.7.2/pkg/merkle/verify.go000066400000000000000000000226721455374457700173630ustar00rootroot00000000000000package merkle import ( "bytes" "fmt" "math/bits" "sigsum.org/sigsum-go/pkg/crypto" ) func pathLength(index, size uint64) int { // k is the number of lowend bits that differ between fn and // sn, i.e., number of iterations until fn == sn. k := bits.Len64(index ^ (size - 1)) return k + bits.OnesCount64(index>>k) } // VerifyConsistency verifies that a Merkle tree is consistent. The algorithm // used is in RFC 9162, §2.1.4.2. It is the same proof technique as RFC 6962. func VerifyConsistency(oldSize, newSize uint64, oldRoot, newRoot *crypto.Hash, path []crypto.Hash) error { // First handle the easy cases where an empty proof is valid. if oldSize == newSize { // Consistent if and only if roots are equal. // Require empty path. if len(path) > 0 { return fmt.Errorf("non-empty consistency path for trees of equal size") } if *oldRoot != *newRoot { return fmt.Errorf("consistency check failed: same size, but roots differ") } return nil } if oldSize == 0 { // Anything is consistent with the empty tree. // Require empty path. if len(path) > 0 { return fmt.Errorf("non-empty consistency path for empty old tree") } if *oldRoot != HashEmptyTree() { return fmt.Errorf("unexpected root hash for the empty tree") } return nil } // The last leaf of the old tree is at index fn. Eliminate // bottom layers of the tree, until fn points at a subtree // that is a left child; that subtree is included as-is also // in the new tree, and that is the starting point for the // traversal. trimBits := bits.TrailingZeros64(oldSize) // Ones of oldSize - 1 fn := (oldSize - 1) >> trimBits sn := (newSize - 1) >> trimBits wantLength := pathLength(fn, sn+1) if fn > 0 { wantLength++ } if len(path) != wantLength { return fmt.Errorf("proof input is malformed: path length %d, should be %d", len(path), wantLength) } // If fn == 0, we start at the oldRoot, otherwise, the // starting point is the first element of the supplied path. var fr crypto.Hash if fn == 0 { fr = *oldRoot } else { fr, path = path[0], path[1:] } sr := fr for ; sn > 0; fn, sn = fn>>1, sn>>1 { if isOdd(fn) { // Node on path is left sibling fr = HashInteriorNode(&path[0], &fr) sr = HashInteriorNode(&path[0], &sr) path = path[1:] } else if fn < sn { // Node on path is right sibling for the larger tree. sr = HashInteriorNode(&sr, &path[0]) path = path[1:] } } if len(path) > 0 { panic("internal error: left over path elements") } if !bytes.Equal(fr[:], oldRoot[:]) { return fmt.Errorf("invalid proof: old root mismatch") } if !bytes.Equal(sr[:], newRoot[:]) { return fmt.Errorf("invalid proof: new root mismatch") } return nil } // VerifyInclusion verifies that something is in a Merkle tree. The // algorithm used is equivalent to the one in in RFC 9162, §2.1.3.2. // Note that with index == 0, size == 1, the empty path is considered // a valid inclusion proof, and inclusion means that *leaf == *root. func VerifyInclusion(leaf *crypto.Hash, index, size uint64, root *crypto.Hash, path []crypto.Hash) error { if index >= size { return fmt.Errorf("proof input is malformed: index out of range") } if got, want := len(path), pathLength(index, size); got != want { return fmt.Errorf("proof input is malformed: path length %d, should be %d", got, want) } // Each iteration of the loop eliminates the bottom layer of // the tree. fn is the index in the tree for the hash of // interest, r. sn is the index of the last node in the // tree. All leaf nodes, in particular the final one with // index sn, are considered to be at the bottom layer, but // possibly with the parent located more than one layer above. // E.g., the tree with 3 leaves: // // o Root node // / \ // o \ // / \ \ // o o o The three leaf nodes // 0 1 2 r := *leaf fn := index for sn := size - 1; sn > 0; fn, sn = fn>>1, sn>>1 { if isOdd(fn) { // Node on path is left sibling r = HashInteriorNode(&path[0], &r) path = path[1:] } else if fn < sn { // Node on path is right sibling r = HashInteriorNode(&r, &path[0]) path = path[1:] } } if len(path) > 0 { panic("internal error: left over path elements") } if r != *root { return fmt.Errorf("invalid proof: root mismatch") } return nil } // Returns the compact range of a leaf interval ending at 2^k, in // reverse order, rightmost tree first. func makeLeftRange(leaves []crypto.Hash) compactRange { cr := compactRange{} for i := 0; i < len(leaves); i++ { cr = cr.extend(uint64(i), leaves[len(leaves)-1-i], func(left, right *crypto.Hash) crypto.Hash { return HashInteriorNode(right, left) }) } return cr } // Verify inclusion of a range of leaves ending at a multiple of 2^k, // where the path has k entries. func verifyInclusionLeft(leaves []crypto.Hash, path []crypto.Hash) (crypto.Hash, error) { if len(leaves) > (1 << len(path)) { panic(fmt.Sprintf("internal error: %d leaves, %d path elements", len(leaves), len(path))) } cRange := makeLeftRange(leaves[1:]) r := leaves[0] fn := (uint64(1) << len(path)) - uint64(len(leaves)) for _, s := range path { if isOdd(fn) { // Node on path is left sibling r = HashInteriorNode(&s, &r) } else { // Node on path is right sibling, and must // match left compact range. if s != cRange[len(cRange)-1] { return crypto.Hash{}, fmt.Errorf("unexpected path, inconsistent with leaf range") } cRange = cRange[:len(cRange)-1] r = HashInteriorNode(&r, &s) } fn >>= 1 } if len(cRange) > 0 { panic("internal error, left over compact range elements") } return r, nil } // VerifyBatchInclusion verifies a consecutive sequence of leaves are // included in a Merkle tree. The algorithm is an extension of the // inclusion proof in RFC 9162, §2.1.3.2, using inclusion proofs for // the first and last (inclusive) leaves in the sequence. func VerifyInclusionBatch(leaves []crypto.Hash, fn, size uint64, root *crypto.Hash, startPath []crypto.Hash, endPath []crypto.Hash) error { if len(leaves) == 0 { return fmt.Errorf("range must be non-empty") } en := fn + uint64(len(leaves)) - 1 if en >= size { return fmt.Errorf("end of range exceeds tree size") } if len(leaves) == 1 { if !pathEqual(startPath, endPath) { return fmt.Errorf("proof invalid, inconsistent paths") } return VerifyInclusion(&leaves[0], fn, size, root, startPath) } if len(startPath) != pathLength(fn, size) { return fmt.Errorf("proof invalid, wrong inclusion path length for first node") } if len(endPath) != pathLength(en, size) { return fmt.Errorf("proof invalid, wrong inclusion path length for last node") } // Find the bit index of the most significant bit where fn and en differ. k := bits.Len64(fn^en) - 1 // Split the range at a multiple of 2^k, so that // split - 2^k <= fn < split <= en < split + 2^k split := en & -(uint64(1) << k) fr, err := verifyInclusionLeft(leaves[:split-fn], startPath[:k]) if err != nil { return err } // Construct the right part of the compact range of the // intermediate leaves. rightRange := newCompactRange(leaves[split-fn : len(leaves)-1]) // Process right path; left siblings for the first k levels // should match the compact range. sn := size - 1 er := leaves[len(leaves)-1] for i := 0; i < k; en, sn, i = en>>1, sn>>1, i+1 { if isOdd(en) { // Node on path is left sibling, and must match right compact range. s := &rightRange[len(rightRange)-1] rightRange = rightRange[:len(rightRange)-1] if *s != endPath[0] { return fmt.Errorf("unexpected path, inconsistent with leaf range") } er = HashInteriorNode(s, &er) endPath = endPath[1:] } else if en < sn { // Node on path is right sibling. er = HashInteriorNode(&er, &endPath[0]) endPath = endPath[1:] } } if len(rightRange) > 0 { panic("internal error, left over compact range elements") } // Now we're just about to merge to a single node if startPath[k] != er || endPath[0] != fr { return fmt.Errorf("start and end trees not consistent") } if !pathEqual(startPath[k+1:], endPath[1:]) { return fmt.Errorf("proof invalid, inconsistent paths") } fr = HashInteriorNode(&fr, &er) return VerifyInclusion(&fr, fn>>(k+1), (sn>>1)+1, root, startPath[k+1:]) } // Verifies inclusion of all the leaves, to a root hash // corresponding to size index + len(leaves). func VerifyInclusionTail(leaves []crypto.Hash, fn uint64, root *crypto.Hash, path []crypto.Hash) error { if len(leaves) == 0 { return fmt.Errorf("range must be non-empty") } if len(leaves) == 1 { return VerifyInclusion(&leaves[0], fn, fn+1, root, path) } sn := fn + uint64(len(leaves)) - 1 if got, want := len(path), pathLength(fn, sn+1); got != want { return fmt.Errorf("proof input is malformed: path length %d, should be %d", got, want) } // Find the bit index of the most significant bit where fn and sn differ. k := bits.Len64(fn^sn) - 1 // Split the range at a multiple of 2^k, so that // split - 2^k <= fn < split <= sn < split + 2^k split := sn & -(uint64(1) << k) fr, err := verifyInclusionLeft(leaves[:split-fn], path[:k]) if err != nil { return err } er := rootOf(leaves[split-fn:]) if er != path[k] { return fmt.Errorf("unexpected path, inconsistent with leaf range") } fr = HashInteriorNode(&fr, &er) return VerifyInclusion(&fr, fn>>(k+1), (sn>>(k+1))+1, root, path[k+1:]) } func isOdd(num uint64) bool { return (num & 1) != 0 } func pathEqual(a, b []crypto.Hash) bool { if len(a) != len(b) { return false } for i, h := range a { if h != b[i] { return false } } return true } sigsum-go-0.7.2/pkg/mocks/000077500000000000000000000000001455374457700153545ustar00rootroot00000000000000sigsum-go-0.7.2/pkg/mocks/Makefile000066400000000000000000000007101455374457700170120ustar00rootroot00000000000000MOCK_FILES = api.go metrics.go all: $(MOCK_FILES) api.go: ../api/api.go go run github.com/golang/mock/mockgen --destination $@ --package mocks --mock_names Log=MockLog,Secondary=MockSecondary,Witness=MockWitness sigsum.org/sigsum-go/pkg/api Log,Secondary,Witness metrics.go: ../server/config.go go run github.com/golang/mock/mockgen --destination $@ --package mocks sigsum.org/sigsum-go/pkg/server Metrics clean: rm -f $(MOCK_FILES) .PHONY: clean sigsum-go-0.7.2/pkg/mocks/api.go000066400000000000000000000157071455374457700164660ustar00rootroot00000000000000// Code generated by MockGen. DO NOT EDIT. // Source: sigsum.org/sigsum-go/pkg/api (interfaces: Log,Secondary,Witness) // Package mocks is a generated GoMock package. package mocks import ( context "context" reflect "reflect" gomock "github.com/golang/mock/gomock" requests "sigsum.org/sigsum-go/pkg/requests" token "sigsum.org/sigsum-go/pkg/submit-token" types "sigsum.org/sigsum-go/pkg/types" ) // MockLog is a mock of Log interface. type MockLog struct { ctrl *gomock.Controller recorder *MockLogMockRecorder } // MockLogMockRecorder is the mock recorder for MockLog. type MockLogMockRecorder struct { mock *MockLog } // NewMockLog creates a new mock instance. func NewMockLog(ctrl *gomock.Controller) *MockLog { mock := &MockLog{ctrl: ctrl} mock.recorder = &MockLogMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockLog) EXPECT() *MockLogMockRecorder { return m.recorder } // AddLeaf mocks base method. func (m *MockLog) AddLeaf(arg0 context.Context, arg1 requests.Leaf, arg2 *token.SubmitHeader) (bool, error) { m.ctrl.T.Helper() 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 *MockLogMockRecorder) AddLeaf(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddLeaf", reflect.TypeOf((*MockLog)(nil).AddLeaf), arg0, arg1, arg2) } // GetConsistencyProof mocks base method. func (m *MockLog) 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 *MockLogMockRecorder) GetConsistencyProof(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConsistencyProof", reflect.TypeOf((*MockLog)(nil).GetConsistencyProof), arg0, arg1) } // GetInclusionProof mocks base method. func (m *MockLog) 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 *MockLogMockRecorder) GetInclusionProof(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInclusionProof", reflect.TypeOf((*MockLog)(nil).GetInclusionProof), arg0, arg1) } // GetLeaves mocks base method. func (m *MockLog) GetLeaves(arg0 context.Context, arg1 requests.Leaves) ([]types.Leaf, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetLeaves", arg0, arg1) ret0, _ := ret[0].([]types.Leaf) ret1, _ := ret[1].(error) return ret0, ret1 } // GetLeaves indicates an expected call of GetLeaves. func (mr *MockLogMockRecorder) GetLeaves(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLeaves", reflect.TypeOf((*MockLog)(nil).GetLeaves), arg0, arg1) } // GetTreeHead mocks base method. func (m *MockLog) GetTreeHead(arg0 context.Context) (types.CosignedTreeHead, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetTreeHead", arg0) ret0, _ := ret[0].(types.CosignedTreeHead) ret1, _ := ret[1].(error) return ret0, ret1 } // GetTreeHead indicates an expected call of GetTreeHead. func (mr *MockLogMockRecorder) GetTreeHead(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTreeHead", reflect.TypeOf((*MockLog)(nil).GetTreeHead), arg0) } // MockSecondary is a mock of Secondary interface. type MockSecondary struct { ctrl *gomock.Controller recorder *MockSecondaryMockRecorder } // MockSecondaryMockRecorder is the mock recorder for MockSecondary. type MockSecondaryMockRecorder struct { mock *MockSecondary } // NewMockSecondary creates a new mock instance. func NewMockSecondary(ctrl *gomock.Controller) *MockSecondary { mock := &MockSecondary{ctrl: ctrl} mock.recorder = &MockSecondaryMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockSecondary) EXPECT() *MockSecondaryMockRecorder { return m.recorder } // GetSecondaryTreeHead mocks base method. func (m *MockSecondary) GetSecondaryTreeHead(arg0 context.Context) (types.SignedTreeHead, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSecondaryTreeHead", arg0) ret0, _ := ret[0].(types.SignedTreeHead) ret1, _ := ret[1].(error) return ret0, ret1 } // GetSecondaryTreeHead indicates an expected call of GetSecondaryTreeHead. func (mr *MockSecondaryMockRecorder) GetSecondaryTreeHead(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSecondaryTreeHead", reflect.TypeOf((*MockSecondary)(nil).GetSecondaryTreeHead), arg0) } // MockWitness is a mock of Witness interface. type MockWitness struct { ctrl *gomock.Controller recorder *MockWitnessMockRecorder } // MockWitnessMockRecorder is the mock recorder for MockWitness. type MockWitnessMockRecorder struct { mock *MockWitness } // NewMockWitness creates a new mock instance. func NewMockWitness(ctrl *gomock.Controller) *MockWitness { mock := &MockWitness{ctrl: ctrl} mock.recorder = &MockWitnessMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockWitness) EXPECT() *MockWitnessMockRecorder { return m.recorder } // AddTreeHead mocks base method. func (m *MockWitness) AddTreeHead(arg0 context.Context, arg1 requests.AddTreeHead) (types.Cosignature, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "AddTreeHead", arg0, arg1) ret0, _ := ret[0].(types.Cosignature) ret1, _ := ret[1].(error) return ret0, ret1 } // AddTreeHead indicates an expected call of AddTreeHead. func (mr *MockWitnessMockRecorder) AddTreeHead(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddTreeHead", reflect.TypeOf((*MockWitness)(nil).AddTreeHead), arg0, arg1) } // GetTreeSize mocks base method. func (m *MockWitness) GetTreeSize(arg0 context.Context, arg1 requests.GetTreeSize) (uint64, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetTreeSize", arg0, arg1) ret0, _ := ret[0].(uint64) ret1, _ := ret[1].(error) return ret0, ret1 } // GetTreeSize indicates an expected call of GetTreeSize. func (mr *MockWitnessMockRecorder) GetTreeSize(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTreeSize", reflect.TypeOf((*MockWitness)(nil).GetTreeSize), arg0, arg1) } sigsum-go-0.7.2/pkg/mocks/metrics.go000066400000000000000000000033771455374457700173630ustar00rootroot00000000000000// Code generated by MockGen. DO NOT EDIT. // Source: sigsum.org/sigsum-go/pkg/server (interfaces: Metrics) // Package mocks is a generated GoMock package. package mocks import ( reflect "reflect" time "time" gomock "github.com/golang/mock/gomock" ) // MockMetrics is a mock of Metrics interface. type MockMetrics struct { ctrl *gomock.Controller recorder *MockMetricsMockRecorder } // MockMetricsMockRecorder is the mock recorder for MockMetrics. type MockMetricsMockRecorder struct { mock *MockMetrics } // NewMockMetrics creates a new mock instance. func NewMockMetrics(ctrl *gomock.Controller) *MockMetrics { mock := &MockMetrics{ctrl: ctrl} mock.recorder = &MockMetricsMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockMetrics) EXPECT() *MockMetricsMockRecorder { return m.recorder } // OnRequest mocks base method. func (m *MockMetrics) OnRequest(arg0 string) { m.ctrl.T.Helper() m.ctrl.Call(m, "OnRequest", arg0) } // OnRequest indicates an expected call of OnRequest. func (mr *MockMetricsMockRecorder) OnRequest(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnRequest", reflect.TypeOf((*MockMetrics)(nil).OnRequest), arg0) } // OnResponse mocks base method. func (m *MockMetrics) OnResponse(arg0 string, arg1 int, arg2 time.Duration) { m.ctrl.T.Helper() m.ctrl.Call(m, "OnResponse", arg0, arg1, arg2) } // OnResponse indicates an expected call of OnResponse. func (mr *MockMetricsMockRecorder) OnResponse(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnResponse", reflect.TypeOf((*MockMetrics)(nil).OnResponse), arg0, arg1, arg2) } sigsum-go-0.7.2/pkg/monitor/000077500000000000000000000000001455374457700157275ustar00rootroot00000000000000sigsum-go-0.7.2/pkg/monitor/alert.go000066400000000000000000000015141455374457700173660ustar00rootroot00000000000000package monitor import ( "fmt" ) type AlertType int const ( AlertOther AlertType = iota // Indicates log is misbehaving, or not responding. AlertLogError AlertInvalidLogSignature AlertInconsistentTreeHead ) func (t AlertType) String() string { switch t { case AlertOther: return "Other" case AlertLogError: return "Log not responding as expected" case AlertInvalidLogSignature: return "Invalid log signature" case AlertInconsistentTreeHead: return "Log tree head not consistent" default: return fmt.Sprintf("Unknown alert type %d", t) } } type Alert struct { Type AlertType Err error } func (a *Alert) Error() string { return fmt.Sprintf("monitoring alert: %s: %s", a.Type, a.Err) } func newAlert(t AlertType, msg string, args ...interface{}) *Alert { return &Alert{Type: t, Err: fmt.Errorf(msg, args...)} } sigsum-go-0.7.2/pkg/monitor/client.go000066400000000000000000000122351455374457700175370ustar00rootroot00000000000000package monitor import ( "context" "fmt" "sigsum.org/sigsum-go/pkg/api" "sigsum.org/sigsum-go/pkg/client" "sigsum.org/sigsum-go/pkg/crypto" "sigsum.org/sigsum-go/pkg/merkle" "sigsum.org/sigsum-go/pkg/requests" "sigsum.org/sigsum-go/pkg/types" ) // A monitoringLogClient can retrieve tree heads and leafs from a log, // and it verifies consistency and inclusion of anything it returns. type monitoringLogClient struct { logKey crypto.PublicKey // Identifies the log monitored. client api.Log } func newMonitoringLogClient(logKey *crypto.PublicKey, URL string) *monitoringLogClient { return &monitoringLogClient{ logKey: *logKey, client: client.New(client.Config{URL: URL, UserAgent: "sigsum-monitor"}), } } // Request log's tree head, and check that it is consistent with local // state. TODO: Figure out cosignatures should be processed; it would // make some sense to return a CosignedTreeHead but where only // properly verified cosignatures are kept. func (c *monitoringLogClient) getTreeHead(ctx context.Context, treeHead *types.TreeHead) (types.SignedTreeHead, error) { cth, err := c.client.GetTreeHead(ctx) if err != nil { return types.SignedTreeHead{}, newAlert(AlertLogError, "get-tree-head failed: %v", err) } // For now, only check log's signature. TODO: Also check cosignatures. if !cth.Verify(&c.logKey) { return types.SignedTreeHead{}, newAlert(AlertInvalidLogSignature, "log signature invalid") } if cth.Size < treeHead.Size { return types.SignedTreeHead{}, newAlert(AlertInconsistentTreeHead, "monitored log has shrunk, size %d, previous size %d", cth.Size, treeHead.Size) } var proof types.ConsistencyProof if treeHead.Size > 0 && cth.Size > treeHead.Size { var err error proof, err = c.client.GetConsistencyProof(ctx, requests.ConsistencyProof{OldSize: treeHead.Size, NewSize: cth.Size}) if err != nil { return types.SignedTreeHead{}, newAlert(AlertLogError, "get-consistency-proof failed: %v", err) } } if err := proof.Verify(treeHead, &cth.TreeHead); err != nil { return types.SignedTreeHead{}, newAlert(AlertInconsistentTreeHead, "consistency proof not valid: %v", err) } return cth.SignedTreeHead, nil } func (c *monitoringLogClient) getInclusionProofAtIndex(ctx context.Context, index uint64, req requests.InclusionProof) (types.InclusionProof, error) { if req.Size == 1 { // Trivial proof: index 0, empty path return types.InclusionProof{}, nil } proof, err := c.client.GetInclusionProof(ctx, req) if err != nil { return types.InclusionProof{}, newAlert(AlertLogError, "get-inclusion-proof failed: %v", err) } if proof.LeafIndex != index { return types.InclusionProof{}, newAlert(AlertLogError, "unexpected get-inclusion-proof index, got %d, want %d", proof.LeafIndex, index) } return proof, nil } // Caches previous leaf hash and inclusion proof. Valid only for // retrieving the next range starting at LeafIndex + 1, and with the // same tree head. type getLeavesState struct { leafHash crypto.Hash proof types.InclusionProof } // Retrieves at most count leaves, starting at index, and check that // they are included in the latest retrieved tree head. func (c *monitoringLogClient) getLeaves(ctx context.Context, state *getLeavesState, treeHead *types.TreeHead, req requests.Leaves) ([]types.Leaf, *getLeavesState, error) { leaves, err := c.client.GetLeaves(ctx, req) if err != nil { return nil, nil, err } start := req.StartIndex end := req.StartIndex + uint64(len(leaves)) leafHashes := make([]crypto.Hash, 0, len(leaves)+1) var proof types.InclusionProof if state != nil { if state.proof.LeafIndex+1 != req.StartIndex { panic(fmt.Sprintf("invalid state, LeafIndex (%d), StartIndex (%d) should be adjacent", state.proof.LeafIndex, req.StartIndex)) } start = state.proof.LeafIndex proof = state.proof leafHashes = append(leafHashes, state.leafHash) } for _, leaf := range leaves { leafHashes = append(leafHashes, leaf.ToHash()) } if state == nil { var err error proof, err = c.getInclusionProofAtIndex(ctx, start, requests.InclusionProof{Size: treeHead.Size, LeafHash: leafHashes[0]}) if err != nil { return nil, nil, err } } if len(leaves) == 1 { if err := proof.Verify(&leafHashes[0], treeHead); err != nil { return nil, nil, newAlert(AlertLogError, "inclusion proof for leaf %d not valid", proof.LeafIndex) } return leaves, &getLeavesState{leafHash: leafHashes[0], proof: proof}, nil } if end == treeHead.Size { if err := merkle.VerifyInclusionTail(leafHashes, start, &treeHead.RootHash, proof.Path); err != nil { return nil, nil, newAlert(AlertLogError, "inclusion proof not valid for tail range %d:%d: %v", start, end, err) } return leaves, nil, nil } endProof, err := c.getInclusionProofAtIndex(ctx, end-1, requests.InclusionProof{Size: treeHead.Size, LeafHash: leafHashes[len(leafHashes)-1]}) if err != nil { return nil, nil, err } if err := merkle.VerifyInclusionBatch(leafHashes, start, treeHead.Size, &treeHead.RootHash, proof.Path, endProof.Path); err != nil { return nil, nil, newAlert(AlertLogError, "inclusion proof not valid for range %d:%d: %v", start, end, err) } return leaves, &getLeavesState{leafHash: leafHashes[len(leafHashes)-1], proof: endProof}, nil } sigsum-go-0.7.2/pkg/monitor/client_test.go000066400000000000000000000135061455374457700206000ustar00rootroot00000000000000package monitor import ( "context" "encoding/binary" "fmt" "math/rand" "testing" "github.com/golang/mock/gomock" "sigsum.org/sigsum-go/pkg/api" "sigsum.org/sigsum-go/pkg/crypto" "sigsum.org/sigsum-go/pkg/merkle" "sigsum.org/sigsum-go/pkg/mocks" "sigsum.org/sigsum-go/pkg/requests" token "sigsum.org/sigsum-go/pkg/submit-token" "sigsum.org/sigsum-go/pkg/types" ) // Implements api.Log. // TODO: Move to some public package, and add an RWMutex for // syncronization. type testLog struct { leaves []types.Leaf tree merkle.Tree signer crypto.Signer } func (l *testLog) GetTreeHead(_ context.Context) (types.CosignedTreeHead, error) { th := types.TreeHead{ Size: uint64(l.tree.Size()), RootHash: l.tree.GetRootHash(), } sth, err := th.Sign(l.signer) return types.CosignedTreeHead{SignedTreeHead: sth}, err } func (l *testLog) GetInclusionProof(_ context.Context, req requests.InclusionProof) (types.InclusionProof, error) { index, err := l.tree.GetLeafIndex(&req.LeafHash) if err != nil || index >= req.Size { return types.InclusionProof{}, api.ErrNotFound } path, err := l.tree.ProveInclusion(index, req.Size) return types.InclusionProof{ LeafIndex: index, Path: path, }, err } func (l *testLog) GetConsistencyProof(_ context.Context, req requests.ConsistencyProof) (types.ConsistencyProof, error) { path, err := l.tree.ProveConsistency(req.OldSize, req.NewSize) return types.ConsistencyProof{Path: path}, err } func (l *testLog) GetLeaves(_ context.Context, req requests.Leaves) ([]types.Leaf, error) { size := l.tree.Size() if req.StartIndex >= size || req.EndIndex > size || req.StartIndex >= req.EndIndex { return nil, fmt.Errorf("out of range request: start %d, end %d, size %d\n", req.StartIndex, req.EndIndex, size) } return l.leaves[req.StartIndex:req.EndIndex], nil } func (l *testLog) AddLeaf(_ context.Context, req requests.Leaf, _ *token.SubmitHeader) (bool, error) { leaf, err := req.Verify() if err != nil { return false, api.ErrForbidden } h := leaf.ToHash() if l.tree.AddLeafHash(&h) { l.leaves = append(l.leaves, leaf) } return true, nil } func makeLeafRequest(t *testing.T, signer crypto.Signer, msg *crypto.Hash) requests.Leaf { signature, err := types.SignLeafMessage(signer, msg[:]) if err != nil { t.Fatalf("Leaf signing failed: %v\n", err) } return requests.Leaf{ Message: *msg, Signature: signature, PublicKey: signer.Public(), } } // Test for successful case. func TestGetTreeHead(t *testing.T) { logSigner := crypto.NewEd25519Signer(&crypto.PrivateKey{2}) leafSigner := crypto.NewEd25519Signer(&crypto.PrivateKey{3}) log := testLog{signer: logSigner, tree: merkle.NewTree()} monitorClient := monitoringLogClient{ logKey: logSigner.Public(), client: &log, } r := rand.New(rand.NewSource(10)) prevTree := types.NewEmptyTreeHead() for i := 0; i < 100; i++ { // Ensures that batch is of zero size, so that first // GetTreeHead returns an empty tree. c := uint64(r.Intn(i + 1)) newSize := log.tree.Size() + c addLeaves(t, &log, leafSigner, uint64(i), c) sth, err := monitorClient.getTreeHead(context.Background(), &prevTree) if err != nil { t.Fatalf("GetTreeHead failed: %v", err) } if got, want := sth.Size, newSize; got != want { t.Fatalf("Unexpected log size: got %d, want %d", got, want) } prevTree = sth.TreeHead } } // Test for invalid answers from log. func TestGetTreeHeadErrors(t *testing.T) { logSigner := crypto.NewEd25519Signer(&crypto.PrivateKey{2}) leafSigner := crypto.NewEd25519Signer(&crypto.PrivateKey{3}) log := testLog{signer: logSigner, tree: merkle.NewTree()} addLeaves(t, &log, leafSigner, 0, 20) oldTh, err := log.GetTreeHead(context.Background()) if err != nil { t.Fatalf("GetTreeHead failed: %v", err) } addLeaves(t, &log, leafSigner, 1, 20) oneTest := func(description string, mungeTreeHead func(*types.CosignedTreeHead), mungeConsistency func(*types.ConsistencyProof)) { ctrl := gomock.NewController(t) defer ctrl.Finish() mockLog := mocks.NewMockLog(ctrl) mockLog.EXPECT().GetTreeHead(gomock.Any()).DoAndReturn( func(ctx context.Context) (types.CosignedTreeHead, error) { cth, err := log.GetTreeHead(ctx) if err == nil && mungeTreeHead != nil { mungeTreeHead(&cth) } return cth, err }) mockLog.EXPECT().GetConsistencyProof(gomock.Any(), gomock.Any()).AnyTimes().DoAndReturn( func(ctx context.Context, req requests.ConsistencyProof) (types.ConsistencyProof, error) { proof, err := log.GetConsistencyProof(ctx, req) if err == nil && mungeConsistency != nil { mungeConsistency(&proof) } return proof, err }) monitorClient := monitoringLogClient{ logKey: logSigner.Public(), client: mockLog, } _, err := monitorClient.getTreeHead(context.Background(), &oldTh.TreeHead) if err == nil { if description != "" { t.Errorf("%s: Unexpectedly succeeded", description) } return } if description == "" { t.Fatalf("Unexpected getTreeHead failure: %v", err) } t.Logf("%s: (expected) failure: %v", description, err) } oneTest("", nil, nil) // No failure; checks test wireup. oneTest("bad signature", func(cth *types.CosignedTreeHead) { cth.Signature[2] ^= 1 }, nil) oneTest("bad signature (hash)", func(cth *types.CosignedTreeHead) { cth.RootHash[5] ^= 1 }, nil) oldTh.Size++ oneTest("bad consistency", nil, nil) } func addLeaves(t *testing.T, log *testLog, signer crypto.Signer, id, count uint64) { oldSize := log.tree.Size() for j := uint64(0); j < count; j++ { var msg crypto.Hash binary.BigEndian.PutUint64(msg[:], id) binary.BigEndian.PutUint64(msg[8:], j) _, err := log.AddLeaf(context.Background(), makeLeafRequest(t, signer, &msg), nil) if err != nil { t.Fatalf("AddLeaf failed: %v", err) } } if got, want := log.tree.Size(), oldSize+count; got != want { t.Fatalf("Unexpected merkle tree size: got %d, want %d", got, want) } } sigsum-go-0.7.2/pkg/monitor/monitor.go000066400000000000000000000120351455374457700177460ustar00rootroot00000000000000package monitor import ( "context" "sync" "time" "sigsum.org/sigsum-go/pkg/crypto" "sigsum.org/sigsum-go/pkg/policy" "sigsum.org/sigsum-go/pkg/requests" "sigsum.org/sigsum-go/pkg/types" ) const ( DefaultBatchSize = 512 DefaultQueryInterval = 10 * time.Minute ) // TODO: Figure out the proper interface to the monitor. Are callbacks // the right way, or should we instead have one or more channels to // pass new data items and alerts? type Callbacks interface { // Called when a log (identified by key hash) has a new tree // head; application can use this to persist the tree head. // TODO: Also include cosignatures? NewTreeHead(logKeyHash crypto.Hash, signedTreeHead types.SignedTreeHead) // Called when there are new leaves with submit key of // interest. Includes only leaves with a known submit key, and // where signature and inclusion proof are valid. // // The numberOfProcessedLeaves reports the monitoring // progress; it is the number of leaves that have been // retrieved from the log and that have been checked for // proper inclusion; it may lag behind the tree size of the // latest seen tree. indices and leaves represents the subset // of new leaves that are of interest. NewLeaves(logKeyHash crypto.Hash, numberOfProcessedLeaves uint64, indices []uint64, leaves []types.Leaf) Alert(logKeyHash crypto.Hash, e error) } type MonitorState struct { TreeHead types.TreeHead // Index of next leaf to process. NextLeafIndex uint64 } type Config struct { QueryInterval time.Duration // Maximum number of leaves to request at a time BatchSize uint64 // Keys of interest. If nil, all keys are of interest (but no // signatures are verified). SubmitKeys map[crypto.Hash]crypto.PublicKey Callbacks Callbacks } func (c *Config) applyDefaults() Config { r := *c if r.QueryInterval <= 0 { r.QueryInterval = DefaultQueryInterval } if r.BatchSize == 0 { r.BatchSize = DefaultBatchSize } return r } func (c *Config) filterLeaves( leaves []types.Leaf, startIndex uint64, alertCallback func(*Alert)) ([]uint64, []types.Leaf) { if c.SubmitKeys == nil { indices := make([]uint64, len(leaves)) for i := range indices { indices[i] = startIndex + uint64(i) } return indices, leaves } indices := []uint64{} matchedLeaves := []types.Leaf{} for i, leaf := range leaves { index := startIndex + uint64(i) if key, ok := c.SubmitKeys[leaf.KeyHash]; ok { if !leaf.Verify(&key) { // Indicates log is misbehaving. // Generate alert and continue // processing remaining leaves. This // is an issue where inconsistent // verification conditions could // matter, see // https://hdevalence.ca/blog/2020-10-04-its-25519am alertCallback(newAlert(AlertLogError, "invalid signature on leaf %d, keyhash %x", index, leaf.KeyHash)) } else { matchedLeaves = append(matchedLeaves, leaf) indices = append(indices, index) } } } return indices, matchedLeaves } // Monitor a single sigsum log. A monitor program is expected to call // this function in one goroutine per log it monitors. func MonitorLog(ctx context.Context, client *monitoringLogClient, state MonitorState, c *Config) { config := c.applyDefaults() keyHash := crypto.HashBytes(client.logKey[:]) for ctx.Err() == nil { updateCtx, _ := context.WithTimeout(ctx, config.QueryInterval) if state.TreeHead.Size == state.NextLeafIndex { cth, err := client.getTreeHead(ctx, &state.TreeHead) if err != nil { config.Callbacks.Alert(keyHash, err) } else if cth.Size > state.TreeHead.Size { config.Callbacks.NewTreeHead(keyHash, cth) state.TreeHead = cth.TreeHead } } for glState := (*getLeavesState)(nil); state.NextLeafIndex < state.TreeHead.Size; { end := state.TreeHead.Size if end-state.NextLeafIndex > config.BatchSize { end = state.NextLeafIndex + config.BatchSize } var allLeaves []types.Leaf var err error allLeaves, glState, err = client.getLeaves(ctx, glState, &state.TreeHead, requests.Leaves{StartIndex: state.NextLeafIndex, EndIndex: end}) if err != nil { config.Callbacks.Alert(keyHash, err) break } indices, leaves := config.filterLeaves(allLeaves, state.NextLeafIndex, func(alert *Alert) { config.Callbacks.Alert(keyHash, err) }) state.NextLeafIndex += uint64(len(allLeaves)) config.Callbacks.NewLeaves(keyHash, state.NextLeafIndex, indices, leaves) } // Waits until end of interval <-updateCtx.Done() } } // Runs monitor in the background, until ctx is cancelled. func StartMonitoring( ctx context.Context, p *policy.Policy, config *Config, state map[crypto.Hash]MonitorState) <-chan struct{} { var wg sync.WaitGroup for _, l := range p.GetLogsWithUrl() { keyHash := crypto.HashBytes(l.PublicKey[:]) initialState, ok := state[keyHash] if !ok { initialState = MonitorState{ TreeHead: types.NewEmptyTreeHead(), NextLeafIndex: 0, } } wg.Add(1) go func() { MonitorLog(ctx, newMonitoringLogClient(&l.PublicKey, l.URL), initialState, config) wg.Done() }() } ch := make(chan struct{}) go func() { wg.Wait() close(ch) }() return ch } sigsum-go-0.7.2/pkg/policy/000077500000000000000000000000001455374457700155375ustar00rootroot00000000000000sigsum-go-0.7.2/pkg/policy/config.go000066400000000000000000000100161455374457700173310ustar00rootroot00000000000000package policy import ( "bufio" "fmt" "io" "os" "strconv" "strings" "sigsum.org/sigsum-go/pkg/crypto" ) // Config file syntax is // log [] // witness [] // group ... // quorum // with # used for comments. const ( // Predefined name representing an empty group. Using "quorum // none" defines a policy that doesn't require any cosignatures. ConfigNone = "none" ) // Represents a config file being parsed. type config struct { policy *Policy names map[string]Quorum } func (c *config) ifdef(name string) bool { _, ok := c.names[name] return ok } func (c *config) parseLog(args []string) error { if len(args) < 1 || len(args) > 2 { return fmt.Errorf("invalid log policy line, public key required, url optional") } key, err := crypto.PublicKeyFromHex(args[0]) if err != nil { return err } var url string if len(args) > 1 { url = args[1] } _, err = c.policy.addLog(&Entity{PublicKey: key, URL: url}) return err } func (c *config) parseWitness(args []string) error { if len(args) < 2 || len(args) > 3 { return fmt.Errorf("invalid witness policy line, public key and name required, url optional") } name := args[0] key, err := crypto.PublicKeyFromHex(args[1]) if err != nil { return err } if c.ifdef(name) { return fmt.Errorf("duplicate name: %q", name) } var url string if len(args) > 2 { url = args[2] } h, err := c.policy.addWitness(&Entity{PublicKey: key, URL: url}) if err != nil { return err } c.names[name] = &quorumSingle{h} return nil } func (c *config) parseGroup(args []string) error { if len(args) < 3 { return fmt.Errorf("too few arguments, name, threshold and at least one member is required") } n := len(args) - 2 name := args[0] if c.ifdef(name) { return fmt.Errorf("duplicate name %q", name) } var threshold int switch s := string(args[1]); s { case "any": threshold = 1 case "all": threshold = n default: var err error threshold, err = strconv.Atoi(s) if err != nil { return err } } if threshold < 1 || threshold > n { return fmt.Errorf("threshold out of range") } subQuorums := []Quorum{} // TODO: Warn or fail if there's overlap between group members? for _, member := range args[2:] { if q, ok := c.names[member]; ok { subQuorums = append(subQuorums, q) } else { return fmt.Errorf("undefined name: %q", member) } } if len(subQuorums) != n { panic("internal error") } c.names[name] = &quorumKofN{subQuorums: subQuorums, k: threshold} return nil } func (c *config) parseQuorum(args []string) error { if len(args) != 1 { return fmt.Errorf("incorrect number of arguments: group or witness required") } if c.policy.quorum != nil { return fmt.Errorf("quorum can only be set once") } name := args[0] if q, ok := c.names[name]; ok { c.policy.quorum = q } else { return fmt.Errorf("undefined name %q", name) } return nil } func (c *config) parseLine(fields []string) (err error) { keyword, args := fields[0], fields[1:] switch keyword { case "log": err = c.parseLog(args) case "witness": err = c.parseWitness(args) case "group": err = c.parseGroup(args) case "quorum": err = c.parseQuorum(args) default: err = fmt.Errorf("unknown keyword: %q", keyword) } return } func ParseConfig(file io.Reader) (*Policy, error) { config := config{ policy: newEmptyPolicy(), names: map[string]Quorum{ConfigNone: &quorumKofN{}}, } lineno := 0 for scanner := bufio.NewScanner(file); scanner.Scan(); { lineno++ line := scanner.Text() if comment := strings.Index(line, "#"); comment >= 0 { line = line[:comment] } fields := strings.Fields(line) if len(fields) == 0 { continue } if err := config.parseLine(fields); err != nil { return nil, fmt.Errorf("%d: %v", lineno, err) } } if config.policy.quorum == nil { return nil, fmt.Errorf("no quorum defined") } return config.policy, nil } func ReadPolicyFile(name string) (*Policy, error) { f, err := os.Open(name) if err != nil { return nil, err } defer f.Close() return ParseConfig(f) } sigsum-go-0.7.2/pkg/policy/config_test.go000066400000000000000000000171221455374457700203750ustar00rootroot00000000000000package policy import ( "bytes" "strings" "testing" "sigsum.org/sigsum-go/pkg/crypto" ) func TestValidConfig(t *testing.T) { policy, err := ParseConfig(bytes.NewBufferString(` # example config log aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa http://sigsum.example.org log bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb#comment witness W1 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc http://w1 # same key for log and key is undesirable, but not an error witness W2 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa witness W3 dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd http://w3 group G1 any W1 W2 group G2 2 W1 W2 W3 group G3 all G1 W3 quorum G3 log cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc witness W4 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb `)) if err != nil { t.Fatal(err) } if policy == nil { t.Fatalf("ParseConfig returned nil policy") } if got, want := len(policy.logs), 3; got != want { t.Errorf("Unexpected number of logs in policy, got %d, expected %d", got, want) } if got, want := len(policy.witnesses), 4; got != want { t.Errorf("Unexpected number of logs in policy, got %d, expected %d", got, want) } logs := policy.GetLogsWithUrl() if got, want := len(logs), 1; got != want { t.Errorf("Unexpected number of logs with url in policy, got %d, expected %d", got, want) } else if got, want := logs[0].URL, "http://sigsum.example.org"; got != want { t.Errorf("Unexpected log url, got %q, expected %q", got, want) } witnesses := policy.GetWitnessesWithUrl() if got, want := len(witnesses), 2; got != want { t.Errorf("Unexpected number of witnesses with url in policy, got %d, expected %d", got, want) } else if !((witnesses[0].URL == "http://w1" && witnesses[1].URL == "http://w3") || (witnesses[1].URL == "http://w1" && witnesses[0].URL == "http://w3")) { t.Errorf("Unexpected witness urls, got %v, %v", witnesses[0].URL, witnesses[1].URL) } if policy.quorum == nil { t.Fatalf("No quorum defined") } kh := func(hex string) crypto.Hash { key, err := crypto.PublicKeyFromHex(hex) if err != nil { t.Fatalf("internal error, bad key %q", hex) } return crypto.HashBytes(key[:]) } witnessHashes := []crypto.Hash{ kh("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"), kh("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), kh("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"), } for _, table := range []struct { witnesses []int sufficient bool }{ {[]int{}, false}, {[]int{1}, false}, {[]int{2}, false}, {[]int{3}, false}, {[]int{1, 2}, false}, {[]int{1, 3}, true}, {[]int{2, 3}, true}, {[]int{1, 2, 3}, true}, } { m := make(map[crypto.Hash]struct{}) for _, i := range table.witnesses { m[witnessHashes[i-1]] = struct{}{} } if got, want := policy.quorum.IsQuorum(m), table.sufficient; got != want { t.Errorf("Unexpected result of IsQuorum for set %v, got %v, expected %v", table.witnesses, got, want) } } } func TestNumericThreshold(t *testing.T) { policy, err := ParseConfig(bytes.NewBufferString(` # example config log aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa http://sigsum.example.org witness A1 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1 witness A2 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa2 witness A3 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa3 witness B1 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb1 witness B2 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb2 witness B3 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb3 group A-group 1 A1 A2 A3 group B-group 2 B1 B2 B3 group G any A-group B-group quorum G `)) if err != nil { t.Fatal(err) } if policy == nil { t.Fatalf("ParseConfig returned nil policy") } if got, want := len(policy.logs), 1; got != want { t.Errorf("Unexpected number of logs in policy, got %d, expected %d", got, want) } if got, want := len(policy.witnesses), 6; got != want { t.Errorf("Unexpected number of logs in policy, got %d, expected %d", got, want) } if policy.quorum == nil { t.Fatalf("No quorum defined") } kh := func(hex string) crypto.Hash { key, err := crypto.PublicKeyFromHex(hex) if err != nil { t.Fatalf("internal error, bad key %q", hex) } return crypto.HashBytes(key[:]) } aHashes := []crypto.Hash{ kh("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1"), kh("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa2"), kh("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa3"), } bHashes := []crypto.Hash{ kh("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb1"), kh("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb2"), kh("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb3"), } for _, table := range []struct { aWitnesses []int bWitnesses []int sufficient bool }{ {[]int{}, []int{}, false}, // One A witness is sufficient. {[]int{1}, []int{}, true}, {[]int{2}, []int{}, true}, {[]int{3}, []int{}, true}, {[]int{1, 3}, []int{}, true}, {[]int{1, 3}, []int{1}, true}, // Two B witnesses are sufficient. {[]int{}, []int{1}, false}, {[]int{}, []int{2}, false}, {[]int{}, []int{3}, false}, {[]int{}, []int{1, 2}, true}, {[]int{}, []int{1, 3}, true}, {[]int{}, []int{2, 3}, true}, {[]int{}, []int{1, 2, 3}, true}, {[]int{2}, []int{1, 2, 3}, true}, } { m := make(map[crypto.Hash]struct{}) for _, i := range table.aWitnesses { m[aHashes[i-1]] = struct{}{} } for _, i := range table.bWitnesses { m[bHashes[i-1]] = struct{}{} } if got, want := policy.quorum.IsQuorum(m), table.sufficient; got != want { t.Errorf("Unexpected result of IsQuorum for set A %v, B %v, got %v, expected %v", table.aWitnesses, table.bWitnesses, got, want) } } } func TestInvalidConfig(t *testing.T) { for _, table := range []struct { desc string err string config string }{ {"empty", "no quorum", ""}, {"duplicate log", "duplicate log: aaa", ` log aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa #foo log aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa #bar `}, {"duplicate witness", "duplicate witness: ccc", ` witness W1 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc witness W2 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc `}, {"duplicate name", "duplicate name: \"W1\"", ` witness W1 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc witness W1 dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd `}, {"duplicate none", "duplicate name: \"none\"", ` witness none cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc `}, {"undef name", "undefined name: \"W3\"", ` witness W1 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc witness W2 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa group G all W1 W3 W2 `}, {"missing quorum", "no quorum", ` witness W1 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc witness W2 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa group G all W1 W2 `}, } { policy, err := ParseConfig(bytes.NewBufferString(table.config)) if err == nil { t.Errorf("%s: invalid config not rejected", table.desc) } else { if strings.Index(err.Error(), table.err) < 0 { t.Errorf("%s: expected error containing %q: %v", table.desc, table.err, err) } if policy != nil { t.Errorf("returned policy (for invalid config) is non-nil") } } } } sigsum-go-0.7.2/pkg/policy/policy.go000066400000000000000000000067421455374457700173760ustar00rootroot00000000000000package policy import ( "fmt" "math/rand" "sigsum.org/sigsum-go/pkg/crypto" "sigsum.org/sigsum-go/pkg/types" ) type Entity struct { PublicKey crypto.PublicKey URL string } // The method gets a set of witnesses for which a cosignature was // verified, and returns whether or not they are sufficient. type Quorum interface { IsQuorum(map[crypto.Hash]struct{}) bool } type Policy struct { logs map[crypto.Hash]Entity witnesses map[crypto.Hash]Entity quorum Quorum } func (p *Policy) VerifyCosignedTreeHead(logKeyHash *crypto.Hash, cth *types.CosignedTreeHead) error { log, ok := p.logs[*logKeyHash] if !ok { return fmt.Errorf("unknown log") } if !cth.Verify(&log.PublicKey) { return fmt.Errorf("invalid log signature") } verified := make(map[crypto.Hash]struct{}) failed := 0 for _, cs := range cth.Cosignatures { if witness, ok := p.witnesses[cs.KeyHash]; ok { if cs.Verify(&witness.PublicKey, logKeyHash, &cth.TreeHead) { verified[cs.KeyHash] = struct{}{} } else { failed++ } } } if !p.quorum.IsQuorum(verified) { return fmt.Errorf("not enough cosignatures, total: %d, verified: %d, failed to verify: %d", len(cth.Cosignatures), len(verified), failed) } return nil } type quorumSingle struct { w crypto.Hash } func (q *quorumSingle) IsQuorum(verified map[crypto.Hash]struct{}) bool { _, ok := verified[q.w] return ok } type quorumKofN struct { subQuorums []Quorum k int } func (q *quorumKofN) IsQuorum(verified map[crypto.Hash]struct{}) bool { c := 0 for _, sq := range q.subQuorums { if sq.IsQuorum(verified) { c++ } } return c >= q.k } func newEmptyPolicy() *Policy { return &Policy{ logs: make(map[crypto.Hash]Entity), witnesses: make(map[crypto.Hash]Entity), } } func (p *Policy) addLog(log *Entity) (crypto.Hash, error) { h := crypto.HashBytes(log.PublicKey[:]) if _, dup := p.logs[h]; dup { return crypto.Hash{}, fmt.Errorf("duplicate log: %x\n", log.PublicKey) } p.logs[h] = *log return h, nil } func (p *Policy) addWitness(witness *Entity) (crypto.Hash, error) { h := crypto.HashBytes(witness.PublicKey[:]) if _, dup := p.witnesses[h]; dup { return crypto.Hash{}, fmt.Errorf("duplicate witness: %x\n", witness.PublicKey) } p.witnesses[h] = *witness return h, nil } func randomizeEntities(m map[crypto.Hash]Entity) []Entity { entities := make([]Entity, 0, len(m)) for _, entity := range m { if len(entity.URL) > 0 { entities = append(entities, entity) } } // Return in randomized order. rand.Shuffle(len(entities), func(i, j int) { entities[i], entities[j] = entities[j], entities[i] }) return entities } // Returns all logs with url specified, in randomized order. func (p *Policy) GetLogsWithUrl() []Entity { return randomizeEntities(p.logs) } // Returns all witnesses with url specified, in randomized order. func (p *Policy) GetWitnessesWithUrl() []Entity { return randomizeEntities(p.witnesses) } func NewKofNPolicy(logs, witnesses []crypto.PublicKey, k int) (*Policy, error) { if k > len(witnesses) { return nil, fmt.Errorf("invalid policy k (%d) > n (%d)\n", k, len(witnesses)) } p := newEmptyPolicy() for _, l := range logs { if _, err := p.addLog(&Entity{PublicKey: l}); err != nil { return nil, err } } subQuorums := []Quorum{} for _, w := range witnesses { h, err := p.addWitness(&Entity{PublicKey: w}) if err != nil { return nil, err } subQuorums = append(subQuorums, &quorumSingle{h}) } p.quorum = &quorumKofN{subQuorums: subQuorums, k: k} return p, nil } sigsum-go-0.7.2/pkg/policy/policy_test.go000066400000000000000000000063361455374457700204340ustar00rootroot00000000000000package policy import ( "testing" "sigsum.org/sigsum-go/pkg/crypto" "sigsum.org/sigsum-go/pkg/types" ) func TestLogPolicy(t *testing.T) { th := types.TreeHead{Size: 3} var cths []types.CosignedTreeHead var logKeys []crypto.PublicKey var logHashes []crypto.Hash for i := 0; i < 3; i++ { pub, s, err := crypto.NewKeyPair() if err != nil { t.Fatal(err) } sth, err := th.Sign(s) if err != nil { t.Fatal(err) } cths = append(cths, types.CosignedTreeHead{SignedTreeHead: sth}) logKeys = append(logKeys, pub) logHashes = append(logHashes, crypto.HashBytes(pub[:])) } p, err := NewKofNPolicy(logKeys[:2], nil, 0) if err != nil { t.Fatal(err) } if err := p.VerifyCosignedTreeHead(&logHashes[0], &cths[0]); err != nil { t.Errorf("verifying treehead for log 0 failed: %v", err) } if err := p.VerifyCosignedTreeHead(&logHashes[1], &cths[1]); err != nil { t.Errorf("verifying treehead for log 1 failed: %v", err) } if err := p.VerifyCosignedTreeHead(&logHashes[2], &cths[2]); err == nil { t.Errorf("verifying treehead for log 2 succeeded, but it's not allowed by policy") } if err := p.VerifyCosignedTreeHead(&logHashes[1], &cths[0]); err == nil { t.Errorf("verifying treehead for log 0 with log hash 1 succeeeded") } } func TestWitnessPolicy(t *testing.T) { th := types.TreeHead{Size: 3} logPub, logSigner, err := crypto.NewKeyPair() if err != nil { t.Fatal(err) } logHash := crypto.HashBytes(logPub[:]) sth, err := th.Sign(logSigner) if err != nil { t.Fatal(err) } var witnessKeys []crypto.PublicKey var witnessHashes []crypto.Hash var cosignatures []types.Cosignature for i := 0; i < 5; i++ { pub, s, err := crypto.NewKeyPair() if err != nil { t.Fatal(err) } cosignature, err := th.Cosign(s, &logHash, 0) if err != nil { t.Fatal(err) } cosignatures = append(cosignatures, cosignature) witnessKeys = append(witnessKeys, pub) witnessHashes = append(witnessHashes, crypto.HashBytes(pub[:])) } // Four known witnesses, at least 3 cosignatures required. p, err := NewKofNPolicy([]crypto.PublicKey{logPub}, witnessKeys[:4], 3) if err != nil { t.Fatal(err) } for _, s := range []struct { desc string w []int // Indices of witnesses to include invalidate int // Signature to invalidate (-1 if none) expectValid bool }{ {"no cosignature", nil, -1, false}, {"only one cosignature", []int{0}, -1, false}, {"only two cosignatures", []int{0, 1, 4}, -1, false}, {"three cosignature", []int{0, 1, 2}, -1, true}, {"other three cosignature", []int{1, 2, 3}, -1, true}, {"all cosignatures", []int{0, 1, 2, 3, 4}, 4, true}, {"all cosignatures, one invalid", []int{0, 1, 2, 3, 4}, 2, true}, {"three cosignatures, but one invalid", []int{0, 2, 3, 4}, 2, false}, } { var present []types.Cosignature for _, i := range s.w { present = append(present, cosignatures[i]) if i == s.invalidate { present[len(present)-1].Signature[3] ^= 1 } } err := p.VerifyCosignedTreeHead(&logHash, &types.CosignedTreeHead{SignedTreeHead: sth, Cosignatures: present}) if s.expectValid && err != nil { t.Errorf("%s: Failed on valid cth: %v", s.desc, err) } if !s.expectValid && err == nil { t.Errorf("%s: Expected error, but got none", s.desc) } } } sigsum-go-0.7.2/pkg/proof/000077500000000000000000000000001455374457700153655ustar00rootroot00000000000000sigsum-go-0.7.2/pkg/proof/proof.go000066400000000000000000000114341455374457700170440ustar00rootroot00000000000000package proof import ( "bytes" "encoding/hex" "fmt" "io" "sigsum.org/sigsum-go/pkg/ascii" "sigsum.org/sigsum-go/pkg/crypto" "sigsum.org/sigsum-go/pkg/policy" "sigsum.org/sigsum-go/pkg/types" ) const ( SigsumProofVersion = 1 ShortChecksumSize = 2 ) type ShortChecksum [ShortChecksumSize]byte // Variant of types.Leaf, with truncated checksum. type ShortLeaf struct { ShortChecksum ShortChecksum Signature crypto.Signature KeyHash crypto.Hash } func NewShortLeaf(leaf *types.Leaf) ShortLeaf { proofLeaf := ShortLeaf{Signature: leaf.Signature, KeyHash: leaf.KeyHash} copy(proofLeaf.ShortChecksum[:], leaf.Checksum[:ShortChecksumSize]) return proofLeaf } func (l *ShortLeaf) ToLeaf(checksum *crypto.Hash) (types.Leaf, error) { if !bytes.Equal(l.ShortChecksum[:], checksum[:ShortChecksumSize]) { return types.Leaf{}, fmt.Errorf("checksum doesn't match truncated checksum") } return types.Leaf{Checksum: *checksum, Signature: l.Signature, KeyHash: l.KeyHash}, nil } func (l *ShortLeaf) Parse(p ascii.Parser) error { // Same as a leaf line from get-leaves, except that checksum is truncated. v, err := p.GetValues("leaf", 3) if err != nil { return err } l.ShortChecksum, err = decodeShortChecksum(v[0]) if err != nil { return fmt.Errorf("invalid submitter checksum: %v", err) } l.KeyHash, err = crypto.HashFromHex(v[1]) if err != nil { return fmt.Errorf("invalid submitter key hash: %v", err) } l.Signature, err = crypto.SignatureFromHex(v[2]) if err != nil { return fmt.Errorf("invalid leaf signature: %v", err) } return nil } func (l *ShortLeaf) ToASCII(w io.Writer) error { return ascii.WriteLine(w, "leaf", l.ShortChecksum[:], l.KeyHash[:], l.Signature[:]) } type SigsumProof struct { LogKeyHash crypto.Hash Leaf ShortLeaf TreeHead types.CosignedTreeHead Inclusion types.InclusionProof } func decodeShortChecksum(s string) (out ShortChecksum, err error) { var b []byte b, err = hex.DecodeString(s) if err != nil { return } if len(b) != len(out) { err = fmt.Errorf("unexpected checksum length, expected %d, got %d", len(out), len(b)) return } copy(out[:], b) return } func (sp *SigsumProof) FromASCII(f io.Reader) error { r := ascii.NewParagraphReader(f) p := ascii.NewParser(r) version, err := p.GetInt("version") if err != nil { return fmt.Errorf("invalid version line: %v", err) } if version != SigsumProofVersion { return fmt.Errorf("unexpected version %d, wanted %d", version, SigsumProofVersion) } sp.LogKeyHash, err = p.GetHash("log") if err != nil { return fmt.Errorf("invalid log line: %v", err) } if err := sp.Leaf.Parse(p); err != nil { return err } if err := p.GetEOF(); err != nil { return err } if err := r.NextParagraph(); err != nil { return fmt.Errorf("missing tree head part: %v", err) } if err := sp.TreeHead.FromASCII(r); err != nil { return err } if sp.TreeHead.Size == 0 { return fmt.Errorf("invalid tree: empty") } if sp.TreeHead.Size == 1 { sp.Inclusion = types.InclusionProof{} } else { if err := r.NextParagraph(); err != nil { return fmt.Errorf("missing inclusion proof part: %v", err) } if err := sp.Inclusion.FromASCII(r); err != nil { return err } } if err := r.NextParagraph(); err != io.EOF { if err != nil { return err } return fmt.Errorf("too many parts") } return nil } func (sp *SigsumProof) ToASCII(w io.Writer) error { if err := ascii.WriteInt(w, "version", SigsumProofVersion); err != nil { return err } if err := ascii.WriteHash(w, "log", &sp.LogKeyHash); err != nil { return err } if err := sp.Leaf.ToASCII(w); err != nil { return err } // Empty line as separator. if _, err := fmt.Fprint(w, "\n"); err != nil { return err } if err := sp.TreeHead.ToASCII(w); err != nil { return err } if sp.TreeHead.Size <= 1 { return nil } // Empty line as separator. if _, err := fmt.Fprint(w, "\n"); err != nil { return err } return sp.Inclusion.ToASCII(w) } func (sp *SigsumProof) Verify(msg *crypto.Hash, submitKey *crypto.PublicKey, policy *policy.Policy) error { checksum := crypto.HashBytes(msg[:]) leaf, err := sp.Leaf.ToLeaf(&checksum) if err != nil { return err } if sp.Leaf.KeyHash != crypto.HashBytes(submitKey[:]) { return fmt.Errorf("unexpected submit key hash") } if !leaf.Verify(submitKey) { return fmt.Errorf("leaf signature not valid") } if err := policy.VerifyCosignedTreeHead(&sp.LogKeyHash, &sp.TreeHead); err != nil { return err } leafHash := leaf.ToHash() return sp.Inclusion.Verify(&leafHash, &sp.TreeHead.TreeHead) } func (sp *SigsumProof) VerifyNoCosignatures(msg *crypto.Hash, submitKey *crypto.PublicKey, logKey *crypto.PublicKey) error { policy, err := policy.NewKofNPolicy([]crypto.PublicKey{*logKey}, nil, 0) if err != nil { return fmt.Errorf("internal error: %v", err) } return sp.Verify(msg, submitKey, policy) } sigsum-go-0.7.2/pkg/proof/proof_test.go000066400000000000000000000130441455374457700201020ustar00rootroot00000000000000package proof import ( "bytes" "strings" "testing" "sigsum.org/sigsum-go/pkg/crypto" "sigsum.org/sigsum-go/pkg/key" "sigsum.org/sigsum-go/pkg/policy" ) func TestASCII(t *testing.T) { for _, table := range []struct { desc string ascii string }{ // Examples from running sigsum-submit-test. {"size 1", `version=1 log=24a68b92fe18d8fb6dce4b3a3c8ac25453eb4ee6c3bb575651bdfbda95e2e952 leaf=5cc0 518ac523804cb74e2cb41f219aed1bfccc76a1202d8b891eed1a7cf3791eab9c 90c47772e2758fac56740ad52913af66874dc49b31ef21e4fab544a2836b7d9991f07559792f22c617c172e10391317b4a0a4396c4eb9cfc1871ed07a360240f size=1 root_hash=b02bd71073448d7a3ee402892f96c9d78b712242deed7e6fd8a98abcde33f46d signature=2eb4bfb59aa08531f325b8b233859d5c62187a311c7bb32e4cbd61e3a2b458d4e4451cfeb8a920d3cb4f755ed2f5f895628c0d92463f6f2d7d12fdf56f070d04 `}, {"size 4", `version=1 log=24a68b92fe18d8fb6dce4b3a3c8ac25453eb4ee6c3bb575651bdfbda95e2e952 leaf=7e28 518ac523804cb74e2cb41f219aed1bfccc76a1202d8b891eed1a7cf3791eab9c 5c46852140e41b49925f8c93dee5c3e776ababdd230425d17f44b519f5565e0026f86aea998ccb7685fbc672c7d016a3940db5d684279a39c870318c840bf002 size=4 root_hash=ca5e9898dd77d24019bee526e3cafa2c0c2c47e82897f5d237fdfa6f132ec0a8 signature=207347dc94e5ca8525a2d03901223064c96fa7a245f502c64b6dff2d50d6dd3bc9e809f81e0867b839e41e73296876dcef514ec5f323ccadd3cc5b0b0049730f leaf_index=3 node_hash=8a419a476109a749732ee0d9845470c995ae6502225647f4b3bbb1dff61a5b4f node_hash=eb94766b094058835d61c551a8ef581e8242ea419b665a2d2043291b98524e14 `}, } { indent := func(s string) string { return " " + strings.ReplaceAll(s, "\n", "\n ") } var proof SigsumProof if err := proof.FromASCII(bytes.NewBufferString(table.ascii)); err != nil { t.Errorf("%s: FromASCII failed, %v", table.desc, err) continue } var buf bytes.Buffer if err := proof.ToASCII(&buf); err != nil { t.Errorf("%s: ToASCII failed %v", table.desc, err) continue } if got, want := buf.String(), table.ascii; got != want { t.Errorf("%s: ascii roundtrip failed, got:\n%s\nexpected:\n%s", table.desc, indent(got), indent(want)) } } } func TestVerifyNoCosignatures(t *testing.T) { // Example from running sigsum-submit-test. proofASCII := `version=1 log=1f8d4547082a5985ad0e59ffe219f7a065e09c6b77a0012daf276e5dd1805b4b leaf=7e28 69512577a0f3c2695011ddc549756099017b7e2c8390341cbb24c57e886775f1 262737d935123272b9e3265fe2e38a014a9c1b13951e864737666251ada26dabbc6e699a4e527ec52a0be970e158abef35f087766d18d560853a44855119cf01 size=4 root_hash=7bca01e88737999fde5c1d6ecac27ae3cb49e14f21bcd3e7245c276877b899c9 signature=c60e5151b9d0f0efaf57022c0ec306c0f0275afef69333cc89df4fda328c87949fcfa44564f35020938a4cd6c1c50bc0349b2f54b82f5f6104b9cd52be2cd90e leaf_index=3 node_hash=e7d222a285ca81fdc76bfcd5513408c87dd42a18e03d6c3b672a05982163c01b node_hash=15cdc42440689a6f7599e09f61a4d638420cb58662f5994def1624ea4d923879 ` msg := crypto.Hash{ ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'f', 'o', 'o', '-', '4', '\n', } logKey := mustParsePublicKey(t, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN6kw3w2BWjlKLdrtnv4IaN+zg8/RpKGA98AbbTwjpdQ") submitKey := mustParsePublicKey(t, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMCMTGNMNe1HP2us/dR5dBpyrSPDgPQ9mX5j9iqbLIS+") var proof SigsumProof if err := proof.FromASCII(bytes.NewBufferString(proofASCII)); err != nil { t.Fatal(err) } if err := proof.VerifyNoCosignatures(&msg, &submitKey, &logKey); err != nil { t.Error(err) } // TODO: Test invalidating proof in different ways. } func TestVerify(t *testing.T) { // Example from running sigsum-submit-witness-test. proofASCII := `version=1 log=7c5fafc796c201e0fcd7567c5033a2777ec28363f54ea0ba97b57bece0d96acd leaf=7e28 8a578b9649ba01b7d29dd557906975d68a3aec50e3f9c08690420b8c6426856d 79b489a38548a67d78f06221b014d41be58b703237d17b4f203f0dd4ead9e2597149c2f118894581ce7473a61fa880716af6ff2138bade2cecc4b297099bf104 size=4 root_hash=3ddc56fd46e71e517b6936b977a457da7d398108141fcdf5c8386cdd724ab7a8 signature=ccbdd8c784726b732b8edd2039fbad5506e4acccd56e3e5d86c0ee109b3d2662e6881fe3d09fc48f9ddd31494463c5ec44926ff9158785ad1dd9b5d6434b0804 cosignature=bd8385aa82e07c3e1e297a1600c12bb25ce7a9490b5c1287ec30e09ac4c8b884 1683202758 e8d6c447d7847d5c1431ef86f8c60fa0cbacd975388b2a8f202fe4b0f9d0d544989c9d9351752d86aae2df72b9d7135b6b09de2ccaa6d68edf638105d69be609 leaf_index=3 node_hash=61010ae798308f5b97237615ab8c1b14f2c782c37616e97d0a170b617bc7a4ce node_hash=a5c3752be610d605ce5c64ee2e28ee5b94a1cc0a68742f18f24c9b5c82d07298 ` msg := crypto.Hash{ ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'f', 'o', 'o', '-', '4', '\n', } logKey := mustParsePublicKey(t, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKwmwKhVrEUaZTlHjhoWA4jwJLOF8TY+/NpHAXAHbAHl") submitKey := mustParsePublicKey(t, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMdLcxVjCAQUHbD4jCfFP+f8v1nmyjWkq6rXiexrK8II") witnessKey := mustParsePublicKey(t, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMvjV+a0ZASecDt75siSARk6zCoYwJWwaRqvULmx4VeK") var proof SigsumProof if err := proof.FromASCII(bytes.NewBufferString(proofASCII)); err != nil { t.Fatal(err) } policy, err := policy.NewKofNPolicy([]crypto.PublicKey{logKey}, []crypto.PublicKey{witnessKey}, 1) if err != nil { t.Fatal(err) } if err := proof.Verify(&msg, &submitKey, policy); err != nil { t.Error(err) } // TODO: Test invalidating proof in different ways. } func mustParsePublicKey(t *testing.T, ascii string) crypto.PublicKey { key, err := key.ParsePublicKey(ascii) if err != nil { t.Fatal(err) } return key } sigsum-go-0.7.2/pkg/requests/000077500000000000000000000000001455374457700161135ustar00rootroot00000000000000sigsum-go-0.7.2/pkg/requests/requests.go000066400000000000000000000101511455374457700203130ustar00rootroot00000000000000package requests import ( "encoding/hex" "fmt" "io" "strings" "sigsum.org/sigsum-go/pkg/ascii" "sigsum.org/sigsum-go/pkg/crypto" "sigsum.org/sigsum-go/pkg/types" ) type Leaf struct { Message crypto.Hash Signature crypto.Signature PublicKey crypto.PublicKey } type Leaves struct { StartIndex uint64 EndIndex uint64 } type InclusionProof struct { Size uint64 LeafHash crypto.Hash } type ConsistencyProof struct { OldSize uint64 NewSize uint64 } // Returns the index of the nth last occurence of the substr, or -1 if // there are not enough occurences. If n <= 0, returns len(s). func nLastIndex(s, substr string, n int) int { for i := 0; i < n; i++ { index := strings.LastIndex(s, substr) if index < 0 { return index } s = s[:index] } return len(s) } func (req *Leaf) ToASCII(w io.Writer) error { if err := ascii.WriteLine(w, "message", req.Message[:]); err != nil { return err } if err := ascii.WriteLine(w, "signature", req.Signature[:]); err != nil { return err } return ascii.WriteLine(w, "public_key", req.PublicKey[:]) } // Verifies the request signature, and creates a corresponding leaf on success. func (req *Leaf) Verify() (types.Leaf, error) { if !types.VerifyLeafMessage(&req.PublicKey, req.Message[:], &req.Signature) { return types.Leaf{}, fmt.Errorf("invalid signature") } return types.Leaf{ Checksum: crypto.HashBytes(req.Message[:]), Signature: req.Signature, KeyHash: crypto.HashBytes(req.PublicKey[:]), }, nil } // ToURL encodes request parameters at the end of a slash-terminated URL func (req *Leaves) ToURL(url string) string { return url + fmt.Sprintf("%d/%d", req.StartIndex, req.EndIndex) } // ToURL encodes request parameters at the end of a slash-terminated URL func (req *InclusionProof) ToURL(url string) string { return url + fmt.Sprintf("%d/%s", req.Size, hex.EncodeToString(req.LeafHash[:])) } // ToURL encodes request parameters at the end of a slash-terminated URL func (req *ConsistencyProof) ToURL(url string) string { return url + fmt.Sprintf("%d/%d", req.OldSize, req.NewSize) } func (req *Leaf) FromASCII(r io.Reader) error { p := ascii.NewParser(r) var err error req.Message, err = p.GetHash("message") if err != nil { return err } req.Signature, err = p.GetSignature("signature") if err != nil { return err } req.PublicKey, err = p.GetPublicKey("public_key") if err != nil { return err } return p.GetEOF() } func (req *Leaves) FromURLArgs(args string) (err error) { split := strings.Split(args, "/") if len(split) != 2 { return fmt.Errorf("invalid arguments") } if req.StartIndex, err = ascii.IntFromDecimal(split[0]); err != nil { return err } req.EndIndex, err = ascii.IntFromDecimal(split[1]) return err } // FromURL parses request parameters from a URL that is not slash-terminated func (req *Leaves) FromURL(url string) (err error) { index := nLastIndex(url, "/", 2) if index < 0 { return fmt.Errorf("not enough input") } return req.FromURLArgs(url[index+1:]) } func (req *InclusionProof) FromURLArgs(args string) (err error) { split := strings.Split(args, "/") if len(split) != 2 { return fmt.Errorf("invalid arguments") } if req.Size, err = ascii.IntFromDecimal(split[0]); err != nil { return err } req.LeafHash, err = crypto.HashFromHex(split[1]) return err } // FromURL parses request parameters from a URL that is not slash-terminated func (req *InclusionProof) FromURL(url string) (err error) { index := nLastIndex(url, "/", 2) if index < 0 { return fmt.Errorf("not enough input") } return req.FromURLArgs(url[index+1:]) } func (req *ConsistencyProof) FromURLArgs(args string) (err error) { split := strings.Split(args, "/") if len(split) != 2 { return fmt.Errorf("invalid arguments") } if req.OldSize, err = ascii.IntFromDecimal(split[0]); err != nil { return err } req.NewSize, err = ascii.IntFromDecimal(split[1]) return err } // FromURL parses request parameters from a URL that is not slash-terminated func (req *ConsistencyProof) FromURL(url string) (err error) { index := nLastIndex(url, "/", 2) if index < 0 { return fmt.Errorf("not enough input") } return req.FromURLArgs(url[index+1:]) } sigsum-go-0.7.2/pkg/requests/requests_test.go000066400000000000000000000140611455374457700213560ustar00rootroot00000000000000package requests import ( "bytes" "fmt" "io" "testing" "sigsum.org/sigsum-go/pkg/crypto" "sigsum.org/sigsum-go/pkg/types" ) func TestLeafToASCII(t *testing.T) { desc := "valid" buf := bytes.Buffer{} if err := validLeaf(t).ToASCII(&buf); err != nil { t.Fatalf("got error true but wanted false in test %q: %v", desc, err) } if got, want := buf.String(), validLeafASCII(t); got != want { t.Errorf("got leaf request\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, desc) } } func TestLeavesToURL(t *testing.T) { url := types.EndpointGetLeaves.Path("https://poc.sigsum.org") req := Leaves{1, 2} want := url + "1/2" if got := req.ToURL(url); got != want { t.Errorf("got url %s but wanted %s", got, want) } } func TestInclusionProofToURL(t *testing.T) { url := types.EndpointGetInclusionProof.Path("https://poc.sigsum.org") req := InclusionProof{1, crypto.Hash{}} want := url + "1/0000000000000000000000000000000000000000000000000000000000000000" if got := req.ToURL(url); got != want { t.Errorf("got url %s but wanted %s", got, want) } } func TestConsistencyProofToURL(t *testing.T) { url := types.EndpointGetConsistencyProof.Path("https://poc.sigsum.org") req := ConsistencyProof{1, 2} want := url + "1/2" if got := req.ToURL(url); got != want { t.Errorf("got url %s but wanted %s", got, want) } } func TestLeafFromASCII(t *testing.T) { for _, table := range []struct { desc string serialized io.Reader wantErr bool want *Leaf }{ { desc: "invalid: not a leaf request (unexpected key-value pair)", serialized: bytes.NewBufferString(validLeafASCII(t) + "key=4"), wantErr: true, }, { desc: "valid", serialized: bytes.NewBufferString(validLeafASCII(t)), want: validLeaf(t), }, } { var leaf Leaf err := leaf.FromASCII(table.serialized) 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 } if got, want := leaf, *table.want; got != want { t.Errorf("got leaf request\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, table.desc) } } } func TestLeavesFromURL(t *testing.T) { for _, table := range []struct { desc string input string want Leaves wantErr bool }{ {"invalid: not enough parameters", "some-url", Leaves{}, true}, {"invalid: start index has a leading sign", "some-url/+1/2", Leaves{}, true}, {"invalid: start index is empty", "some-url//2", Leaves{}, true}, {"invalid: end index is empty", "some-url/1/", Leaves{}, true}, {"valid", "some-url/1/2", Leaves{1, 2}, false}, } { var req Leaves err := req.FromURL(table.input) if got, want := err != nil, table.wantErr; got != want { t.Errorf("%s: got error %v but wanted %v: %v", table.desc, got, want, err) } if err != nil { continue } if got, want := req, table.want; got != want { t.Errorf("%s: got leaves request\n%v\nbut wanted\n%v", table.desc, got, want) } } } func TestInclusionProofFromURL(t *testing.T) { badHex := "F0000000x0000000000000000000000000000000000000000000000000000000" shortHex := "00ff" zeroHash := "0000000000000000000000000000000000000000000000000000000000000000" for _, table := range []struct { desc string input string want InclusionProof wantErr bool }{ {"invalid: not enough parameters", "some-url", InclusionProof{}, true}, {"invalid: tree size has a leading sign", "some-url/+1/" + zeroHash, InclusionProof{}, true}, {"invalid: tree size is empty", "some-url//" + zeroHash, InclusionProof{}, true}, {"invalid: leaf hash is invalid hex", "some-url/1/" + badHex, InclusionProof{}, true}, {"invalid: leaf hash is hex but too short", "some-url/1/" + shortHex, InclusionProof{}, true}, {"valid", "some-url/1/" + zeroHash, InclusionProof{1, crypto.Hash{}}, false}, } { var req InclusionProof err := req.FromURL(table.input) if got, want := err != nil, table.wantErr; got != want { t.Errorf("%s: got error %v but wanted %v: %v", table.desc, got, want, err) } if err != nil { continue } if got, want := req, table.want; got != want { t.Errorf("%s: got inclusion proof request\n%v\nbut wanted\n%v", table.desc, got, want) } } } func TestConsistencyProofFromURL(t *testing.T) { for _, table := range []struct { desc string input string want ConsistencyProof wantErr bool }{ {"invalid: not enough parameters", "some-url", ConsistencyProof{}, true}, {"invalid: old size has a leading sign", "some-url/+1/2", ConsistencyProof{}, true}, {"invalid: old size is empty", "some-url//2", ConsistencyProof{}, true}, {"invalid: new size is empty", "some-url/1/", ConsistencyProof{}, true}, {"valid", "some-url/1/2", ConsistencyProof{1, 2}, false}, } { var req ConsistencyProof err := req.FromURL(table.input) if got, want := err != nil, table.wantErr; got != want { t.Errorf("%s: got error %v but wanted %v: %v", table.desc, got, want, err) } if err != nil { continue } if got, want := req, table.want; got != want { t.Errorf("%s: got consistency proof request\n%v\nbut wanted\n%v", table.desc, got, want) } } } func validLeaf(t *testing.T) *Leaf { t.Helper() return &Leaf{ Message: *newHashBufferInc(t), Signature: *newSigBufferInc(t), PublicKey: *newPubBufferInc(t), } } func validLeafASCII(t *testing.T) string { t.Helper() return fmt.Sprintf("%s=%x\n%s=%x\n%s=%x\n", "message", newHashBufferInc(t)[:], "signature", newSigBufferInc(t)[:], "public_key", newPubBufferInc(t)[:], ) } func validLeaves(t *testing.T) *Leaves { t.Helper() return &Leaves{ StartIndex: 1, EndIndex: 4, } } func newHashBufferInc(t *testing.T) *crypto.Hash { t.Helper() var buf crypto.Hash for i := 0; i < len(buf); i++ { buf[i] = byte(i) } return &buf } func newSigBufferInc(t *testing.T) *crypto.Signature { t.Helper() var buf crypto.Signature for i := 0; i < len(buf); i++ { buf[i] = byte(i) } return &buf } func newPubBufferInc(t *testing.T) *crypto.PublicKey { t.Helper() var buf crypto.PublicKey for i := 0; i < len(buf); i++ { buf[i] = byte(i) } return &buf } sigsum-go-0.7.2/pkg/requests/witness.go000066400000000000000000000032241455374457700201370ustar00rootroot00000000000000package requests import ( "fmt" "io" "strings" "sigsum.org/sigsum-go/pkg/ascii" "sigsum.org/sigsum-go/pkg/crypto" "sigsum.org/sigsum-go/pkg/types" ) type AddTreeHead struct { KeyHash crypto.Hash TreeHead types.SignedTreeHead OldSize uint64 Proof types.ConsistencyProof } func (req *AddTreeHead) FromASCII(r io.Reader) error { p := ascii.NewParser(r) var err error req.KeyHash, err = p.GetHash("key_hash") if err != nil { return err } if err := req.TreeHead.Parse(&p); err != nil { return err } req.OldSize, err = p.GetInt("old_size") if err != nil { return err } if req.OldSize > req.TreeHead.Size { return fmt.Errorf("invalid request, old_size(%d) > size(%d)", req.OldSize, req.TreeHead.Size) } // Cases of trivial consistency. if req.OldSize == 0 || req.OldSize == req.TreeHead.Size { return p.GetEOF() } return req.Proof.Parse(&p) } func (req *AddTreeHead) ToASCII(w io.Writer) error { if err := ascii.WriteHash(w, "key_hash", &req.KeyHash); err != nil { return err } if err := req.TreeHead.ToASCII(w); err != nil { return err } if err := ascii.WriteInt(w, "old_size", req.OldSize); err != nil { return err } return req.Proof.ToASCII(w) } type GetTreeSize struct { KeyHash crypto.Hash } func (req *GetTreeSize) ToURL(url string) string { return fmt.Sprintf("%s%x", url, req.KeyHash) } func (req *GetTreeSize) FromURLArgs(args string) error { var err error req.KeyHash, err = crypto.HashFromHex(args) return err } func (req *GetTreeSize) FromURL(url string) error { split := strings.Split(url, "/") if len(split) < 1 { return fmt.Errorf("not enough input") } return req.FromURLArgs(split[len(split)-1]) } sigsum-go-0.7.2/pkg/requests/witness_test.go000066400000000000000000000030241455374457700211740ustar00rootroot00000000000000package requests import ( "bytes" "reflect" "testing" "sigsum.org/sigsum-go/pkg/crypto" "sigsum.org/sigsum-go/pkg/types" ) func TestAddTreeHeadToASCII(t *testing.T) { req := validAddTreeHead() buf := bytes.Buffer{} if err := req.ToASCII(&buf); err != nil { t.Fatalf("ToASCII failed: %v", err) } if got, want := buf.String(), validAddTreeHeadASCII(); got != want { t.Errorf("unexpected ToASCII, got: %q, want: %q", got, want) } } func TestAddTreeHeadFromASCII(t *testing.T) { var req AddTreeHead if err := req.FromASCII(bytes.NewBufferString(validAddTreeHeadASCII())); err != nil { t.Errorf("FromASCII failed: %v", err) } else if got, want := req, validAddTreeHead(); !reflect.DeepEqual(got, want) { t.Errorf("unexpected FromASCII, got: %#v, want: %#v", got, want) } } func validAddTreeHead() AddTreeHead { return AddTreeHead{ KeyHash: crypto.Hash{1}, TreeHead: types.SignedTreeHead{ TreeHead: types.TreeHead{ Size: 2, RootHash: crypto.Hash{2}, }, Signature: crypto.Signature{3}, }, OldSize: 1, Proof: types.ConsistencyProof{[]crypto.Hash{crypto.Hash{4}}}, } } func validAddTreeHeadASCII() string { return `key_hash=0100000000000000000000000000000000000000000000000000000000000000 size=2 root_hash=0200000000000000000000000000000000000000000000000000000000000000 signature=03000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 old_size=1 node_hash=0400000000000000000000000000000000000000000000000000000000000000 ` } sigsum-go-0.7.2/pkg/server/000077500000000000000000000000001455374457700155465ustar00rootroot00000000000000sigsum-go-0.7.2/pkg/server/config.go000066400000000000000000000010511455374457700173370ustar00rootroot00000000000000package server import ( "time" ) const ( defaultTimeout = 30 * time.Second ) type Metrics interface { OnRequest(pattern string) OnResponse(pattern string, status int, latency time.Duration) } type noMetrics struct{} func (_ noMetrics) OnRequest(_ string) {} func (_ noMetrics) OnResponse(_ string, _ int, _ time.Duration) {} type Config struct { Prefix string Timeout time.Duration Metrics Metrics } func (c *Config) getTimeout() time.Duration { if c.Timeout > 0 { return c.Timeout } return defaultTimeout } sigsum-go-0.7.2/pkg/server/log.go000066400000000000000000000107631455374457700166650ustar00rootroot00000000000000package server import ( "context" "fmt" "net/http" "sigsum.org/sigsum-go/pkg/api" "sigsum.org/sigsum-go/pkg/requests" "sigsum.org/sigsum-go/pkg/submit-token" "sigsum.org/sigsum-go/pkg/types" ) func newGetLeavesServer(config *Config, getLeaves func(context.Context, requests.Leaves) ([]types.Leaf, error)) *server { server := newServer(config) server.register(types.EndpointGetLeaves, http.MethodGet, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var req requests.Leaves if err := req.FromURLArgs(GetSigsumURLArguments(r)); err != nil { reportErrorCode(w, r.URL, http.StatusBadRequest, err) return } if req.StartIndex >= req.EndIndex { reportErrorCode(w, r.URL, http.StatusBadRequest, fmt.Errorf("start_index(%d) must be less than end_index(%d)", req.StartIndex, req.EndIndex)) return } leaves, err := getLeaves(r.Context(), req) if err != nil { reportError(w, r.URL, err) return } if got, max := uint64(len(leaves)), req.EndIndex-req.StartIndex; got == 0 || got > max { reportError(w, r.URL, fmt.Errorf("bad leaf count %d, should have 0 < count <= %d", got, max)) return } if err := types.LeavesToASCII(w, leaves); err != nil { reportError(w, r.URL, err) } })) return server } // Exported for the benefit of the primary node's internal endpoint. func NewGetLeavesServer(config *Config, getLeaves func(context.Context, requests.Leaves) ([]types.Leaf, error)) http.Handler { return newGetLeavesServer(config, getLeaves) } func NewLog(config *Config, log api.Log) http.Handler { server := newGetLeavesServer(config, log.GetLeaves) server.register(types.EndpointGetTreeHead, http.MethodGet, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { cth, err := log.GetTreeHead(r.Context()) if err != nil { reportError(w, r.URL, err) return } if err = cth.ToASCII(w); err != nil { reportError(w, r.URL, err) } })) server.register(types.EndpointGetInclusionProof, http.MethodGet, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var req requests.InclusionProof if err := req.FromURLArgs(GetSigsumURLArguments(r)); err != nil { reportErrorCode(w, r.URL, http.StatusBadRequest, err) return } if req.Size < 2 { // Size:0 => not possible to prove inclusion of anything // Size:1 => you don't need an inclusion proof (it is always empty) reportErrorCode(w, r.URL, http.StatusBadRequest, fmt.Errorf("size(%d) must be larger than one", req.Size)) return } proof, err := log.GetInclusionProof(r.Context(), req) if err != nil { reportError(w, r.URL, err) return } if err := proof.ToASCII(w); err != nil { reportError(w, r.URL, err) } })) server.register(types.EndpointGetConsistencyProof, http.MethodGet, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var req requests.ConsistencyProof if err := req.FromURLArgs(GetSigsumURLArguments(r)); err != nil { reportErrorCode(w, r.URL, http.StatusBadRequest, err) } if req.OldSize < 1 { reportErrorCode(w, r.URL, http.StatusBadRequest, fmt.Errorf("old_size(%d) must be larger than zero", req.OldSize)) return } if req.NewSize <= req.OldSize { reportErrorCode(w, r.URL, http.StatusBadRequest, fmt.Errorf("new_size(%d) must be larger than old_size(%d)", req.NewSize, req.OldSize)) return } proof, err := log.GetConsistencyProof(r.Context(), req) if err != nil { reportError(w, r.URL, err) return } if err := proof.ToASCII(w); err != nil { reportError(w, r.URL, err) } })) server.register(types.EndpointAddLeaf, http.MethodPost, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var req requests.Leaf var submitHeader *token.SubmitHeader if err := req.FromASCII(r.Body); err != nil { reportErrorCode(w, r.URL, http.StatusBadRequest, err) return } if headerValue := r.Header.Get("Sigsum-Token"); len(headerValue) > 0 { submitHeader = &token.SubmitHeader{} if err := submitHeader.FromHeader(headerValue); err != nil { reportErrorCode(w, r.URL, http.StatusBadRequest, fmt.Errorf("Invalid Sigsum-Submit: header: %v", err)) return } } // TODO: Change AddLeaf to return api.ErrAccepted, instead of the persisted flag? persisted, err := log.AddLeaf(r.Context(), req, submitHeader) if err != nil { reportError(w, r.URL, err) return } if !persisted { reportError(w, r.URL, api.ErrAccepted) } })) return server } sigsum-go-0.7.2/pkg/server/log_test.go000066400000000000000000000250101455374457700177130ustar00rootroot00000000000000package server import ( "fmt" "io" "net/http" "testing" "time" "github.com/golang/mock/gomock" "sigsum.org/sigsum-go/pkg/api" "sigsum.org/sigsum-go/pkg/crypto" "sigsum.org/sigsum-go/pkg/mocks" "sigsum.org/sigsum-go/pkg/requests" "sigsum.org/sigsum-go/pkg/submit-token" "sigsum.org/sigsum-go/pkg/types" ) func TestGetTreeHead(t *testing.T) { cth := types.CosignedTreeHead{ SignedTreeHead: types.SignedTreeHead{ TreeHead: types.TreeHead{ Size: 3, RootHash: crypto.Hash{1}, }, Signature: crypto.Signature{2}, }, Cosignatures: []types.Cosignature{ types.Cosignature{ KeyHash: crypto.Hash{3}, Timestamp: 17, Signature: crypto.Signature{4}, }, }, } ctrl := gomock.NewController(t) defer ctrl.Finish() log := mocks.NewMockLog(ctrl) config := Config{Prefix: "foo", Timeout: 5 * time.Minute} server := NewLog(&config, log) log.EXPECT().GetTreeHead(gomock.Any()).Return(cth, nil) result, body := queryServer(t, server, http.MethodGet, "/foo/get-tree-head", "") if got, want := result.StatusCode, 200; got != want { t.Errorf("Unexpected status code, got %d, want %d", got, want) return } if got, want := body, writeFuncToString(t, cth.ToASCII); got != want { t.Errorf("Unexpected tree head, got %v, want %v", got, want) } } func TestGetInclusionProof(t *testing.T) { req := requests.InclusionProof{ Size: 2, LeafHash: crypto.Hash{ 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, }, } proof := types.InclusionProof{ LeafIndex: 1, Path: []crypto.Hash{crypto.Hash{2}}, } for _, table := range []struct { url string req *requests.InclusionProof rsp types.InclusionProof status int err error }{ {url: "/foo/get-inclusion-proof", status: 301}, {url: "/foo/get-inclusion-proof/", status: 400}, {url: "/foo/get-inclusion-proof/x", status: 400}, {url: "/foo/get-inclusion-proof/2/x", status: 400}, {url: "/foo/get-inclusion-proof/2/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", req: &req, rsp: proof, status: 200, }, {url: "/foo/get-inclusion-proof/2/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", req: &req, rsp: proof, status: 404, err: api.ErrNotFound, }, {url: "/foo/get-inclusion-proof/0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", status: 400}, {url: "/foo/get-inclusion-proof/1/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", status: 400}, {url: "/foo/get-inclusion-proof/2/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/b", status: 400}, } { func() { ctrl := gomock.NewController(t) defer ctrl.Finish() log := mocks.NewMockLog(ctrl) config := Config{Prefix: "foo", Timeout: 5 * time.Minute} server := NewLog(&config, log) if table.req != nil { log.EXPECT().GetInclusionProof(gomock.Any(), *table.req).Return(table.rsp, table.err) } result, body := queryServer(t, server, http.MethodGet, table.url, "") if got, want := result.StatusCode, table.status; got != want { t.Errorf("Unexpected status code for %q, got %d, want %d", table.url, got, want) } if table.status != 200 { return } if got, want := body, writeFuncToString(t, proof.ToASCII); got != want { t.Errorf("Unexpected response for %q, got %q, want %q", table.url, got, want) } }() } } func TestGetConsistencyProof(t *testing.T) { req := requests.ConsistencyProof{ OldSize: 2, NewSize: 5, } proof := types.ConsistencyProof{ Path: []crypto.Hash{crypto.Hash{2}}, } for _, table := range []struct { url string req *requests.ConsistencyProof rsp types.ConsistencyProof status int err error }{ {url: "/foo/get-consistency-proof", status: 301}, {url: "/foo/get-consistency-proof/", status: 400}, {url: "/foo/get-consistency-proof/x", status: 400}, {url: "/foo/get-consistency-proof/2/x", status: 400}, {url: "/foo/get-consistency-proof/2/5", req: &req, rsp: proof, status: 200, }, {url: "/foo/get-consistency-proof/2/5", req: &req, rsp: proof, status: 403, // Arbitrary error err: api.ErrForbidden, }, {url: "/foo/get-consistency-proof/2/2", status: 400}, {url: "/foo/get-consistency-proof/0/2", status: 400}, {url: "/foo/get-consistency-proof/2/1", status: 400}, } { func() { ctrl := gomock.NewController(t) defer ctrl.Finish() log := mocks.NewMockLog(ctrl) config := Config{Prefix: "foo", Timeout: 5 * time.Minute} server := NewLog(&config, log) if table.req != nil { log.EXPECT().GetConsistencyProof(gomock.Any(), *table.req).Return(table.rsp, table.err) } result, body := queryServer(t, server, http.MethodGet, table.url, "") if got, want := result.StatusCode, table.status; got != want { t.Errorf("Unexpected status code for %q, got %d, want %d", table.url, got, want) } if table.status != 200 { return } if got, want := body, writeFuncToString(t, proof.ToASCII); got != want { t.Errorf("Unexpected response for %q, got %q, want %q", table.url, got, want) } }() } } func TestGetLeaves(t *testing.T) { req := requests.Leaves{StartIndex: 2, EndIndex: 5} for _, table := range []struct { url string req *requests.Leaves rsp []types.Leaf status int err error }{ {url: "/foo/get-leaves", status: 301}, {url: "/foo/get-leaves/", status: 400}, {url: "/foo/get-leaves/x", status: 400}, {url: "/foo/get-leaves/2/x", status: 400}, {url: "/foo/get-leaves/2/5", req: &req, rsp: make([]types.Leaf, 3), status: 200, }, {url: "/foo/get-leaves/2/5", req: &req, rsp: make([]types.Leaf, 4), status: 500, }, {url: "/foo/get-leaves/2/5", req: &req, status: 500, }, {url: "/foo/get-leaves/2/5", req: &req, rsp: make([]types.Leaf, 3), status: 403, // Arbitrary error err: api.ErrForbidden, }, {url: "/foo/get-leaves/2/2", status: 400}, {url: "/foo/get-leaves/2/1", status: 400}, } { func() { ctrl := gomock.NewController(t) defer ctrl.Finish() log := mocks.NewMockLog(ctrl) config := Config{Prefix: "foo", Timeout: 5 * time.Minute} server := NewLog(&config, log) if table.req != nil { log.EXPECT().GetLeaves(gomock.Any(), *table.req).Return(table.rsp, table.err) } result, body := queryServer(t, server, http.MethodGet, table.url, "") if got, want := result.StatusCode, table.status; got != want { t.Errorf("Unexpected status code for %q, got %d, want %d", table.url, got, want) } if table.status != 200 { return } if got, want := body, writeFuncToString(t, func(w io.Writer) error { return types.LeavesToASCII(w, table.rsp) }); got != want { t.Errorf("Unexpected response for %q, got %q, want %q", table.url, got, want) } }() } } // Matches a pointer if both are nil, or point to equal objects. type ptrMatcher[T comparable] struct { ptr *T } func (m ptrMatcher[T]) Matches(x any) bool { if ptr, ok := x.(*T); ok { if ptr == nil { return m.ptr == nil } return m.ptr != nil && *m.ptr == *ptr } return false } func (m ptrMatcher[T]) String() string { if m.ptr == nil { var zero T return fmt.Sprintf("nil ptr to %T", zero) } return fmt.Sprintf("ptr to %#v", *m.ptr) } func TestAddLeaf(t *testing.T) { req := requests.Leaf{ Message: crypto.Hash{1}, Signature: crypto.Signature{2}, PublicKey: crypto.PublicKey{3}, } tokenSignature, err := crypto.SignatureFromHex( "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") if err != nil { t.Fatalf("internal test error: %v", err) } for _, table := range []struct { desc string url string asciiHeader string submitHeader *token.SubmitHeader exp bool rsp bool status int err error }{ {desc: "accepted", exp: true, rsp: false, status: 202}, {desc: "success", exp: true, rsp: true, status: 200}, {desc: "bad url", url: "/foo/add-leaf/", rsp: true, status: 404}, {desc: "forbidden", err: api.ErrForbidden, status: 403}, {desc: "rate limit", err: api.ErrTooManyRequests, status: 429}, { desc: "success with submit token", exp: true, asciiHeader: "foo.example.org aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", submitHeader: &token.SubmitHeader{Domain: "foo.example.org", Token: tokenSignature}, status: 202, }, { desc: "forbidden with submit token", asciiHeader: "foo.example.org aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", submitHeader: &token.SubmitHeader{Domain: "foo.example.org", Token: tokenSignature}, err: api.ErrForbidden, status: 403, }, { desc: "rate limit with with submit token", asciiHeader: "foo.example.org aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", submitHeader: &token.SubmitHeader{Domain: "foo.example.org", Token: tokenSignature}, err: api.ErrTooManyRequests, status: 429, }, { desc: "invalid submit token", asciiHeader: "foo.example.org aaaaaxaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", status: 400, }, } { func() { ctrl := gomock.NewController(t) defer ctrl.Finish() log := mocks.NewMockLog(ctrl) config := Config{Prefix: "foo", Timeout: 5 * time.Minute} server := NewLog(&config, log) if table.exp || table.err != nil { log.EXPECT().AddLeaf(gomock.Any(), req, ptrMatcher[token.SubmitHeader]{table.submitHeader}).Return(table.rsp, table.err) } url := "/foo/add-leaf" if table.url != "" { url = table.url } result, body := queryServerHook(t, server, http.MethodPost, url, writeFuncToString(t, req.ToASCII), func(req *http.Request) *http.Request { if table.asciiHeader != "" { req.Header.Set("sigsum-token", table.asciiHeader) } return req }) if got, want := result.StatusCode, table.status; got != want { t.Errorf("%s: Unexpected status code for, got %d, want %d", table.desc, got, want) } if table.status != 200 { t.Logf("%s: response body: %q", table.desc, body) return } if body != "" { t.Errorf("%s: Unexpected response body: %q", table.desc, body) } }() } } sigsum-go-0.7.2/pkg/server/secondary.go000066400000000000000000000010731455374457700200650ustar00rootroot00000000000000package server import ( "net/http" "sigsum.org/sigsum-go/pkg/api" "sigsum.org/sigsum-go/pkg/types" ) func NewSecondary(config *Config, secondary api.Secondary) http.Handler { server := newServer(config) server.register(types.EndpointGetSecondaryTreeHead, http.MethodGet, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { sth, err := secondary.GetSecondaryTreeHead(r.Context()) if err != nil { reportError(w, r.URL, err) return } if err := sth.ToASCII(w); err != nil { reportError(w, r.URL, err) } })) return server } sigsum-go-0.7.2/pkg/server/secondary_test.go000066400000000000000000000017621455374457700211310ustar00rootroot00000000000000package server import ( "net/http" "testing" "time" "github.com/golang/mock/gomock" "sigsum.org/sigsum-go/pkg/crypto" "sigsum.org/sigsum-go/pkg/mocks" "sigsum.org/sigsum-go/pkg/types" ) func TestGetSecondaryTreeHead(t *testing.T) { sth := types.SignedTreeHead{ TreeHead: types.TreeHead{ Size: 3, RootHash: crypto.Hash{1}, }, Signature: crypto.Signature{2}, } ctrl := gomock.NewController(t) defer ctrl.Finish() log := mocks.NewMockSecondary(ctrl) config := Config{Prefix: "foo", Timeout: 5 * time.Minute} server := NewSecondary(&config, log) log.EXPECT().GetSecondaryTreeHead(gomock.Any()).Return(sth, nil) result, body := queryServer(t, server, http.MethodGet, "/foo/get-secondary-tree-head", "") if got, want := result.StatusCode, 200; got != want { t.Errorf("Unexpected status code, got %d, want %d", got, want) return } if got, want := body, writeFuncToString(t, sth.ToASCII); got != want { t.Errorf("Unexpected tree head, got %v, want %v", got, want) } } sigsum-go-0.7.2/pkg/server/server.go000066400000000000000000000077231455374457700174140ustar00rootroot00000000000000// package server implements the http-layer of the Sigsum apis. // It defines handlers for incoming HTTP requests, converting to // request to a method call on the approriate api interface. It checks // for errors where it's clear that a request is bad according to the // specs, regardless of what's backing the api interface. It converts // the api method's return values (success or errors) into a http // response to be returned to the client. Optionally, it can produce // basic request and response metrics. package server import ( "context" "net/http" "net/url" "strings" "time" "sigsum.org/sigsum-go/pkg/api" "sigsum.org/sigsum-go/pkg/log" "sigsum.org/sigsum-go/pkg/types" ) type server struct { config Config mux *http.ServeMux } func newServer(config *Config) *server { server := server{config: *config, mux: http.NewServeMux()} if server.config.Metrics == nil { server.config.Metrics = noMetrics{} } return &server } // Wrapper to check that the appropriate method is used. Also used to // distinguish our registered handlers from internally generated ones. type handlerWithMethod struct { method string handler http.Handler } type sigsumURLArguments struct{} func (h *handlerWithMethod) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Error handling is based on RFC 7231, see Sections 6.5.5 // (Status 405) and 6.5.1 (Status 400). if r.Method != h.method { statusCode := http.StatusBadRequest switch r.Method { case http.MethodGet, http.MethodHead, http.MethodPost, http.MethodPut: w.Header().Set("Allow", h.method) statusCode = http.StatusMethodNotAllowed } http.Error(w, http.StatusText(statusCode), statusCode) return } h.handler.ServeHTTP(w, r) } // A response writer that records the status code. type responseWriterWithStatus struct { statusCode int w http.ResponseWriter } func (ws *responseWriterWithStatus) Header() http.Header { return ws.w.Header() } func (ws *responseWriterWithStatus) Write(data []byte) (int, error) { return ws.w.Write(data) } func (ws *responseWriterWithStatus) WriteHeader(statusCode int) { ws.statusCode = statusCode ws.w.WriteHeader(statusCode) } func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { handler, pattern := s.mux.Handler(r) if _, ok := handler.(*handlerWithMethod); !ok { // Some internally generated handler (redirect, or // page not found), just call it with no additional // processing. handler.ServeHTTP(w, r) return } endpoint := strings.TrimPrefix(pattern, "/") if len(s.config.Prefix) > 0 { endpoint = strings.TrimPrefix(endpoint, s.config.Prefix+"/") } s.config.Metrics.OnRequest(endpoint) start := time.Now() response := responseWriterWithStatus{w: w, statusCode: http.StatusOK} defer func() { latency := time.Now().Sub(start) s.config.Metrics.OnResponse(endpoint, response.statusCode, latency) }() ctx, cancel := context.WithTimeout(r.Context(), s.config.getTimeout()) defer cancel() if strings.HasSuffix(pattern, "/") { ctx = context.WithValue(ctx, sigsumURLArguments{}, strings.TrimPrefix(r.URL.Path, pattern)) } handler.ServeHTTP(&response, r.WithContext(ctx)) } // Returns an empty string for missing arguments. func GetSigsumURLArguments(r *http.Request) string { if args, ok := r.Context().Value(sigsumURLArguments{}).(string); ok { return args } return "" } func (s *server) register(endpoint types.Endpoint, method string, handler http.Handler) { s.mux.Handle("/"+endpoint.Path(s.config.Prefix), &handlerWithMethod{method: method, handler: handler}) } func reportErrorCode(w http.ResponseWriter, url *url.URL, statusCode int, err error) { // Log all internal server errors. if statusCode == http.StatusInternalServerError { log.Error("Internal server error for %q: %v", url.Path, err) } else { log.Debug("%q: status %d, %v", url.Path, statusCode, err) } http.Error(w, err.Error(), statusCode) } func reportError(w http.ResponseWriter, url *url.URL, err error) { reportErrorCode(w, url, api.ErrorStatusCode(err), err) } sigsum-go-0.7.2/pkg/server/server_test.go000066400000000000000000000161231455374457700204450ustar00rootroot00000000000000package server import ( "bytes" "fmt" "io" "net/http" "net/http/httptest" "testing" "time" "github.com/golang/mock/gomock" "sigsum.org/sigsum-go/pkg/api" "sigsum.org/sigsum-go/pkg/mocks" ) // Run HTTP request func queryServerHook(t *testing.T, server http.Handler, method, url, body string, hook func(req *http.Request) *http.Request) (*http.Response, string) { t.Helper() var reqBody io.Reader if len(body) > 0 { reqBody = bytes.NewBufferString(body) } req, err := http.NewRequest(method, url, reqBody) if err != nil { t.Fatalf("creating http %s request for %q failed: %v", method, url, err) } req = hook(req) w := httptest.NewRecorder() server.ServeHTTP(w, req) result := w.Result() defer result.Body.Close() respBody, err := io.ReadAll(result.Body) if err != nil { t.Fatalf("reading http response for %q failed: %v", url, err) } return result, string(respBody) } func queryServer(t *testing.T, server http.Handler, method, url, body string) (*http.Response, string) { t.Helper() return queryServerHook(t, server, method, url, body, func(req *http.Request) *http.Request { return req }) } func TestGet(t *testing.T) { config := Config{Prefix: "foo", Timeout: 5 * time.Minute} server := newServer(&config) server.register("get-x", http.MethodGet, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, err := fmt.Fprintf(w, "x-response\n") if err != nil { t.Fatalf("writing response failed: %v\n", err) } })) server.register("get-y/", http.MethodGet, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { args := GetSigsumURLArguments(r) if len(args) == 0 { reportErrorCode(w, r.URL, http.StatusBadRequest, fmt.Errorf("missing y")) return } _, err := fmt.Fprintf(w, "y-response: %s\n", GetSigsumURLArguments(r)) if err != nil { t.Fatalf("writing response failed: %v\n", err) } })) for _, table := range []struct { url string status int response string htmlContentType bool usePost bool }{ {url: "/foo/get-x", status: 200, response: "x-response\n"}, {url: "/foo/get-x", status: 405, usePost: true, response: "Method Not Allowed\n"}, {url: "/foo/get-xx", status: 404}, {url: "/foo/get-y", status: 301, htmlContentType: true}, {url: "/foo/get-y/", status: 400, response: "missing y\n"}, {url: "/foo/get-y/bar", status: 200, response: "y-response: bar\n"}, } { method := "GET" if table.usePost { method = "POST" } result, body := queryServer(t, server, method, table.url, "") if got, want := result.StatusCode, table.status; got != want { t.Errorf("Unexpected status code for %q, got %d, want %d", table.url, got, want) } contentType := "text/plain; charset=utf-8" if table.htmlContentType { // For internally generated redirects or errors. contentType = "text/html; charset=utf-8" } if got, want := result.Header.Get("content-type"), contentType; got != want { t.Errorf("Unexpected content type for %q, got %q, want %q", table.url, got, want) } if got, want := body, table.response; got != want { if table.status == 200 || len(table.response) > 0 { t.Errorf("Unexpected response for %q, got %q, want %q", table.url, got, want) } } } } func TestPost(t *testing.T) { config := Config{Prefix: "foo", Timeout: 5 * time.Minute} server := newServer(&config) server.register("add-x", http.MethodPost, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil { t.Fatalf("reading request failed: %v\n", err) } switch string(body) { default: reportErrorCode(w, r.URL, http.StatusBadRequest, fmt.Errorf("bad request %q", body)) case "accept": reportError(w, r.URL, api.ErrAccepted) case "ok": _, err := fmt.Fprintf(w, "add-x ok\n") if err != nil { t.Fatalf("writing response failed: %v\n", err) } } })) for _, table := range []struct { url string body string status int response string htmlContentType bool useGet bool }{ {url: "/foo/add-x", body: "ok", status: 200, response: "add-x ok\n"}, {url: "/foo/add-x", body: "accept", status: 202}, {url: "/foo/add-x/", body: "ok", status: 404}, {url: "/foo/add-x", body: "ok", status: 405, useGet: true}, } { method := "POST" if table.useGet { method = "GET" } result, body := queryServer(t, server, method, table.url, table.body) if got, want := result.StatusCode, table.status; got != want { t.Errorf("Unexpected status code for %q, got %d, want %d", table.url, got, want) } contentType := "text/plain; charset=utf-8" if table.htmlContentType { // For internally generated redirects or errors. contentType = "text/html; charset=utf-8" } if got, want := result.Header.Get("content-type"), contentType; got != want { t.Errorf("Unexpected content type for %q, got %q, want %q", table.url, got, want) } if got, want := body, table.response; got != want { if table.status == 200 || len(table.response) > 0 { t.Errorf("Unexpected response for %q, got %q, want %q", table.url, got, want) } } } } func TestMetrics(t *testing.T) { // If this delay is exceeded, don't fail test, just log a // warning, since we may be delayed due to bad luck in // scheduling on an overloaded machine. maxExpectedDelay := 100 * time.Millisecond // Just long enough to be noticable. testDelay := 200 * time.Millisecond handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch args := GetSigsumURLArguments(r); args { default: reportErrorCode(w, r.URL, http.StatusBadRequest, fmt.Errorf("bad request %q", args)) case "ok": // Do nothing case "accept": reportError(w, r.URL, api.ErrAccepted) case "slow": time.Sleep(testDelay) } }) for _, table := range []struct { url string status int usePost bool slow bool }{ {url: "/foo/get-x", status: 301}, {url: "/foo/get-x/ok", status: 200}, {url: "/foo/get-x/bad", status: 400}, {url: "/foo/get-x/accept", status: 202}, {url: "/foo/get-x/slow", status: 200, slow: true}, } { func() { ctrl := gomock.NewController(t) defer ctrl.Finish() metrics := mocks.NewMockMetrics(ctrl) config := Config{Prefix: "foo", Timeout: 5 * time.Minute, Metrics: metrics} server := newServer(&config) server.register("get-x/", http.MethodGet, handler) method := "GET" if table.usePost { method = "POST" } if table.status != 301 { metrics.EXPECT().OnRequest("get-x/") metrics.EXPECT().OnResponse("get-x/", table.status, gomock.Any()).Do( func(_ string, _ int, latency time.Duration) { if table.slow { if latency < testDelay { t.Errorf("Expected latency (got %v) >= %v", latency, testDelay) } } else if latency > maxExpectedDelay { t.Logf("warn: Unexpectedly high latency (%v), expected at most %v", latency, maxExpectedDelay) } }) } result, _ := queryServer(t, server, method, table.url, "") if got, want := result.StatusCode, table.status; got != want { t.Errorf("Unexpected status code for %q, got %d, want %d", table.url, got, want) } }() } } sigsum-go-0.7.2/pkg/server/witness.go000066400000000000000000000023711455374457700175740ustar00rootroot00000000000000package server import ( "fmt" "net/http" "sigsum.org/sigsum-go/pkg/api" "sigsum.org/sigsum-go/pkg/requests" "sigsum.org/sigsum-go/pkg/types" ) func NewWitness(config *Config, witness api.Witness) http.Handler { server := newServer(config) server.register(types.EndpointGetTreeSize, http.MethodGet, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var req requests.GetTreeSize if err := req.FromURLArgs(GetSigsumURLArguments(r)); err != nil { reportErrorCode(w, r.URL, http.StatusBadRequest, err) return } size, err := witness.GetTreeSize(r.Context(), req) if err != nil { reportError(w, r.URL, err) return } if _, err = fmt.Fprintf(w, "size=%d", size); err != nil { reportError(w, r.URL, err) } })) server.register(types.EndpointAddTreeHead, http.MethodPost, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var req requests.AddTreeHead if err := req.FromASCII(r.Body); err != nil { reportErrorCode(w, r.URL, http.StatusBadRequest, err) return } cs, err := witness.AddTreeHead(r.Context(), req) if err != nil { reportError(w, r.URL, err) return } if err := cs.ToASCII(w); err != nil { reportError(w, r.URL, err) } })) return server } sigsum-go-0.7.2/pkg/server/witness_test.go000066400000000000000000000066331455374457700206400ustar00rootroot00000000000000package server import ( "bytes" "fmt" "io" "net/http" "testing" "time" "github.com/golang/mock/gomock" "sigsum.org/sigsum-go/pkg/api" "sigsum.org/sigsum-go/pkg/crypto" "sigsum.org/sigsum-go/pkg/mocks" "sigsum.org/sigsum-go/pkg/requests" "sigsum.org/sigsum-go/pkg/types" ) func TestGetTreeSize(t *testing.T) { hash := crypto.Hash{0, 1, 2} for _, table := range []struct { url string status int err error size uint64 }{ {url: fmt.Sprintf("/foo/get-tree-size/%x", hash), status: 200, size: 500}, {url: fmt.Sprintf("/foo/get-tree-size/%x", hash), status: 403, err: api.ErrForbidden}, {url: "/foo/get-tree-size/aabb", status: 400}, } { func() { ctrl := gomock.NewController(t) defer ctrl.Finish() witness := mocks.NewMockWitness(ctrl) config := Config{Prefix: "foo", Timeout: 5 * time.Minute} server := NewWitness(&config, witness) if table.status != 400 { witness.EXPECT().GetTreeSize(gomock.Any(), requests.GetTreeSize{KeyHash: hash}).Return(table.size, table.err) } result, body := queryServer(t, server, http.MethodGet, table.url, "") if got, want := result.StatusCode, table.status; got != want { t.Errorf("Unexpected status code for %q, got %d, want %d", table.url, got, want) } if table.status != 200 { return } if got, want := body, fmt.Sprintf("size=%d", table.size); got != want { t.Errorf("Unexpected size for %q, got %q, want %q", table.url, got, want) } }() } } func writeFuncToString(t *testing.T, f func(w io.Writer) error) string { t.Helper() var buf bytes.Buffer if err := f(&buf); err != nil { t.Fatal(err) } return buf.String() } func TestAddTreeHead(t *testing.T) { req := requests.AddTreeHead{ KeyHash: crypto.Hash{1, 2, 3}, TreeHead: types.SignedTreeHead{ TreeHead: types.TreeHead{Size: 5, RootHash: crypto.Hash{4, 5, 6}}, Signature: crypto.Signature{7, 8, 9}, }, OldSize: 3, Proof: types.ConsistencyProof{Path: []crypto.Hash{crypto.Hash{10, 11, 12}}}, } cs := types.Cosignature{ KeyHash: crypto.Hash{13, 14, 15}, Timestamp: 11111, Signature: crypto.Signature{16, 17, 18}, } for _, table := range []struct { url string status int err error hook func(*requests.AddTreeHead) }{ {url: "/foo/add-tree-head", status: 200}, {url: "/foo/add-tree-head/", status: 404}, {url: "/foo/add-tree-head", status: 403, err: api.ErrForbidden}, {url: "/foo/add-tree-head", status: 409, err: api.ErrConflict}, {url: "/foo/add-tree-head", status: 400, hook: func(req *requests.AddTreeHead) { req.OldSize = 6 }, }, } { func(req requests.AddTreeHead) { ctrl := gomock.NewController(t) defer ctrl.Finish() witness := mocks.NewMockWitness(ctrl) config := Config{Prefix: "foo", Timeout: 5 * time.Minute} server := NewWitness(&config, witness) if table.hook != nil { table.hook(&req) } else if table.status != 404 { witness.EXPECT().AddTreeHead(gomock.Any(), req).Return(cs, table.err) } result, body := queryServer(t, server, http.MethodPost, table.url, writeFuncToString(t, req.ToASCII)) if got, want := result.StatusCode, table.status; got != want { t.Errorf("Unexpected status code for %q, got %d, want %d", table.url, got, want) } if table.status != 200 { return } if got, want := body, writeFuncToString(t, cs.ToASCII); got != want { t.Errorf("Unexpected response for %q, got %q, want %q", table.url, got, want) } }(req) } } sigsum-go-0.7.2/pkg/submit-token/000077500000000000000000000000001455374457700166615ustar00rootroot00000000000000sigsum-go-0.7.2/pkg/submit-token/normalize.go000066400000000000000000000016011455374457700212060ustar00rootroot00000000000000package token import ( "fmt" "strings" "golang.org/x/net/idna" "golang.org/x/text/unicode/norm" ) // Normalizes a utf8 domain name. func NormalizeDomainName(domain string) (string, error) { n := norm.NFKC.String(domain) // Unicode normalization l := strings.ToLower(n) // Unicode lowercase a, err := idna.ToASCII(l) // A-label form (no-op for all-ascii labels) if err != nil { return "", fmt.Errorf("failed converting domain %q to a-label form: %v", l, err) } u, err := idna.ToUnicode(a) if err != nil { return "", fmt.Errorf("failed converting domain %q to u-label form: %v", a, err) } if !norm.NFKC.IsNormalString(u) { return "", fmt.Errorf("a-label domain %q was decoded to un-normalized unicode %q", a, u) } if strings.ToLower(u) != u { return "", fmt.Errorf("a-label domain %q was decoded to not all-lowercase unicode %q", a, u) } return u, nil } sigsum-go-0.7.2/pkg/submit-token/normalize_test.go000066400000000000000000000024401455374457700222470ustar00rootroot00000000000000package token import ( "strings" "testing" ) func TestNormalize(t *testing.T) { for _, table := range [][2]string{ {"foo.com", "foo.com"}, // No-op {"foO.coM", "foo.com"}, // ASCII to lower {"räka.se", "räka.se"}, // No-op {"rÄKa.se", "räka.se"}, // Unicode to lower {"Ra\u0308ka.se", "räka.se"}, // Combining char {"\u212bngström.se", "ångström.se"}, // Compatibility char {"FAß.de", "faß.de"}, // IDNA2008 (not IDNA2003) } { out, err := NormalizeDomainName(table[0]) if err != nil { t.Fatalf("normalization failed on %q: %v", table[0], err) } if out != table[1] { t.Errorf("unexpected normalization of %q, got %q, wanted %q", table[0], out, table[1]) } } } func TestNormalizeReject(t *testing.T) { for _, table := range [][2]string{ {"xn--72g.com", "un-normalized unicode"}, // Compatibility char {"xn--RKA-2ha.se", "not all-lowercase"}, // Uppercase characters } { out, err := NormalizeDomainName(table[0]) if err == nil { t.Errorf("accepted invalid domain %q, returned %q", table[0], out) continue } if !strings.Contains(err.Error(), table[1]) { t.Errorf("unexpected error type for %q, got %v, expected substring %q\n", table[0], err, table[1]) } } } sigsum-go-0.7.2/pkg/submit-token/token.go000066400000000000000000000057241455374457700203400ustar00rootroot00000000000000// package token validates a sigsum submit-token. package token import ( "context" "errors" "fmt" "net" "strings" "sigsum.org/sigsum-go/pkg/crypto" ) const ( Label = "_sigsum_v1" oldLabel = "_sigsum_v0" HeaderName = "Sigsum-Token" namespace = "sigsum.org/v1/submit-token" maxNumberOfKeys = 10 ) // Represents the contents of the HTTP Sigsum-Submit: header- type SubmitHeader struct { Domain string Token crypto.Signature } func (s *SubmitHeader) ToHeader() string { return fmt.Sprintf("%s %x", s.Domain, s.Token) } func (s *SubmitHeader) FromHeader(header string) error { parts := strings.Split(header, " ") if n := len(parts); n != 2 { return fmt.Errorf("expected 2 parts, got %d", n) } if len(parts[0]) == 0 { return fmt.Errorf("malformed header, domain part empty") } var err error s.Token, err = crypto.SignatureFromHex(parts[1]) if err == nil { s.Domain = parts[0] } return err } func MakeToken(signer crypto.Signer, logKey *crypto.PublicKey) (crypto.Signature, error) { return signer.Sign(crypto.AttachNamespace(namespace, logKey[:])) } // Verify a token using a given key, with no DNS loookup. func VerifyToken(key *crypto.PublicKey, logKey *crypto.PublicKey, token *crypto.Signature) error { if !crypto.Verify(key, crypto.AttachNamespace(namespace, logKey[:]), token) { return fmt.Errorf("invalid token signature") } return nil } // Looks up the appropriate TXT records for a domain. func LookupDomain(ctx context.Context, lookupTXT func(context.Context, string) ([]string, error), domain string) ([]string, error) { rsps, err := lookupTXT(ctx, Label+"."+domain) var dnsError *net.DNSError if errors.As(err, &dnsError) && dnsError.IsNotFound { r2, e2 := lookupTXT(ctx, oldLabel+"."+domain) if e2 == nil { return r2, nil } } return rsps, err } // DnsResolver implements the Verifier interface by querying DNS. type DnsVerifier struct { // Usually, net.Resolver.LookupTXT, but set differently for testing. lookupTXT func(ctx context.Context, name string) ([]string, error) logKey crypto.PublicKey } func NewDnsVerifier(logKey *crypto.PublicKey) *DnsVerifier { var resolver net.Resolver return &DnsVerifier{ lookupTXT: resolver.LookupTXT, logKey: *logKey, } } func (dv *DnsVerifier) Verify(ctx context.Context, header *SubmitHeader) error { rsps, err := LookupDomain(ctx, dv.lookupTXT, header.Domain) if err != nil { return fmt.Errorf("token: dns look-up failed: %v", err) } var ignoredKeys, badKeys int if len(rsps) > maxNumberOfKeys { ignoredKeys = len(rsps) - maxNumberOfKeys rsps = rsps[:maxNumberOfKeys] } signedData := crypto.AttachNamespace(namespace, dv.logKey[:]) for _, keyHex := range rsps { key, err := crypto.PublicKeyFromHex(keyHex) if err != nil { badKeys++ continue } if crypto.Verify(&key, signedData, &header.Token) { return nil } } return fmt.Errorf("validating token signature failed, ignored keys: %d, syntactically bad keys: %d", ignoredKeys, badKeys) } sigsum-go-0.7.2/pkg/submit-token/token_test.go000066400000000000000000000147631455374457700214020ustar00rootroot00000000000000package token import ( "context" "encoding/hex" "log" "net" "testing" "fmt" "sigsum.org/sigsum-go/pkg/crypto" "strings" ) func lookupTXTWithResponses(name string, responses []string) func(context.Context, string) ([]string, error) { return func(_ context.Context, queryName string) ([]string, error) { if queryName != name { return nil, &net.DNSError{ Err: "NXDOMAIN", Name: queryName, IsNotFound: true, } } return responses, nil } } func verifierWithResponses(logKey *crypto.PublicKey, queryName string, responses []string) *DnsVerifier { return &DnsVerifier{ lookupTXT: lookupTXTWithResponses(queryName, responses), logKey: *logKey, } } func newKeyPair(t *testing.T) (crypto.PublicKey, crypto.Signer) { pub, signer, err := crypto.NewKeyPair() if err != nil { t.Fatal(err) } return pub, signer } func TestSubmitHeaderFromHeader(t *testing.T) { for _, table := range []struct { desc string input string exp *SubmitHeader }{ {"no domain", " aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", nil}, { "valid, lowercase", "foo.example.com aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", &SubmitHeader{Domain: "foo.example.com", Token: mustSignatureFromHex(t, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")}, }, { "valid, mixed case", "foo.example.com aaaaaaaaaaaaaaaaaaaaaaaaaaaAaaaaaaaaaaaaaaaaaaaaaaAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", &SubmitHeader{Domain: "foo.example.com", Token: mustSignatureFromHex(t, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")}, }, {"extra space", "foo.example.com aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", nil}, {"bad hex", "foo.example.com aaxaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", nil}, {"bad hex length", "foo.example.com aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", nil}, {"bad signature length", "foo.example.com aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", nil}, } { var header SubmitHeader if err := header.FromHeader(table.input); err != nil { if table.exp == nil { // Expected error t.Logf("%s: error (expected): %v\n", table.desc, err) } else { t.Errorf("%s: FromHeader failed: %v\n", table.desc, err) } } else { if table.exp == nil { t.Errorf("%s: unexpected non-failure, got result: %x\n", table.desc, header) } else if got, want := header, *table.exp; got != want { t.Errorf("%s: unexpected result, got: %x, wanted: %x\n", table.desc, got, want) } } } } func TestSubmitHeaderToHeader(t *testing.T) { for _, table := range []struct { input SubmitHeader exp string }{ { SubmitHeader{Domain: "foo.example.org", Token: mustSignatureFromHex(t, "BBbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")}, "foo.example.org bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", }, } { if got, want := table.input.ToHeader(), table.exp; got != want { t.Errorf("unexpected result from ToHeader, got: %q, want: %q\n", got, want) } } } func TestVerify(t *testing.T) { logKeyHex := "cda2517e17dcba133eb0e71bf77473f94a77d7e61b1de4e1e64adfd0938d6182" logKey, err := crypto.PublicKeyFromHex(logKeyHex) if err != nil { log.Fatal(err.Error()) } pub, signer := newKeyPair(t) hexKey := hex.EncodeToString(pub[:]) signature, err := MakeToken(signer, &logKey) if err != nil { log.Fatal(err.Error()) } testOne := func(desc, tokenDomain string, signature *crypto.Signature, registeredDomain string, records []string, check func(err error) error) { t.Helper() if err := check(verifierWithResponses(&logKey, registeredDomain, records).Verify( context.Background(), &SubmitHeader{Domain: tokenDomain, Token: *signature})); err != nil { t.Errorf("%s: %v", desc, err) } } testValid := func(desc, tokenDomain string, signature *crypto.Signature, registeredDomain string, records []string) { t.Helper() testOne("valid: "+desc, tokenDomain, signature, "_sigsum_v1."+registeredDomain, records, func(err error) error { return err }) } testValidFallback := func(desc, tokenDomain string, signature *crypto.Signature, registeredDomain string, records []string) { t.Helper() testOne("valid: "+desc+" (fallback)", tokenDomain, signature, "_sigsum_v0."+registeredDomain, records, func(err error) error { return err }) } testInvalid := func(desc, tokenDomain string, signature *crypto.Signature, registeredDomain string, records []string, msg string) { t.Helper() testOne("invalid: "+desc, tokenDomain, signature, "_sigsum_v1."+registeredDomain, records, func(err error) error { if err == nil { return fmt.Errorf("unexpected success from invalid token") } if strings.Contains(err.Error(), msg) { // As expected return nil } return fmt.Errorf("unexpected type of error: %v", err) }) } testValid("single key", "foo.example.org", &signature, "foo.example.org", []string{hexKey}) testInvalid("nxdomain", "foo.example.org", &signature, "bar.example.org", []string{hexKey}, "NXDOMAIN") testInvalid("no matching key", "foo.example.org", &signature, "foo.example.org", []string{ logKeyHex, hexKey + "aa", "bad"}, "bad keys: 2") testValid("multiple keys", "foo.example.org", &signature, "foo.example.org", []string{ logKeyHex, hexKey + "aa", "bad", hexKey}) testValidFallback("multiple keys", "foo.example.org", &signature, "foo.example.org", []string{ logKeyHex, hexKey + "aa", "bad", hexKey}) testInvalid("too many keys", "foo.example.org", &signature, "foo.example.org", []string{ logKeyHex, hexKey + "aa", "bad", "4", "5", "6", "7", "8", "9", "10", hexKey}, "ignored keys: 1") } func mustSignatureFromHex(t *testing.T, ascii string) crypto.Signature { sig, err := crypto.SignatureFromHex(ascii) if err != nil { t.Fatal(err) } return sig } sigsum-go-0.7.2/pkg/submit/000077500000000000000000000000001455374457700155435ustar00rootroot00000000000000sigsum-go-0.7.2/pkg/submit/submit.go000066400000000000000000000133151455374457700174000ustar00rootroot00000000000000// package submit acts as a sigsum submit client // It submits a leaf to a log, and collects a sigsum proof. package submit import ( "context" "errors" "fmt" "time" "sigsum.org/sigsum-go/pkg/api" "sigsum.org/sigsum-go/pkg/client" "sigsum.org/sigsum-go/pkg/crypto" "sigsum.org/sigsum-go/pkg/log" "sigsum.org/sigsum-go/pkg/policy" "sigsum.org/sigsum-go/pkg/proof" "sigsum.org/sigsum-go/pkg/requests" token "sigsum.org/sigsum-go/pkg/submit-token" "sigsum.org/sigsum-go/pkg/types" ) const ( defaultPollDelay = 2 * time.Second // Default log server publishing interval is 30 seconds, so // use something longer. defaultTimeout = 45 * time.Second defaultUserAgent = "sigsum-go submit" ) type Config struct { // Domain and signer to use for rate limit sigsum-token: header. Domain string RateLimitSigner crypto.Signer // Timeout, before trying try next log. Zero implies a default // timeout is used. PerLogTimeout time.Duration // Delay when repeating add-leaf requests to the log, as well // as for polling for a cosigned tree head and inclusion // proof. PollDelay time.Duration UserAgent string // The policy specifies the logs and witnesses to use. Policy *policy.Policy } func (c *Config) getPollDelay() time.Duration { if c.PollDelay <= 0 { return defaultPollDelay } return c.PollDelay } func (c *Config) getTimeout() time.Duration { if c.PerLogTimeout <= 0 { return defaultTimeout } return c.PerLogTimeout } func (c *Config) getUserAgent() string { if len(c.UserAgent) == 0 { return defaultUserAgent } return c.UserAgent } // Sleep for the given delay, but fail early if the context is // cancelled. func sleepWithContext(ctx context.Context, d time.Duration) error { timer := time.NewTimer(d) defer timer.Stop() select { case <-timer.C: return nil case <-ctx.Done(): return ctx.Err() } } func (c *Config) sleep(ctx context.Context) error { return sleepWithContext(ctx, c.getPollDelay()) } func SubmitMessage(ctx context.Context, config *Config, signer crypto.Signer, message *crypto.Hash) (proof.SigsumProof, error) { signature, err := types.SignLeafMessage(signer, message[:]) if err != nil { return proof.SigsumProof{}, err } return SubmitLeafRequest(ctx, config, &requests.Leaf{ Message: *message, Signature: signature, PublicKey: signer.Public(), }) } func SubmitLeafRequest(ctx context.Context, config *Config, req *requests.Leaf) (proof.SigsumProof, error) { leaf, err := req.Verify() if err != nil { return proof.SigsumProof{}, fmt.Errorf("verifying leaf request failed: %v", err) } leafHash := leaf.ToHash() logs := config.Policy.GetLogsWithUrl() if len(logs) == 0 { return proof.SigsumProof{}, fmt.Errorf("no logs defined in policy") } for _, entity := range logs { log.Info("Attempting submit to log: %s", entity.URL) var header *token.SubmitHeader if config.RateLimitSigner != nil && len(config.Domain) > 0 { signature, err := token.MakeToken(config.RateLimitSigner, &entity.PublicKey) if err != nil { return proof.SigsumProof{}, fmt.Errorf("creating submit token failed: %v", err) } header = &token.SubmitHeader{Domain: config.Domain, Token: signature} } client := client.New(client.Config{ UserAgent: config.getUserAgent(), URL: entity.URL, }) logKeyHash := crypto.HashBytes(entity.PublicKey[:]) pr, err := func() (proof.SigsumProof, error) { ctx, cancel := context.WithTimeout(ctx, config.getTimeout()) defer cancel() return submitLeafToLog(ctx, config.Policy, client, &logKeyHash, header, config.sleep, req, &leafHash) }() if err == nil { pr.Leaf = proof.NewShortLeaf(&leaf) return pr, nil } log.Error("Submitting to log %q failed: %v", entity.URL, err) } return proof.SigsumProof{}, fmt.Errorf("all logs failed, giving up") } func submitLeafToLog(ctx context.Context, policy *policy.Policy, cli api.Log, logKeyHash *crypto.Hash, header *token.SubmitHeader, sleep func(context.Context) error, req *requests.Leaf, leafHash *crypto.Hash) (proof.SigsumProof, error) { pr := proof.SigsumProof{ // Note: Leaves to caller to populate proof.Leaf. LogKeyHash: *logKeyHash, } for { persisted, err := cli.AddLeaf(ctx, *req, header) if err != nil { return proof.SigsumProof{}, err } if persisted { break } log.Debug("Leaf submitted, waiting for it to be persisted.") if err := sleep(ctx); err != nil { return proof.SigsumProof{}, err } } // Leaf submitted, now get a signed tree head + inclusion proof. for { var err error pr.TreeHead, err = cli.GetTreeHead(ctx) if err != nil { return proof.SigsumProof{}, err } if err := policy.VerifyCosignedTreeHead(&pr.LogKeyHash, &pr.TreeHead); err != nil { return proof.SigsumProof{}, fmt.Errorf("verifying tree head failed: %v", err) } // See if we can have an inclusion proof for this tree size. if pr.TreeHead.Size == 0 { // Certainly not included yet. log.Debug("Signed tree is still empty, waiting.") if err := sleep(ctx); err != nil { return proof.SigsumProof{}, err } continue } // For TreeHead.Size == 1, inclusion proof is trivial. if pr.TreeHead.Size > 1 { pr.Inclusion, err = cli.GetInclusionProof(ctx, requests.InclusionProof{ Size: pr.TreeHead.Size, LeafHash: *leafHash, }) if errors.Is(err, api.ErrNotFound) { log.Debug("No inclusion proof yet, waiting.") if err := sleep(ctx); err != nil { return proof.SigsumProof{}, err } continue } if err != nil { return proof.SigsumProof{}, fmt.Errorf("failed to get inclusion proof: %v", err) } } // Check validity. if err = pr.Inclusion.Verify(leafHash, &pr.TreeHead.TreeHead); err != nil { return proof.SigsumProof{}, fmt.Errorf("inclusion proof invalid: %v", err) } return pr, nil } } sigsum-go-0.7.2/pkg/submit/submit_test.go000066400000000000000000000116131455374457700204360ustar00rootroot00000000000000package submit import ( "context" "errors" "fmt" "testing" "github.com/golang/mock/gomock" "sigsum.org/sigsum-go/pkg/crypto" "sigsum.org/sigsum-go/pkg/merkle" "sigsum.org/sigsum-go/pkg/mocks" "sigsum.org/sigsum-go/pkg/policy" "sigsum.org/sigsum-go/pkg/proof" "sigsum.org/sigsum-go/pkg/requests" "sigsum.org/sigsum-go/pkg/types" ) func TestSubmitSuccess(t *testing.T) { logPub, logSigner, err := crypto.NewKeyPair() if err != nil { t.Fatalf("creating log key failed: %v", err) } submitPub, submitSigner, err := crypto.NewKeyPair() if err != nil { t.Fatalf("creating submit key failed: %v", err) } logKeyHash := crypto.HashBytes(logPub[:]) policy, err := policy.NewKofNPolicy([]crypto.PublicKey{logPub}, nil, 0) if err != nil { t.Fatalf("creating policy failed: %v", err) } tree := merkle.NewTree() oneTest := func(t *testing.T, i int) { ctrl := gomock.NewController(t) defer ctrl.Finish() client := mocks.NewMockLog(ctrl) msg, sth, inclusionProof, req, leaf, leafHash := prepareResponse(t, submitSigner, logSigner, &tree, i) client.EXPECT().AddLeaf(gomock.Any(), req, gomock.Any()).Return(false, nil) client.EXPECT().AddLeaf(gomock.Any(), req, gomock.Any()).Return(true, nil) client.EXPECT().GetTreeHead(gomock.Any()).Return( types.CosignedTreeHead{SignedTreeHead: sth}, nil) if len(inclusionProof.Path) > 0 { client.EXPECT().GetInclusionProof(gomock.Any(), gomock.Any()).Return(inclusionProof, nil) } pr, err := submitLeafToLog(context.Background(), policy, client, &logKeyHash, nil, func(_ context.Context) error { return nil }, &req, &leafHash) if err != nil { t.Errorf("submit failed: %v", err) } else { pr.Leaf = proof.NewShortLeaf(&leaf) if err := pr.Verify(&msg, &submitPub, policy); err != nil { t.Errorf("returned sigsum proof failed to verify: %v", err) } } } for i := 1; i < 10; i++ { t.Run(fmt.Sprintf("leaf %d", i), func(t *testing.T) { oneTest(t, i) }) } } func TestSubmitFailure(t *testing.T) { logPub, logSigner, err := crypto.NewKeyPair() if err != nil { t.Fatalf("creating log key failed: %v", err) } submitPub, submitSigner, err := crypto.NewKeyPair() if err != nil { t.Fatalf("creating submit key failed: %v", err) } logKeyHash := crypto.HashBytes(logPub[:]) policy, err := policy.NewKofNPolicy([]crypto.PublicKey{logPub}, nil, 0) if err != nil { t.Fatalf("creating policy failed: %v", err) } tree := merkle.NewTree() oneTest := func(t *testing.T, i int) { ctrl := gomock.NewController(t) defer ctrl.Finish() client := mocks.NewMockLog(ctrl) msg, sth, inclusionProof, req, leaf, leafHash := prepareResponse(t, submitSigner, logSigner, &tree, i) var addError, getTHError, getInclusionError error switch i { case 1: leaf.Checksum[0] ^= 1 case 2: sth.Signature[0] ^= 1 case 3: inclusionProof.Path[0][0] ^= 1 case 4: leafHash[0] ^= 1 case 5: addError = errors.New("mock error") case 6: getTHError = errors.New("mock error") case 7: getInclusionError = errors.New("mock error") } client.EXPECT().AddLeaf(gomock.Any(), req, gomock.Any()).Return(true, addError) client.EXPECT().GetTreeHead(gomock.Any()).Return( types.CosignedTreeHead{SignedTreeHead: sth}, getTHError).AnyTimes() client.EXPECT().GetInclusionProof(gomock.Any(), gomock.Any()).Return(inclusionProof, getInclusionError).AnyTimes() pr, err := submitLeafToLog(context.Background(), policy, client, &logKeyHash, nil, func(_ context.Context) error { return nil }, &req, &leafHash) if err == nil { pr.Leaf = proof.NewShortLeaf(&leaf) err := pr.Verify(&msg, &submitPub, policy) if err == nil { t.Errorf("case %d submit and verify succeeded; should have failed", i) } } } for i := 1; i <= 7; i++ { t.Run(fmt.Sprintf("leaf %d", i), func(t *testing.T) { oneTest(t, i) }) } } func prepareResponse(t *testing.T, submitSigner, logSigner crypto.Signer, tree *merkle.Tree, i int) (crypto.Hash, types.SignedTreeHead, types.InclusionProof, requests.Leaf, types.Leaf, crypto.Hash) { msg := crypto.HashBytes([]byte{byte(i)}) signature, err := types.SignLeafMessage(submitSigner, msg[:]) if err != nil { t.Fatalf("signing message failed: %v", err) } req := requests.Leaf{ Message: msg, Signature: signature, PublicKey: submitSigner.Public(), } leaf, err := req.Verify() if err != nil { t.Fatalf("leaf verify failed: %v", err) } leafHash := leaf.ToHash() if !tree.AddLeafHash(&leafHash) { t.Fatalf("unexpected leaf duplicate, leaf %d", i) } th := types.TreeHead{ RootHash: tree.GetRootHash(), Size: tree.Size(), } sth, err := th.Sign(logSigner) if err != nil { t.Fatalf("signing tree head failed: %v", err) } path, err := tree.ProveInclusion(tree.Size()-1, tree.Size()) if err != nil { t.Fatalf("failed to prove inclusion: %v", err) } inclusionProof := types.InclusionProof{ LeafIndex: tree.Size() - 1, Path: path, } return msg, sth, inclusionProof, req, leaf, leafHash } sigsum-go-0.7.2/pkg/types/000077500000000000000000000000001455374457700154045ustar00rootroot00000000000000sigsum-go-0.7.2/pkg/types/endpoint.go000066400000000000000000000016631455374457700175610ustar00rootroot00000000000000package types type Endpoint string const ( // Sigsum log api. EndpointAddLeaf = Endpoint("add-leaf") EndpointGetTreeHead = Endpoint("get-tree-head") EndpointGetInclusionProof = Endpoint("get-inclusion-proof/") EndpointGetConsistencyProof = Endpoint("get-consistency-proof/") EndpointGetLeaves = Endpoint("get-leaves/") // For primary/secondary replication. EndpointGetSecondaryTreeHead = Endpoint("get-secondary-tree-head") // Sigsum witness api. EndpointAddTreeHead = Endpoint("add-tree-head") EndpointGetTreeSize = Endpoint("get-tree-size/") ) // Path adds endpoint name to a service prefix. If prefix is empty, nothing is added. // For example, // EndpointAddLeaf.Path("example.com/sigsum") -> "example.com/sigsum/add-leaf". // EndpointAddLeaf.Path("") -> "add-leaf". func (e Endpoint) Path(prefix string) string { if len(prefix) == 0 { return string(e) } return prefix + "/" + string(e) } sigsum-go-0.7.2/pkg/types/leaf.go000066400000000000000000000056301455374457700166460ustar00rootroot00000000000000package types import ( "fmt" "io" "sigsum.org/sigsum-go/pkg/ascii" "sigsum.org/sigsum-go/pkg/crypto" "sigsum.org/sigsum-go/pkg/merkle" ) const ( TreeLeafNamespace = "sigsum.org/v1/tree-leaf" ) type Leaf struct { Checksum crypto.Hash Signature crypto.Signature KeyHash crypto.Hash } func leafSignedData(checksum *crypto.Hash) []byte { return crypto.AttachNamespace(TreeLeafNamespace, checksum[:]) } func SignLeafChecksum(signer crypto.Signer, checksum *crypto.Hash) (crypto.Signature, error) { return signer.Sign(leafSignedData(checksum)) } func VerifyLeafChecksum(key *crypto.PublicKey, checksum *crypto.Hash, sig *crypto.Signature) bool { return crypto.Verify(key, leafSignedData(checksum), sig) } func SignLeafMessage(signer crypto.Signer, msg []byte) (crypto.Signature, error) { checksum := crypto.HashBytes(msg) return SignLeafChecksum(signer, &checksum) } func VerifyLeafMessage(key *crypto.PublicKey, msg []byte, sig *crypto.Signature) bool { checksum := crypto.HashBytes(msg) return VerifyLeafChecksum(key, &checksum, sig) } func (l *Leaf) Verify(key *crypto.PublicKey) bool { if l.KeyHash != crypto.HashBytes(key[:]) { return false } return VerifyLeafChecksum(key, &l.Checksum, &l.Signature) } func (l *Leaf) ToBinary() []byte { b := make([]byte, 128) copy(b[:32], l.Checksum[:]) copy(b[32:96], l.Signature[:]) copy(b[96:], l.KeyHash[:]) return b } func (l *Leaf) ToHash() crypto.Hash { return merkle.HashLeafNode(l.ToBinary()) } func (l *Leaf) FromBinary(b []byte) error { if len(b) != 128 { return fmt.Errorf("types: invalid leaf size: %d", len(b)) } copy(l.Checksum[:], b[:32]) copy(l.Signature[:], b[32:96]) copy(l.KeyHash[:], b[96:]) return nil } func (l *Leaf) ToASCII(w io.Writer) error { return ascii.WriteLine(w, "leaf", l.Checksum[:], l.Signature[:], l.KeyHash[:]) } func LeavesToASCII(w io.Writer, leaves []Leaf) error { for _, leaf := range leaves { if err := leaf.ToASCII(w); err != nil { return err } } return nil } func (l *Leaf) Parse(p *ascii.Parser) error { v, err := p.GetValues("leaf", 3) if err != nil { return err } l.Checksum, err = crypto.HashFromHex(v[0]) if err != nil { return fmt.Errorf("invalid leaf checksum: %v", err) } l.Signature, err = crypto.SignatureFromHex(v[1]) if err != nil { return fmt.Errorf("invalid leaf signature: %v", err) } l.KeyHash, err = crypto.HashFromHex(v[2]) if err != nil { return fmt.Errorf("invalid leaf key hash: %v", err) } return nil } func LeavesFromASCII(r io.Reader, maxCount uint64) ([]Leaf, error) { var leaves []Leaf p := ascii.NewParser(r) for { var leaf Leaf err := leaf.Parse(&p) if err == io.EOF { if len(leaves) == 0 { return nil, fmt.Errorf("no leaves") } return leaves, nil } if err != nil { return nil, err } if uint64(len(leaves)) >= maxCount { return nil, fmt.Errorf("too many leaves, expected at most %d", maxCount) } leaves = append(leaves, leaf) } } sigsum-go-0.7.2/pkg/types/leaf_test.go000066400000000000000000000210461455374457700177040ustar00rootroot00000000000000package types import ( "bytes" "fmt" "io" "reflect" "strings" "testing" "sigsum.org/sigsum-go/internal/mocks/signer" "sigsum.org/sigsum-go/pkg/ascii" "sigsum.org/sigsum-go/pkg/crypto" ) func TestLeafSignedData(t *testing.T) { desc := "valid: checksum 0x00,0x01,..." if got, want := leafSignedData(validChecksum(t)), validLeafSignedDataBytes(t); !bytes.Equal(got, want) { t.Errorf("got\n\t%x\nbut wanted\n\t%x\nin test %q\n", got, want, desc) } if got, want := validLeafSignedDataBytes(t), 56; len(got) != want { t.Errorf("got len %d\n\t%x\nbut wanted %d in test %q\n", len(got), got, want, desc) } } func TestSignLeaf(t *testing.T) { for _, table := range []struct { desc string checksum *crypto.Hash signer crypto.Signer wantSig *crypto.Signature wantErr bool }{ { desc: "invalid: signer error", checksum: validChecksum(t), signer: &signer.Signer{*newPubBufferInc(t), *newSigBufferInc(t), fmt.Errorf("signing error")}, wantErr: true, }, { desc: "valid", checksum: validChecksum(t), signer: &signer.Signer{*newPubBufferInc(t), *newSigBufferInc(t), nil}, wantSig: newSigBufferInc(t), }, } { sig, err := SignLeafChecksum(table.signer, table.checksum) 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 } if got, want := sig[:], table.wantSig[:]; !bytes.Equal(got, want) { t.Errorf("got signature\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.desc) } } } func TestLeafVerify(t *testing.T) { checksum := validChecksum(t) pub, signer := newKeyPair(t) sig, err := SignLeafChecksum(signer, checksum) if err != nil { t.Fatal(err) } leaf := Leaf{ Checksum: *checksum, Signature: sig, KeyHash: crypto.HashBytes(pub[:]), } if !leaf.Verify(&pub) { t.Errorf("failed verifying a valid statement") } leaf.Checksum[0] += 1 if leaf.Verify(&pub) { t.Errorf("succeeded verifying an invalid statement") } } func TestLeafToBinary(t *testing.T) { desc := "valid: buffers 0x00,0x01,..." if got, want := validLeaf(t).ToBinary(), validLeafBytes(t); !bytes.Equal(got, want) { t.Errorf("got leaf\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, desc) } } func TestLeafFromBinary(t *testing.T) { for _, table := range []struct { desc string serialized []byte wantErr bool want *Leaf }{ { desc: "invalid: not enough bytes", serialized: make([]byte, 135), wantErr: true, }, { desc: "invalid: too many bytes", serialized: make([]byte, 137), wantErr: true, }, { desc: "valid: buffers 0x00,0x01,...", serialized: validLeafBytes(t), want: validLeaf(t), }, } { var leaf Leaf err := leaf.FromBinary(table.serialized) 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 } if got, want := leaf, *table.want; got != want { t.Errorf("got leaf\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, table.desc) } } } func TestLeafToASCII(t *testing.T) { desc := "valid:, buffers 0x00,0x01,..." buf := bytes.Buffer{} if err := validLeaf(t).ToASCII(&buf); err != nil { t.Fatalf("got error true but wanted false in test %q: %v", desc, err) } if got, want := buf.String(), validLeafASCII(t); got != want { t.Errorf("got leaf\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, desc) } } func TestLeafFromASCII(t *testing.T) { for _, table := range []struct { desc string serialized io.Reader wantErr bool want *Leaf }{ { desc: "invalid: not a tree leaf (wrong key)", serialized: bytes.NewBufferString("size=0\n"), wantErr: true, }, { desc: "invalid: not a tree leaf (too many values)", serialized: bytes.NewBufferString(invalidLeafASCII(t)), wantErr: true, }, { desc: "valid: buffers 0x00,0x01,...", serialized: bytes.NewBufferString(validLeafASCII(t)), want: validLeaf(t), }, } { var leaf Leaf p := ascii.NewParser(table.serialized) err := leaf.Parse(&p) 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 } if got, want := leaf, *table.want; got != want { t.Errorf("got leaf\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, table.desc) } } } func TestLeavesFromASCII(t *testing.T) { for _, table := range []struct { desc string serialized string lenDiff int wantErr bool want []Leaf }{ { desc: "invalid: not a list of tree leaves (too few key-value pairs)", serialized: "checksum=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n", wantErr: true, }, { desc: "invalid: not a list of tree leaves (too many key-value pairs)", serialized: validLeafASCII(t) + "key=value\n", wantErr: true, }, { desc: "invalid: not a list of tree leaves (too few checksums))", serialized: invalidLeavesASCII(t, "checksum"), wantErr: true, }, { desc: "invalid: not a list of tree leaves (too few signatures))", serialized: invalidLeavesASCII(t, "signature"), wantErr: true, }, { desc: "invalid: not a list of tree leaves (too few key hashes))", serialized: invalidLeavesASCII(t, "key_hash"), wantErr: true, }, { desc: "valid leaves", serialized: validLeavesASCII(t), want: validLeaves(t), }, { desc: "valid fewer leaves", serialized: validLeavesASCII(t), want: validLeaves(t), lenDiff: 1, }, { desc: "invalid, too many leaves", serialized: validLeavesASCII(t), want: validLeaves(t), lenDiff: -1, wantErr: true, }, } { leaves, err := LeavesFromASCII(bytes.NewBufferString(table.serialized), uint64(len(table.want)+table.lenDiff)) 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 } if got, want := leaves, table.want; !reflect.DeepEqual(got, want) { t.Errorf("got leaves\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, table.desc) } } } func validChecksum(t *testing.T) *crypto.Hash { hash := crypto.HashBytes(newHashBufferInc(t)[:]) return &hash } func validLeafSignedDataBytes(t *testing.T) []byte { hash := crypto.HashBytes(newHashBufferInc(t)[:]) return bytes.Join([][]byte{ []byte("sigsum.org/v1/tree-leaf"), []byte{0}, hash[:], }, nil) } func validLeaf(t *testing.T) *Leaf { return &Leaf{ Checksum: crypto.HashBytes(newHashBufferInc(t)[:]), Signature: *newSigBufferInc(t), KeyHash: *newHashBufferInc(t), } } func validLeafBytes(t *testing.T) []byte { checksum := crypto.HashBytes(newHashBufferInc(t)[:]) return bytes.Join([][]byte{ checksum[:], newSigBufferInc(t)[:], newHashBufferInc(t)[:], }, nil) } func validLeafASCII(t *testing.T) string { checksum := crypto.HashBytes(newHashBufferInc(t)[:]) return fmt.Sprintf("%s=%x %x %x\n", "leaf", checksum, newSigBufferInc(t)[:], newHashBufferInc(t)[:]) } func invalidLeafASCII(t *testing.T) string { s := validLeafASCII(t) // Add an extra value return fmt.Sprintf("%s 0\n", strings.TrimSpace(s)) } func validLeaves(t *testing.T) []Leaf { t.Helper() return []Leaf{*validLeaf(t), Leaf{}} } func validLeavesASCII(t *testing.T) string { t.Helper() return validLeafASCII(t) + fmt.Sprintf("%s=%x %x %x\n", "leaf", crypto.Hash{}, crypto.Signature{}, crypto.Hash{}) } func invalidLeavesASCII(t *testing.T, key string) string { buf := validLeavesASCII(t) switch key { case "checksum": return buf[:11] + buf[12:] case "signature": return buf[:80] + buf[82:] case "key_hash": return buf[:len(buf)-10] + buf[len(buf)-9:] default: t.Fatalf("must have a valid field to invalidate") return "" } } func newKeyPair(t *testing.T) (crypto.PublicKey, crypto.Signer) { pub, signer, err := crypto.NewKeyPair() if err != nil { t.Fatal(err) } return pub, signer } func newHashBufferInc(t *testing.T) *crypto.Hash { t.Helper() var buf crypto.Hash for i := 0; i < len(buf); i++ { buf[i] = byte(i) } return &buf } func newSigBufferInc(t *testing.T) *crypto.Signature { t.Helper() var buf crypto.Signature for i := 0; i < len(buf); i++ { buf[i] = byte(i) } return &buf } func newPubBufferInc(t *testing.T) *crypto.PublicKey { t.Helper() var buf crypto.PublicKey for i := 0; i < len(buf); i++ { buf[i] = byte(i) } return &buf } sigsum-go-0.7.2/pkg/types/proof.go000066400000000000000000000036541455374457700170700ustar00rootroot00000000000000package types import ( "fmt" "io" "sigsum.org/sigsum-go/pkg/ascii" "sigsum.org/sigsum-go/pkg/crypto" "sigsum.org/sigsum-go/pkg/merkle" ) type InclusionProof struct { LeafIndex uint64 Path []crypto.Hash } type ConsistencyProof struct { Path []crypto.Hash } func hashesToASCII(w io.Writer, hashes []crypto.Hash) error { for _, hash := range hashes { err := ascii.WriteHash(w, "node_hash", &hash) if err != nil { return err } } return nil } // Treats empty list as an error. func hashesFromASCII(p *ascii.Parser) ([]crypto.Hash, error) { var hashes []crypto.Hash for { hash, err := p.GetHash("node_hash") if err == io.EOF { if len(hashes) == 0 { return nil, fmt.Errorf("invalid path, empty") } return hashes, nil } if err != nil { return nil, err } hashes = append(hashes, hash) } } // Note the size is not included on the wire. func (pr *InclusionProof) ToASCII(w io.Writer) error { if err := ascii.WriteInt(w, "leaf_index", pr.LeafIndex); err != nil { return err } return hashesToASCII(w, pr.Path) } func (pr *InclusionProof) FromASCII(r io.Reader) error { p := ascii.NewParser(r) var err error pr.LeafIndex, err = p.GetInt("leaf_index") if err != nil { return err } pr.Path, err = hashesFromASCII(&p) return err } func (pr *InclusionProof) Verify(leaf *crypto.Hash, th *TreeHead) error { return merkle.VerifyInclusion(leaf, pr.LeafIndex, th.Size, &th.RootHash, pr.Path) } func (pr *ConsistencyProof) ToASCII(w io.Writer) error { return hashesToASCII(w, pr.Path) } func (pr *ConsistencyProof) Parse(p *ascii.Parser) error { var err error pr.Path, err = hashesFromASCII(p) return err } func (pr *ConsistencyProof) FromASCII(r io.Reader) error { p := ascii.NewParser(r) return pr.Parse(&p) } func (pr *ConsistencyProof) Verify(oldTree, newTree *TreeHead) error { return merkle.VerifyConsistency( oldTree.Size, newTree.Size, &oldTree.RootHash, &newTree.RootHash, pr.Path) } sigsum-go-0.7.2/pkg/types/proof_test.go000066400000000000000000000071471455374457700201300ustar00rootroot00000000000000package types import ( "bytes" "fmt" "io" "reflect" "testing" "sigsum.org/sigsum-go/pkg/crypto" ) func TestInclusionProofToASCII(t *testing.T) { desc := "valid" buf := bytes.Buffer{} if err := validInclusionProof(t).ToASCII(&buf); err != nil { t.Fatalf("got error true but wanted false in test %q: %v", desc, err) } if got, want := buf.String(), validInclusionProofASCII(t); got != want { t.Errorf("got inclusion proof\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, desc) } } func TestInclusionProofFromASCII(t *testing.T) { for _, table := range []struct { desc string serialized io.Reader wantErr bool want *InclusionProof }{ { desc: "invalid: not an inclusion proof (unexpected key-value pair)", serialized: bytes.NewBufferString(validInclusionProofASCII(t) + "size=4"), wantErr: true, want: validInclusionProof(t), // to populate input to FromASCII }, { desc: "valid", serialized: bytes.NewBufferString(validInclusionProofASCII(t)), want: validInclusionProof(t), }, } { var proof InclusionProof err := proof.FromASCII(table.serialized) 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 } if got, want := &proof, table.want; !reflect.DeepEqual(got, want) { t.Errorf("got inclusion proof\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, table.desc) } } } func TestConsistencyProofToASCII(t *testing.T) { desc := "valid" buf := bytes.Buffer{} if err := validConsistencyProof(t).ToASCII(&buf); err != nil { t.Fatalf("got error true but wanted false in test %q: %v", desc, err) } if got, want := buf.String(), validConsistencyProofASCII(t); got != want { t.Errorf("got consistency proof\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, desc) } } func TestConsistencyProofFromASCII(t *testing.T) { for _, table := range []struct { desc string serialized io.Reader wantErr bool want *ConsistencyProof }{ { desc: "invalid: not a consistency proof (unexpected key-value pair)", serialized: bytes.NewBufferString(validConsistencyProofASCII(t) + "start_size=1"), wantErr: true, want: validConsistencyProof(t), // to populate input to FromASCII }, { desc: "valid", serialized: bytes.NewBufferString(validConsistencyProofASCII(t)), want: validConsistencyProof(t), }, } { var proof ConsistencyProof err := proof.FromASCII(table.serialized) 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 } if got, want := &proof, table.want; !reflect.DeepEqual(got, want) { t.Errorf("got consistency proof\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, table.desc) } } } func validInclusionProof(t *testing.T) *InclusionProof { t.Helper() return &InclusionProof{ LeafIndex: 1, Path: []crypto.Hash{ crypto.Hash{}, *newHashBufferInc(t), }, } } func validInclusionProofASCII(t *testing.T) string { t.Helper() return fmt.Sprintf("%s=%d\n%s=%x\n%s=%x\n", "leaf_index", 1, "node_hash", crypto.Hash{}, "node_hash", newHashBufferInc(t)[:], ) } func validConsistencyProof(t *testing.T) *ConsistencyProof { t.Helper() return &ConsistencyProof{ Path: []crypto.Hash{ crypto.Hash{}, *newHashBufferInc(t), }, } } func validConsistencyProofASCII(t *testing.T) string { t.Helper() return fmt.Sprintf("%s=%x\n%s=%x\n", "node_hash", crypto.Hash{}, "node_hash", newHashBufferInc(t)[:], ) } sigsum-go-0.7.2/pkg/types/tree_head.go000066400000000000000000000127221455374457700176570ustar00rootroot00000000000000package types import ( "encoding/base64" "encoding/binary" "fmt" "io" "sigsum.org/sigsum-go/internal/ssh" "sigsum.org/sigsum-go/pkg/ascii" "sigsum.org/sigsum-go/pkg/crypto" "sigsum.org/sigsum-go/pkg/merkle" ) const ( CosignatureNamespace = "cosignature/v1" CheckpointNamePrefix = "sigsum.org/v1/tree/" ) type TreeHead struct { Size uint64 RootHash crypto.Hash } type SignedTreeHead struct { TreeHead Signature crypto.Signature } type Cosignature struct { KeyHash crypto.Hash Timestamp uint64 Signature crypto.Signature } type CosignedTreeHead struct { SignedTreeHead Cosignatures []Cosignature } func NewEmptyTreeHead() TreeHead { return TreeHead{Size: 0, RootHash: merkle.HashEmptyTree()} } func (th *TreeHead) formatCheckpoint(prefix string, keyHash *crypto.Hash) string { return fmt.Sprintf("%s%x\n%d\n%s\n", prefix, *keyHash, th.Size, base64.StdEncoding.EncodeToString(th.RootHash[:])) } func (th *TreeHead) toCheckpoint(keyHash *crypto.Hash) string { return th.formatCheckpoint(CheckpointNamePrefix, keyHash) } func (th *TreeHead) Sign(signer crypto.Signer) (SignedTreeHead, error) { pub := signer.Public() keyHash := crypto.HashBytes(pub[:]) sig, err := signer.Sign([]byte(th.toCheckpoint(&keyHash))) if err != nil { return SignedTreeHead{}, fmt.Errorf("failed signing tree head: %w", err) } return SignedTreeHead{ TreeHead: *th, Signature: sig, }, nil } // TODO: Should the Cosign method be attached to SignedTreeHead instead? func (th *TreeHead) toCosignedData(logKeyHash *crypto.Hash, timestamp uint64) string { return fmt.Sprintf("%s\ntime %d\n%s", CosignatureNamespace, timestamp, th.toCheckpoint(logKeyHash)) } func (th *TreeHead) Cosign(signer crypto.Signer, logKeyHash *crypto.Hash, timestamp uint64) (Cosignature, error) { signature, err := signer.Sign([]byte(th.toCosignedData(logKeyHash, timestamp))) if err != nil { return Cosignature{}, fmt.Errorf("failed co-signing tree head: %w", err) } pub := signer.Public() return Cosignature{ KeyHash: crypto.HashBytes(pub[:]), Timestamp: timestamp, Signature: signature, }, nil } func (th *TreeHead) ToASCII(w io.Writer) error { if err := ascii.WriteInt(w, "size", th.Size); err != nil { return err } return ascii.WriteHash(w, "root_hash", &th.RootHash) } // Doesn't require EOF, so it can be used for parsing a tree head // embedded in a larger struct. func (th *TreeHead) Parse(p *ascii.Parser) error { var err error th.Size, err = p.GetInt("size") if err != nil { return err } th.RootHash, err = p.GetHash("root_hash") return err } func (th *TreeHead) FromASCII(r io.Reader) error { p := ascii.NewParser(r) err := th.Parse(&p) if err != nil { return err } return p.GetEOF() } func (sth *SignedTreeHead) ToASCII(w io.Writer) error { if err := sth.TreeHead.ToASCII(w); err != nil { return err } return ascii.WriteSignature(w, "signature", &sth.Signature) } func (sth *SignedTreeHead) Parse(p *ascii.Parser) error { err := sth.TreeHead.Parse(p) if err != nil { return err } sth.Signature, err = p.GetSignature("signature") return err } func (sth *SignedTreeHead) FromASCII(r io.Reader) error { p := ascii.NewParser(r) err := sth.Parse(&p) if err != nil { return err } return p.GetEOF() } func (sth *SignedTreeHead) Verify(key *crypto.PublicKey) bool { keyHash := crypto.HashBytes(key[:]) return crypto.Verify(key, []byte(sth.toCheckpoint(&keyHash)), &sth.Signature) } func (sth *SignedTreeHead) VerifyVersion0(key *crypto.PublicKey) bool { // Prefix used temporarily, for version v0.1.15 and v0.2.0. keyHash := crypto.HashBytes(key[:]) if crypto.Verify(key, []byte(sth.formatCheckpoint("sigsum.org/v1/", &keyHash)), &sth.Signature) { return true } b := make([]byte, 40) binary.BigEndian.PutUint64(b[:8], sth.Size) copy(b[8:40], sth.RootHash[:]) return crypto.Verify(key, ssh.SignedData("signed-tree-head:v0@sigsum.org", b), &sth.Signature) } func (cs *Cosignature) Verify(key *crypto.PublicKey, logKeyHash *crypto.Hash, th *TreeHead) bool { return cs.KeyHash == crypto.HashBytes(key[:]) && crypto.Verify(key, []byte(th.toCosignedData(logKeyHash, cs.Timestamp)), &cs.Signature) } func (cs *Cosignature) ToASCII(w io.Writer) error { return ascii.WriteLine(w, "cosignature", cs.KeyHash[:], cs.Timestamp, cs.Signature[:]) } func (cs *Cosignature) Parse(p *ascii.Parser) error { v, err := p.GetValues("cosignature", 3) if err != nil { return err } cs.KeyHash, err = crypto.HashFromHex(v[0]) if err != nil { return err } cs.Timestamp, err = ascii.IntFromDecimal(v[1]) if err != nil { return err } cs.Signature, err = crypto.SignatureFromHex(v[2]) return err } func (cs *Cosignature) FromASCII(r io.Reader) error { p := ascii.NewParser(r) err := cs.Parse(&p) if err != nil { return err } return p.GetEOF() } func (cth *CosignedTreeHead) ToASCII(w io.Writer) error { if err := cth.SignedTreeHead.ToASCII(w); err != nil { return err } for _, cs := range cth.Cosignatures { if err := cs.ToASCII(w); err != nil { return err } } return nil } func ParseCosignatures(p *ascii.Parser) ([]Cosignature, error) { var cosignatures []Cosignature for { var cs Cosignature err := cs.Parse(p) if err == io.EOF { return cosignatures, nil } if err != nil { return nil, err } cosignatures = append(cosignatures, cs) } } func (cth *CosignedTreeHead) FromASCII(r io.Reader) error { p := ascii.NewParser(r) err := cth.SignedTreeHead.Parse(&p) if err != nil { return err } cth.Cosignatures, err = ParseCosignatures(&p) return err } sigsum-go-0.7.2/pkg/types/tree_head_test.go000066400000000000000000000277631455374457700207310ustar00rootroot00000000000000package types import ( "bytes" "fmt" "io" "reflect" "testing" "sigsum.org/sigsum-go/internal/mocks/signer" "sigsum.org/sigsum-go/pkg/crypto" "sigsum.org/sigsum-go/pkg/key" ) const ( testCosignTimestamp uint64 = 72623859790382856 ) func TestTreeHeadToCheckpoint(t *testing.T) { desc := "valid" pub := crypto.PublicKey{} keyHash := crypto.HashBytes(pub[:]) if got, want := validTreeHead(t).toCheckpoint(&keyHash), validTreeHeadCheckpoint(t); got != want { t.Errorf("got tree head checkpoint\n\t%q\nbut wanted\n\t%q\nin test %q\n", got, want, desc) } } func TestTreeHeadToCosignedData(t *testing.T) { desc := "valid" pub := crypto.PublicKey{} keyHash := crypto.HashBytes(pub[:]) if got, want := validTreeHead(t).toCosignedData(&keyHash, testCosignTimestamp), validTreeHeadCosignedData(t); got != want { t.Errorf("got tree head checkpoint\n\t%q\nbut wanted\n\t%q\nin test %q\n", got, want, desc) } } func TestTreeHeadSign(t *testing.T) { for _, table := range []struct { desc string th *TreeHead signer crypto.Signer wantSig *crypto.Signature wantErr bool }{ { desc: "invalid: signer error", th: validTreeHead(t), signer: &signer.Signer{*newPubBufferInc(t), *newSigBufferInc(t), fmt.Errorf("signing error")}, wantErr: true, }, { desc: "valid", th: validTreeHead(t), signer: &signer.Signer{*newPubBufferInc(t), *newSigBufferInc(t), nil}, wantSig: newSigBufferInc(t), }, } { sth, err := table.th.Sign(table.signer) 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 } wantSTH := SignedTreeHead{ TreeHead: *table.th, Signature: *table.wantSig, } if got, want := sth, wantSTH; sth != wantSTH { t.Errorf("got sth\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.desc) } } } func TestSignedTreeHeadToASCII(t *testing.T) { desc := "valid" buf := bytes.Buffer{} if err := validSignedTreeHead(t).ToASCII(&buf); err != nil { t.Fatalf("got error true but wanted false in test %q: %v", desc, err) } if got, want := buf.String(), validSignedTreeHeadASCII(t); got != want { t.Errorf("got signed tree head\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, desc) } } func TestSignedTreeHeadFromASCII(t *testing.T) { for _, table := range []struct { desc string serialized io.Reader wantErr bool want *SignedTreeHead }{ { desc: "invalid: not a signed tree head (unexpected key-value pair)", serialized: bytes.NewBufferString(validSignedTreeHeadASCII(t) + "key=4"), wantErr: true, }, { desc: "valid", serialized: bytes.NewBufferString(validSignedTreeHeadASCII(t)), want: validSignedTreeHead(t), }, } { var sth SignedTreeHead err := sth.FromASCII(table.serialized) 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 } if got, want := sth, *table.want; got != want { t.Errorf("got signed tree head\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, table.desc) } } } func TestTreeHeadSignAndVerify(t *testing.T) { th := validTreeHead(t) pub, signer := newKeyPair(t) sth, err := th.Sign(signer) if err != nil { t.Fatal(err) } if !sth.Verify(&pub) { t.Errorf("failed verifying a valid signed tree head") } sth.Size += 1 if sth.Verify(&pub) { t.Errorf("succeeded verifying an invalid signed tree head") } } func TestSignedTreeHeadVerify(t *testing.T) { pub := mustParsePublicKey(t, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN6kw3w2BWjlKLdrtnv4IaN+zg8/RpKGA98AbbTwjpdQ") sth := SignedTreeHead{ TreeHead: TreeHead{ Size: 4, RootHash: mustHashFromHex(t, "7bca01e88737999fde5c1d6ecac27ae3cb49e14f21bcd3e7245c276877b899c9"), }, Signature: mustSignatureFromHex(t, "c60e5151b9d0f0efaf57022c0ec306c0f0275afef69333cc89df4fda328c87949fcfa44564f35020938a4cd6c1c50bc0349b2f54b82f5f6104b9cd52be2cd90e"), } if !sth.Verify(&pub) { t.Errorf("failed verifying a valid signed tree head") } sth.Size += 1 if sth.Verify(&pub) { t.Errorf("succeeded verifying an invalid signed tree head") } } func TestSignedTreeHeadVerifyVersion0(t *testing.T) { // Example based on a run of tests/sigsum-submit-test pub := mustParsePublicKey(t, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICLAkeP3VJfvGQFcXa8UORDiDNpylbD9Hd+DglaG7+ym") sth := SignedTreeHead{ TreeHead: TreeHead{ Size: 4, RootHash: mustHashFromHex(t, "84ec3e1ba5433358988ac74bed33a30bda42cc983b87e4940a423c2d84890f0f"), }, Signature: mustSignatureFromHex(t, "7e2084ded0f7625136e6c811ac7eae2cb79613cadb12a6437b391cdae3a5c915dcd30b5b5fe4fbf417a2d607a4cfcb3612d7fd4ffe9453c0d29ec002a6d47709"), } if !sth.VerifyVersion0(&pub) { t.Errorf("failed verifying a valid signed tree head") } sth.Size += 1 if sth.VerifyVersion0(&pub) { t.Errorf("succeeded verifying an invalid signed tree head") } } func TestCosignatureToASCII(t *testing.T) { desc := "valid" buf := bytes.Buffer{} if err := validCosignature(t).ToASCII(&buf); err != nil { t.Fatalf("got error true but wanted false in test %q: %v", desc, err) } if got, want := buf.String(), validCosignatureASCII(t); got != want { t.Errorf("got cosignature request\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, desc) } } func TestCosignatureFromASCII(t *testing.T) { for _, table := range []struct { desc string serialized io.Reader wantErr bool want *Cosignature }{ { desc: "invalid: not a cosignature request (unexpected key-value pair)", serialized: bytes.NewBufferString(validCosignatureASCII(t) + "key=4"), wantErr: true, }, { desc: "valid", serialized: bytes.NewBufferString(validCosignatureASCII(t)), want: validCosignature(t), }, } { var req Cosignature err := req.FromASCII(table.serialized) 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 } if got, want := req, *table.want; got != want { t.Errorf("got cosignature request\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, table.desc) } } } func TestCosignAndVerify(t *testing.T) { th := *validTreeHead(t) pub, signer := newKeyPair(t) keyHash := crypto.HashBytes(pub[:]) logKeyHash := *newHashBufferInc(t) cosignature, err := th.Cosign(signer, &logKeyHash, testCosignTimestamp) if err != nil { t.Fatal(err) } if cosignature.Timestamp != testCosignTimestamp { t.Errorf("unexpected cosignature timestamp, wanted %d, got %d", testCosignTimestamp, cosignature.Timestamp) } if cosignature.KeyHash != keyHash { t.Errorf("unexpected cosignature keyhash, wanted %x, got %x", keyHash, cosignature.KeyHash) } if !cosignature.Verify(&pub, &logKeyHash, &th) { t.Errorf("failed verifying a valid cosignature") } // Test mutation of signed items. for _, f := range [](func() (string, TreeHead, Cosignature, crypto.Hash)){ func() (string, TreeHead, Cosignature, crypto.Hash) { mTh := th mTh.Size++ return "bad size", mTh, cosignature, logKeyHash }, func() (string, TreeHead, Cosignature, crypto.Hash) { mCs := cosignature mCs.Timestamp++ return "bad timestamp", th, mCs, logKeyHash }, func() (string, TreeHead, Cosignature, crypto.Hash) { mCs := cosignature mCs.KeyHash[2]++ return "bad cs key hash", th, mCs, logKeyHash }, func() (string, TreeHead, Cosignature, crypto.Hash) { mKh := logKeyHash mKh[3]++ return "bad log key hash", th, cosignature, mKh }, func() (string, TreeHead, Cosignature, crypto.Hash) { return "", th, cosignature, logKeyHash }, } { desc, mTh, mCs, mLogHash := f() valid := mCs.Verify(&pub, &mLogHash, &mTh) if len(desc) > 0 && valid { t.Errorf("%s: succeeded verifying invalid cosignature", desc) } else if len(desc) == 0 && !valid { t.Errorf("internal test failure, failed to verify unmodified cosignature") } } } func TestCosignedTreeHeadToASCII(t *testing.T) { desc := "valid" buf := bytes.Buffer{} if err := validCosignedTreeHead(t).ToASCII(&buf); err != nil { t.Fatalf("got error true but wanted false in test %q: %v", desc, err) } if got, want := buf.String(), validCosignedTreeHeadASCII(t); got != want { t.Errorf("got cosigned tree head\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, desc) } } func TestCosignedTreeHeadFromASCII(t *testing.T) { for _, table := range []struct { desc string serialized io.Reader wantErr bool want *CosignedTreeHead }{ { desc: "invalid: not a cosigned tree head (unexpected key-value pair)", serialized: bytes.NewBufferString(validCosignedTreeHeadASCII(t) + "key=4"), wantErr: true, }, { desc: "invalid: not a cosigned tree head (not enough cosignatures)", serialized: bytes.NewBufferString(validCosignedTreeHeadASCII(t) + fmt.Sprintf("key_hash=%x\n", crypto.Hash{})), wantErr: true, }, { desc: "valid", serialized: bytes.NewBufferString(validCosignedTreeHeadASCII(t)), want: validCosignedTreeHead(t), }, } { var cth CosignedTreeHead err := cth.FromASCII(table.serialized) 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 } if got, want := &cth, table.want; !reflect.DeepEqual(got, want) { t.Errorf("got cosigned tree head\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, table.desc) } } } func validTreeHead(t *testing.T) *TreeHead { return &TreeHead{ Size: 257, RootHash: *newHashBufferInc(t), } } func validTreeHeadCheckpoint(t *testing.T) string { return ` sigsum.org/v1/tree/66687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925 257 AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8= `[1:] } func validTreeHeadCosignedData(t *testing.T) string { return ` cosignature/v1 time 72623859790382856 sigsum.org/v1/tree/66687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925 257 AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8= `[1:] } func validSignedTreeHead(t *testing.T) *SignedTreeHead { t.Helper() return &SignedTreeHead{ TreeHead: TreeHead{ Size: 2, RootHash: *newHashBufferInc(t), }, Signature: *newSigBufferInc(t), } } func validSignedTreeHeadASCII(t *testing.T) string { t.Helper() return fmt.Sprintf("%s=%d\n%s=%x\n%s=%x\n", "size", 2, "root_hash", newHashBufferInc(t)[:], "signature", newSigBufferInc(t)[:], ) } func validCosignature(t *testing.T) *Cosignature { t.Helper() return &Cosignature{ Signature: *newSigBufferInc(t), Timestamp: 1, KeyHash: *newHashBufferInc(t), } } func validCosignatureASCII(t *testing.T) string { t.Helper() return fmt.Sprintf("%s=%x %d %x\n", "cosignature", newHashBufferInc(t)[:], 1, newSigBufferInc(t)[:]) } func validCosignedTreeHead(t *testing.T) *CosignedTreeHead { t.Helper() return &CosignedTreeHead{ SignedTreeHead: SignedTreeHead{ TreeHead: TreeHead{ Size: 2, RootHash: *newHashBufferInc(t), }, Signature: *newSigBufferInc(t), }, Cosignatures: []Cosignature{ Cosignature{}, Cosignature{ KeyHash: *newHashBufferInc(t), Timestamp: 1, Signature: *newSigBufferInc(t), }, }, } } func validCosignedTreeHeadASCII(t *testing.T) string { t.Helper() return fmt.Sprintf("%s=%d\n%s=%x\n%s=%x\n%s=%x %d %x\n%s=%x %d %x\n", "size", 2, "root_hash", newHashBufferInc(t)[:], "signature", newSigBufferInc(t)[:], "cosignature", crypto.Hash{}, 0, crypto.Signature{}, "cosignature", newHashBufferInc(t)[:], 1, newSigBufferInc(t)[:], ) } func mustParsePublicKey(t *testing.T, sshKey string) crypto.PublicKey { pub, err := key.ParsePublicKey(sshKey) if err != nil { t.Fatal(err) } return pub } func mustHashFromHex(t *testing.T, hex string) crypto.Hash { h, err := crypto.HashFromHex(hex) if err != nil { t.Fatal(err) } return h } func mustSignatureFromHex(t *testing.T, hex string) crypto.Signature { s, err := crypto.SignatureFromHex(hex) if err != nil { t.Fatal(err) } return s } sigsum-go-0.7.2/tests/000077500000000000000000000000001455374457700146215ustar00rootroot00000000000000sigsum-go-0.7.2/tests/.gitignore000066400000000000000000000000151455374457700166050ustar00rootroot00000000000000/test.* /bin sigsum-go-0.7.2/tests/Makefile000066400000000000000000000006061455374457700162630ustar00rootroot00000000000000TESTS = ssh-agent-test \ sign-verify-test sign-agent-test \ keyhash-test keyhex-test help-msg-test \ token-record-test token-create-raw-test token-create-header-test \ sigsum-submit-test sigsum-submit-batch-test \ witness-get-tree-size-test witness-add-tree-head-test \ sigsum-submit-witness-test all: check: ./run-tests $(TESTS) clean: rm -rf bin test.* .PHONY: all check clean sigsum-go-0.7.2/tests/help-msg-test000077500000000000000000000016351455374457700172450ustar00rootroot00000000000000#! /bin/sh set -e # Collect help messages from all tools, and verify that it is written # to stdout (not stderr), and that exit code is success. exec > test.help die() { echo "$@" >&2 exit 1 } test_one() { echo "=== $@ ===" if "$@" 2> test.stderr ; then true; else die "Exit code $? from: $@"; fi if [ -s test.stderr ] ; then die "Stderr output from: $@" ; fi } test_one ./bin/sigsum-key --help test_one ./bin/sigsum-key gen --help test_one ./bin/sigsum-key sign --help test_one ./bin/sigsum-key verify --help test_one ./bin/sigsum-key hash --help test_one ./bin/sigsum-key hex --help test_one ./bin/sigsum-key hex-to-pub --help test_one ./bin/sigsum-token --help test_one ./bin/sigsum-token create --help test_one ./bin/sigsum-token record --help test_one ./bin/sigsum-token verify --help test_one ./bin/sigsum-submit --help test_one ./bin/sigsum-verify --help test_one ./bin/sigsum-witness --help sigsum-go-0.7.2/tests/keyhash-test000077500000000000000000000005421455374457700171610ustar00rootroot00000000000000#! /bin/sh set -e ./bin/sigsum-key gen -o test.key kh=$(./bin/sigsum-key hash -k test.key.pub) # basenc requires uppercase hex ref=$(./bin/sigsum-key hex -k test.key.pub | tr a-f A-F | basenc -d --base16 | sha256sum | sed 's/ .*$//') if [ "$kh" != "$ref" ] ; then printf "unexpected keyhash\n got: %s\n wanted: %s\n" "$kh" "$ref" exit 1 fi sigsum-go-0.7.2/tests/keyhex-test000077500000000000000000000010021455374457700170120ustar00rootroot00000000000000#! /bin/sh set -e # The pub key at _sigsum_v1.test.sigsum.org. HEXKEY=4cb5abf6ad79fbf5abbccafcc269d85cd2651ed4b885b5869f241aedf0a5ba29 PUBKEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEy1q/atefv1q7zK/MJp2FzSZR7UuIW1hp8kGu3wpbop" echo ${HEXKEY} | ./bin/sigsum-key hex-to-pub > test.key.pub [ "$(cut -d' ' -f1,2 < test.key.pub )" = "${PUBKEY}" ] || ( echo >&2 "sigsum-key hex-to-pub failed" exit 1 ) [ $(./bin/sigsum-key hex -k test.key.pub) = ${HEXKEY} ] || ( echo >&2 "sigsum-key hex failed" exit 1 ) sigsum-go-0.7.2/tests/mk-add-tree-request/000077500000000000000000000000001455374457700204015ustar00rootroot00000000000000sigsum-go-0.7.2/tests/mk-add-tree-request/mk-add-tree-request.go000066400000000000000000000035111455374457700245100ustar00rootroot00000000000000package main // Program to generate a witness add-tree request, from a // deterministically built tree. import ( "encoding/binary" "fmt" "io" "log" "os" "strconv" "sigsum.org/sigsum-go/pkg/crypto" "sigsum.org/sigsum-go/pkg/key" "sigsum.org/sigsum-go/pkg/merkle" "sigsum.org/sigsum-go/pkg/types" ) func main() { log.SetFlags(0) if len(os.Args) != 3 { log.Fatalf("Usage: %s old-size new-size < private-key", os.Args[0]) } oldSize, err := strconv.ParseUint(os.Args[1], 10, 63) if err != nil || oldSize < 0 { log.Fatalf("Invalid old size %q", os.Args[1]) } newSize, err := strconv.ParseUint(os.Args[2], 10, 63) if err != nil || newSize < oldSize { log.Fatalf("Invalid old size %q", os.Args[1]) } data, err := io.ReadAll(os.Stdin) if err != nil { log.Fatalf("reading key from stdin failed: %v", err) } ascii := string(data) signer, err := key.ParsePrivateKey(ascii) if err != nil { log.Fatalf("parsing public key failed: %v", err) } pub := signer.Public() t := merkle.NewTree() for i := uint64(0); i < newSize; i++ { h := crypto.Hash{} binary.BigEndian.PutUint64(h[:8], i) if !t.AddLeafHash(&h) { log.Fatalf("Unexpected leaf duplicate for leaf %d", i) } } th := types.TreeHead{Size: t.Size(), RootHash: t.GetRootHash()} if th.Size != newSize { panic("internal error") } sth, err := th.Sign(signer) if err != nil { log.Fatal(err) } proof, err := t.ProveConsistency(oldSize, newSize) if err != nil { log.Fatal(err) } if _, err := fmt.Printf("key_hash=%x\n", crypto.HashBytes(pub[:])); err != nil { log.Fatal(err) } if err := sth.ToASCII(os.Stdout); err != nil { log.Fatal(err) } if _, err := fmt.Printf("old_size=%d\n", oldSize); err != nil { log.Fatal(err) } if len(proof) > 0 { if err := (&types.ConsistencyProof{proof}).ToASCII(os.Stdout); err != nil { log.Fatal(err) } } } sigsum-go-0.7.2/tests/run-tests000077500000000000000000000053171455374457700165210ustar00rootroot00000000000000#! /bin/sh # Copyright (C) 2000-2002, 2004, 2005, 2011, 2012, 2016, 2020 Niels Möller # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. failed=0 all=0 debug='no' testflags='' if [ -z "$srcdir" ] ; then srcdir=`pwd` fi export srcdir if [ -n "$TEST_SHLIB_DIR" ] ; then # Prepend to LD_LIBRARY_PATH, if it is alredy set. LD_LIBRARY_PATH="${TEST_SHLIB_DIR}${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" # For MACOS DYLD_LIBRARY_PATH="$TEST_SHLIB_DIR" # For Windows PATH="${TEST_SHLIB_DIR}:${PATH}" # For Wine WINEPATH="${TEST_SHLIB_DIR}${WINEPATH:+;$WINEPATH}" export LD_LIBRARY_PATH export DYLD_LIBRARY_PATH export PATH export WINEPATH fi # When used in make rules, we sometimes get the filenames VPATH # expanded, but usually not. find_program () { case "$1" in */*) echo "$1" ;; *) if [ -x "$1" ] ; then echo "./$1" elif [ -x "$1.exe" ] ; then echo "./$1.exe" else echo "$srcdir/$1" fi ;; esac } env_program () { if [ -x "$1" ] ; then if "$1"; then : ; else echo FAIL: $1 exit 1 fi fi } test_program () { testname=`basename "$1" .exe` testname=`basename "$testname" -test` if [ -z "$EMULATOR" ] || head -1 "$1" | grep '^#!' > /dev/null; then "$1" $testflags else $EMULATOR "$1" $testflags fi case "$?" in 0) echo PASS: $testname all=`expr $all + 1` ;; 77) echo SKIP: $testname ;; *) echo FAIL: $testname failed=`expr $failed + 1` all=`expr $all + 1` ;; esac } env_program `find_program setup-env` while test $# != 0 do case "$1" in --debug) debug=yes ;; -v) testflags='-v' ;; -*) echo >&2 'Unknown option `'"$1'" exit 1 ;; *) break ;; esac shift done for f in "$@" ; do test_program `find_program "$f"`; done if [ $failed -eq 0 ] ; then banner="All $all tests passed" else banner="$failed of $all tests failed" fi dashes=`echo "$banner" | sed s/./=/g` echo "$dashes" echo "$banner" echo "$dashes" if [ "x$debug" = xno ] ; then env_program `find_program teardown-env` fi [ "$failed" -eq 0 ] sigsum-go-0.7.2/tests/setup-env000077500000000000000000000013711455374457700164770ustar00rootroot00000000000000#! /bin/sh set -e if [ "$GOARCH" ] ; then # When crosscompiling, nevertheless build a native log server. We # also need to use go build rather than go install, see # https://github.com/golang/go/issues/57485 # Running the test scripts in cross compile environment assumes # that we can run the cross-compiled executables; that may work # for 386 executables on a amd64 system, or if qemu-user + binfmt # magic is installed. echo >&2 Cross-compiling for GOARCH=${GOARCH} GOARCH="" GOBIN=$(pwd)/bin go install sigsum.org/log-go/cmd/sigsum-log-primary@v0.14.0 go build -o bin/ ../cmd/... else GOBIN=$(pwd)/bin go install ../cmd/... GOBIN=$(pwd)/bin go install sigsum.org/log-go/cmd/sigsum-log-primary@v0.14.0 fi sigsum-go-0.7.2/tests/sha256-n/000077500000000000000000000000001455374457700160645ustar00rootroot00000000000000sigsum-go-0.7.2/tests/sha256-n/sha256-n.go000066400000000000000000000010201455374457700176470ustar00rootroot00000000000000package main import ( "fmt" "log" "os" "strconv" "sigsum.org/sigsum-go/pkg/crypto" ) func main() { log.SetFlags(0) n := 1 if len(os.Args) > 1 { var err error n, err = strconv.Atoi(os.Args[1]) if err != nil { log.Fatalf("invalid number: %v", err) } if n < 1 { log.Fatalf("count must be positive, but got %d", n) } } h, err := crypto.HashFile(os.Stdin) if err != nil { log.Fatalf("failed reading input: %v", err) } for n--; n > 0; n-- { h = crypto.HashBytes(h[:]) } fmt.Printf("%x\n", h) } sigsum-go-0.7.2/tests/sign-agent-test000077500000000000000000000006241455374457700175620ustar00rootroot00000000000000#! /bin/sh set -e ./bin/sigsum-key gen -o test.key echo foo > test.msg rm -f test.msg.sig ssh-agent sh </dev/null rm test.key # Pass only public key to sigsum. ./bin/sigsum-key sign -n sigsum-test -k test.key.pub -o test.msg.sig < test.msg EOF ./bin/sigsum-key verify -k test.key.pub -s test.msg.sig -n sigsum-test < test.msg sigsum-go-0.7.2/tests/sign-verify-test000077500000000000000000000007131455374457700177670ustar00rootroot00000000000000#! /bin/sh set -e ./bin/sigsum-key gen -o test.key echo foo > test.msg rm -f test.msg.sig ./bin/sigsum-key sign -n sigsum-test -k test.key -o test.msg.sig < test.msg ./bin/sigsum-key verify -n sigsum-test -k test.key.pub -s test.msg.sig < test.msg # Check that modified message makes verification fail. if (cat test.msg && echo) \ | ./bin/sigsum-key verify -n sigsum-test -k test.key.pub -s test.msg.sig 2>/dev/null ; then false else true fi sigsum-go-0.7.2/tests/sigsum-monitor-test000077500000000000000000000025511455374457700205230ustar00rootroot00000000000000#! /bin/sh set -e ./bin/sigsum-key gen -o test.log.key ./bin/sigsum-key gen -o test.submit.key # Reading private key files still supports raw hex. printf '%064x' 1 > test.token.key # Start sigsum log server rm -f test.log.sth echo "startup=empty" > test.log.sth.startup ./bin/sigsum-log-primary --key-file test.log.key \ --interval=1s --log-level=error --backend=ephemeral --sth-file test.log.sth & SIGSUM_PID=$! MONITOR_PID= cleanup () { kill ${SIGSUM_PID} [ -z ${MONITOR_PID} ] || kill ${MONITOR_PID} } trap cleanup EXIT # Give log server some time to get ready. sleep 2 echo "log $(./bin/sigsum-key hex -k test.log.key.pub) http://localhost:6965" > test.policy echo "quorum none" >> test.policy ./bin/sigsum-monitor -p test.policy --interval=2s test.submit.key.pub > test.monitor.out & MONITOR_PID=$! die() { echo "$@" >&2 exit 1 } search_output() { for f in $(seq 10) ; do if grep -- "$1" test.monitor.out >/dev/null ; then return 0 fi sleep 2 done return 1 } for x in $(seq 5); do echo >&2 "submit $x" echo "msg $x" | ./bin/sigsum-submit --diagnostics=warning --token-domain test.sigsum.org --token-key test.token.key -o /dev/null -k test.submit.key --policy test.policy echo >&2 "waiting on monitor $x" search_output $(echo "msg $x" | go run ./sha256-n/sha256-n.go 2) || die "Monitor not finding leaf $x" done sigsum-go-0.7.2/tests/sigsum-submit-batch-test000077500000000000000000000026461455374457700214230ustar00rootroot00000000000000#! /bin/sh set -e ./bin/sigsum-key gen -o test.log.key ./bin/sigsum-key gen -o test.submit.key # Reading private key files still supports raw hex. printf '%064x' 1 > test.token.key # Start sigsum log server rm -f test.log.sth echo "startup=empty" > test.log.sth.startup ./bin/sigsum-log-primary --key-file test.log.key \ --interval=1s --log-level=error --backend=ephemeral --sth-file test.log.sth & SIGSUM_PID=$! cleanup () { kill ${SIGSUM_PID} } trap cleanup EXIT # Give log server some time to get ready. sleep 2 echo "log $(./bin/sigsum-key hex -k test.log.key.pub) http://localhost:6965" > test.policy echo "quorum none" >> test.policy for x in $(seq 5); do echo foo-$x > test.$x.msg done rm -f test.*.req rm -f test.*.proof ./bin/sigsum-submit -k test.submit.key --diagnostics=warning test.1.msg test.2.msg test.3.msg test.4.msg test.5.msg ./bin/sigsum-submit -p test.policy --diagnostics=warning --timeout=5s \ --token-domain test.sigsum.org --token-signing-key test.token.key \ test.1.msg.req test.2.msg.req test.3.msg.req test.4.msg.req test.5.msg.req for x in $(seq 5); do echo >&2 "verify $x" ./bin/sigsum-verify < test.$x.msg --key test.submit.key.pub --policy test.policy "test.$x.msg.proof" done # Check that the message is taken into account in validation. if ./bin/sigsum-verify < test.2.msg --key test.submit.key.pub --policy test.policy "test.1.msg.proof" ; then false else true fi sigsum-go-0.7.2/tests/sigsum-submit-test000077500000000000000000000025361455374457700203420ustar00rootroot00000000000000#! /bin/sh set -e ./bin/sigsum-key gen -o test.log.key ./bin/sigsum-key gen -o test.submit.key # Reading private key files still supports raw hex. printf '%064x' 1 > test.token.key # Start sigsum log server rm -f test.log.sth echo "startup=empty" > test.log.sth.startup ./bin/sigsum-log-primary --key-file test.log.key \ --interval=1s --log-level=error --backend=ephemeral --sth-file test.log.sth & SIGSUM_PID=$! cleanup () { kill ${SIGSUM_PID} } trap cleanup EXIT # Give log server some time to get ready. sleep 2 echo "log $(./bin/sigsum-key hex -k test.log.key.pub) http://localhost:6965" > test.policy echo "quorum none" >> test.policy for x in $(seq 5); do echo >&2 "submit $x" # Must be exactly 32 bytes printf "%31s\n" foo-$x \ | ./bin/sigsum-submit --diagnostics=warning --timeout=5s \ --token-domain test.sigsum.org --token-signing-key test.token.key \ --raw-hash -o test.$x.proof --signing-key test.submit.key --policy test.policy done for x in $(seq 5); do echo >&2 "verify $x" printf "%31s\n" foo-$x \ | ./bin/sigsum-verify --raw-hash -k test.submit.key.pub --policy test.policy "test.$x.proof" done # Check that the message is taken into account in validation. if printf "%31s\n" foo-2 \ | ./bin/sigsum-verify --key test.submit.key.pub --policy test.policy "test.1.proof" ; then false else true fi sigsum-go-0.7.2/tests/sigsum-submit-witness-test000077500000000000000000000025001455374457700220230ustar00rootroot00000000000000#! /bin/sh set -e ./bin/sigsum-key gen -o test.log.key ./bin/sigsum-key gen -o test.submit.key ./bin/sigsum-key gen -o test.witness.key echo "log $(./bin/sigsum-key hex -k test.log.key.pub) http://localhost:6965" > test.policy echo "witness W $(./bin/sigsum-key hex -k test.witness.key.pub) http://localhost:7777" >> test.policy echo "quorum W" >> test.policy # Start witness server rm -f test.witness.cth ./bin/sigsum-witness -k test.witness.key --log-key test.log.key.pub \ --state-file test.witness.cth localhost:7777 & WITNESS_PID=$! # Start sigsum log server rm -f test.log.sth echo "startup=empty" > test.log.sth.startup ./bin/sigsum-log-primary --key-file test.log.key \ --policy-file=test.policy \ --interval=1s --log-level=error --backend=ephemeral --sth-file test.log.sth & SIGSUM_PID=$! cleanup () { kill ${SIGSUM_PID} ${WITNESS_PID} } trap cleanup EXIT # Give log server some time to get ready. sleep 2 for x in $(seq 5); do echo >&2 "submit $x" # Must be exactly 32 bytes printf "%31s\n" foo-$x \ | ./bin/sigsum-submit --diagnostics=warning --raw-hash -o test.$x.proof -k test.submit.key --policy test.policy done for x in $(seq 5); do echo >&2 "verify $x" printf "%31s\n" foo-$x \ | ./bin/sigsum-verify --raw-hash --key test.submit.key.pub --policy test.policy "test.$x.proof" done sigsum-go-0.7.2/tests/ssh-agent-test000077500000000000000000000003131455374457700174120ustar00rootroot00000000000000#! /bin/sh set -e ./bin/sigsum-key gen -o test.key ssh-agent sh </dev/null rm test.key go run ./use-agent < test.key.pub < test.sigsum.key ./bin/sigsum-token create -k test.sigsum.key --log-key test.log.key.pub --domain test.sigsum.org -o test.header ./bin/sigsum-token verify --log-key test.log.key.pub < test.header # Check that validation fails when log key is changed. if ./bin/sigsum-token verify --log-key test.key.pub < test.header 2>/dev/null; then exit 1 fi sigsum-go-0.7.2/tests/token-create-raw-test000077500000000000000000000006441455374457700207000ustar00rootroot00000000000000#! /bin/sh set -e ./bin/sigsum-key gen -o test.key ./bin/sigsum-key gen -o test.log.key ./bin/sigsum-token create -k test.key --log-key test.log.key.pub -o test.token ./bin/sigsum-token verify --log-key test.log.key.pub -k test.key.pub < test.token # Check that validation fails when log key is changed. if ./bin/sigsum-token verify --log-key test.key.pub -k test.key.pub < test.token 2>/dev/null; then exit 1 fi sigsum-go-0.7.2/tests/token-record-test000077500000000000000000000002721455374457700201210ustar00rootroot00000000000000#! /bin/sh set -e ./bin/sigsum-key gen -o test.key ./bin/sigsum-token record -k test.key.pub -o test.record.txt grep >/dev/null '^_sigsum_v1 IN TXT "[0-9a-z]\{64\}"$' test.record.txt sigsum-go-0.7.2/tests/use-agent/000077500000000000000000000000001455374457700165115ustar00rootroot00000000000000sigsum-go-0.7.2/tests/use-agent/use-agent.go000066400000000000000000000017501455374457700207330ustar00rootroot00000000000000package main import ( "io" "log" "os" "strings" "sigsum.org/sigsum-go/pkg/crypto" "sigsum.org/sigsum-go/pkg/key" ) func main() { data, err := io.ReadAll(os.Stdin) if err != nil { log.Fatalf("reading key from stdin failed: %v", err) } ascii := string(data) if !strings.HasPrefix(ascii, "ssh-ed25519 ") { log.Fatalf("reading key input doesn't look like an openssh public key: %q", ascii) } publicKey, err := key.ParsePublicKey(ascii) if err != nil { log.Fatalf("parsing public key failed: %v", err) } signer, err := key.ParsePrivateKey(ascii) if err != nil { log.Fatalf("parsing key failed: %v", err) } if signer.Public() != publicKey { log.Fatalf("internal error, public key inconsistency\n %x\n %x\n", publicKey, signer.Public()) } msg := []byte("squemish ossifrage") signature, err := signer.Sign(msg) if err != nil { log.Fatalf("signing failed: %v", err) } if !crypto.Verify(&publicKey, msg, &signature) { log.Fatal("signature appears invalid!") } } sigsum-go-0.7.2/tests/witness-add-tree-head-test000077500000000000000000000017721455374457700216110ustar00rootroot00000000000000#! /bin/sh set -e ./bin/sigsum-key gen -o test.log.key ./bin/sigsum-key gen -o test.witness.key # Start witness server rm -f test.witness.cth ./bin/sigsum-witness -k test.witness.key --log-key test.log.key.pub \ --state-file test.witness.cth localhost:7777 & WITNESS_PID=$! cleanup () { kill ${WITNESS_PID} } trap cleanup EXIT # Give server some time to start sleep 1 die() { echo 2>&1 $@ exit 1 } # test_one old_size new_size code test_one() { go run ./mk-add-tree-request $1 $2 < test.log.key \ | curl -s -w "%{http_code}" --data-binary @- http://localhost:7777/add-tree-head > test.rsp [ "$(tail -n1 test.rsp)" = $3 ] || die "Unexpected exit code for range $1, $2" if [ "$3" = 200 ] ; then grep -E '^cosignature=[0-9a-f]{64} [0-9]+ [0-9a-f]{128}$' test.rsp >/dev/null \ || die "cosignature missing in response" fi } test_one 0 2 200 test_one 1 2 409 # bad old size test_one 2 4 200 test_one 4 4 200 test_one 4 5 200 # TODO: Add tests with invalid signature or proof? sigsum-go-0.7.2/tests/witness-get-tree-size-test000077500000000000000000000020011455374457700216730ustar00rootroot00000000000000#! /bin/sh set -e ./bin/sigsum-key gen -o test.log.key ./bin/sigsum-key gen -o test.witness.key # Start witness server rm -f test.witness.cth ./bin/sigsum-witness -k test.witness.key --log-key test.log.key.pub \ --state-file test.witness.cth localhost:7777 & WITNESS_PID=$! cleanup () { kill ${WITNESS_PID} } trap cleanup EXIT # Give server some time to start sleep 1 curl -s http://localhost:7777/get-tree-size/$(./bin/sigsum-key hash -k test.log.key.pub) > test.rsp [ "size=0" = "$(cat test.rsp)" ] || ( echo 1>&2 Unexpected response exit 1 ) curl -s -w '%{http_code}\n' http://localhost:7777/get-tree-size/x > test.rsp [ "400" = "$(tail -1 test.rsp)" ] || ( echo 1>&2 'Unexpected status code, expected 400 (Bad request)' exit 1 ) curl -s -w '%{http_code}\n' http://localhost:7777/get-tree-size/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa > test.rsp [ "404" = "$(tail -1 test.rsp)" ] || ( echo 1>&2 'Unexpected status code, expected 404 (Not found)' exit 1 ) sigsum-go-0.7.2/tools.go000066400000000000000000000001201455374457700151370ustar00rootroot00000000000000//go:build tools package tools import ( _ "github.com/golang/mock/mockgen" )