access-engine/internal/taskexecution/identity.go
tegwick 24c1f56f00
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Container Image / build-and-push (push) Successful in 58s
Add proposed task execution policy and initiating identity contract
Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a126e1-76f8-7643-8cf3-e93336539ade
2026-10-11 19:06:01 +02:00

168 lines
4.7 KiB
Go

// 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
}