Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786
170 lines
5.7 KiB
Go
170 lines
5.7 KiB
Go
package approvalfact
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"regexp"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
var errClaim = errors.New("Approval fact does not match required claim")
|
|
var digestPattern = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`)
|
|
var uuidPattern = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`)
|
|
var timePattern = regexp.MustCompile(`^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,9})?(Z|[+-]([01][0-9]|2[0-3]):[0-5][0-9])$`)
|
|
|
|
// Expected contains independently owner-derived native act bytes and the actual
|
|
// Access approval-free digest. Marshaling an already float64-decoded Go request
|
|
// is not an authoritative reconstruction of the native act.
|
|
type Expected struct {
|
|
ApprovalID string
|
|
NativeBindingJSON []byte
|
|
PDPDigest string
|
|
}
|
|
|
|
// Fact is validated data shape/correspondence, NOT authenticated provider origin
|
|
// or execution authority. Access must wrap it only after its own source admission.
|
|
type Fact struct {
|
|
raw []byte
|
|
stable string
|
|
digest string
|
|
observed, timeLimit, validatedAt time.Time
|
|
}
|
|
|
|
func (f Fact) RawJSON() []byte { return append([]byte(nil), f.raw...) }
|
|
func (f Fact) ObservedAt() time.Time { return f.observed }
|
|
func (f Fact) Deadline() time.Time { return f.timeLimit }
|
|
func (f Fact) NativeDigest() string { return f.digest }
|
|
|
|
func members(m map[string]any, required, optional []string) bool {
|
|
allowed := map[string]bool{}
|
|
for _, k := range required {
|
|
allowed[k] = true
|
|
if _, ok := m[k]; !ok {
|
|
return false
|
|
}
|
|
}
|
|
for _, k := range optional {
|
|
allowed[k] = true
|
|
}
|
|
for k := range m {
|
|
if !allowed[k] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
func object(v any) (map[string]any, bool) { m, ok := v.(map[string]any); return m, ok }
|
|
func stamp(v any) (time.Time, error) {
|
|
s, ok := v.(string)
|
|
if !ok || !timePattern.MatchString(s) {
|
|
return time.Time{}, errClaim
|
|
}
|
|
t, e := time.Parse(time.RFC3339Nano, s)
|
|
if e != nil {
|
|
return time.Time{}, errClaim
|
|
}
|
|
return t, nil
|
|
}
|
|
|
|
// ValidateClaim checks one original or fetched schema-0.1 human/PDP-path claim
|
|
// at a caller-captured instant, using the existing Approval producer semantics.
|
|
// It has no I/O, policy evaluator, credentials, cache, or consume mutation.
|
|
func ValidateClaim(raw []byte, expected Expected, now time.Time) (Fact, error) {
|
|
if now.IsZero() || !uuidPattern.MatchString(expected.ApprovalID) || !digestPattern.MatchString(expected.PDPDigest) {
|
|
return Fact{}, errClaim
|
|
}
|
|
_, want, e := CanonicalBinding(expected.NativeBindingJSON)
|
|
if e != nil {
|
|
return Fact{}, errClaim
|
|
}
|
|
v, e := DecodeJSON(raw)
|
|
if e != nil {
|
|
return Fact{}, errClaim
|
|
}
|
|
m, ok := object(v)
|
|
if !ok || !members(m, []string{"schema_version", "kind", "issuer", "approval_id", "state", "valid_now", "consumed", "binding", "freshness", "validity", "reason_code"}, []string{"yields_to"}) {
|
|
return Fact{}, errClaim
|
|
}
|
|
if y, ok := m["yields_to"]; ok {
|
|
if _, ok = y.(string); !ok {
|
|
return Fact{}, errClaim
|
|
}
|
|
}
|
|
if m["schema_version"] != "0.1" || m["kind"] != "approval-claim" || m["issuer"] != "approval-engine" || m["approval_id"] != expected.ApprovalID || m["state"] != "valid" || m["valid_now"] != true || m["consumed"] != false || m["reason_code"] != "ok" {
|
|
return Fact{}, errClaim
|
|
}
|
|
b, ok := object(m["binding"])
|
|
if !ok || !members(b, []string{"action", "actor", "principal", "purpose", "target", "digest", "pdp_digest", "pdp_path", "human_control"}, nil) {
|
|
return Fact{}, errClaim
|
|
}
|
|
if b["digest"] != want || b["pdp_digest"] != expected.PDPDigest || b["pdp_path"] != true || b["human_control"] != true {
|
|
return Fact{}, errClaim
|
|
}
|
|
text, e := canonical(b)
|
|
if e != nil {
|
|
return Fact{}, errClaim
|
|
}
|
|
_, got, e := CanonicalBinding([]byte(text))
|
|
if e != nil || got != want {
|
|
return Fact{}, errClaim
|
|
}
|
|
f, ok := object(m["freshness"])
|
|
if !ok || !members(f, []string{"observed_at", "ttl_seconds", "not_after"}, nil) {
|
|
return Fact{}, errClaim
|
|
}
|
|
observed, e := stamp(f["observed_at"])
|
|
if e != nil {
|
|
return Fact{}, e
|
|
}
|
|
notAfter, e := stamp(f["not_after"])
|
|
if e != nil {
|
|
return Fact{}, e
|
|
}
|
|
ttl, ok := f["ttl_seconds"].(json.Number)
|
|
if !ok {
|
|
return Fact{}, errClaim
|
|
}
|
|
seconds, e := strconv.ParseInt(string(ttl), 10, 64)
|
|
if e != nil || seconds < 1 {
|
|
return Fact{}, errClaim
|
|
}
|
|
// Bound arithmetic to time.Duration's representable seconds, not a new
|
|
// semantic freshness grant or configured TTL. Unsupported extremes refuse.
|
|
if seconds > int64((1<<63-1)/int64(time.Second)) || !observed.Add(time.Duration(seconds)*time.Second).Equal(notAfter) || now.Before(observed) || !now.Before(notAfter) {
|
|
return Fact{}, errClaim
|
|
}
|
|
validity, ok := object(m["validity"])
|
|
if !ok || !members(validity, []string{"not_before", "expires_at"}, nil) {
|
|
return Fact{}, errClaim
|
|
}
|
|
before, e := stamp(validity["not_before"])
|
|
if e != nil {
|
|
return Fact{}, e
|
|
}
|
|
expires, e := stamp(validity["expires_at"])
|
|
if e != nil || now.Before(before) || !now.Before(expires) {
|
|
return Fact{}, errClaim
|
|
}
|
|
limit := notAfter
|
|
if expires.Before(limit) {
|
|
limit = expires
|
|
}
|
|
delete(m, "freshness")
|
|
delete(m, "yields_to")
|
|
stable, e := canonical(m)
|
|
if e != nil {
|
|
return Fact{}, errClaim
|
|
}
|
|
return Fact{raw: append([]byte(nil), raw...), stable: stable, digest: want, observed: observed, timeLimit: limit, validatedAt: now}, nil
|
|
}
|
|
|
|
// MatchObserved requires independently validated original/fetched facts at the
|
|
// same captured instant and exact nonfreshness authority correspondence. It
|
|
// never rewrites the original request or extends its original deadline.
|
|
func MatchObserved(submitted, observed Fact) error {
|
|
if submitted.stable == "" || observed.stable == "" || !submitted.validatedAt.Equal(observed.validatedAt) || submitted.stable != observed.stable {
|
|
return errClaim
|
|
}
|
|
return nil
|
|
}
|