// Package invite encodes and decodes waste invite strings. // // Format: "waste:" // // The JSON payload is compatible with yaw2: the `net` field carries the full // 64-char hex SHA-256("yaw2-net:"+name) hash that yaw2 clients pass directly // to the signaling server. A yaw2 client that can parse the base64 JSON can join // the same network without knowing the plaintext name. // // Signed invites (waste-go extension): when `inviter` and `sig` are present, // the invite was issued by a known peer. Receiving peers that enforce // RequireInvite will reject hellos that carry no valid signed invite. // YAW/2-only peers ignore both fields. package invite import ( "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "fmt" "strings" ) const prefix = "waste:" // Invite holds the information needed to join a network. type Invite struct { Anchor string `json:"anchor"` // WebSocket anchor URL Network string `json:"network"` // plaintext network name Net string `json:"net,omitempty"` // 64-char hex SHA-256("yaw2-net:"+name) — yaw2 `net` field Inviter string `json:"inviter,omitempty"` // hex Ed25519 pubkey of the signing peer (waste-go extension) Sig string `json:"sig,omitempty"` // hex Ed25519 sig over canonical payload (waste-go extension) } // IsSigned reports whether the invite carries a signature. func (inv Invite) IsSigned() bool { return inv.Inviter != "" && inv.Sig != "" } // Signer can sign data and report its own peer ID. type Signer interface { Sign(data []byte) string PeerIDHex() string } // Verifier verifies an Ed25519 signature given a hex public key. type Verifier func(publicKeyHex string, data []byte, sigHex string) error // Encode returns an unsigned waste: invite string (backward compatible). func Encode(anchor, network string) (string, error) { return marshal(Invite{ Anchor: anchor, Network: network, Net: NetHash(network), }) } // EncodeSigned returns a signed waste: invite string. // The signature covers: anchor + NUL + network + NUL + net + NUL + inviter. func EncodeSigned(anchor, network string, signer Signer) (string, error) { if anchor == "" { return "", fmt.Errorf("anchor URL is required") } if network == "" { return "", fmt.Errorf("network name is required") } inviter := signer.PeerIDHex() net := NetHash(network) sig := signer.Sign(sigPayload(anchor, network, net, inviter)) return marshal(Invite{ Anchor: anchor, Network: network, Net: net, Inviter: inviter, Sig: sig, }) } // Verify checks the invite signature and that the inviter is in the trusted set. // Unsigned invites return nil — the caller decides whether to accept them. func Verify(inv Invite, trusted map[string]bool, verify Verifier) error { if !inv.IsSigned() { return nil } payload := sigPayload(inv.Anchor, inv.Network, inv.Net, inv.Inviter) if err := verify(inv.Inviter, payload, inv.Sig); err != nil { return fmt.Errorf("invite signature invalid: %w", err) } if !trusted[inv.Inviter] { return fmt.Errorf("invite signed by unknown peer %s", inv.Inviter[:16]) } return nil } // Decode parses a waste: invite string and returns the Invite. func Decode(s string) (Invite, error) { s = strings.TrimSpace(s) if !strings.HasPrefix(s, prefix) { return Invite{}, fmt.Errorf("not a waste invite (expected 'waste:' prefix)") } b, err := base64.URLEncoding.DecodeString(strings.TrimPrefix(s, prefix)) if err != nil { return Invite{}, fmt.Errorf("invalid invite: %w", err) } var inv Invite if err := json.Unmarshal(b, &inv); err != nil { return Invite{}, fmt.Errorf("invalid invite payload: %w", err) } if inv.Anchor == "" || inv.Network == "" { return Invite{}, fmt.Errorf("invite is missing anchor or network name") } return inv, nil } // NetHash returns the full 64-char hex network hash for the given name. func NetHash(name string) string { h := sha256.Sum256([]byte("yaw2-net:" + name)) return hex.EncodeToString(h[:]) } func marshal(inv Invite) (string, error) { b, err := json.Marshal(inv) if err != nil { return "", err } return prefix + base64.URLEncoding.EncodeToString(b), nil } func sigPayload(anchor, network, net, inviter string) []byte { return []byte(anchor + "\x00" + network + "\x00" + net + "\x00" + inviter) }