- 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
89 lines
2.8 KiB
Go
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
|
|
}
|