niko_trust/internal/protocol/example_test.go
Niko Marmeladkov 9d66003689
Initial commit: signed-object trust relay, verifier, and docs
- server: relay storing signed objects (PUT/GET), per-IP rate limiting,
  per-subject quota (1000), one-response-per-request, pagination,
  /v1/healthz /v1/readyz /v1/metrics
- verify: signature-verifying trust evaluator; every object is checked via
  env.Verify(), approvals via VerifyApprovalResponse, revocations via
  VerifyRevocationOf; k-of-n approval quorum
- docs: TRUST-MODEL.md and API.md describing issuer-anchored signatures and
  the endpoint/status-code contract
- tests: server, verify, and ratelimit packages
2026-08-12 22:36:49 +03:00

89 lines
2.8 KiB
Go

package protocol_test
import (
"bytes"
"fmt"
"git.n1ko.dev/Niko/niko_trust/internal/identity/signer"
"git.n1ko.dev/Niko/niko_trust/internal/protocol"
"git.n1ko.dev/Niko/niko_trust/internal/tce"
)
// ExampleClaimLifecycle is the whole client-side story for one claim, with no
// server in the loop: generate keys, build the canonical bytes, sign them, and
// let any verifier decode and verify locally. The signature is computed over
// the exact TCE bytes, so it stays valid no matter which relay later serves
// them (PROTOCOL.md section 7.3).
func Example_claimLifecycle() {
// In production use signer.Generate(); fixed seeds keep this example
// deterministic so its output can be asserted.
issuer, _ := signer.FromSeed(bytes.Repeat([]byte{0x01}, 32))
subject, _ := signer.FromSeed(bytes.Repeat([]byte{0x02}, 32))
c := &protocol.Claim{
Issuer: issuer.Public(),
Subject: subject.Public(),
Claims: map[string]tce.Value{"example.flag": tce.Bool(true)},
CreatedAt: 1_700_000_000,
ExpiresAt: 1_700_086_400,
Serial: 1,
Nonce: bytes.Repeat([]byte{0x10}, tce.NonceSize),
}
// 1. Client builds and signs the canonical bytes.
tceBytes, err := protocol.EncodeClaim(c)
if err != nil {
fmt.Println("encode:", err)
return
}
sig := issuer.Sign(tceBytes)
// 2. Any verifier, anywhere, with only the bytes + signature.
got, err := protocol.DecodeClaim(tceBytes)
if err != nil {
fmt.Println("decode:", err)
return
}
if _, err := protocol.VerifyClaim(tceBytes, sig); err != nil {
fmt.Println("verify:", err)
return
}
v, ok := got.Claims["example.flag"].Bool()
fmt.Println("verified", got.CreatedAt == c.CreatedAt, ok && v)
// Output: verified true true
}
// ExampleAuthHandshake is the login flow (PROTOCOL.md section 8.6). The server
// mints a single-use challenge; the client proves possession of its key by
// signing an AuthAssertion bound to that challenge and to the server's
// audience. The server verifies the assertion but never sees a private key.
func Example_authHandshake() {
client, _ := signer.FromSeed(bytes.Repeat([]byte{0x03}, 32))
// Server side: issue a 32-byte challenge (CSPRNG in reality).
challenge := bytes.Repeat([]byte{0x07}, tce.ChallengeSize)
audience := "trust.n1ko.dev"
// Client side: build, sign, return.
a := &protocol.AuthAssertion{
PubKey: client.Public(),
Challenge: challenge,
Scope: "read:claims",
Audience: audience,
CreatedAt: 1_700_000_000,
}
b, err := protocol.EncodeAuthAssertion(a)
if err != nil {
fmt.Println("encode:", err)
return
}
sig := client.Sign(b)
// Server side: verify the assertion for this exact audience.
if _, err := protocol.VerifyAuthAssertion(b, sig, audience); err != nil {
fmt.Println("auth failed:", err)
return
}
fmt.Println("auth ok")
// Output: auth ok
}