Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a126e1-76f8-7643-8cf3-e93336539ade
168 lines
4.7 KiB
Go
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
|
|
}
|