// Package taskexecution contains task input validation, not policy evaluation. // The human component remains source-only. The B1 service HTTP path can emit // only signed DENY 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 }