Add proposed task execution policy and initiating identity contract
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a126e1-76f8-7643-8cf3-e93336539ade
This commit is contained in:
parent
1d3a8727f5
commit
24c1f56f00
8 changed files with 658 additions and 0 deletions
104
internal/policy/task_execution_test.go
Normal file
104
internal/policy/task_execution_test.go
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
package policy_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/netkingdom/access-engine/internal/policy"
|
||||
"github.com/netkingdom/access-engine/pkg/api"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTaskExecutionPackageClosedAdmission(t *testing.T) {
|
||||
p, e := policy.LoadAndValidateFile(context.Background(), filepath.Join("..", "..", "examples", "secrets-engine-task-execution", "policy_package.md"))
|
||||
if e != nil || !p.Valid {
|
||||
t.Fatalf("package: %v %+v", e, p.Validation)
|
||||
}
|
||||
for _, action := range []string{"execute-ccr", "apply", "exec"} {
|
||||
r := api.CheckRequest{Tenant: "tenant:platform", Subject: api.SubjectRef{ID: "secrets-engine", Type: "service"}, Action: action, Resource: api.ResourceRef{ID: "synthetic", Type: "secret-catalog-lane", System: "secrets-engine"}, Context: map[string]any{"owner_admission": true, "verified": true, "human_control": true, "task_execution_projection": map[string]any{"verified": true}}}
|
||||
d, e := p.Evaluate(context.Background(), r)
|
||||
if e != nil || d.Effect != api.DecisionEffectDeny {
|
||||
t.Fatalf("unsigned caller opened admission: %v %+v", e, d)
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestTaskExecutionSourceHasNoActivationSwitch(t *testing.T) {
|
||||
raw, e := os.ReadFile(filepath.Join("..", "..", "examples", "secrets-engine-task-execution", "policy_package.md"))
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
if !strings.Contains(string(raw), "owner_admission := false") {
|
||||
t.Fatal("source refusal gate absent")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskExecutionPythonDescriptorAndSyntheticFutureProjection(t *testing.T) {
|
||||
output, e := exec.Command("/home/worsch/secrets-engine/.venv/bin/python", "../../tools/task_execution_interop_fixture.py").Output()
|
||||
if e != nil {
|
||||
t.Fatal("Python actual descriptor/canonical generator failed:", e)
|
||||
}
|
||||
var fixture struct {
|
||||
Request api.CheckRequest `json:"request"`
|
||||
RequestDigest string `json:"request_digest"`
|
||||
ApprovalDigest string `json:"approval_binding_digest"`
|
||||
DescriptorDigest string `json:"descriptor_digest"`
|
||||
}
|
||||
if json.Unmarshal(output, &fixture) != nil {
|
||||
t.Fatal("fixture JSON")
|
||||
}
|
||||
r := fixture.Request
|
||||
if api.RequestDigest(r) != fixture.RequestDigest || api.ApprovalBindingDigest(r) != fixture.ApprovalDigest || api.CanonicalDigest(r.Context["execution_contract"]) != fixture.DescriptorDigest {
|
||||
t.Fatal("actual Python/Go canonical mismatch")
|
||||
}
|
||||
raw, e := os.ReadFile(filepath.Join("..", "..", "examples", "secrets-engine-task-execution", "policy_package.md"))
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
// This source replacement is intentionally test-only; no caller/config flag
|
||||
// can change the production package's hard admission=false constant.
|
||||
raw = []byte(strings.Replace(string(raw), "owner_admission := false", "owner_admission := true", 1))
|
||||
p, e := policy.Load(raw, "synthetic-future-provider-test.md")
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
d, e := p.Evaluate(context.Background(), r)
|
||||
if e != nil || d.Effect != api.DecisionEffectAllow {
|
||||
t.Fatalf("synthetic future projection did not match: %v %+v", e, d)
|
||||
}
|
||||
cases := map[string]func(*api.CheckRequest){
|
||||
"action": func(r *api.CheckRequest) { r.Action = "apply" }, "service subject": func(r *api.CheckRequest) { r.Subject.ID = "other" }, "foreign tenant": func(r *api.CheckRequest) { r.Tenant = "tenant:coulomb" },
|
||||
"plan hash": func(r *api.CheckRequest) {
|
||||
r.Context["execution_contract"].(map[string]any)["expected_after"] = "sha256:" + strings.Repeat("b", 64)
|
||||
},
|
||||
"unsigned human": func(r *api.CheckRequest) { delete(r.Context, "task_execution_projection") },
|
||||
"registered tenant": func(r *api.CheckRequest) {
|
||||
r.Context["task_execution_projection"].(map[string]any)["tenant_source"] = "registration"
|
||||
},
|
||||
"membership": func(r *api.CheckRequest) {
|
||||
r.Context["task_execution_projection"].(map[string]any)["member_active"] = false
|
||||
},
|
||||
"transport": func(r *api.CheckRequest) {
|
||||
r.Context["task_execution_projection"].(map[string]any)["transport_principal"] = "other"
|
||||
},
|
||||
"expiry": func(r *api.CheckRequest) {
|
||||
r.Context["task_execution_projection"].(map[string]any)["not_after_ns"] = float64(1)
|
||||
},
|
||||
"no human control": func(r *api.CheckRequest) { r.Context["human_control"] = false },
|
||||
}
|
||||
for name, mutate := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
var f struct {
|
||||
Request api.CheckRequest `json:"request"`
|
||||
}
|
||||
json.Unmarshal(output, &f)
|
||||
mutate(&f.Request)
|
||||
d, e := p.Evaluate(context.Background(), f.Request)
|
||||
if e != nil || d.Effect != api.DecisionEffectDeny {
|
||||
t.Fatalf("mutation allowed: %v %+v", e, d)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
168
internal/taskexecution/identity.go
Normal file
168
internal/taskexecution/identity.go
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
// Package taskexecution contains proposed source-only identity input validation.
|
||||
// It is not registered with HTTP evaluation and grants no executable admission.
|
||||
package taskexecution
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var ErrIdentity = errors.New("task execution initiating identity refused")
|
||||
|
||||
// IssuerPins must come from reviewed owner configuration, never CheckRequest.
|
||||
// No production pins or default issuer/client are supplied by this package.
|
||||
type IssuerPins struct {
|
||||
Issuer, Audience string
|
||||
Keys map[string]*rsa.PublicKey
|
||||
}
|
||||
|
||||
// Initiator has no exported constructor or mutable fields. Verification proves
|
||||
// identity only, not membership, representation, approval or policy allowance.
|
||||
type Initiator struct {
|
||||
subject string
|
||||
expires time.Time
|
||||
}
|
||||
|
||||
func (i Initiator) Subject() string { return i.subject }
|
||||
func (i Initiator) Expires() time.Time { return i.expires }
|
||||
|
||||
// strictObject rejects duplicate names (including nested ones), trailing JSON
|
||||
// and excessive depth. JWT claims are never copied to logs or CheckRequest.
|
||||
func strictObject(raw []byte, out any) error {
|
||||
d := json.NewDecoder(bytes.NewReader(raw))
|
||||
depth := 0
|
||||
var walk func() error
|
||||
walk = func() error {
|
||||
depth++
|
||||
defer func() { depth-- }()
|
||||
if depth > 16 {
|
||||
return ErrIdentity
|
||||
}
|
||||
t, e := d.Token()
|
||||
if e != nil {
|
||||
return ErrIdentity
|
||||
}
|
||||
delim, ok := t.(json.Delim)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
switch delim {
|
||||
case '{':
|
||||
seen := map[string]bool{}
|
||||
for d.More() {
|
||||
k, e := d.Token()
|
||||
s, ok := k.(string)
|
||||
if e != nil || !ok || seen[s] {
|
||||
return ErrIdentity
|
||||
}
|
||||
seen[s] = true
|
||||
if walk() != nil {
|
||||
return ErrIdentity
|
||||
}
|
||||
}
|
||||
case '[':
|
||||
for d.More() {
|
||||
if walk() != nil {
|
||||
return ErrIdentity
|
||||
}
|
||||
}
|
||||
default:
|
||||
return ErrIdentity
|
||||
}
|
||||
_, e = d.Token()
|
||||
return e
|
||||
}
|
||||
if walk() != nil {
|
||||
return ErrIdentity
|
||||
}
|
||||
if _, e := d.Token(); e == nil {
|
||||
return ErrIdentity
|
||||
}
|
||||
if json.Unmarshal(raw, out) != nil {
|
||||
return ErrIdentity
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyKeyCapeHuman reuses KeyCape's RS256 access-token claim contract. Pins
|
||||
// and trusted time are server inputs. No URL/key discovery from token headers.
|
||||
func VerifyKeyCapeHuman(token string, pins IssuerPins, now time.Time) (Initiator, error) {
|
||||
refuse := func() (Initiator, error) { return Initiator{}, ErrIdentity }
|
||||
if len(token) > 16384 || len(token) == 0 || pins.Issuer == "" || pins.Audience == "" || now.IsZero() {
|
||||
return refuse()
|
||||
}
|
||||
parts := strings.Split(token, ".")
|
||||
if len(parts) != 3 {
|
||||
return refuse()
|
||||
}
|
||||
decode := func(s string) ([]byte, error) { return base64.RawURLEncoding.Strict().DecodeString(s) }
|
||||
header, e := decode(parts[0])
|
||||
if e != nil {
|
||||
return refuse()
|
||||
}
|
||||
payload, e := decode(parts[1])
|
||||
if e != nil {
|
||||
return refuse()
|
||||
}
|
||||
signature, e := decode(parts[2])
|
||||
if e != nil {
|
||||
return refuse()
|
||||
}
|
||||
var h map[string]any
|
||||
if strictObject(header, &h) != nil || h["alg"] != "RS256" {
|
||||
return refuse()
|
||||
}
|
||||
kid, ok := h["kid"].(string)
|
||||
if !ok {
|
||||
return refuse()
|
||||
}
|
||||
for k := range h {
|
||||
if k != "alg" && k != "kid" && k != "typ" {
|
||||
return refuse()
|
||||
}
|
||||
}
|
||||
key := pins.Keys[kid]
|
||||
if key == nil || key.N == nil || key.N.BitLen() < 2048 || key.E < 3 {
|
||||
return refuse()
|
||||
}
|
||||
digest := sha256.Sum256([]byte(parts[0] + "." + parts[1]))
|
||||
if rsa.VerifyPKCS1v15(key, crypto.SHA256, digest[:], signature) != nil {
|
||||
return refuse()
|
||||
}
|
||||
var c struct {
|
||||
Issuer string `json:"iss"`
|
||||
Audience json.RawMessage `json:"aud"`
|
||||
Subject string `json:"sub"`
|
||||
Principal string `json:"principal_type"`
|
||||
Tenant string `json:"tenant"`
|
||||
TenantSource string `json:"tenant_source"`
|
||||
Expires int64 `json:"exp"`
|
||||
Issued int64 `json:"iat"`
|
||||
NotBefore int64 `json:"nbf"`
|
||||
Assurance struct {
|
||||
Level string `json:"level"`
|
||||
MFA bool `json:"mfa"`
|
||||
Source string `json:"source"`
|
||||
At int64 `json:"at"`
|
||||
} `json:"assurance"`
|
||||
}
|
||||
if strictObject(payload, &c) != nil {
|
||||
return refuse()
|
||||
}
|
||||
var aud string
|
||||
if json.Unmarshal(c.Audience, &aud) != nil || aud != pins.Audience {
|
||||
return refuse()
|
||||
}
|
||||
n := now.Unix()
|
||||
if c.Issuer != pins.Issuer || c.Subject == "" || len(c.Subject) > 512 || strings.ContainsAny(c.Subject, "\r\n") || c.Principal != "human" || c.Tenant != "tenant:platform" || c.TenantSource != "directory" || c.Expires <= n || c.Issued > n || c.Issued <= 0 || c.NotBefore > n || c.Expires <= c.Issued || c.Assurance.Level != "aal2" || !c.Assurance.MFA || c.Assurance.Source != "key-cape" || c.Assurance.At <= 0 || c.Assurance.At > n || n-c.Assurance.At > 300 {
|
||||
return refuse()
|
||||
}
|
||||
return Initiator{subject: c.Subject, expires: time.Unix(c.Expires, 0)}, nil
|
||||
}
|
||||
61
internal/taskexecution/identity_test.go
Normal file
61
internal/taskexecution/identity_test.go
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
package taskexecution
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCryptographicInitiatorContract(t *testing.T) {
|
||||
key, e := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
now := time.Now()
|
||||
pins := IssuerPins{Issuer: "https://synthetic.invalid", Audience: "synthetic-task-test", Keys: map[string]*rsa.PublicKey{"synthetic": &key.PublicKey}}
|
||||
claims := func() map[string]any {
|
||||
return map[string]any{"iss": pins.Issuer, "aud": pins.Audience, "sub": "synthetic-human", "principal_type": "human", "tenant": "tenant:platform", "tenant_source": "directory", "iat": now.Unix() - 1, "exp": now.Unix() + 60, "assurance": map[string]any{"level": "aal2", "mfa": true, "source": "key-cape", "at": now.Unix() - 1}}
|
||||
}
|
||||
sign := func(c map[string]any) string {
|
||||
h, _ := json.Marshal(map[string]any{"alg": "RS256", "kid": "synthetic"})
|
||||
b, _ := json.Marshal(c)
|
||||
v := base64.RawURLEncoding.EncodeToString(h) + "." + base64.RawURLEncoding.EncodeToString(b)
|
||||
d := sha256.Sum256([]byte(v))
|
||||
s, _ := rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA256, d[:])
|
||||
return v + "." + base64.RawURLEncoding.EncodeToString(s)
|
||||
}
|
||||
token := sign(claims())
|
||||
i, e := VerifyKeyCapeHuman(token, pins, now)
|
||||
if e != nil || i.Subject() != "synthetic-human" {
|
||||
t.Fatal("synthetic signature verification failed")
|
||||
}
|
||||
cases := map[string]func(map[string]any){"service": func(c map[string]any) { c["principal_type"] = "service" }, "registered tenant": func(c map[string]any) { c["tenant_source"] = "registration" }, "foreign tenant": func(c map[string]any) { c["tenant"] = "tenant:coulomb" }, "issuer": func(c map[string]any) { c["iss"] = "https://wrong.invalid" }, "audience": func(c map[string]any) { c["aud"] = "approval-engine" }, "expired": func(c map[string]any) { c["exp"] = now.Unix() }, "future": func(c map[string]any) { c["nbf"] = now.Unix() + 1 }, "no mfa": func(c map[string]any) { c["assurance"].(map[string]any)["mfa"] = false }, "old mfa": func(c map[string]any) { c["assurance"].(map[string]any)["at"] = now.Unix() - 301 }}
|
||||
for name, mutate := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
c := claims()
|
||||
mutate(c)
|
||||
if _, e := VerifyKeyCapeHuman(sign(c), pins, now); e == nil {
|
||||
t.Fatal("invalid human accepted")
|
||||
}
|
||||
})
|
||||
}
|
||||
if _, e := VerifyKeyCapeHuman(token, IssuerPins{}, now); e == nil {
|
||||
t.Fatal("unadmitted issuer accepted")
|
||||
}
|
||||
if _, e := VerifyKeyCapeHuman(token+"x", pins, now); e == nil {
|
||||
t.Fatal("tampered signature accepted")
|
||||
}
|
||||
}
|
||||
func TestStrictClaimJSON(t *testing.T) {
|
||||
for _, raw := range []string{`{"iss":"a","iss":"b"}`, `{"assurance":{"mfa":true,"mfa":false}}`, `{"x":1} {"x":2}`} {
|
||||
var v map[string]any
|
||||
if strictObject([]byte(raw), &v) == nil {
|
||||
t.Fatal("ambiguous claims accepted")
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue