niko_trust/examples/approve/main.go
Niko Marmeladkov 3bf13fa488 Public SDK packages, proxy-aware rate limits, service login recipe
- internal/{address,identity,protocol,tce,transport,verify} -> pkg/ so
  external Go projects can import the verified core; invariant tests
  updated for the new paths
- Config.TrustProxy: key rate limiting by X-Forwarded-For when the relay
  sits behind a reverse proxy (off by default, header never trusted
  otherwise)
- examples/service + examples/approve: complete passwordless login round
  trip (mint request -> wallet approves -> local verify), run live in CI
- docs/SERVICE-GUIDE.md: the integration recipe
2026-08-26 12:49:54 +03:00

131 lines
3.5 KiB
Go

// Command approve acts as the user's wallet for the demo: it fetches an
// ApprovalRequest from the relay by id, checks it is addressed to us, signs
// an ApprovalResponse and stores it. Combined with examples/service this
// exercises a full login round trip on one machine.
package main
import (
"crypto/rand"
"encoding/base64"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net/http"
"strings"
"time"
"git.n1ko.dev/Niko/niko_trust/pkg/identity/signer"
"git.n1ko.dev/Niko/niko_trust/pkg/protocol"
"git.n1ko.dev/Niko/niko_trust/pkg/tce"
)
var (
relayURL = flag.String("relay", "http://127.0.0.1:8080", "relay base URL")
idHex = flag.String("id", "", "request id (hex) to answer")
deny = flag.Bool("deny", false, "deny instead of allow")
seedHex = flag.String("seed", "", "user identity seed (hex); generated when empty")
)
func main() {
flag.Parse()
if *idHex == "" {
log.Fatal("-id is required")
}
var user *signer.Signer
var err error
if *seedHex == "" {
user, err = signer.Generate()
} else {
b, e := hex.DecodeString(*seedHex)
if e != nil || len(b) != ed25519SeedSize {
log.Fatal("-seed must be 64 hex characters")
}
user, err = signer.FromSeed(b)
}
if err != nil {
log.Fatal(err)
}
fmt.Println("wallet identity:", user.Address())
// Fetch the exact request bytes by their content address. A hostile
// relay can substitute anything here — that is why we verify instead of
// just decoding.
reqTCE, reqSig := getObject(*relayURL, *idHex)
req, err := protocol.VerifyApprovalRequest(reqTCE, reqSig)
if err != nil {
log.Fatalf("request does not verify: %v", err)
}
if string(req.Recipient) != string(user.Public()) {
log.Fatal("this request is not addressed to our identity")
}
fmt.Printf("action=%q message=%q\n", req.Action, req.Message)
decision := protocol.Allow
if *deny {
decision = protocol.Deny
}
resp := &protocol.ApprovalResponse{
RequestHash: tce.ComputeID(reqTCE), // INV-4: commits to the exact bytes
Responder: user.Public(),
Decision: decision,
CreatedAt: uint64(time.Now().Unix()),
Nonce: nonce(),
}
respTCE, err := protocol.EncodeApprovalResponse(resp)
if err != nil {
log.Fatal(err)
}
out := post(*relayURL+"/v1/objects",
fmt.Sprintf(`{"tce":%q,"signature":%q}`,
base64.StdEncoding.EncodeToString(respTCE),
base64.StdEncoding.EncodeToString(user.Sign(respTCE))))
fmt.Printf("response stored: %s\n", strings.TrimSpace(out))
}
// ------------------------------------------------------------- tiny http io
func getObject(base, id string) ([]byte, []byte) {
httpResp, err := http.Get(base + "/v1/objects/" + id)
if err != nil {
log.Fatal(err)
}
defer httpResp.Body.Close()
raw, _ := io.ReadAll(httpResp.Body)
var env struct {
Tce string `json:"tce"`
Signature string `json:"signature"`
Error string `json:"error"`
}
json.Unmarshal(raw, &env)
if env.Tce == "" {
log.Fatalf("fetch object %s: %s", id, env.Error)
}
tceB, e1 := base64.StdEncoding.DecodeString(env.Tce)
sigB, e2 := base64.StdEncoding.DecodeString(env.Signature)
if e1 != nil || e2 != nil {
log.Fatal("bad envelope encoding")
}
return tceB, sigB
}
func post(url, body string) string {
httpResp, err := http.Post(url, "application/json", strings.NewReader(body))
if err != nil {
log.Fatal(err)
}
defer httpResp.Body.Close()
b, _ := io.ReadAll(httpResp.Body)
return string(b)
}
func nonce() []byte {
n := make([]byte, tce.NonceSize)
rand.Read(n)
return n
}
const ed25519SeedSize = 32