- 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
89 lines
2.7 KiB
Go
89 lines
2.7 KiB
Go
package protocol_test
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
|
|
"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"
|
|
)
|
|
|
|
// 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
|
|
}
|