diff options
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 +} |