Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

statement.gno

5.50 Kb · 155 lines
  1package wesh
  2
  3import (
  4	"crypto/ed25519"
  5	"crypto/sha256"
  6	"encoding/hex"
  7	"errors"
  8	"strconv"
  9	"strings"
 10)
 11
 12// Statement kinds. A signature is only ever valid for the kind it was made
 13// under, so a binding signature can never be replayed as a device statement.
 14const (
 15	kindBind   = "gno.wesh/v0:bind"
 16	kindDevice = "gno.wesh/v0:device"
 17)
 18
 19// Device sigchain operations.
 20const (
 21	OpAdd    = "add"
 22	OpRevoke = "revoke"
 23)
 24
 25var (
 26	ErrBadOp        = errors.New("wesh: operation must be add or revoke")
 27	ErrBadSignature = errors.New("wesh: signature does not verify under the account key")
 28	ErrBadSeq       = errors.New("wesh: sequence number must not be negative")
 29	ErrBadPrevLen   = errors.New("wesh: previous digest must be 32 bytes")
 30)
 31
 32// DigestLen is the length of a statement digest (SHA-256).
 33const DigestLen = 32
 34
 35// ValidOp reports whether op is a known sigchain operation.
 36func ValidOp(op string) bool { return op == OpAdd || op == OpRevoke }
 37
 38// BindStatement returns the canonical text an account signs, with its Wesh
 39// account private key, to claim a directory entry.
 40//
 41// The signature is what makes a directory entry meaningful. Without it anyone
 42// could publish anyone else's account key next to a seed of their choosing and
 43// harvest the contact requests that followed. With it, a directory entry is a
 44// statement by the account itself, verifiable by anyone, replayable nowhere:
 45//
 46//   - the kind prefix stops a signature made for one purpose being reused for
 47//     another;
 48//   - chainID stops a binding signed for a testnet being replayed on mainnet;
 49//   - gnoAddr binds the Wesh identity to exactly one gno account, so a leaked
 50//     signature cannot be used to claim the identity from a different address;
 51//   - revision is monotonic, so an old, superseded binding cannot be replayed
 52//     to roll a rotation back.
 53//
 54// payload is the seed for a publicly published identity, or the seed
 55// commitment from [SeedCommitment] for one that keeps its rendezvous point off
 56// chain. The two are indistinguishable on the wire, which is deliberate: an
 57// observer cannot tell a committed identity from a published one without the
 58// realm's own mode flag.
 59//
 60// Fields are newline-framed and hex-encoded, never concatenated raw: with
 61// plain concatenation a different split of the same bytes would produce the
 62// same statement.
 63func BindStatement(chainID, gnoAddr string, accountPK, payload []byte, revision int) string {
 64	var b strings.Builder
 65	b.WriteString(kindBind)
 66	b.WriteString("\n")
 67	b.WriteString(chainID)
 68	b.WriteString("\n")
 69	b.WriteString(gnoAddr)
 70	b.WriteString("\n")
 71	b.WriteString(hex.EncodeToString(accountPK))
 72	b.WriteString("\n")
 73	b.WriteString(hex.EncodeToString(payload))
 74	b.WriteString("\n")
 75	b.WriteString(strconv.Itoa(revision))
 76	return b.String()
 77}
 78
 79// DeviceStatement returns the canonical text an account signs to append one
 80// entry to its device sigchain.
 81//
 82// This is the piece the Wesh protocol has no answer for. Its own documentation
 83// says a device, once linked, can never be revoked: the account metadata log is
 84// append-only and no authority can mark an entry void. A chain cannot undo
 85// that (the ratchets are already out there and forward secrecy is a local
 86// property), but it can host the record that was missing, so a revocation
 87// becomes publicly visible, ordered, and attributable to the account key.
 88//
 89// prev is the digest of the preceding statement, or [GenesisDigest] for seq 0.
 90// Chaining each entry to its predecessor means the log cannot be reordered or
 91// have an entry quietly dropped: any gap changes every digest after it.
 92func DeviceStatement(chainID string, accountPK []byte, seq int, prev []byte, op string, devicePK []byte) string {
 93	var b strings.Builder
 94	b.WriteString(kindDevice)
 95	b.WriteString("\n")
 96	b.WriteString(chainID)
 97	b.WriteString("\n")
 98	b.WriteString(hex.EncodeToString(accountPK))
 99	b.WriteString("\n")
100	b.WriteString(strconv.Itoa(seq))
101	b.WriteString("\n")
102	b.WriteString(hex.EncodeToString(prev))
103	b.WriteString("\n")
104	b.WriteString(op)
105	b.WriteString("\n")
106	b.WriteString(hex.EncodeToString(devicePK))
107	return b.String()
108}
109
110// StatementDigest returns SHA-256 of a canonical statement. It is the value
111// the next sigchain entry chains to.
112func StatementDigest(statement string) []byte {
113	sum := sha256.Sum256([]byte(statement))
114	return sum[:]
115}
116
117// GenesisDigest is the all-zero digest that the first sigchain entry chains to.
118func GenesisDigest() []byte { return make([]byte, DigestLen) }
119
120// VerifyBind checks a binding signature against the account key.
121func VerifyBind(chainID, gnoAddr string, accountPK, payload []byte, revision int, sig []byte) error {
122	if len(accountPK) != AccountPKLen {
123		return ErrBadAccountPKLen
124	}
125	stmt := BindStatement(chainID, gnoAddr, accountPK, payload, revision)
126	if !ed25519.Verify(accountPK, []byte(stmt), sig) {
127		return ErrBadSignature
128	}
129	return nil
130}
131
132// VerifyDevice checks a sigchain entry's shape and its signature, and returns
133// the digest the next entry must chain to.
134func VerifyDevice(chainID string, accountPK []byte, seq int, prev []byte, op string, devicePK, sig []byte) ([]byte, error) {
135	if len(accountPK) != AccountPKLen {
136		return nil, ErrBadAccountPKLen
137	}
138	if len(devicePK) != DevicePKLen {
139		return nil, ErrBadDevicePKLen
140	}
141	if seq < 0 {
142		return nil, ErrBadSeq
143	}
144	if len(prev) != DigestLen {
145		return nil, ErrBadPrevLen
146	}
147	if !ValidOp(op) {
148		return nil, ErrBadOp
149	}
150	stmt := DeviceStatement(chainID, accountPK, seq, prev, op, devicePK)
151	if !ed25519.Verify(accountPK, []byte(stmt), sig) {
152		return nil, ErrBadSignature
153	}
154	return StatementDigest(stmt), nil
155}