diff options
author | Rasmus Dahlberg <rasmus@mullvad.net> | 2022-04-25 00:43:06 +0200 |
---|---|---|
committer | Rasmus Dahlberg <rasmus@mullvad.net> | 2022-04-25 00:43:06 +0200 |
commit | 528a53f7f76f08af5902f4cfa8235380b3434ba0 (patch) | |
tree | 662b7834d5ce15627554e9307a4e00f7364fba11 /pkg/types/types.go | |
parent | 4fc0ff2ec2f48519ee245d6d7edee1921cb3b8bc (diff) |
drafty types refactor with simple ascii packagergdd/sketch
types.go compiles but that is about it, here be dragons. Pushing so
that we can get an idea of what this refactor would roughly look like.
Diffstat (limited to 'pkg/types/types.go')
-rw-r--r-- | pkg/types/types.go | 590 |
1 files changed, 590 insertions, 0 deletions
diff --git a/pkg/types/types.go b/pkg/types/types.go new file mode 100644 index 0000000..238e48f --- /dev/null +++ b/pkg/types/types.go @@ -0,0 +1,590 @@ +package types + +import ( + "bytes" + "crypto" + "crypto/ed25519" + "crypto/sha256" + "fmt" + "io" + "strings" + + "git.sigsum.org/sigsum-go/pkg/hex" + "git.sigsum.org/sigsum-go/pkg/types/ascii" + "git.sigsum.org/sigsum-go/pkg/types/binary/ssh" + "git.sigsum.org/sigsum-go/pkg/types/binary/trunnel" +) + +// Hash is a SHA256 hash, see §XXX: +// +// u8 Hash[32]; +// +type Hash [HashSize]byte + +const HashSize = 32 + +func HashFn(b []byte) Hash { + return sha256.Sum256(b) +} + +// Signature is an Ed25519 signature, see §XXX: +// +// u8 Signature[64]; +// +type Signature [SignatureSize]byte + +const SignatureSize = 64 + +// PublicKey is an Ed25519 public key, see §XXX: +// +// u8 public_key[32]; +// +type PublicKey [PublicKeySize]byte + +const PublicKeySize = 32 + +func (k *PublicKey) Verify(msg []byte, sig Signature) error { + if !ed25519.Verify(ed25519.PublicKey(k[:]), msg, sig[:]) { + return fmt.Errorf("invalid ed25519 signature") + } + return nil +} + +// PrivateKey provides access to the private part of an Ed25519 key-pair +type PrivateKey struct { + crypto.Signer +} + +func (k *PrivateKey) Sign(message []byte) (s Signature, err error) { + sig, err := k.Signer.Sign(nil, message, crypto.Hash(0)) + if err != nil { + return s, fmt.Errorf("sign: %w", err) + } + if n := len(sig); n != SignatureSize { + return s, fmt.Errorf("invalid signature size %d", n) + } + copy(s[:], sig) + return s, nil +} + +// TreeHead is a Merkle tree head, see §2.3.1: +// +// struct tree_head { +// u64 timestamp; +// u64 tree_size; +// hash root_hash; +// }; +// +type TreeHead struct { + Timestamp uint64 + TreeSize uint64 + RootHash Hash +} + +func (th *TreeHead) ToTrunnel() []byte { + buf := bytes.NewBuffer(nil) + + trunnel.AddUint64(buf, th.Timestamp) + trunnel.AddUint64(buf, th.TreeSize) + buf.Write(th.RootHash[:]) + + return buf.Bytes() +} + +// ToSSH serialization is defined in §2.3.2 +func (th *TreeHead) ToSSH(keyHash Hash) []byte { + namespace := fmt.Sprintf("tree_head:v0:%s@sigsum.org", hex.Serialize(keyHash[:])) + return ssh.ToSignBlob(namespace, th.ToTrunnel()) +} + +func (th *TreeHead) Sign(k PrivateKey, logKeyHash Hash) (Signature, error) { + return k.Sign(th.ToSSH(logKeyHash)) +} + +func (th *TreeHead) Verify(k PublicKey, logKeyHash Hash, sig Signature) error { + return k.Verify(th.ToSSH(logKeyHash), sig) +} + +// Checksum is a checksum, see §XXX: +// +// hash checksum; +// +type Checksum Hash + +// ToSSH serialization is defined in §2.3.3 +func (c *Checksum) ToSSH(shardHint uint64) []byte { + namespace := fmt.Sprintf("tree_leaf:v0:%d@sigsum.org", shardHint) + return ssh.ToSignBlob(namespace, c[:]) +} + +func (c *Checksum) Sign(k PrivateKey, shardHint uint64) (Signature, error) { + return k.Sign(c.ToSSH(shardHint)) +} + +func (c *Checksum) Verify(k PublicKey, shardHint uint64, sig Signature) error { + return k.Verify(c.ToSSH(shardHint), sig) +} + +// TreeLeaf is a Merkle tree leaf, see §2.3.3: +// +// struct tree_leaf { +// u64 shard_hint; +// checksum checksum; +// signature signature; +// hash key_hash; +// }; +// +type TreeLeaf struct { + ShardHint uint64 + Checksum Checksum + Signature Signature + KeyHash Hash +} + +func (tl *TreeLeaf) ToTrunnel() []byte { + buf := bytes.NewBuffer(nil) + + trunnel.AddUint64(buf, tl.ShardHint) + buf.Write(tl.Checksum[:]) + buf.Write(tl.Signature[:]) + buf.Write(tl.KeyHash[:]) + + return buf.Bytes() +} + +func (tl *TreeLeaf) FromTrunnel(buf *bytes.Buffer) error { + if err := trunnel.Uint64(buf, &tl.ShardHint); err != nil { + return fmt.Errorf("tree_leaf.shard_hint: %w", err) + } + if err := trunnel.Array(buf, tl.Checksum[:]); err != nil { + return fmt.Errorf("tree_leaf.checksum: %w", err) + } + if err := trunnel.Array(buf, tl.Signature[:]); err != nil { + return fmt.Errorf("tree_leaf.signature: %w", err) + } + if err := trunnel.Array(buf, tl.KeyHash[:]); err != nil { + return fmt.Errorf("tree_leaf.key_hash: %w", err) + } + if rest, err := io.ReadAll(buf); err != nil || len(rest) != 0 { + return fmt.Errorf("invalid remainder: rest is %x and err %v", rest, err) + } + return nil +} + +// Endpoint is named log endpoint, see §3.1 - §3.7 +type Endpoint string + +const ( + EndpointAddLeaf = Endpoint("add-leaf") + EndpointAddCosignature = Endpoint("add-cosignature") + EndpointGetTreeHeadToCosign = Endpoint("get-tree-head-to-sign") + EndpointGetTreeHeadCosigned = Endpoint("get-tree-head-cosigned") + EndpointGetInclusionProof = Endpoint("get-inclusion-proof") + EndpointGetConsistencyProof = Endpoint("get-consistency-proof") + EndpointGetLeaves = Endpoint("get-leaves") +) + +// Path returns a complete endpoint URL for a given log URL. The format of a +// log's URL is defined in §3, e.g., "https://log.example.com/sigsum/v0". +func (e Endpoint) URL(logURL string) string { + return logURL + "/" + string(e) +} + +const ( + asciiError = "error" // XXX: update s/E/e in api.md + asciiTimestamp = "timestamp" + asciiTreeSize = "tree_size" + asciiRootHash = "root_hash" + asciiSignature = "signature" + asciiCosignature = "cosignature" + asciiKeyHash = "key_hash" + asciiLeafIndex = "leaf_index" + asciiInclusionPath = "inclusion_path" + asciiConsistencyPath = "consistency_path" + asciiShardHint = "shard_hint" + asciiChecksum = "checksum" + asciiMessage = "message" // XXX: update s/preimage/message in api.md + asciiPublicKey = "public_key" // XXX: update s/verification_key/public_key in api.md + asciiDomainHint = "domain_hint" +) + +// Error is an error mesage, see §3 +type Error string + +func (e *Error) ToASCII(w io.Writer) error { + if strings.Contains(string(*e), ascii.EndOfValue) { + return fmt.Errorf("string contains end-of-value pattern") // XXX: in ascii package instead? + } + if err := ascii.WritePair(w, asciiError, string(*e)); err != nil { + fmt.Errorf("%s: %w", asciiError, err) + } + return nil +} + +func (e *Error) FromASCII(r io.Reader) error { + return ascii.ReadPairs(r, func(m *ascii.Map) error { + if err := m.DequeueString(asciiError, (*string)(e)); err != nil { + return fmt.Errorf("%s: %w", asciiError, err) + } + return nil + }) +} + +// SignedTreeHead is the output of get-tree-head-to-cosign, see §3.1 +type SignedTreeHead struct { + TreeHead + Signature Signature +} + +func (sth *SignedTreeHead) ToASCII(w io.Writer) error { + if err := ascii.WritePair(w, asciiTimestamp, fmt.Sprintf("%d", sth.Timestamp)); err != nil { + return fmt.Errorf("%s: %w", asciiTimestamp, err) + } + if err := ascii.WritePair(w, asciiTreeSize, fmt.Sprintf("%d", sth.TreeSize)); err != nil { + return fmt.Errorf("%s: %w", asciiTreeSize, err) + } + if err := ascii.WritePair(w, asciiRootHash, hex.Serialize(sth.RootHash[:])); err != nil { + return fmt.Errorf("%s: %w", asciiRootHash, err) + } + if err := ascii.WritePair(w, asciiSignature, hex.Serialize(sth.Signature[:])); err != nil { + return fmt.Errorf("%s: %w", asciiSignature, err) + } + return nil +} + +func (sth *SignedTreeHead) FromASCII(r io.Reader) error { + return ascii.ReadPairs(r, func(m *ascii.Map) (err error) { + *sth, err = sthFromASCII(m) + return err + }) +} + +// CosignedTreeHead is the output of get-tree-head-cosigned, see §3.2 +type CosignedTreeHead struct { + SignedTreeHead + Cosignatures []Cosignature +} + +func (cth *CosignedTreeHead) ToASCII(w io.Writer) error { + if len(cth.Cosignatures) == 0 { + return fmt.Errorf("no cosignatures") + } + + for i, c := range cth.Cosignatures { + if err := c.ToASCII(w); err != nil { + return fmt.Errorf("%d: %w", i+1, err) + } + } + return cth.SignedTreeHead.ToASCII(w) +} + +func (cth *CosignedTreeHead) FromASCII(r io.Reader) error { + return ascii.ReadPairs(r, func(m *ascii.Map) (err error) { + n := m.NumValues(asciiCosignature) + if n == 0 { + return fmt.Errorf("no cosignatures") + } + + cth.Cosignatures = make([]Cosignature, 0, n) + for i := uint64(0); i < n; i++ { + c, err := cosignatureFromASCII(m) + if err != nil { + return fmt.Errorf("%d: %w", i+1, err) + } + cth.Cosignatures = append(cth.Cosignatures, c) + } + cth.SignedTreeHead, err = sthFromASCII(m) + return err + }) +} + +type Cosignature struct { + KeyHash Hash + Signature Signature +} + +func (c *Cosignature) ToASCII(w io.Writer) error { + if err := ascii.WritePair(w, asciiKeyHash, hex.Serialize(c.KeyHash[:])); err != nil { + return fmt.Errorf("%s: %w", asciiKeyHash, err) + } + if err := ascii.WritePair(w, asciiCosignature, hex.Serialize(c.Signature[:])); err != nil { + return fmt.Errorf("%s: %w", asciiCosignature, err) + } + return nil +} + +func (c *Cosignature) FromASCII(r io.Reader) error { + return ascii.ReadPairs(r, func(m *ascii.Map) (err error) { + *c, err = cosignatureFromASCII(m) + return err + }) +} + +// InclusionProof is the output of get-inclusion-proof, see §3.3 +type InclusionProof struct { + LeafIndex uint64 + InclusionPath []Hash +} + +func (p *InclusionProof) ToASCII(w io.Writer) error { + if len(p.InclusionPath) == 0 { + return fmt.Errorf("no inclusion path") + } + + for i, h := range p.InclusionPath { + if err := ascii.WritePair(w, asciiInclusionPath, hex.Serialize(h[:])); err != nil { + return fmt.Errorf("%d: %s: %w", i+1, asciiInclusionPath, err) + } + } + if err := ascii.WritePair(w, asciiLeafIndex, fmt.Sprintf("%d", p.LeafIndex)); err != nil { + return fmt.Errorf("%s: %w", asciiLeafIndex, err) + } + return nil +} + +func (p *InclusionProof) FromASCII(r io.Reader) error { + return ascii.ReadPairs(r, func(m *ascii.Map) error { + n := m.NumValues(asciiInclusionPath) + if n == 0 { + return fmt.Errorf("no inclusion path") + } + + p.InclusionPath = make([]Hash, 0, n) + for i := uint64(0); i < n; i++ { + var h Hash + if err := m.DequeueArray(asciiInclusionPath, h[:]); err != nil { + return fmt.Errorf("%d: %s: %w", i+1, asciiInclusionPath, err) + } + p.InclusionPath = append(p.InclusionPath, h) + } + if err := m.DequeueUint64(asciiLeafIndex, &p.LeafIndex); err != nil { + return fmt.Errorf("%s: %w", asciiLeafIndex, err) + } + return nil + }) +} + +// ConsistencyProof is the output of get-consistency proof, see §3.4 +type ConsistencyProof struct { + ConsistencyPath []Hash +} + +func (p *ConsistencyProof) ToASCII(w io.Writer) error { + if len(p.ConsistencyPath) == 0 { + return fmt.Errorf("no consistency path") + } + + for i, h := range p.ConsistencyPath { + if err := ascii.WritePair(w, asciiConsistencyPath, hex.Serialize(h[:])); err != nil { + return fmt.Errorf("%d: %s: %w", i+1, asciiConsistencyPath, err) + } + } + return nil +} + +func (p *ConsistencyProof) FromASCII(r io.Reader) error { + return ascii.ReadPairs(r, func(m *ascii.Map) error { + n := m.NumValues(asciiConsistencyPath) + if n == 0 { + return fmt.Errorf("no inclusion path") + } + + p.ConsistencyPath = make([]Hash, 0, n) + for i := uint64(0); i < n; i++ { + var h Hash + if err := m.DequeueArray(asciiConsistencyPath, h[:]); err != nil { + return fmt.Errorf("%d: %s: %w", i+1, asciiConsistencyPath, err) + } + p.ConsistencyPath = append(p.ConsistencyPath, h) + } + return nil + }) +} + +// Leaves is the output of get-leaves, see §3.5 +type Leaves []TreeLeaf + +func (l *Leaves) ToASCII(w io.Writer) error { + if len(*l) == 0 { + return fmt.Errorf("no leaves") + } + + for i, leaf := range *l { + if err := leaf.ToASCII(w); err != nil { + return fmt.Errorf("%d: %w", i+1, err) + } + } + return nil +} + +func (l *Leaves) FromASCII(r io.Reader) error { + return ascii.ReadPairs(r, func(m *ascii.Map) error { + n := m.NumValues(asciiShardHint) + if n == 0 { + return fmt.Errorf("no leaves") + } + + *l = make([]TreeLeaf, 0, n) + for i := uint64(0); i < n; i++ { + var leaf TreeLeaf + if err := m.DequeueUint64(asciiShardHint, &leaf.ShardHint); err != nil { + return fmt.Errorf("%s: %w", asciiShardHint, err) + } + if err := m.DequeueArray(asciiChecksum, leaf.Checksum[:]); err != nil { + return fmt.Errorf("%s: %w", asciiChecksum, err) + } + if err := m.DequeueArray(asciiSignature, leaf.Signature[:]); err != nil { + return fmt.Errorf("%s: %w", asciiSignature, err) + } + if err := m.DequeueArray(asciiKeyHash, leaf.KeyHash[:]); err != nil { + return fmt.Errorf("%s: %w", asciiKeyHash, err) + } + *l = append(*l, leaf) + } + return nil + }) +} + +func (l *TreeLeaf) ToASCII(w io.Writer) error { + if err := ascii.WritePair(w, asciiShardHint, fmt.Sprintf("%d", l.ShardHint)); err != nil { + return fmt.Errorf("%s: %w", asciiShardHint, err) + } + if err := ascii.WritePair(w, asciiChecksum, hex.Serialize(l.Checksum[:])); err != nil { + return fmt.Errorf("%s: %w", asciiChecksum, err) + } + if err := ascii.WritePair(w, asciiSignature, hex.Serialize(l.Signature[:])); err != nil { + return fmt.Errorf("%s: %w", asciiSignature, err) + } + if err := ascii.WritePair(w, asciiKeyHash, hex.Serialize(l.KeyHash[:])); err != nil { + return fmt.Errorf("%s: %w", asciiKeyHash, err) + } + return nil +} + +// RequestInclusionProof is the input of get-inclusion-proof, see §3.3 +type RequestInclusionProof struct { + TreeSize uint64 + LeafHash Hash +} + +func (req *RequestInclusionProof) ToURL(logURL string) string { + return "TODO" +} + +func (req *RequestInclusionProof) FromURL(url string) error { + return nil // TODO +} + +// RequestConsistencyProof is the input of get-consistency-proof, see §3.4 +type RequestConsistencyProof struct { + OldSize uint64 + NewSize uint64 +} + +func (req *RequestConsistencyProof) ToURL(logURL string) string { + return "TODO" +} + +func (req *RequestConsistencyProof) FromURL(url string) error { + return nil // TODO +} + +// RequestLeaves is the input of a get-leaves, see §3.5 +type RequestLeaves struct { + OldSize uint64 + NewSize uint64 +} + +func (req *RequestLeaves) ToURL(logURL string) string { + return "TODO" +} + +func (req *RequestLeaves) FromURL(url string) error { + return nil // TODO +} + +// RequestLeaf is the input of add-leaf, see §3.6 +type RequestLeaf struct { + ShardHint uint64 + Message Hash + Signature Signature + PublicKey PublicKey + DomainHint string +} + +func (req *RequestLeaf) ToASCII(w io.Writer) error { + if err := ascii.WritePair(w, asciiShardHint, fmt.Sprintf("%d", req.ShardHint)); err != nil { + return fmt.Errorf("%s: %w", asciiShardHint, err) + } + if err := ascii.WritePair(w, asciiMessage, hex.Serialize(req.Message[:])); err != nil { + return fmt.Errorf("%s: %w", asciiMessage, err) + } + if err := ascii.WritePair(w, asciiSignature, hex.Serialize(req.Signature[:])); err != nil { + return fmt.Errorf("%s: %w", asciiSignature, err) + } + if err := ascii.WritePair(w, asciiPublicKey, hex.Serialize(req.PublicKey[:])); err != nil { + return fmt.Errorf("%s: %w", asciiPublicKey, err) + } + if err := ascii.WritePair(w, asciiDomainHint, req.DomainHint); err != nil { + return fmt.Errorf("%s: %w", asciiDomainHint, err) + } + return nil +} + +func (req *RequestLeaf) FromASCII(r io.Reader) error { + return ascii.ReadPairs(r, func(m *ascii.Map) (err error) { + if err := m.DequeueUint64(asciiShardHint, &req.ShardHint); err != nil { + return fmt.Errorf("%s: %w", asciiShardHint, err) + } + if err := m.DequeueArray(asciiMessage, req.Message[:]); err != nil { + return fmt.Errorf("%s: %w", asciiMessage, err) + } + if err := m.DequeueArray(asciiSignature, req.Signature[:]); err != nil { + return fmt.Errorf("%s: %w", asciiSignature, err) + } + if err := m.DequeueArray(asciiPublicKey, req.PublicKey[:]); err != nil { + return fmt.Errorf("%s: %w", asciiPublicKey, err) + } + if err := m.DequeueString(asciiDomainHint, &req.DomainHint); err != nil { + return fmt.Errorf("%s: %w", asciiDomainHint, err) + } + return nil + }) +} + +// RequestCosignature is the input of add-cosignature, see §3.7 +type RequestCosignature Cosignature + +func (req *RequestCosignature) ToASCII(w io.Writer) error { + return (*Cosignature)(req).ToASCII(w) +} + +func (req *RequestCosignature) FromASCII(r io.Reader) error { + return (*Cosignature)(req).FromASCII(r) +} + +func sthFromASCII(m *ascii.Map) (sth SignedTreeHead, err error) { + if m.DequeueUint64(asciiTimestamp, &sth.Timestamp); err != nil { + return sth, fmt.Errorf("%s: %w", asciiTimestamp, err) + } + if m.DequeueUint64(asciiTreeSize, &sth.TreeSize); err != nil { + return sth, fmt.Errorf("%s: %w", asciiTreeSize, err) + } + if m.DequeueArray(asciiRootHash, sth.RootHash[:]); err != nil { + return sth, fmt.Errorf("%s: %w", asciiRootHash, err) + } + if m.DequeueArray(asciiSignature, sth.Signature[:]); err != nil { + return sth, fmt.Errorf("%s: %w", asciiSignature, err) + } + return sth, nil +} + +func cosignatureFromASCII(m *ascii.Map) (c Cosignature, err error) { + if err := m.DequeueArray(asciiCosignature, c.Signature[:]); err != nil { + return c, fmt.Errorf("%s: %w", asciiCosignature, err) + } + if err := m.DequeueArray(asciiKeyHash, c.KeyHash[:]); err != nil { + return c, fmt.Errorf("%s: %w", asciiCosignature, err) + } + return c, nil +} |