aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRasmus Dahlberg <rasmus@mullvad.net>2022-03-02 23:16:43 +0100
committerRasmus Dahlberg <rasmus@mullvad.net>2022-03-02 23:17:48 +0100
commit8da382069f42f6d88d3abf914dd38d7e40a845bc (patch)
tree780e8297ee3905ab662c6c88cb8bf33f0717c90c
initial commit
-rw-r--r--LICENSE25
-rw-r--r--README.md2
-rw-r--r--cmd/sigsum/cmd.go131
-rw-r--r--cmd/sigsum/main.go116
-rw-r--r--cmd/sigsum/test/keys/signify.pub2
-rw-r--r--cmd/sigsum/test/keys/signify.sec2
-rwxr-xr-xcmd/sigsum/test/signify.sh55
-rw-r--r--go.mod5
-rw-r--r--go.sum2
-rw-r--r--pkg/client/api/api.go22
-rw-r--r--pkg/client/api/network_client.go133
-rw-r--r--pkg/client/submitter.go129
-rw-r--r--pkg/policy/default.go66
-rw-r--r--pkg/policy/policy.go32
-rw-r--r--pkg/signatures/minisign/minisign.go22
-rw-r--r--pkg/signatures/signatures.go13
-rw-r--r--pkg/signatures/signify/signify.go91
-rw-r--r--pkg/signatures/ssh/ssh.go22
18 files changed, 870 insertions, 0 deletions
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..34e69de
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,25 @@
+BSD 2-Clause License
+
+Copyright (c) 2021, The Sigsum Project
+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.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..124960a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,2 @@
+# sigsum-tool-go
+Mock-up / work-in-progress tooling. May be moved somewhere else.
diff --git a/cmd/sigsum/cmd.go b/cmd/sigsum/cmd.go
new file mode 100644
index 0000000..70a1c51
--- /dev/null
+++ b/cmd/sigsum/cmd.go
@@ -0,0 +1,131 @@
+package main
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "io/ioutil"
+ "time"
+
+ "git.sigsum.org/sigsum-lib-go/pkg/requests"
+ "git.sigsum.org/sigsum-lib-go/pkg/types"
+ "git.sigsum.org/sigsum-tool-go/pkg/client"
+ "git.sigsum.org/sigsum-tool-go/pkg/policy"
+ "git.sigsum.org/sigsum-tool-go/pkg/signatures"
+ "git.sigsum.org/sigsum-tool-go/pkg/signatures/minisign"
+ "git.sigsum.org/sigsum-tool-go/pkg/signatures/signify"
+ "git.sigsum.org/sigsum-tool-go/pkg/signatures/ssh"
+)
+
+func cmdVerify(args []string, policy policy.Policy, optVerifyType, optVerifyKey string) error {
+ return fmt.Errorf("TODO")
+}
+
+func cmdBundle(args []string, policy policy.Policy, optBundleType, optBundleKey, optBundleDomainHint string) error {
+ if len(args) == 0 {
+ return fmt.Errorf("bundle: need at least one file")
+ }
+
+ var parser signatures.Parser
+ switch optBundleType {
+ case "signify":
+ parser = &signify.Parser{}
+ case "minisign":
+ parser = &minisign.Parser{}
+ case "ssh":
+ parser = &ssh.Parser{}
+ default:
+ return fmt.Errorf("bundle: invalid key type %q", optBundleType)
+ }
+
+ b, err := ioutil.ReadFile(optBundleKey)
+ if err != nil {
+ return fmt.Errorf("bundle: failed reading file %q: %v", optBundleKey, err)
+ }
+ pub, err := parser.PublicKey(bytes.NewBuffer(b))
+ if err != nil {
+ return fmt.Errorf("bundle: %v", err)
+ }
+ // TODO: check that domain hint is valid for public key
+
+ var reqs []requests.Leaf
+ for _, path := range args {
+ checksum, err := fileHash(path)
+ if err != nil {
+ return fmt.Errorf("bundle: %v", err)
+ }
+
+ sigPath := path + parser.SignatureSuffix()
+ b, err := ioutil.ReadFile(sigPath)
+ if err != nil {
+ return fmt.Errorf("bundle: failed reading file %q: %v", sigPath, err)
+ }
+ sig, err := parser.Signature(bytes.NewBuffer(b))
+ if err != nil {
+ return fmt.Errorf("bundle: %v", err)
+ }
+
+ req := requests.Leaf{
+ Statement: types.Statement{
+ ShardHint: policy.ShardHint(),
+ Checksum: *checksum,
+ },
+ Signature: *sig,
+ VerificationKey: *pub,
+ DomainHint: optBundleDomainHint,
+ }
+ if !req.Statement.Verify(&req.VerificationKey, &req.Signature) {
+ return fmt.Errorf("bundle: invalid signature for file %q", path)
+ }
+ reqs = append(reqs, req)
+ }
+
+ sc := client.NewSubmitClient(policy)
+ ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
+ defer cancel()
+ bundles, err := sc.AddLeaves(ctx, reqs)
+ if err != nil {
+ return fmt.Errorf("bundle: %v", err)
+ }
+
+ // TODO: verify bundles
+ // TODO: write to files
+ fmt.Printf("got %d bundles\n", len(bundles))
+ return nil
+}
+
+func cmdFormat(args []string, policy policy.Policy) error {
+ if len(args) != 1 {
+ return fmt.Errorf("format: need exactly one file")
+ }
+
+ checksum, err := fileHash(args[0])
+ if err != nil {
+ return fmt.Errorf("format: %v", err)
+ }
+ stm := types.Statement{
+ ShardHint: policy.ShardHint(),
+ Checksum: *checksum,
+ }
+
+ fmt.Printf("%s", stm.ToBinary())
+ return nil
+}
+
+func cmdNamespace(args []string, policy policy.Policy) error {
+ if len(args) != 0 {
+ return fmt.Errorf("namespace: got trailing arguments")
+ }
+
+ fmt.Printf("tree_leaf:v0:%d@sigsum.org", policy.ShardHint())
+ return nil
+}
+
+// TODO: don't read full file into memory at once
+func fileHash(path string) (*types.Hash, error) {
+ b, err := ioutil.ReadFile(path)
+ if err != nil {
+ return nil, fmt.Errorf("failed reading file %q", path)
+ }
+ return types.HashFn(b), nil
+}
diff --git a/cmd/sigsum/main.go b/cmd/sigsum/main.go
new file mode 100644
index 0000000..460eba3
--- /dev/null
+++ b/cmd/sigsum/main.go
@@ -0,0 +1,116 @@
+// package main provides a tool named `sigsum`.
+//
+// Build as follows:
+//
+// $ go build -ldflags="-X 'main.someVersion=git commit $(git rev-list -1 HEAD)'"
+//
+// Install as follows:
+//
+// $ go install -ldflags="-X 'main.someVersion=git commit $(git rev-list -1 HEAD)'"
+//
+package main
+
+import (
+ "flag"
+ "fmt"
+ "log"
+ "os"
+
+ "git.sigsum.org/sigsum-tool-go/pkg/policy"
+)
+
+const usage = `sigsum version %s
+
+Usage:
+ sigsum help
+ Output usage message.
+
+ sigsum verify -t TYPE -k PUBLIC_KEY FILE
+ Verify that a file's signed checksum is public and valid.
+ -t, --type Signature format (Available options: signify, minisign, ssh)
+ -k, --key Path to a public key.
+
+ sigsum bundle -t TYPE -k PUBLIC_KEY -d DOMAIN_HINT FILE...
+ Perform logging request(s) and write inclusion proof bundle(s).
+ -t, --type Signature format (Available options: signify, minisign, ssh)
+ -k, --key Path to a public key.
+ -d, --domain-hint Domain name that is aware of the public key.
+
+ sigsum format FILE
+ Output bytes to be Ed25519-signed.
+
+ sigsum namespace
+ Output namespace to be used in SSH signing context.
+
+Transparency log proofs and signatures must be located at $FILE.sigsum.v0.
+Signatures must be located at $FILE.{sig,minisig}, depending on -t TYPE.
+
+`
+
+var (
+ optBundleType, optBundleKey, optBundleDomainHint string
+ optVerifyType, optVerifyKey string
+
+ someVersion = "unknown"
+)
+
+func main() {
+ log.SetFlags(0)
+
+ var err error
+ var defaultPolicy policy.DefaultPolicy
+ switch cmd := parseCommand(); cmd.Name() {
+ case "help":
+ cmd.Usage()
+ case "verify":
+ err = cmdVerify(cmd.Args(), &defaultPolicy, optVerifyType, optVerifyKey)
+ case "bundle":
+ err = cmdBundle(cmd.Args(), &defaultPolicy, optBundleType, optBundleKey, optBundleDomainHint)
+ case "format":
+ err = cmdFormat(cmd.Args(), &defaultPolicy)
+ case "namespace":
+ err = cmdNamespace(cmd.Args(), &defaultPolicy)
+ default:
+ err = fmt.Errorf("invalid command %q, try %q", cmd.Name(), "sigsum help")
+ }
+
+ if err != nil {
+ log.Printf("%s", err)
+ os.Exit(1)
+ }
+}
+
+func parseCommand() (fs *flag.FlagSet) {
+ args := os.Args
+ if len(args) < 2 {
+ args = append(args, "")
+ }
+ defer func() {
+ registerOptions(fs)
+ fs.Usage = func() {
+ log.Printf(usage, someVersion)
+ }
+ fs.Parse(args)
+ }()
+
+ fs = flag.NewFlagSet(args[1], flag.ExitOnError)
+ args = args[2:]
+ return
+}
+
+func registerOptions(fs *flag.FlagSet) {
+ switch cmd := fs.Name(); cmd {
+ case "verify":
+ registerStringOption(fs, &optVerifyType, "t", "type", "")
+ registerStringOption(fs, &optVerifyKey, "k", "key", "")
+ case "bundle":
+ registerStringOption(fs, &optBundleType, "t", "type", "")
+ registerStringOption(fs, &optBundleKey, "k", "key", "")
+ registerStringOption(fs, &optBundleDomainHint, "d", "domain-hint", "")
+ }
+}
+
+func registerStringOption(fs *flag.FlagSet, opt *string, short, long, value string) {
+ fs.StringVar(opt, short, value, "")
+ fs.StringVar(opt, long, value, "")
+}
diff --git a/cmd/sigsum/test/keys/signify.pub b/cmd/sigsum/test/keys/signify.pub
new file mode 100644
index 0000000..742a66a
--- /dev/null
+++ b/cmd/sigsum/test/keys/signify.pub
@@ -0,0 +1,2 @@
+untrusted comment: signify public key
+RWQhuW/GnP7W13NSC8qzkpnB1BJXk96/GhaWe6f/OpBvMRHFdwuUIYGb
diff --git a/cmd/sigsum/test/keys/signify.sec b/cmd/sigsum/test/keys/signify.sec
new file mode 100644
index 0000000..57cdf84
--- /dev/null
+++ b/cmd/sigsum/test/keys/signify.sec
@@ -0,0 +1,2 @@
+untrusted comment: signify secret key
+RWRCSwAAACrUdp2uXyio8Rdwv0W6PLGiUQei6JeOZAYhuW/GnP7W1655JdycJo4tbOh/ba1OxA7QyVSdNFBs5SyF4eM5yIE98xhTAtizBDxki1Y3sqcFvWFH8ZlKzRjY8rUrTYaaCQE=
diff --git a/cmd/sigsum/test/signify.sh b/cmd/sigsum/test/signify.sh
new file mode 100755
index 0000000..8e86e8d
--- /dev/null
+++ b/cmd/sigsum/test/signify.sh
@@ -0,0 +1,55 @@
+#!/bin/bash
+
+set -e
+trap cleanup EXIT
+
+pass=1234
+priv=keys/signify.sec
+pub=keys/signify.pub
+domain_hint=_sigsum_v0.test-only.rgdd.se
+msg=msg-$(date +%s)
+num_msg=3
+
+function cleanup() {
+ set +e
+
+ rm -f sigsum
+ for i in $(seq 1 $num_msg); do
+ rm -f $msg-$i{,.trunnel,.sig}
+ done
+
+ exit
+}
+
+go build ../
+
+files=""
+for i in $(seq 1 $num_msg); do
+ echo $msg-$i > $msg-$i
+ if ! ./sigsum format $msg-$i > $msg-$i.trunnel; then
+ echo "[FAIL] format for $num_msg signify message(s)" >&2
+ exit 1
+ fi
+ if ! echo $pass | signify-openbsd -Ss $priv -m $msg-$i.trunnel -x $msg-$i.sig; then
+ echo "[FAIL] sign for $num_msg signify message(s)" >&2
+ exit 1
+ fi
+ files=$(echo -n $files $msg-$i)
+done
+
+echo "[PASS] format for $num_msg signify message(s)" >&2
+echo "[PASS] sign for $num_msg signify message(s)" >&2
+
+if ! ./sigsum bundle -t signify -k $pub -d $domain_hint $files; then
+ echo "[FAIL] bundle for $num_msg signify message(s)" >&2
+ exit 1
+fi
+
+echo "[PASS] bundle for $num_msg signify message(s)" >&2
+
+if ! ./sigsum verify -t signify -k $pub $files; then
+ echo "[FAIL] verify for $num_msg signify message(s)" >&2
+ exit 1
+fi
+
+echo "[PASS] verify for $num_msg signify message(s)" >&2
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..a7699f5
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,5 @@
+module git.sigsum.org/sigsum-tool-go
+
+go 1.15
+
+require git.sigsum.org/sigsum-lib-go v0.0.2
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..03ffc2d
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,2 @@
+git.sigsum.org/sigsum-lib-go v0.0.2 h1:1FwdnraPaasw1D1Lb+flRMJRGLTuZrp17AZ6tx+iT/0=
+git.sigsum.org/sigsum-lib-go v0.0.2/go.mod h1:DVmlcf0MBHy4IZdnZ5DcbsKkGEd0EkOAoLINhLgcndY=
diff --git a/pkg/client/api/api.go b/pkg/client/api/api.go
new file mode 100644
index 0000000..d773bcc
--- /dev/null
+++ b/pkg/client/api/api.go
@@ -0,0 +1,22 @@
+// package api implements the Sigsum v0 API. See
+//
+// https://git.sigsum.org/sigsum/tree/doc/api.md
+//
+// for further details. No verification, retries, etc., is done here.
+package api
+
+import (
+ "git.sigsum.org/sigsum-lib-go/pkg/requests"
+ "git.sigsum.org/sigsum-lib-go/pkg/types"
+)
+
+type API interface {
+ GetToCosignTreeHead() (*types.SignedTreeHead, error)
+ GetCosignedTreeHead() (*types.CosignedTreeHead, error)
+ GetInclusionProof(treeSize uint64, leafHash *types.Hash) (*types.InclusionProof, error)
+ GetConsistencyProof(oldSize, newSize uint64) (*types.ConsistencyProof, error)
+ GetLeaves(startSize, endSize uint64) (*types.Leaves, error)
+
+ AddLeaf(*requests.Leaf) error
+ AddCosignature(*requests.Cosignature) error
+}
diff --git a/pkg/client/api/network_client.go b/pkg/client/api/network_client.go
new file mode 100644
index 0000000..f7baf76
--- /dev/null
+++ b/pkg/client/api/network_client.go
@@ -0,0 +1,133 @@
+package api
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "log"
+ "net/http"
+
+ "git.sigsum.org/sigsum-lib-go/pkg/requests"
+ "git.sigsum.org/sigsum-lib-go/pkg/types"
+)
+
+type NetworkClient struct {
+ http.Client
+ LogURL string
+}
+
+func NewNetworkClient(httpClient http.Client, logURL string) *NetworkClient {
+ return &NetworkClient{
+ Client: httpClient,
+ LogURL: logURL,
+ }
+}
+
+func (c *NetworkClient) GetToCosignTreeHead() (*types.SignedTreeHead, error) {
+ return nil, fmt.Errorf("TODO")
+}
+
+func (c *NetworkClient) GetCosignedTreeHead() (*types.CosignedTreeHead, error) {
+ rsp, err := c.get(types.EndpointGetTreeHeadCosigned.Path(c.LogURL))
+ if err != nil {
+ return nil, fmt.Errorf("client: HTTP %d: %s", rsp.StatusCode, errorMessage(rsp.Body))
+ }
+
+ defer rsp.Body.Close()
+ if rsp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("client: HTTP %d: %s", rsp.StatusCode, errorMessage(rsp.Body))
+ }
+
+ var cth types.CosignedTreeHead
+ if err := cth.FromASCII(rsp.Body); err != nil {
+ return nil, fmt.Errorf("client: HTTP %d: %s", rsp.StatusCode, errorMessage(rsp.Body))
+ }
+ return &cth, nil
+}
+
+func (c *NetworkClient) GetInclusionProof(treeSize uint64, leafHash *types.Hash) (*types.InclusionProof, error) {
+ data := requests.InclusionProof{
+ TreeSize: treeSize,
+ LeafHash: *leafHash,
+ }
+ buf := bytes.NewBuffer(nil)
+ if err := data.ToASCII(buf); err != nil {
+ return nil, fmt.Errorf("client: invalid request data: %v", err)
+ }
+
+ rsp, err := c.post(types.EndpointGetInclusionProof.Path(c.LogURL), buf)
+ if err != nil {
+ return nil, fmt.Errorf("client: invalid proof request: %v", err)
+ }
+
+ defer rsp.Body.Close()
+ if rsp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("client: HTTP %d: %s", rsp.StatusCode, errorMessage(rsp.Body))
+ }
+ var proof types.InclusionProof
+ if err := proof.FromASCII(rsp.Body, treeSize); err != nil {
+ return nil, fmt.Errorf("client: failed parsing response: %v", err)
+ }
+
+ return &proof, nil
+}
+
+func (c *NetworkClient) GetConsistencyProof(oldSize, newSize uint64) (*types.ConsistencyProof, error) {
+ return nil, fmt.Errorf("TODO")
+}
+
+func (c *NetworkClient) GetLeaves(startSize, endSize uint64) (*types.Leaves, error) {
+ return nil, fmt.Errorf("TODO")
+}
+
+func (c *NetworkClient) AddLeaf(leaf *requests.Leaf) error {
+ buf := bytes.NewBuffer(nil)
+ if err := leaf.ToASCII(buf); err != nil {
+ return fmt.Errorf("client: invalid request data: %v", err)
+ }
+
+ rsp, err := c.post(types.EndpointAddLeaf.Path(c.LogURL), buf)
+ if err != nil {
+ return fmt.Errorf("client: HTTP %d: %s", rsp.StatusCode, errorMessage(rsp.Body))
+ }
+
+ defer rsp.Body.Close()
+ if rsp.StatusCode != http.StatusOK {
+ return fmt.Errorf("client: HTTP %d: %s", rsp.StatusCode, errorMessage(rsp.Body))
+ }
+ return nil
+}
+
+func (c *NetworkClient) AddCosignature(*requests.Cosignature) error {
+ return fmt.Errorf("TODO")
+}
+
+func (c *NetworkClient) get(url string) (*http.Response, error) {
+ log.Printf("GET %s", url)
+ return c.Get(url)
+}
+
+func (c *NetworkClient) post(url string, buf *bytes.Buffer) (*http.Response, error) {
+ req, err := http.NewRequest("POST", url, buf)
+ if err != nil {
+ return nil, fmt.Errorf("client: invalid leaf request: %v", err)
+ }
+
+ log.Printf("POST %s", url)
+ return c.Do(req)
+}
+
+func errorMessage(r io.Reader) string {
+ b, _ := ioutil.ReadAll(r)
+ return string(b)
+
+ // TODO: the below doesn't work because log error messages are malformed
+ //msg := struct {
+ // Message string `ascii:"Error"`
+ //}{}
+ //if err := ascii.StdEncoding.Deserialize(r, &msg); err != nil {
+ // return fmt.Sprintf("malformed error message: %v", err)
+ //}
+ //return msg.Message
+}
diff --git a/pkg/client/submitter.go b/pkg/client/submitter.go
new file mode 100644
index 0000000..f03e66a
--- /dev/null
+++ b/pkg/client/submitter.go
@@ -0,0 +1,129 @@
+package client
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "time"
+
+ "git.sigsum.org/sigsum-lib-go/pkg/requests"
+ "git.sigsum.org/sigsum-lib-go/pkg/types"
+ "git.sigsum.org/sigsum-tool-go/pkg/client/api"
+ "git.sigsum.org/sigsum-tool-go/pkg/policy"
+)
+
+type Submitter interface {
+ AddLeaves(context.Context, []requests.Leaf) ([]Bundle, error)
+}
+
+type SubmitClient struct {
+ api api.API
+ policy policy.Policy
+}
+
+// TODO: should signature+key_hash (not) be in bundle?
+type Bundle struct {
+ ShardHint uint64
+ Signature types.Signature
+ KeyHash types.Hash
+ InclusionProof types.InclusionProof
+ CosignedTreeHead types.CosignedTreeHead
+}
+
+func NewSubmitClient(policy policy.Policy) *SubmitClient {
+ return &SubmitClient{
+ policy: policy,
+ }
+}
+
+// TODO: feedback on the below sketch; improve it; implement properly.
+// 0. Select one log in policy, setup API network client.
+// 1. Loop over all leaves that have yet to received 200 OK. Move on to
+// the next leaf on a non-200 status code, trying again next itteration.
+// 3. Try to fetch an inclusion proof for the latest HTTP 200 OK
+// response every time a new cosigned tree head becomes available.
+// Output warning if more than two cosigned tree heads pass.
+// 4. Loop over all leaves and fetch inclusion proofs for X.
+// [Exit with error if any inclusion proof is not available / invalid]
+// 5. Return bundles in the same order as leaves were passed.
+func (sc *SubmitClient) AddLeaves(_ context.Context, leaves []requests.Leaf) ([]Bundle, error) {
+ if err := sc.newAPI(); err != nil {
+ return nil, fmt.Errorf("client: %v", err)
+ }
+
+ for _, leaf := range leaves {
+ err := sc.api.AddLeaf(&leaf)
+ if err != nil {
+ return nil, fmt.Errorf("client: %v", err)
+ }
+ }
+
+ start := time.Now().Unix()
+ var bundles []Bundle
+ for {
+ cth, err := sc.api.GetCosignedTreeHead()
+ if err != nil {
+ return nil, fmt.Errorf("client: %v", err)
+ }
+ // TODO: verify that cth is valid for policy
+
+ ok := true
+ bundles = nil
+ for _, leaf := range leaves {
+ l := types.Leaf{
+ Statement: types.Statement{
+ ShardHint: leaf.ShardHint,
+ Checksum: leaf.Checksum,
+ },
+ Signature: leaf.Signature,
+ KeyHash: *types.HashFn(leaf.VerificationKey[:]),
+ }
+ lh := types.HashFn(append([]byte{0x00}, l.ToBinary()...))
+ log.Printf("leaf hash is: %x", lh[:])
+
+ proof, err := sc.api.GetInclusionProof(cth.TreeSize, lh)
+ if err != nil {
+ log.Printf("no inclusion proof for tree size %d yet, please wait.\n", cth.TreeSize)
+ ok = false
+ break
+ }
+
+ // TODO: verify that inclusion proof is valid
+ bundles = append(bundles, Bundle{
+ ShardHint: l.ShardHint,
+ Signature: l.Signature,
+ KeyHash: l.KeyHash,
+ InclusionProof: *proof,
+ CosignedTreeHead: *cth,
+ })
+ }
+ if ok {
+ break
+ }
+
+ time.Sleep(15 * time.Second)
+ log.Printf("waited %d seconds...\n", time.Now().Unix()-start)
+ }
+
+ return bundles, nil
+}
+
+func (sc *SubmitClient) newAPI() error {
+ // TODO: select a log properly.
+ sc.api = api.NewNetworkClient(http.Client{}, sc.policy.Logs()[0].LogURL)
+ return nil
+}
+
+func (b *Bundle) ToASCII(w io.Writer) error {
+ return fmt.Errorf("TODO")
+}
+
+func (b *Bundle) FromASCII(r io.Reader) error {
+ return fmt.Errorf("TODO")
+}
+
+func (b *Bundle) Verify(k *types.PublicKey, p *policy.Policy, r io.Reader) error {
+ return fmt.Errorf("TODO")
+}
diff --git a/pkg/policy/default.go b/pkg/policy/default.go
new file mode 100644
index 0000000..6f35fa4
--- /dev/null
+++ b/pkg/policy/default.go
@@ -0,0 +1,66 @@
+package policy
+
+import (
+ "fmt"
+
+ "git.sigsum.org/sigsum-lib-go/pkg/types"
+)
+
+type DefaultPolicy struct{}
+
+// Verify checks if the cosigned tree head satisifies the policy "one log and
+// a majority of witnesses". Any unverifiable cosignatures are removed.
+func (dp *DefaultPolicy) Verify(*types.CosignedTreeHead) error {
+ return fmt.Errorf("TODO")
+}
+
+func (dp *DefaultPolicy) Logs() []Log {
+ return []Log{
+ Log{
+ Name: "Test log operated by ln5",
+ LogURL: "https://poc.sigsum.org/glasswing-butterfly/sigsum/v0",
+ PublicKey: types.PublicKey{
+ 74, 179, 210, 181,
+ 30, 71, 220, 157,
+ 228, 204, 228, 189,
+ 235, 169, 224, 134,
+ 34, 233, 58, 47,
+ 127, 137, 180, 203,
+ 119, 25, 127, 198,
+ 176, 4, 77, 231,
+ },
+ ShardStart: 1639579652,
+ TrustUntil: 1648771200, // Apr 1 2022
+ },
+ }
+}
+
+func (dp *DefaultPolicy) Witnesses() []Witness {
+ return []Witness{
+ Witness{
+ Name: "Test witness operated by rgdd",
+ PublicKey: types.PublicKey{
+ 129, 45, 190, 240,
+ 21, 107, 7, 158,
+ 45, 4, 135, 71,
+ 178, 24, 156, 191,
+ 166, 79, 150, 226,
+ 32, 74, 23, 203,
+ 35, 203, 82, 128,
+ 128, 135, 21, 3,
+ },
+ TrustUntil: 1648771200, // Apr 1 2022
+ },
+ }
+}
+
+// ShardHint returns the smallest shard hint that all logs accept
+func (dp *DefaultPolicy) ShardHint() uint64 {
+ var shardHint uint64
+ for _, l := range dp.Logs() {
+ if l.ShardStart > shardHint {
+ shardHint = l.ShardStart
+ }
+ }
+ return shardHint
+}
diff --git a/pkg/policy/policy.go b/pkg/policy/policy.go
new file mode 100644
index 0000000..b4fe920
--- /dev/null
+++ b/pkg/policy/policy.go
@@ -0,0 +1,32 @@
+package policy
+
+import (
+ "git.sigsum.org/sigsum-lib-go/pkg/types"
+)
+
+type Policy interface {
+ // Logs returns the list of known logs. It is needed so that an
+ // appropriate log can be picked when submitting a signed checksum.
+ Logs() []Log
+
+ // ShardHint returns a recommended shard hint.
+ ShardHint() uint64
+
+ // Verify checks if a cosigned tree head is trustworthy. On success,
+ // any invalid or unknown cosignatures may be removed.
+ Verify(*types.CosignedTreeHead) error
+}
+
+type Log struct {
+ Name string
+ LogURL string
+ PublicKey types.PublicKey
+ ShardStart uint64
+ TrustUntil uint64
+}
+
+type Witness struct {
+ Name string
+ PublicKey types.PublicKey
+ TrustUntil uint64
+}
diff --git a/pkg/signatures/minisign/minisign.go b/pkg/signatures/minisign/minisign.go
new file mode 100644
index 0000000..8d8a9c4
--- /dev/null
+++ b/pkg/signatures/minisign/minisign.go
@@ -0,0 +1,22 @@
+package minisign
+
+import (
+ "fmt"
+ "io"
+
+ "git.sigsum.org/sigsum-lib-go/pkg/types"
+)
+
+type Parser struct{}
+
+func (p *Parser) SignatureSuffix() string {
+ return ".minisig"
+}
+
+func (p *Parser) PublicKey(r io.Reader) (*types.PublicKey, error) {
+ return nil, fmt.Errorf("TODO")
+}
+
+func (p *Parser) Signature(r io.Reader) (*types.Signature, error) {
+ return nil, fmt.Errorf("TODO")
+}
diff --git a/pkg/signatures/signatures.go b/pkg/signatures/signatures.go
new file mode 100644
index 0000000..184c8f3
--- /dev/null
+++ b/pkg/signatures/signatures.go
@@ -0,0 +1,13 @@
+package signatures
+
+import (
+ "io"
+
+ "git.sigsum.org/sigsum-lib-go/pkg/types"
+)
+
+type Parser interface {
+ PublicKey(io.Reader) (*types.PublicKey, error)
+ Signature(io.Reader) (*types.Signature, error)
+ SignatureSuffix() string
+}
diff --git a/pkg/signatures/signify/signify.go b/pkg/signatures/signify/signify.go
new file mode 100644
index 0000000..6dbbf07
--- /dev/null
+++ b/pkg/signatures/signify/signify.go
@@ -0,0 +1,91 @@
+package signify
+
+import (
+ "bytes"
+ "encoding/base64"
+ "fmt"
+ "io"
+ "io/ioutil"
+
+ "git.sigsum.org/sigsum-lib-go/pkg/types"
+)
+
+type Parser struct{}
+
+func (p *Parser) SignatureSuffix() string {
+ return ".sig"
+}
+
+func (p *Parser) PublicKey(r io.Reader) (*types.PublicKey, error) {
+ b, err := ioutil.ReadAll(r)
+ if err != nil {
+ return nil, fmt.Errorf("signify: read failed: %v", err)
+ }
+ var pub types.PublicKey
+ if err := parse(pub[:], b); err != nil {
+ return nil, fmt.Errorf("signify: %v", err)
+ }
+ return &pub, nil
+}
+
+func (p *Parser) Signature(r io.Reader) (*types.Signature, error) {
+ b, err := ioutil.ReadAll(r)
+ if err != nil {
+ return nil, fmt.Errorf("signify: read failed: %v", err)
+ }
+ var sig types.Signature
+ if err := parse(sig[:], b); err != nil {
+ return nil, fmt.Errorf("signify: %v", err)
+ }
+ return &sig, nil
+}
+
+func parse(dst, lines []byte) error {
+ commentLine, dataLine, err := parseLines(lines)
+ if err != nil {
+ return fmt.Errorf("invalid: %v", err)
+ }
+ if err := parseCommentLine(commentLine); err != nil {
+ return fmt.Errorf("invalid: %v", err)
+ }
+ if err := parseDataLine(dst, dataLine); err != nil {
+ return fmt.Errorf("invalid: %v", err)
+ }
+ return nil
+}
+
+func parseLines(lines []byte) ([]byte, []byte, error) {
+ split := bytes.Split(lines, []byte("\n"))
+ if len(split) != 3 {
+ return nil, nil, fmt.Errorf("number of lines")
+ }
+ return split[0], split[1], nil
+}
+
+func parseCommentLine(line []byte) error {
+ if !bytes.HasPrefix(line, []byte("untrusted comment: ")) {
+ return fmt.Errorf("no untrusted comment")
+ }
+ return nil
+}
+
+func parseDataLine(dst, line []byte) error {
+ data, err := base64.StdEncoding.DecodeString(string(line))
+ if err != nil {
+ return fmt.Errorf("base64 encoding")
+ }
+ if len(data) < 2 || !bytes.Equal(data[:2], []byte("Ed")) {
+ return fmt.Errorf("algorithm")
+ }
+ data = data[2:]
+ if len(data) < 8 {
+ return fmt.Errorf("random fingerprint")
+ }
+ data = data[8:]
+ if len(data) != len(dst) {
+ return fmt.Errorf("data length")
+ }
+
+ copy(dst[:], data)
+ return nil
+}
diff --git a/pkg/signatures/ssh/ssh.go b/pkg/signatures/ssh/ssh.go
new file mode 100644
index 0000000..d5fb8f4
--- /dev/null
+++ b/pkg/signatures/ssh/ssh.go
@@ -0,0 +1,22 @@
+package ssh
+
+import (
+ "fmt"
+ "io"
+
+ "git.sigsum.org/sigsum-lib-go/pkg/types"
+)
+
+type Parser struct{}
+
+func (p *Parser) SignatureSuffix() string {
+ return ".sig"
+}
+
+func (p *Parser) PublicKey(r io.Reader) (*types.PublicKey, error) {
+ return nil, fmt.Errorf("TODO")
+}
+
+func (p *Parser) Signature(r io.Reader) (*types.Signature, error) {
+ return nil, fmt.Errorf("TODO")
+}