- 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
183 lines
4.5 KiB
Go
183 lines
4.5 KiB
Go
package transport
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
|
|
"git.n1ko.dev/Niko/niko_trust/internal/identity"
|
|
"git.n1ko.dev/Niko/niko_trust/internal/protocol"
|
|
"git.n1ko.dev/Niko/niko_trust/internal/tce"
|
|
)
|
|
|
|
func hexStr(b []byte) string { return hex.EncodeToString(b) }
|
|
|
|
// AddrOf renders a public key as its trust address text.
|
|
func AddrOf(pub []byte) string {
|
|
id, err := identity.FromPubKey(pub)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return id.Address().String()
|
|
}
|
|
|
|
func addrOf(pub []byte) string { return AddrOf(pub) }
|
|
|
|
// valueView renders a TCE value for the JSON `object` convenience view.
|
|
func valueView(v tce.Value) any {
|
|
switch v.Tag() {
|
|
case tce.ValNull:
|
|
return nil
|
|
case tce.ValTrue:
|
|
return true
|
|
case tce.ValFalse:
|
|
return false
|
|
case tce.ValString:
|
|
s, _ := v.Str()
|
|
return s
|
|
case tce.ValNumber:
|
|
tok, _ := v.NumberToken()
|
|
return json.Number(tok)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func objectTypeOf(obj any) string {
|
|
switch obj.(type) {
|
|
case *protocol.Identity:
|
|
return "identity"
|
|
case *protocol.Claim:
|
|
return "claim"
|
|
case *protocol.Revocation:
|
|
return "revocation"
|
|
case *protocol.ApprovalRequest:
|
|
return "request"
|
|
case *protocol.ApprovalResponse:
|
|
return "response"
|
|
case *protocol.AuthAssertion:
|
|
return "auth"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// ObjectTypeName returns the type name of a decoded protocol object.
|
|
func ObjectTypeName(obj any) string { return objectTypeOf(obj) }
|
|
|
|
// DecodeObject strict-decodes TCE bytes into the typed protocol object and
|
|
// returns its type name.
|
|
func DecodeObject(b []byte) (string, any, error) {
|
|
d := tce.NewDecoder(b)
|
|
tag, err := d.Header()
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
switch tag {
|
|
case tce.TagIdentity:
|
|
o, e := protocol.DecodeIdentity(b)
|
|
return "identity", o, e
|
|
case tce.TagClaim:
|
|
o, e := protocol.DecodeClaim(b)
|
|
return "claim", o, e
|
|
case tce.TagRevocation:
|
|
o, e := protocol.DecodeRevocation(b)
|
|
return "revocation", o, e
|
|
case tce.TagApprovalRequest:
|
|
o, e := protocol.DecodeApprovalRequest(b)
|
|
return "request", o, e
|
|
case tce.TagApprovalResponse:
|
|
o, e := protocol.DecodeApprovalResponse(b)
|
|
return "response", o, e
|
|
case tce.TagAuthAssertion:
|
|
o, e := protocol.DecodeAuthAssertion(b)
|
|
return "auth", o, e
|
|
default:
|
|
return "", nil, tce.ErrObjectTag
|
|
}
|
|
}
|
|
|
|
// BuildView decodes the TCE bytes and produces the `object` convenience view of
|
|
// PROTOCOL.md section 10. It fails if the bytes do not strict-decode.
|
|
func BuildView(b []byte) (objectType string, view json.RawMessage, err error) {
|
|
typ, obj, decErr := DecodeObject(b)
|
|
if decErr != nil {
|
|
return "", nil, decErr
|
|
}
|
|
var v any
|
|
switch o := obj.(type) {
|
|
case *protocol.Identity:
|
|
v = map[string]any{
|
|
"type": "identity",
|
|
"version": 1,
|
|
"identity": addrOf(o.PubKey),
|
|
"alias": o.Alias,
|
|
"created_at": o.CreatedAt,
|
|
}
|
|
case *protocol.Claim:
|
|
claims := make(map[string]any, len(o.Claims))
|
|
for k, val := range o.Claims {
|
|
claims[k] = valueView(val)
|
|
}
|
|
v = map[string]any{
|
|
"type": "claim",
|
|
"version": 1,
|
|
"issuer": addrOf(o.Issuer),
|
|
"subject": addrOf(o.Subject),
|
|
"claims": claims,
|
|
"created_at": o.CreatedAt,
|
|
"expires_at": o.ExpiresAt,
|
|
"serial": o.Serial,
|
|
"nonce": hexStr(o.Nonce),
|
|
}
|
|
case *protocol.Revocation:
|
|
v = map[string]any{
|
|
"type": "revocation",
|
|
"version": 1,
|
|
"issuer": addrOf(o.Issuer),
|
|
"claim_id": o.ClaimID.String(),
|
|
"reason": o.Reason,
|
|
"created_at": o.CreatedAt,
|
|
"nonce": hexStr(o.Nonce),
|
|
}
|
|
case *protocol.ApprovalRequest:
|
|
payload := make(map[string]any, len(o.Payload))
|
|
for k, val := range o.Payload {
|
|
payload[k] = valueView(val)
|
|
}
|
|
v = map[string]any{
|
|
"type": "request",
|
|
"version": 1,
|
|
"sender": addrOf(o.Sender),
|
|
"recipient": addrOf(o.Recipient),
|
|
"action": o.Action,
|
|
"payload": payload,
|
|
"message": o.Message,
|
|
"created_at": o.CreatedAt,
|
|
"expires_at": o.ExpiresAt,
|
|
"nonce": hexStr(o.Nonce),
|
|
}
|
|
case *protocol.ApprovalResponse:
|
|
v = map[string]any{
|
|
"type": "response",
|
|
"version": 1,
|
|
"request_hash": o.RequestHash.String(),
|
|
"responder": addrOf(o.Responder),
|
|
"decision": o.Decision.String(),
|
|
"created_at": o.CreatedAt,
|
|
"nonce": hexStr(o.Nonce),
|
|
}
|
|
case *protocol.AuthAssertion:
|
|
v = map[string]any{
|
|
"type": "auth",
|
|
"version": 1,
|
|
"identity": addrOf(o.PubKey),
|
|
"challenge": hexStr(o.Challenge),
|
|
"scope": o.Scope,
|
|
"audience": o.Audience,
|
|
"created_at": o.CreatedAt,
|
|
}
|
|
}
|
|
raw, err := json.Marshal(v)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
return typ, raw, nil
|
|
}
|