- 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
131 lines
3.5 KiB
Go
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
|