From 8da382069f42f6d88d3abf914dd38d7e40a845bc Mon Sep 17 00:00:00 2001 From: Rasmus Dahlberg Date: Wed, 2 Mar 2022 23:16:43 +0100 Subject: initial commit --- pkg/client/api/api.go | 22 ++++++ pkg/client/api/network_client.go | 133 ++++++++++++++++++++++++++++++++++++ pkg/client/submitter.go | 129 ++++++++++++++++++++++++++++++++++ pkg/policy/default.go | 66 ++++++++++++++++++ pkg/policy/policy.go | 32 +++++++++ pkg/signatures/minisign/minisign.go | 22 ++++++ pkg/signatures/signatures.go | 13 ++++ pkg/signatures/signify/signify.go | 91 ++++++++++++++++++++++++ pkg/signatures/ssh/ssh.go | 22 ++++++ 9 files changed, 530 insertions(+) create mode 100644 pkg/client/api/api.go create mode 100644 pkg/client/api/network_client.go create mode 100644 pkg/client/submitter.go create mode 100644 pkg/policy/default.go create mode 100644 pkg/policy/policy.go create mode 100644 pkg/signatures/minisign/minisign.go create mode 100644 pkg/signatures/signatures.go create mode 100644 pkg/signatures/signify/signify.go create mode 100644 pkg/signatures/ssh/ssh.go (limited to 'pkg') 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") +} -- cgit v1.2.3