- 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
615 lines
17 KiB
Go
615 lines
17 KiB
Go
// Command client is a minimal trust relay client: it generates keys, publishes
|
|
// signed objects, queries the relay, and performs the auth handshake. It is the
|
|
// "how a client uses the protocol" reference; no network logic lives in the
|
|
// codec packages.
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.n1ko.dev/Niko/niko_trust/internal/address"
|
|
"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"
|
|
"git.n1ko.dev/Niko/niko_trust/internal/transport"
|
|
"git.n1ko.dev/Niko/niko_trust/internal/verify"
|
|
)
|
|
|
|
func main() {
|
|
if len(os.Args) < 2 {
|
|
log.Fatal("usage: client <keygen|publish-claim|get|claims|auth> [flags]")
|
|
}
|
|
switch os.Args[1] {
|
|
case "keygen":
|
|
cmdKeygen(os.Args[2:])
|
|
case "publish-claim":
|
|
cmdPublishClaim(os.Args[2:])
|
|
case "publish-request":
|
|
cmdPublishRequest(os.Args[2:])
|
|
case "respond":
|
|
cmdRespond(os.Args[2:])
|
|
case "revoke":
|
|
cmdRevoke(os.Args[2:])
|
|
case "get":
|
|
cmdGet(os.Args[2:])
|
|
case "claims":
|
|
cmdClaims(os.Args[2:])
|
|
case "requests":
|
|
cmdRequests(os.Args[2:])
|
|
case "responses":
|
|
cmdResponses(os.Args[2:])
|
|
case "revocations":
|
|
cmdRevocations(os.Args[2:])
|
|
case "auth":
|
|
cmdAuth(os.Args[2:])
|
|
case "verify":
|
|
cmdVerify(os.Args[2:])
|
|
default:
|
|
log.Fatalf("unknown command %q", os.Args[1])
|
|
}
|
|
}
|
|
|
|
func loadSigner(path string) *signer.Signer {
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
log.Fatalf("read seed: %v", err)
|
|
}
|
|
seed, err := hex.DecodeString(strings.TrimSpace(string(raw)))
|
|
if err != nil {
|
|
log.Fatalf("bad seed: %v", err)
|
|
}
|
|
s, err := signer.FromSeed(seed)
|
|
if err != nil {
|
|
log.Fatalf("signer: %v", err)
|
|
}
|
|
return s
|
|
}
|
|
|
|
func cmdKeygen(args []string) {
|
|
fs := flag.NewFlagSet("keygen", flag.ExitOnError)
|
|
out := fs.String("out", "", "write hex seed to this file (default stdout)")
|
|
fs.Parse(args)
|
|
|
|
s, err := signer.Generate()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
if *out != "" {
|
|
if err := os.WriteFile(*out, []byte(hex.EncodeToString(s.Seed())+"\n"), 0o600); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
fmt.Printf("address %s\nseed written to %s\n", s.Address().String(), *out)
|
|
return
|
|
}
|
|
fmt.Printf("address %s\nseed %s\n", s.Address().String(), hex.EncodeToString(s.Seed()))
|
|
}
|
|
|
|
func cmdPublishClaim(args []string) {
|
|
fs := flag.NewFlagSet("publish-claim", flag.ExitOnError)
|
|
seedPath := fs.String("seed", "", "issuer seed file")
|
|
subject := fs.String("subject", "", "subject trust address")
|
|
key := fs.String("key", "claim", "claim key (predicate)")
|
|
valType := fs.String("type", "bool", "bool|string|number")
|
|
val := fs.String("val", "true", "claim value")
|
|
url := fs.String("url", "http://localhost:8080", "relay URL")
|
|
fs.Parse(args)
|
|
|
|
s := loadSigner(*seedPath)
|
|
sub, err := address.Parse(*subject)
|
|
if err != nil {
|
|
log.Fatalf("subject: %v", err)
|
|
}
|
|
subPub := sub.PubKey()
|
|
var v tce.Value
|
|
switch *valType {
|
|
case "bool":
|
|
v = tce.Bool(*val == "true")
|
|
case "string":
|
|
v = tce.String(*val)
|
|
case "number":
|
|
v = tce.Number(*val)
|
|
default:
|
|
log.Fatalf("unknown value type %q", *valType)
|
|
}
|
|
|
|
c := &protocol.Claim{
|
|
Issuer: s.Public(),
|
|
Subject: subPub,
|
|
Claims: map[string]tce.Value{*key: v},
|
|
CreatedAt: uint64(time.Now().Unix()),
|
|
ExpiresAt: uint64(time.Now().Add(24 * time.Hour).Unix()),
|
|
Serial: 1,
|
|
Nonce: randBytes(tce.NonceSize),
|
|
}
|
|
tceBytes, err := protocol.EncodeClaim(c)
|
|
if err != nil {
|
|
log.Fatalf("encode: %v", err)
|
|
}
|
|
sig := s.Sign(tceBytes)
|
|
env := &transport.Envelope{TCE: tceBytes, Signature: sig}
|
|
|
|
postJSON(*url+"/v1/objects", env)
|
|
}
|
|
|
|
func cmdGet(args []string) {
|
|
fs := flag.NewFlagSet("get", flag.ExitOnError)
|
|
id := fs.String("id", "", "object id")
|
|
url := fs.String("url", "http://localhost:8080", "relay URL")
|
|
fs.Parse(args)
|
|
getJSON(*url + "/v1/objects/" + *id)
|
|
}
|
|
|
|
func cmdClaims(args []string) {
|
|
fs := flag.NewFlagSet("claims", flag.ExitOnError)
|
|
subject := fs.String("subject", "", "subject trust address")
|
|
token := fs.String("token", "", "session token (from auth)")
|
|
url := fs.String("url", "http://localhost:8080", "relay URL")
|
|
fs.Parse(args)
|
|
|
|
u := *url + "/v1/claims?subject=" + *subject
|
|
req, err := http.NewRequest(http.MethodGet, u, nil)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
if *token != "" {
|
|
req.Header.Set("Authorization", "Bearer "+*token)
|
|
}
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer resp.Body.Close()
|
|
body, _ := io.ReadAll(resp.Body)
|
|
fmt.Println(resp.Status)
|
|
fmt.Println(string(body))
|
|
}
|
|
|
|
func cmdAuth(args []string) {
|
|
fs := flag.NewFlagSet("auth", flag.ExitOnError)
|
|
seedPath := fs.String("seed", "", "client seed file")
|
|
scope := fs.String("scope", "read:claims", "requested scope")
|
|
audience := fs.String("audience", "", "server audience (default: fetched from /v1/config)")
|
|
url := fs.String("url", "http://localhost:8080", "relay URL")
|
|
fs.Parse(args)
|
|
|
|
s := loadSigner(*seedPath)
|
|
|
|
// Resolve the audience the server expects assertions to be bound to.
|
|
aud := *audience
|
|
if aud == "" {
|
|
resp, err := http.Get(*url + "/v1/config")
|
|
if err != nil {
|
|
log.Fatalf("config: %v", err)
|
|
}
|
|
cfg := struct{ Audience string `json:"audience"` }{}
|
|
if resp.StatusCode >= 300 {
|
|
log.Fatalf("config: %s", resp.Status)
|
|
}
|
|
_ = json.NewDecoder(resp.Body).Decode(&cfg)
|
|
resp.Body.Close()
|
|
aud = cfg.Audience
|
|
}
|
|
if aud == "" {
|
|
log.Fatal("could not determine server audience")
|
|
}
|
|
|
|
// 1. Obtain a challenge from the server.
|
|
chRes := struct{ Challenge string `json:"challenge"` }{}
|
|
postJSONDecode(*url+"/v1/auth/challenge", nil, &chRes)
|
|
ch, err := hex.DecodeString(chRes.Challenge)
|
|
if err != nil {
|
|
log.Fatalf("challenge: %v", err)
|
|
}
|
|
|
|
// 2. Build and sign an AuthAssertion bound to the server's audience.
|
|
a := &protocol.AuthAssertion{
|
|
PubKey: s.Public(),
|
|
Challenge: ch,
|
|
Scope: *scope,
|
|
Audience: aud,
|
|
CreatedAt: uint64(time.Now().Unix()),
|
|
}
|
|
b, err := protocol.EncodeAuthAssertion(a)
|
|
if err != nil {
|
|
log.Fatalf("encode: %v", err)
|
|
}
|
|
sig := s.Sign(b)
|
|
env := &transport.Envelope{TCE: b, Signature: sig}
|
|
|
|
// 3. Present it; the server verifies and returns a session token.
|
|
out := struct {
|
|
SessionToken string `json:"session_token"`
|
|
Identity string `json:"identity"`
|
|
Scope string `json:"scope"`
|
|
}{}
|
|
postJSONDecode(*url+"/v1/auth/assert", env, &out)
|
|
fmt.Printf("session %s\nidentity %s\nscope %s\n", out.SessionToken, out.Identity, out.Scope)
|
|
}
|
|
|
|
func randBytes(n int) []byte {
|
|
b := make([]byte, n)
|
|
if _, err := rand.Read(b); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
return b
|
|
}
|
|
|
|
func postJSON(u string, v any) {
|
|
var buf bytes.Buffer
|
|
if err := json.NewEncoder(&buf).Encode(v); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
resp, err := http.Post(u, "application/json", &buf)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer resp.Body.Close()
|
|
body, _ := io.ReadAll(resp.Body)
|
|
fmt.Println(resp.Status)
|
|
fmt.Println(string(body))
|
|
}
|
|
|
|
func postJSONDecode(u string, v any, out any) {
|
|
var buf bytes.Buffer
|
|
if v != nil {
|
|
if err := json.NewEncoder(&buf).Encode(v); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
resp, err := http.Post(u, "application/json", &buf)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer resp.Body.Close()
|
|
body, _ := io.ReadAll(resp.Body)
|
|
if resp.StatusCode >= 300 {
|
|
log.Fatalf("%s: %s", resp.Status, string(body))
|
|
}
|
|
if out != nil {
|
|
if err := json.Unmarshal(body, out); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func getJSON(u string) {
|
|
resp, err := http.Get(u)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer resp.Body.Close()
|
|
body, _ := io.ReadAll(resp.Body)
|
|
fmt.Println(resp.Status)
|
|
fmt.Println(string(body))
|
|
}
|
|
|
|
func getAuthJSON(u, token string) {
|
|
req, err := http.NewRequest(http.MethodGet, u, nil)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
if token != "" {
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
}
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer resp.Body.Close()
|
|
body, _ := io.ReadAll(resp.Body)
|
|
fmt.Println(resp.Status)
|
|
fmt.Println(string(body))
|
|
}
|
|
|
|
func mustID(s string) tce.ID {
|
|
id, err := tce.ParseID(s)
|
|
if err != nil {
|
|
log.Fatalf("bad id %q: %v", s, err)
|
|
}
|
|
return id
|
|
}
|
|
|
|
func cmdPublishRequest(args []string) {
|
|
fs := flag.NewFlagSet("publish-request", flag.ExitOnError)
|
|
seedPath := fs.String("seed", "", "sender seed file")
|
|
recipient := fs.String("recipient", "", "recipient trust address")
|
|
action := fs.String("action", "", "opaque action the recipient approves")
|
|
message := fs.String("message", "", "human-readable message")
|
|
url := fs.String("url", "http://localhost:8080", "relay URL")
|
|
fs.Parse(args)
|
|
|
|
s := loadSigner(*seedPath)
|
|
rec, err := address.Parse(*recipient)
|
|
if err != nil {
|
|
log.Fatalf("recipient: %v", err)
|
|
}
|
|
now := uint64(time.Now().Unix())
|
|
req := &protocol.ApprovalRequest{
|
|
Sender: s.Public(),
|
|
Recipient: rec.PubKey(),
|
|
Action: *action,
|
|
Message: *message,
|
|
CreatedAt: now,
|
|
ExpiresAt: now + 30,
|
|
Nonce: randBytes(tce.NonceSize),
|
|
}
|
|
tceBytes, err := protocol.EncodeApprovalRequest(req)
|
|
if err != nil {
|
|
log.Fatalf("encode: %v", err)
|
|
}
|
|
sig := s.Sign(tceBytes)
|
|
postJSON(*url+"/v1/objects", &transport.Envelope{TCE: tceBytes, Signature: sig})
|
|
}
|
|
|
|
func cmdRespond(args []string) {
|
|
fs := flag.NewFlagSet("respond", flag.ExitOnError)
|
|
seedPath := fs.String("seed", "", "responder seed file")
|
|
request := fs.String("request", "", "request object id")
|
|
decision := fs.String("decision", "allow", "allow|deny")
|
|
url := fs.String("url", "http://localhost:8080", "relay URL")
|
|
fs.Parse(args)
|
|
|
|
s := loadSigner(*seedPath)
|
|
|
|
// Fetch the request to confirm we are its recipient.
|
|
env := struct {
|
|
TCE string `json:"tce"`
|
|
}{}
|
|
greq, err := http.Get(*url + "/v1/objects/" + *request)
|
|
if err != nil {
|
|
log.Fatalf("fetch request: %v", err)
|
|
}
|
|
body, _ := io.ReadAll(greq.Body)
|
|
greq.Body.Close()
|
|
if greq.StatusCode >= 300 {
|
|
log.Fatalf("fetch request: %s", greq.Status)
|
|
}
|
|
if err := json.Unmarshal(body, &env); err != nil {
|
|
log.Fatalf("request json: %v", err)
|
|
}
|
|
reqBytes, err := base64.StdEncoding.DecodeString(env.TCE)
|
|
if err != nil {
|
|
log.Fatalf("request tce: %v", err)
|
|
}
|
|
req, err := protocol.DecodeApprovalRequest(reqBytes)
|
|
if err != nil {
|
|
log.Fatalf("decode request: %v", err)
|
|
}
|
|
if transport.AddrOf(req.Recipient) != s.Address().String() {
|
|
log.Fatalf("this key is not the recipient of the request")
|
|
}
|
|
var dec protocol.Decision
|
|
switch *decision {
|
|
case "allow":
|
|
dec = protocol.Allow
|
|
case "deny":
|
|
dec = protocol.Deny
|
|
default:
|
|
log.Fatalf("unknown decision %q", *decision)
|
|
}
|
|
resp := &protocol.ApprovalResponse{
|
|
RequestHash: mustID(*request),
|
|
Responder: s.Public(),
|
|
Decision: dec,
|
|
CreatedAt: uint64(time.Now().Unix()),
|
|
Nonce: randBytes(tce.NonceSize),
|
|
}
|
|
tceBytes, err := protocol.EncodeApprovalResponse(resp)
|
|
if err != nil {
|
|
log.Fatalf("encode: %v", err)
|
|
}
|
|
sig := s.Sign(tceBytes)
|
|
postJSON(*url+"/v1/objects", &transport.Envelope{TCE: tceBytes, Signature: sig})
|
|
}
|
|
|
|
func cmdRevoke(args []string) {
|
|
fs := flag.NewFlagSet("revoke", flag.ExitOnError)
|
|
seedPath := fs.String("seed", "", "issuer seed file")
|
|
claim := fs.String("claim", "", "claim object id to withdraw")
|
|
reason := fs.String("reason", "", "revocation reason")
|
|
url := fs.String("url", "http://localhost:8080", "relay URL")
|
|
fs.Parse(args)
|
|
|
|
s := loadSigner(*seedPath)
|
|
rv := &protocol.Revocation{
|
|
Issuer: s.Public(),
|
|
ClaimID: mustID(*claim),
|
|
Reason: *reason,
|
|
CreatedAt: uint64(time.Now().Unix()),
|
|
Nonce: randBytes(tce.NonceSize),
|
|
}
|
|
tceBytes, err := protocol.EncodeRevocation(rv)
|
|
if err != nil {
|
|
log.Fatalf("encode: %v", err)
|
|
}
|
|
sig := s.Sign(tceBytes)
|
|
postJSON(*url+"/v1/objects", &transport.Envelope{TCE: tceBytes, Signature: sig})
|
|
}
|
|
|
|
func cmdRequests(args []string) {
|
|
fs := flag.NewFlagSet("requests", flag.ExitOnError)
|
|
recipient := fs.String("recipient", "", "recipient trust address")
|
|
token := fs.String("token", "", "session token (from auth, scope read:requests)")
|
|
url := fs.String("url", "http://localhost:8080", "relay URL")
|
|
fs.Parse(args)
|
|
getAuthJSON(*url+"/v1/requests?recipient="+*recipient, *token)
|
|
}
|
|
|
|
func cmdResponses(args []string) {
|
|
fs := flag.NewFlagSet("responses", flag.ExitOnError)
|
|
request := fs.String("request", "", "request object id")
|
|
token := fs.String("token", "", "session token (from auth, scope read:responses)")
|
|
url := fs.String("url", "http://localhost:8080", "relay URL")
|
|
fs.Parse(args)
|
|
getAuthJSON(*url+"/v1/responses?request="+*request, *token)
|
|
}
|
|
|
|
func cmdRevocations(args []string) {
|
|
fs := flag.NewFlagSet("revocations", flag.ExitOnError)
|
|
claim := fs.String("claim", "", "claim object id")
|
|
token := fs.String("token", "", "session token (from auth, scope read:revocations)")
|
|
url := fs.String("url", "http://localhost:8080", "relay URL")
|
|
fs.Parse(args)
|
|
getAuthJSON(*url+"/v1/revocations?claim="+*claim, *token)
|
|
}
|
|
|
|
// rawEnvelope decodes a relay envelope JSON object into a transport.Envelope.
|
|
func rawEnvelope(r json.RawMessage) *transport.Envelope {
|
|
var e struct {
|
|
TCE string `json:"tce"`
|
|
Signature string `json:"signature"`
|
|
ObjectID string `json:"object_id"`
|
|
}
|
|
_ = json.Unmarshal(r, &e)
|
|
tceB, err1 := base64.StdEncoding.DecodeString(e.TCE)
|
|
sigB, err2 := base64.StdEncoding.DecodeString(e.Signature)
|
|
if err1 != nil || err2 != nil {
|
|
return nil
|
|
}
|
|
return &transport.Envelope{TCE: tceB, Signature: sigB, ObjectID: e.ObjectID}
|
|
}
|
|
|
|
// fetchEnvelopes GETs a relay list endpoint and returns its envelopes.
|
|
func fetchEnvelopes(u, token string) []*transport.Envelope {
|
|
req, err := http.NewRequest(http.MethodGet, u, nil)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
if token != "" {
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
}
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer resp.Body.Close()
|
|
body, _ := io.ReadAll(resp.Body)
|
|
if resp.StatusCode >= 300 {
|
|
log.Fatalf("%s: %s", resp.Status, string(body))
|
|
}
|
|
var wrap struct {
|
|
Claims []json.RawMessage `json:"claims"`
|
|
Requests []json.RawMessage `json:"requests"`
|
|
Responses []json.RawMessage `json:"responses"`
|
|
Revocations []json.RawMessage `json:"revocations"`
|
|
}
|
|
if err := json.Unmarshal(body, &wrap); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
out := make([]*transport.Envelope, 0)
|
|
for _, r := range wrap.Claims {
|
|
if e := rawEnvelope(r); e != nil {
|
|
out = append(out, e)
|
|
}
|
|
}
|
|
for _, r := range wrap.Requests {
|
|
if e := rawEnvelope(r); e != nil {
|
|
out = append(out, e)
|
|
}
|
|
}
|
|
for _, r := range wrap.Responses {
|
|
if e := rawEnvelope(r); e != nil {
|
|
out = append(out, e)
|
|
}
|
|
}
|
|
for _, r := range wrap.Revocations {
|
|
if e := rawEnvelope(r); e != nil {
|
|
out = append(out, e)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func cmdVerify(args []string) {
|
|
fs := flag.NewFlagSet("verify", flag.ExitOnError)
|
|
subject := fs.String("subject", "", "subject trust address")
|
|
predicate := fs.String("predicate", "", "claim key to evaluate")
|
|
approversStr := fs.String("approvers", "", "comma-separated approver addresses (optional)")
|
|
token := fs.String("token", "", "session token (scope read, or per-endpoint tokens)")
|
|
url := fs.String("url", "http://localhost:8080", "relay URL")
|
|
fs.Parse(args)
|
|
|
|
sub, err := address.Parse(*subject)
|
|
if err != nil {
|
|
log.Fatalf("subject: %v", err)
|
|
}
|
|
var approvers []address.Address
|
|
for _, a := range strings.Split(*approversStr, ",") {
|
|
a = strings.TrimSpace(a)
|
|
if a == "" {
|
|
continue
|
|
}
|
|
ad, err := address.Parse(a)
|
|
if err != nil {
|
|
log.Fatalf("approver %q: %v", a, err)
|
|
}
|
|
approvers = append(approvers, ad)
|
|
}
|
|
|
|
g := verify.NewGraph()
|
|
|
|
// Claims about the subject.
|
|
for _, e := range fetchEnvelopes(*url+"/v1/claims?subject="+*subject, *token) {
|
|
if err := g.Add(e); err != nil {
|
|
log.Printf("skip claim: %v", err)
|
|
}
|
|
}
|
|
// Revocations for each claim we saw.
|
|
for _, e := range fetchEnvelopes(*url+"/v1/claims?subject="+*subject, *token) {
|
|
id := e.ContentID()
|
|
for _, r := range fetchEnvelopes(*url+"/v1/revocations?claim="+id, *token) {
|
|
if err := g.Add(r); err != nil {
|
|
log.Printf("skip revocation: %v", err)
|
|
}
|
|
}
|
|
}
|
|
// Approvals, if required: requests to each approver and their responses.
|
|
for _, ad := range approvers {
|
|
for _, req := range fetchEnvelopes(*url+"/v1/requests?recipient="+ad.String(), *token) {
|
|
if err := g.Add(req); err != nil {
|
|
log.Printf("skip request: %v", err)
|
|
}
|
|
for _, resp := range fetchEnvelopes(*url+"/v1/responses?request="+req.ContentID(), *token) {
|
|
if err := g.Add(resp); err != nil {
|
|
log.Printf("skip response: %v", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
res := g.Evaluate(verify.Policy{
|
|
Subject: sub,
|
|
Predicate: *predicate,
|
|
Approvers: approvers,
|
|
Now: uint64(time.Now().Unix()),
|
|
})
|
|
if res.Trusted {
|
|
fmt.Println("TRUSTED")
|
|
} else {
|
|
fmt.Println("NOT TRUSTED")
|
|
}
|
|
if res.Issuer.String() != "" {
|
|
fmt.Printf("issuer %s\n", res.Issuer)
|
|
}
|
|
if res.ApprovedBy.String() != "" {
|
|
fmt.Printf("approved_by %s\n", res.ApprovedBy)
|
|
}
|
|
if res.Revoked {
|
|
fmt.Println("revoked: yes")
|
|
}
|
|
if res.Reason != "" {
|
|
fmt.Printf("reason %s\n", res.Reason)
|
|
}
|
|
}
|