feat: migrate refimpls from draft-00 to draft-01 claim names
- Rename `par` to `pred` (predecessor) in types, serialization, tests - Remove `pol`, `pol_decision` from core payload; move to `ect_ext` - Remove `sub` from payload (not part of ECT spec) - Update `typ` from `wimse-exec+jwt` to `exec+jwt` (accept both) - Rename MaxParLength to MaxPredLength everywhere - Update testdata, demos, READMEs with migration table - All Go tests pass, all 56 Python tests pass (90% coverage)
This commit is contained in:
@@ -21,8 +21,8 @@ type CreateOptions struct {
|
||||
DefaultExpiry time.Duration
|
||||
// ValidateUUIDs when true requires jti and wid (if set) to be UUID format (RFC 9562).
|
||||
ValidateUUIDs bool
|
||||
// MaxParLength is the max number of parent references (0 = no limit; recommended 100).
|
||||
MaxParLength int
|
||||
// MaxPredLength is the max number of predecessor references (0 = no limit; recommended 100).
|
||||
MaxPredLength int
|
||||
}
|
||||
|
||||
// DefaultCreateOptions returns recommended defaults.
|
||||
@@ -53,11 +53,8 @@ func Create(payload *Payload, privateKey *ecdsa.PrivateKey, opts CreateOptions)
|
||||
}
|
||||
payload.Exp = now.Add(opts.DefaultExpiry).Unix()
|
||||
}
|
||||
if payload.Sub == "" {
|
||||
payload.Sub = payload.Iss
|
||||
}
|
||||
if payload.Par == nil {
|
||||
payload.Par = []string{}
|
||||
if payload.Pred == nil {
|
||||
payload.Pred = []string{}
|
||||
}
|
||||
|
||||
if err := validatePayloadForCreate(payload, opts); err != nil {
|
||||
@@ -110,8 +107,8 @@ func validatePayloadForCreate(p *Payload, opts CreateOptions) error {
|
||||
return ErrInvalidWID
|
||||
}
|
||||
}
|
||||
if opts.MaxParLength > 0 && len(p.Par) > opts.MaxParLength {
|
||||
return ErrParLength
|
||||
if opts.MaxPredLength > 0 && len(p.Pred) > opts.MaxPredLength {
|
||||
return ErrPredLength
|
||||
}
|
||||
if p.InpHash != "" {
|
||||
if err := ValidateHashFormat(p.InpHash); err != nil {
|
||||
@@ -126,15 +123,6 @@ func validatePayloadForCreate(p *Payload, opts CreateOptions) error {
|
||||
if err := ValidateExt(p.Ext); err != nil {
|
||||
return err
|
||||
}
|
||||
// pol/pol_decision are OPTIONAL; if either is set, both must be present and valid
|
||||
if p.Pol != "" || p.PolDecision != "" {
|
||||
if p.Pol == "" || p.PolDecision == "" {
|
||||
return ErrPolPolDecisionPair
|
||||
}
|
||||
if !ValidPolDecision(p.PolDecision) {
|
||||
return ErrInvalidPolDecision
|
||||
}
|
||||
}
|
||||
// compensation_* live in ext per spec
|
||||
if p.Ext != nil {
|
||||
if _, hasReason := p.Ext["compensation_reason"]; hasReason {
|
||||
|
||||
@@ -15,15 +15,13 @@ func TestCreateRoundtrip(t *testing.T) {
|
||||
}
|
||||
now := time.Now()
|
||||
payload := &Payload{
|
||||
Iss: "spiffe://example.com/agent/a",
|
||||
Aud: []string{"spiffe://example.com/agent/b"},
|
||||
Iat: now.Unix(),
|
||||
Exp: now.Add(10 * time.Minute).Unix(),
|
||||
Jti: "e4f5a6b7-c8d9-0123-ef01-234567890abc",
|
||||
ExecAct: "review_spec",
|
||||
Par: []string{},
|
||||
Pol: "spec_review_policy_v2",
|
||||
PolDecision: PolDecisionApproved,
|
||||
Iss: "spiffe://example.com/agent/a",
|
||||
Aud: []string{"spiffe://example.com/agent/b"},
|
||||
Iat: now.Unix(),
|
||||
Exp: now.Add(10 * time.Minute).Unix(),
|
||||
Jti: "e4f5a6b7-c8d9-0123-ef01-234567890abc",
|
||||
ExecAct: "review_spec",
|
||||
Pred: []string{},
|
||||
}
|
||||
compact, err := Create(payload, key, CreateOptions{KeyID: "agent-a-key-1"})
|
||||
if err != nil {
|
||||
@@ -68,7 +66,7 @@ func TestDefaultCreateOptions(t *testing.T) {
|
||||
|
||||
func TestCreate_Errors(t *testing.T) {
|
||||
key, _ := GenerateKey()
|
||||
payload := &Payload{Iss: "i", Aud: []string{"a"}, Jti: "j", ExecAct: "e", Par: []string{}, Pol: "p", PolDecision: PolDecisionApproved, Iat: 1, Exp: 2}
|
||||
payload := &Payload{Iss: "i", Aud: []string{"a"}, Jti: "j", ExecAct: "e", Pred: []string{}, Iat: 1, Exp: 2}
|
||||
if _, err := Create(nil, key, CreateOptions{KeyID: "k"}); err == nil {
|
||||
t.Error("expected error for nil payload")
|
||||
}
|
||||
@@ -85,7 +83,7 @@ func TestCreate_OptionalPol(t *testing.T) {
|
||||
now := time.Now()
|
||||
payload := &Payload{
|
||||
Iss: "iss", Aud: []string{"aud"}, Iat: now.Unix(), Exp: now.Add(time.Hour).Unix(),
|
||||
Jti: "jti-nopol", ExecAct: "act", Par: []string{},
|
||||
Jti: "jti-nopol", ExecAct: "act", Pred: []string{},
|
||||
}
|
||||
compact, err := Create(payload, key, CreateOptions{KeyID: "kid"})
|
||||
if err != nil {
|
||||
@@ -100,7 +98,7 @@ func TestCreate_ZeroExpiryUsesDefault(t *testing.T) {
|
||||
key, _ := GenerateKey()
|
||||
payload := &Payload{
|
||||
Iss: "i", Aud: []string{"a"}, Iat: 0, Exp: 0,
|
||||
Jti: "jti-z", ExecAct: "e", Par: []string{},
|
||||
Jti: "jti-z", ExecAct: "e", Pred: []string{},
|
||||
}
|
||||
_, err := Create(payload, key, CreateOptions{KeyID: "kid", DefaultExpiry: 5 * time.Minute})
|
||||
if err != nil {
|
||||
@@ -115,7 +113,7 @@ func TestCreate_ExtCompensationReasonRequiresRequired(t *testing.T) {
|
||||
key, _ := GenerateKey()
|
||||
payload := &Payload{
|
||||
Iss: "i", Aud: []string{"a"}, Iat: 1, Exp: 2,
|
||||
Jti: "j", ExecAct: "e", Par: []string{},
|
||||
Jti: "j", ExecAct: "e", Pred: []string{},
|
||||
Ext: map[string]interface{}{"compensation_reason": "rollback", "compensation_required": false},
|
||||
}
|
||||
_, err := Create(payload, key, CreateOptions{KeyID: "k"})
|
||||
@@ -130,12 +128,10 @@ func TestCreate_ValidationErrors(t *testing.T) {
|
||||
name string
|
||||
p *Payload
|
||||
}{
|
||||
{"missing iss", &Payload{Iss: "", Aud: []string{"a"}, Jti: "j", ExecAct: "e", Par: []string{}, Iat: 1, Exp: 2}},
|
||||
{"missing aud", &Payload{Iss: "i", Aud: nil, Jti: "j", ExecAct: "e", Par: []string{}, Iat: 1, Exp: 2}},
|
||||
{"missing jti", &Payload{Iss: "i", Aud: []string{"a"}, Jti: "", ExecAct: "e", Par: []string{}, Iat: 1, Exp: 2}},
|
||||
{"missing exec_act", &Payload{Iss: "i", Aud: []string{"a"}, Jti: "j", ExecAct: "", Par: []string{}, Iat: 1, Exp: 2}},
|
||||
{"pol without pol_decision", &Payload{Iss: "i", Aud: []string{"a"}, Jti: "j", ExecAct: "e", Par: []string{}, Pol: "p", PolDecision: "", Iat: 1, Exp: 2}},
|
||||
{"invalid pol_decision", &Payload{Iss: "i", Aud: []string{"a"}, Jti: "j", ExecAct: "e", Par: []string{}, Pol: "p", PolDecision: "bad", Iat: 1, Exp: 2}},
|
||||
{"missing iss", &Payload{Iss: "", Aud: []string{"a"}, Jti: "j", ExecAct: "e", Pred: []string{}, Iat: 1, Exp: 2}},
|
||||
{"missing aud", &Payload{Iss: "i", Aud: nil, Jti: "j", ExecAct: "e", Pred: []string{}, Iat: 1, Exp: 2}},
|
||||
{"missing jti", &Payload{Iss: "i", Aud: []string{"a"}, Jti: "", ExecAct: "e", Pred: []string{}, Iat: 1, Exp: 2}},
|
||||
{"missing exec_act", &Payload{Iss: "i", Aud: []string{"a"}, Jti: "j", ExecAct: "", Pred: []string{}, Iat: 1, Exp: 2}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
@@ -24,7 +24,7 @@ type ECTStore interface {
|
||||
type DAGConfig struct {
|
||||
ClockSkewTolerance int // seconds; recommended 30
|
||||
MaxAncestorLimit int // recommended 10000
|
||||
MaxParLength int // max par length (0 = no limit; recommended 100)
|
||||
MaxPredLength int // max pred length (0 = no limit; recommended 100)
|
||||
}
|
||||
|
||||
// DefaultDAGConfig returns recommended defaults.
|
||||
@@ -32,7 +32,7 @@ func DefaultDAGConfig() DAGConfig {
|
||||
return DAGConfig{
|
||||
ClockSkewTolerance: DefaultClockSkewTolerance,
|
||||
MaxAncestorLimit: DefaultMaxAncestorLimit,
|
||||
MaxParLength: DefaultMaxParLength,
|
||||
MaxPredLength: DefaultMaxPredLength,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,8 +48,8 @@ func ValidateDAG(ect *Payload, store ECTStore, cfg DAGConfig) error {
|
||||
if cfg.MaxAncestorLimit <= 0 {
|
||||
cfg.MaxAncestorLimit = DefaultMaxAncestorLimit
|
||||
}
|
||||
if cfg.MaxParLength > 0 && len(ect.Par) > cfg.MaxParLength {
|
||||
return ErrParLength
|
||||
if cfg.MaxPredLength > 0 && len(ect.Pred) > cfg.MaxPredLength {
|
||||
return ErrPredLength
|
||||
}
|
||||
|
||||
// 1. Task ID Uniqueness (task id = jti per spec)
|
||||
@@ -57,31 +57,33 @@ func ValidateDAG(ect *Payload, store ECTStore, cfg DAGConfig) error {
|
||||
return fmt.Errorf("ect: task ID (jti) already exists: %s", ect.Jti)
|
||||
}
|
||||
|
||||
// 2. Parent Existence and 3. Temporal Ordering
|
||||
for _, parentID := range ect.Par {
|
||||
parent := store.GetByTid(parentID)
|
||||
if parent == nil {
|
||||
return fmt.Errorf("ect: parent task not found: %s", parentID)
|
||||
// 2. Predecessor Existence and 3. Temporal Ordering
|
||||
for _, predID := range ect.Pred {
|
||||
pred := store.GetByTid(predID)
|
||||
if pred == nil {
|
||||
return fmt.Errorf("ect: predecessor task not found: %s", predID)
|
||||
}
|
||||
// parent.iat < child.iat + clock_skew_tolerance => parent.iat - ect.iat <= clock_skew_tolerance
|
||||
if parent.Iat >= ect.Iat+int64(cfg.ClockSkewTolerance) {
|
||||
return fmt.Errorf("ect: parent task not earlier than current: %s", parentID)
|
||||
// pred.iat < child.iat + clock_skew_tolerance
|
||||
if pred.Iat >= ect.Iat+int64(cfg.ClockSkewTolerance) {
|
||||
return fmt.Errorf("ect: predecessor task not earlier than current: %s", predID)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Acyclicity (and depth limit)
|
||||
visited := make(map[string]struct{})
|
||||
if hasCycle(ect.Jti, ect.Par, store, visited, cfg.MaxAncestorLimit) {
|
||||
if hasCycle(ect.Jti, ect.Pred, store, visited, cfg.MaxAncestorLimit) {
|
||||
return errors.New("ect: circular dependency or depth limit exceeded")
|
||||
}
|
||||
|
||||
// 5. Parent Policy Decision (only when parent has policy claims per spec)
|
||||
for _, parentID := range ect.Par {
|
||||
parent := store.GetByTid(parentID)
|
||||
if parent != nil && parent.HasPolicyClaims() &&
|
||||
(parent.PolDecision == PolDecisionRejected || parent.PolDecision == PolDecisionPendingHumanReview) {
|
||||
if !ect.CompensationRequired() {
|
||||
return errors.New("ect: parent has non-approved pol_decision; current ECT must be compensation/remediation or have ext.compensation_required true")
|
||||
// 5. Predecessor Policy Decision (only when predecessor has policy claims in ext per -01)
|
||||
for _, predID := range ect.Pred {
|
||||
pred := store.GetByTid(predID)
|
||||
if pred != nil && pred.HasPolicyClaims() {
|
||||
polDec := pred.PolDecision()
|
||||
if polDec == "rejected" || polDec == "pending_human_review" {
|
||||
if !ect.CompensationRequired() {
|
||||
return errors.New("ect: predecessor has non-approved pol_decision; current ECT must be compensation/remediation or have ext.compensation_required true")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,23 +91,23 @@ func ValidateDAG(ect *Payload, store ECTStore, cfg DAGConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// hasCycle returns true if following par from the given parent IDs leads back to targetTid
|
||||
// hasCycle returns true if following pred from the given predecessor IDs leads back to targetTid
|
||||
// or if traversal exceeds maxDepth. visited is mutated.
|
||||
func hasCycle(targetTid string, parentIDs []string, store ECTStore, visited map[string]struct{}, maxDepth int) bool {
|
||||
func hasCycle(targetTid string, predIDs []string, store ECTStore, visited map[string]struct{}, maxDepth int) bool {
|
||||
if len(visited) >= maxDepth {
|
||||
return true
|
||||
}
|
||||
for _, parentID := range parentIDs {
|
||||
if parentID == targetTid {
|
||||
for _, predID := range predIDs {
|
||||
if predID == targetTid {
|
||||
return true
|
||||
}
|
||||
if _, ok := visited[parentID]; ok {
|
||||
if _, ok := visited[predID]; ok {
|
||||
continue
|
||||
}
|
||||
visited[parentID] = struct{}{}
|
||||
parent := store.GetByTid(parentID)
|
||||
if parent != nil {
|
||||
if hasCycle(targetTid, parent.Par, store, visited, maxDepth) {
|
||||
visited[predID] = struct{}{}
|
||||
pred := store.GetByTid(predID)
|
||||
if pred != nil {
|
||||
if hasCycle(targetTid, pred.Pred, store, visited, maxDepth) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,9 @@ import (
|
||||
func TestValidateDAG_Root(t *testing.T) {
|
||||
store := NewMemoryLedger()
|
||||
payload := &Payload{
|
||||
Jti: "jti-001",
|
||||
Wid: "wf-1",
|
||||
Par: []string{},
|
||||
PolDecision: PolDecisionApproved,
|
||||
Jti: "jti-001",
|
||||
Wid: "wf-1",
|
||||
Pred: []string{},
|
||||
}
|
||||
err := ValidateDAG(payload, store, DefaultDAGConfig())
|
||||
if err != nil {
|
||||
@@ -21,23 +20,22 @@ func TestValidateDAG_Root(t *testing.T) {
|
||||
|
||||
func TestValidateDAG_DuplicateJti(t *testing.T) {
|
||||
store := NewMemoryLedger()
|
||||
_, _ = store.Append("dummy-jws", &Payload{Jti: "jti-001", Wid: "wf-1", Par: []string{}, PolDecision: PolDecisionApproved})
|
||||
payload := &Payload{Jti: "jti-001", Wid: "wf-1", Par: []string{}, PolDecision: PolDecisionApproved}
|
||||
_, _ = store.Append("dummy-jws", &Payload{Jti: "jti-001", Wid: "wf-1", Pred: []string{}})
|
||||
payload := &Payload{Jti: "jti-001", Wid: "wf-1", Pred: []string{}}
|
||||
err := ValidateDAG(payload, store, DefaultDAGConfig())
|
||||
if err == nil {
|
||||
t.Fatal("expected error for duplicate jti")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDAG_ParentExists(t *testing.T) {
|
||||
func TestValidateDAG_PredExists(t *testing.T) {
|
||||
store := NewMemoryLedger()
|
||||
_, _ = store.Append("jws1", &Payload{Jti: "jti-001", Wid: "wf-1", Par: []string{}, PolDecision: PolDecisionApproved, Iat: time.Now().Unix() - 60})
|
||||
_, _ = store.Append("jws1", &Payload{Jti: "jti-001", Wid: "wf-1", Pred: []string{}, Iat: time.Now().Unix() - 60})
|
||||
payload := &Payload{
|
||||
Jti: "jti-002",
|
||||
Wid: "wf-1",
|
||||
Par: []string{"jti-001"},
|
||||
PolDecision: PolDecisionApproved,
|
||||
Iat: time.Now().Unix(),
|
||||
Jti: "jti-002",
|
||||
Wid: "wf-1",
|
||||
Pred: []string{"jti-001"},
|
||||
Iat: time.Now().Unix(),
|
||||
}
|
||||
err := ValidateDAG(payload, store, DefaultDAGConfig())
|
||||
if err != nil {
|
||||
@@ -45,17 +43,16 @@ func TestValidateDAG_ParentExists(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDAG_ParentNotFound(t *testing.T) {
|
||||
func TestValidateDAG_PredNotFound(t *testing.T) {
|
||||
store := NewMemoryLedger()
|
||||
payload := &Payload{
|
||||
Jti: "jti-002",
|
||||
Par: []string{"jti-missing"},
|
||||
PolDecision: PolDecisionApproved,
|
||||
Iat: time.Now().Unix(),
|
||||
Jti: "jti-002",
|
||||
Pred: []string{"jti-missing"},
|
||||
Iat: time.Now().Unix(),
|
||||
}
|
||||
err := ValidateDAG(payload, store, DefaultDAGConfig())
|
||||
if err == nil {
|
||||
t.Fatal("expected error when parent not found")
|
||||
t.Fatal("expected error when predecessor not found")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,10 +60,10 @@ func TestValidateDAG_DepthLimit(t *testing.T) {
|
||||
store := NewMemoryLedger()
|
||||
now := time.Now().Unix()
|
||||
// Chain: jti-1 -> jti-2 -> jti-3 -> ...; validate with maxAncestorLimit=2 so we exceed it
|
||||
_, _ = store.Append("jws1", &Payload{Jti: "jti-1", Wid: "wf", Par: []string{}, PolDecision: PolDecisionApproved, Iat: now - 100})
|
||||
_, _ = store.Append("jws2", &Payload{Jti: "jti-2", Wid: "wf", Par: []string{"jti-1"}, PolDecision: PolDecisionApproved, Iat: now - 50})
|
||||
_, _ = store.Append("jws1", &Payload{Jti: "jti-1", Wid: "wf", Pred: []string{}, Iat: now - 100})
|
||||
_, _ = store.Append("jws2", &Payload{Jti: "jti-2", Wid: "wf", Pred: []string{"jti-1"}, Iat: now - 50})
|
||||
cfg := DAGConfig{ClockSkewTolerance: DefaultClockSkewTolerance, MaxAncestorLimit: 2}
|
||||
payload := &Payload{Jti: "jti-3", Wid: "wf", Par: []string{"jti-2"}, PolDecision: PolDecisionApproved, Iat: now}
|
||||
payload := &Payload{Jti: "jti-3", Wid: "wf", Pred: []string{"jti-2"}, Iat: now}
|
||||
err := ValidateDAG(payload, store, cfg)
|
||||
if err == nil {
|
||||
t.Fatal("expected error when ancestor limit exceeded")
|
||||
@@ -74,7 +71,7 @@ func TestValidateDAG_DepthLimit(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestValidateDAG_StoreNil(t *testing.T) {
|
||||
payload := &Payload{Jti: "j1", Par: []string{}, PolDecision: PolDecisionApproved, Iat: time.Now().Unix()}
|
||||
payload := &Payload{Jti: "j1", Pred: []string{}, Iat: time.Now().Unix()}
|
||||
err := ValidateDAG(payload, nil, DefaultDAGConfig())
|
||||
if err == nil {
|
||||
t.Fatal("expected error when store is nil")
|
||||
@@ -84,53 +81,56 @@ func TestValidateDAG_StoreNil(t *testing.T) {
|
||||
func TestValidateDAG_TemporalOrdering(t *testing.T) {
|
||||
store := NewMemoryLedger()
|
||||
now := time.Now().Unix()
|
||||
_, _ = store.Append("jws1", &Payload{Jti: "jti-1", Wid: "wf", Par: []string{}, PolDecision: PolDecisionApproved, Iat: now})
|
||||
// child has iat before parent + skew: parent.iat (now) >= child.iat (now+100) + 30 => invalid
|
||||
payload := &Payload{Jti: "jti-2", Wid: "wf", Par: []string{"jti-1"}, PolDecision: PolDecisionApproved, Iat: now + 100}
|
||||
_, _ = store.Append("jws1", &Payload{Jti: "jti-1", Wid: "wf", Pred: []string{}, Iat: now})
|
||||
// child has iat after pred: valid
|
||||
payload := &Payload{Jti: "jti-2", Wid: "wf", Pred: []string{"jti-1"}, Iat: now + 100}
|
||||
err := ValidateDAG(payload, store, DAGConfig{ClockSkewTolerance: 30, MaxAncestorLimit: 10000})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// parent.iat >= child.iat + skew: parent at now+50, child at now+10, skew 30 => 50 >= 40 => invalid
|
||||
_, _ = store.Append("jws2", &Payload{Jti: "jti-1b", Wid: "wf", Par: []string{}, PolDecision: PolDecisionApproved, Iat: now + 50})
|
||||
payload2 := &Payload{Jti: "jti-2b", Wid: "wf", Par: []string{"jti-1b"}, PolDecision: PolDecisionApproved, Iat: now + 10}
|
||||
// pred.iat >= child.iat + skew: pred at now+50, child at now+10, skew 30 => 50 >= 40 => invalid
|
||||
_, _ = store.Append("jws2", &Payload{Jti: "jti-1b", Wid: "wf", Pred: []string{}, Iat: now + 50})
|
||||
payload2 := &Payload{Jti: "jti-2b", Wid: "wf", Pred: []string{"jti-1b"}, Iat: now + 10}
|
||||
err = ValidateDAG(payload2, store, DAGConfig{ClockSkewTolerance: 30, MaxAncestorLimit: 10000})
|
||||
if err == nil {
|
||||
t.Fatal("expected error when parent not earlier than child")
|
||||
t.Fatal("expected error when predecessor not earlier than child")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDAG_DirectCycle(t *testing.T) {
|
||||
// par contains own jti (direct self-reference) -> parent not found
|
||||
// pred contains own jti (direct self-reference) -> predecessor not found
|
||||
store := NewMemoryLedger()
|
||||
now := time.Now().Unix()
|
||||
payload := &Payload{Jti: "jti-self", Wid: "wf", Par: []string{"jti-self"}, PolDecision: PolDecisionApproved, Iat: now}
|
||||
payload := &Payload{Jti: "jti-self", Wid: "wf", Pred: []string{"jti-self"}, Iat: now}
|
||||
err := ValidateDAG(payload, store, DefaultDAGConfig())
|
||||
if err == nil {
|
||||
t.Fatal("expected error for direct cycle (par contains self)")
|
||||
t.Fatal("expected error for direct cycle (pred contains self)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDAG_hasCycle_visitedContinue(t *testing.T) {
|
||||
// par has duplicate parent ID so we hit "if _, ok := visited[parentID]; ok { continue }"
|
||||
// pred has duplicate predecessor ID so we hit "if _, ok := visited[predID]; ok { continue }"
|
||||
store := NewMemoryLedger()
|
||||
now := time.Now().Unix()
|
||||
_, _ = store.Append("jws1", &Payload{Jti: "jti-a", Wid: "wf", Par: []string{}, PolDecision: PolDecisionApproved, Iat: now - 10})
|
||||
payload := &Payload{Jti: "jti-b", Wid: "wf", Par: []string{"jti-a", "jti-a"}, PolDecision: PolDecisionApproved, Iat: now}
|
||||
_, _ = store.Append("jws1", &Payload{Jti: "jti-a", Wid: "wf", Pred: []string{}, Iat: now - 10})
|
||||
payload := &Payload{Jti: "jti-b", Wid: "wf", Pred: []string{"jti-a", "jti-a"}, Iat: now}
|
||||
err := ValidateDAG(payload, store, DefaultDAGConfig())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDAG_ParentPolicyRejected_RequiresCompensation(t *testing.T) {
|
||||
func TestValidateDAG_PredPolicyRejected_RequiresCompensation(t *testing.T) {
|
||||
store := NewMemoryLedger()
|
||||
now := time.Now().Unix()
|
||||
_, _ = store.Append("jws1", &Payload{Jti: "jti-rej", Wid: "wf", Par: []string{}, Pol: "p", PolDecision: PolDecisionRejected, Iat: now - 60})
|
||||
payload := &Payload{Jti: "jti-child", Wid: "wf", Par: []string{"jti-rej"}, PolDecision: PolDecisionApproved, Iat: now}
|
||||
_, _ = store.Append("jws1", &Payload{
|
||||
Jti: "jti-rej", Wid: "wf", Pred: []string{}, Iat: now - 60,
|
||||
Ext: map[string]interface{}{"pol": "p", "pol_decision": "rejected"},
|
||||
})
|
||||
payload := &Payload{Jti: "jti-child", Wid: "wf", Pred: []string{"jti-rej"}, Iat: now}
|
||||
err := ValidateDAG(payload, store, DefaultDAGConfig())
|
||||
if err == nil {
|
||||
t.Fatal("expected error when parent rejected and no compensation")
|
||||
t.Fatal("expected error when predecessor rejected and no compensation")
|
||||
}
|
||||
payload.Ext = map[string]interface{}{"compensation_required": true}
|
||||
err = ValidateDAG(payload, store, DefaultDAGConfig())
|
||||
|
||||
@@ -15,15 +15,13 @@ var (
|
||||
ErrExpired = errors.New("ect: token expired")
|
||||
ErrIATTooOld = errors.New("ect: iat too far in the past")
|
||||
ErrIATInFuture = errors.New("ect: iat in the future")
|
||||
ErrMissingClaims = errors.New("ect: missing required claims (jti, exec_act, par)")
|
||||
ErrPolPolDecisionPair = errors.New("ect: pol and pol_decision must both be present when either is set")
|
||||
ErrInvalidPolDecision = errors.New("ect: invalid pol_decision value")
|
||||
ErrMissingClaims = errors.New("ect: missing required claims (jti, exec_act, pred)")
|
||||
ErrReplay = errors.New("ect: jti already seen (replay)")
|
||||
ErrResolveKeyRequired = errors.New("ect: ResolveKey required")
|
||||
ErrExtSize = errors.New("ect: ext exceeds max size (4096 bytes)")
|
||||
ErrExtDepth = errors.New("ect: ext exceeds max nesting depth (5)")
|
||||
ErrInvalidJTI = errors.New("ect: jti must be UUID format")
|
||||
ErrInvalidWID = errors.New("ect: wid must be UUID format when set")
|
||||
ErrParLength = errors.New("ect: par exceeds max length")
|
||||
ErrPredLength = errors.New("ect: pred exceeds max length")
|
||||
ErrHashFormat = errors.New("ect: inp_hash/out_hash must be algorithm:base64url (e.g. sha-256:...)")
|
||||
)
|
||||
|
||||
@@ -12,7 +12,7 @@ type LedgerEntry struct {
|
||||
TaskID string `json:"task_id"`
|
||||
AgentID string `json:"agent_id"`
|
||||
Action string `json:"action"`
|
||||
Parents []string `json:"parents"`
|
||||
Predecessors []string `json:"predecessors"`
|
||||
ECTJWS string `json:"ect_jws"`
|
||||
SignatureVerified bool `json:"signature_verified"`
|
||||
VerificationTime time.Time `json:"verification_timestamp"`
|
||||
@@ -66,7 +66,7 @@ func (m *MemoryLedger) Append(ectJWS string, payload *Payload) (int64, error) {
|
||||
TaskID: payload.Jti, // task id = jti per spec
|
||||
AgentID: payload.Iss,
|
||||
Action: payload.ExecAct,
|
||||
Parents: append([]string(nil), payload.Par...),
|
||||
Predecessors: append([]string(nil), payload.Pred...),
|
||||
ECTJWS: ectJWS,
|
||||
SignatureVerified: true,
|
||||
VerificationTime: time.Now().UTC(),
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
func TestMemoryLedger_AppendAndGet(t *testing.T) {
|
||||
m := NewMemoryLedger()
|
||||
p := &Payload{Jti: "jti-1", Iss: "iss", ExecAct: "act", Par: []string{}, Iat: time.Now().Unix(), Exp: time.Now().Add(time.Hour).Unix()}
|
||||
p := &Payload{Jti: "jti-1", Iss: "iss", ExecAct: "act", Pred: []string{}, Iat: time.Now().Unix(), Exp: time.Now().Add(time.Hour).Unix()}
|
||||
seq, err := m.Append("jws1", p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -23,7 +23,7 @@ func TestMemoryLedger_AppendAndGet(t *testing.T) {
|
||||
|
||||
func TestMemoryLedger_ErrTaskIDExists(t *testing.T) {
|
||||
m := NewMemoryLedger()
|
||||
p := &Payload{Jti: "jti-dup", Iss: "i", ExecAct: "e", Par: []string{}, Iat: 1, Exp: 2}
|
||||
p := &Payload{Jti: "jti-dup", Iss: "i", ExecAct: "e", Pred: []string{}, Iat: 1, Exp: 2}
|
||||
_, _ = m.Append("jws1", p)
|
||||
_, err := m.Append("jws2", p)
|
||||
if err != ErrTaskIDExists {
|
||||
@@ -33,7 +33,7 @@ func TestMemoryLedger_ErrTaskIDExists(t *testing.T) {
|
||||
|
||||
func TestMemoryLedger_ContainsWid(t *testing.T) {
|
||||
m := NewMemoryLedger()
|
||||
p := &Payload{Jti: "j1", Wid: "wf1", Iss: "i", ExecAct: "e", Par: []string{}, Iat: 1, Exp: 2}
|
||||
p := &Payload{Jti: "j1", Wid: "wf1", Iss: "i", ExecAct: "e", Pred: []string{}, Iat: 1, Exp: 2}
|
||||
_, _ = m.Append("jws", p)
|
||||
if !m.Contains("j1", "") {
|
||||
t.Error("Contains(j1, \"\") should be true")
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
// Package ect implements Execution Context Tokens (ECTs) per
|
||||
// draft-nennemann-wimse-execution-context-00.
|
||||
// draft-nennemann-wimse-execution-context-01.
|
||||
package ect
|
||||
|
||||
import "time"
|
||||
|
||||
// ECTType is the JOSE typ value for ECTs.
|
||||
const ECTType = "wimse-exec+jwt"
|
||||
|
||||
// PolDecision values per Section 4.2.3.
|
||||
// ECTType is the preferred JOSE typ value for ECTs per -01.
|
||||
// ECTTypeLegacy is accepted for backward compatibility with -00.
|
||||
const (
|
||||
PolDecisionApproved = "approved"
|
||||
PolDecisionRejected = "rejected"
|
||||
PolDecisionPendingHumanReview = "pending_human_review"
|
||||
ECTType = "exec+jwt"
|
||||
ECTTypeLegacy = "wimse-exec+jwt"
|
||||
)
|
||||
|
||||
// Payload holds ECT JWT claims per Section 4.2.
|
||||
type Payload struct {
|
||||
// Standard JWT claims (required unless noted)
|
||||
Iss string `json:"iss"` // REQUIRED: issuer, SPIFFE ID
|
||||
Sub string `json:"sub,omitempty"`
|
||||
Aud Audience `json:"aud"` // REQUIRED
|
||||
Iat int64 `json:"iat"` // REQUIRED: NumericDate
|
||||
Exp int64 `json:"exp"` // REQUIRED
|
||||
@@ -26,15 +22,9 @@ type Payload struct {
|
||||
|
||||
// Execution context (Section 4.2.2 / exec-claims)
|
||||
// Task identity is jti only; no separate "tid" claim per spec.
|
||||
Wid string `json:"wid,omitempty"` // OPTIONAL: workflow ID, UUID
|
||||
ExecAct string `json:"exec_act"` // REQUIRED
|
||||
Par []string `json:"par"` // REQUIRED: parent jti values
|
||||
|
||||
// Policy evaluation (Section 4.2.3 / policy-claims) — OPTIONAL
|
||||
Pol string `json:"pol,omitempty"`
|
||||
PolDecision string `json:"pol_decision,omitempty"`
|
||||
PolEnforcer string `json:"pol_enforcer,omitempty"`
|
||||
PolTimestamp int64 `json:"pol_timestamp,omitempty"`
|
||||
Wid string `json:"wid,omitempty"` // OPTIONAL: workflow ID, UUID
|
||||
ExecAct string `json:"exec_act"` // REQUIRED
|
||||
Pred []string `json:"pred"` // REQUIRED: predecessor jti values (renamed from par in -01)
|
||||
|
||||
// Data integrity (Section 4.2.4)
|
||||
InpHash string `json:"inp_hash,omitempty"`
|
||||
@@ -42,8 +32,9 @@ type Payload struct {
|
||||
InpClassification string `json:"inp_classification,omitempty"`
|
||||
|
||||
// Extensions (Section 4.2.7): exec_time_ms, regulated_domain, model_version,
|
||||
// witnessed_by, inp_classification, pol_timestamp, compensation_required, compensation_reason
|
||||
Ext map[string]interface{} `json:"ext,omitempty"`
|
||||
// witnessed_by, inp_classification, compensation_required, compensation_reason,
|
||||
// and domain-specific claims like pol, pol_decision (moved from core in -01).
|
||||
Ext map[string]interface{} `json:"ect_ext,omitempty"`
|
||||
}
|
||||
|
||||
// Audience is aud claim: string or array of strings.
|
||||
@@ -62,11 +53,6 @@ func (a *Audience) UnmarshalJSON(data []byte) error {
|
||||
return unmarshalAudience(data, a)
|
||||
}
|
||||
|
||||
// ValidPolDecision returns true if s is a registered pol_decision value.
|
||||
func ValidPolDecision(s string) bool {
|
||||
return s == PolDecisionApproved || s == PolDecisionRejected || s == PolDecisionPendingHumanReview
|
||||
}
|
||||
|
||||
// ContainsAudience returns true if verifierID is in the audience.
|
||||
func (p *Payload) ContainsAudience(verifierID string) bool {
|
||||
for _, id := range p.Aud {
|
||||
@@ -96,7 +82,21 @@ func (p *Payload) CompensationRequired() bool {
|
||||
return v
|
||||
}
|
||||
|
||||
// HasPolicyClaims returns true if both pol and pol_decision are present (optional pair per spec).
|
||||
// HasPolicyClaims returns true if both pol and pol_decision are present in ext (per -01, moved to extension).
|
||||
func (p *Payload) HasPolicyClaims() bool {
|
||||
return p.Pol != "" && p.PolDecision != ""
|
||||
if p.Ext == nil {
|
||||
return false
|
||||
}
|
||||
pol, _ := p.Ext["pol"].(string)
|
||||
polDec, _ := p.Ext["pol_decision"].(string)
|
||||
return pol != "" && polDec != ""
|
||||
}
|
||||
|
||||
// PolDecision returns the pol_decision value from ext, or empty string.
|
||||
func (p *Payload) PolDecision() string {
|
||||
if p.Ext == nil {
|
||||
return ""
|
||||
}
|
||||
v, _ := p.Ext["pol_decision"].(string)
|
||||
return v
|
||||
}
|
||||
|
||||
@@ -59,18 +59,6 @@ func TestAudience_UnmarshalJSON_invalid(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidPolDecision(t *testing.T) {
|
||||
if !ValidPolDecision(PolDecisionApproved) {
|
||||
t.Error("approved should be valid")
|
||||
}
|
||||
if !ValidPolDecision(PolDecisionRejected) {
|
||||
t.Error("rejected should be valid")
|
||||
}
|
||||
if ValidPolDecision("invalid") {
|
||||
t.Error("invalid should not be valid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPayload_ContainsAudience(t *testing.T) {
|
||||
p := &Payload{Aud: []string{"a", "b"}}
|
||||
if !p.ContainsAudience("a") {
|
||||
@@ -104,12 +92,27 @@ func TestPayload_CompensationRequired(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPayload_HasPolicyClaims(t *testing.T) {
|
||||
p := &Payload{Pol: "p", PolDecision: PolDecisionApproved}
|
||||
p := &Payload{Ext: map[string]interface{}{"pol": "p", "pol_decision": "approved"}}
|
||||
if !p.HasPolicyClaims() {
|
||||
t.Error("both pol and pol_decision set should have policy claims")
|
||||
t.Error("both pol and pol_decision in ext should have policy claims")
|
||||
}
|
||||
p.Pol = ""
|
||||
p.Ext = map[string]interface{}{"pol_decision": "approved"}
|
||||
if p.HasPolicyClaims() {
|
||||
t.Error("missing pol should not have policy claims")
|
||||
t.Error("missing pol in ext should not have policy claims")
|
||||
}
|
||||
p.Ext = nil
|
||||
if p.HasPolicyClaims() {
|
||||
t.Error("nil ext should not have policy claims")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPayload_PolDecision(t *testing.T) {
|
||||
p := &Payload{Ext: map[string]interface{}{"pol_decision": "rejected"}}
|
||||
if p.PolDecision() != "rejected" {
|
||||
t.Errorf("expected rejected, got %q", p.PolDecision())
|
||||
}
|
||||
p.Ext = nil
|
||||
if p.PolDecision() != "" {
|
||||
t.Errorf("expected empty for nil ext, got %q", p.PolDecision())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ const ExtMaxSize = 4096
|
||||
// ExtMaxDepth is the recommended max JSON nesting depth in ext.
|
||||
const ExtMaxDepth = 5
|
||||
|
||||
// DefaultMaxParLength is the recommended max number of parent references.
|
||||
const DefaultMaxParLength = 100
|
||||
// DefaultMaxPredLength is the recommended max number of predecessor references.
|
||||
const DefaultMaxPredLength = 100
|
||||
|
||||
// uuidRegex matches RFC 9562 UUID: 8-4-4-4-12 hex.
|
||||
var uuidRegex = 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}$`)
|
||||
|
||||
@@ -44,8 +44,8 @@ type VerifyOptions struct {
|
||||
WITSubject string
|
||||
// ValidateUUIDs when true requires jti and wid (if set) to be UUID format.
|
||||
ValidateUUIDs bool
|
||||
// MaxParLength caps par length (0 = no limit). Applied before DAG; DAG may also enforce via DAG.MaxParLength.
|
||||
MaxParLength int
|
||||
// MaxPredLength caps pred length (0 = no limit). Applied before DAG; DAG may also enforce via DAG.MaxPredLength.
|
||||
MaxPredLength int
|
||||
// LogVerify if set is called after verification with jti and any error (for observability).
|
||||
LogVerify func(jti string, err error)
|
||||
}
|
||||
@@ -104,12 +104,13 @@ func Verify(compact string, opts VerifyOptions) (parsed *ParsedECT, err error) {
|
||||
sig := jws.Signatures[0]
|
||||
header := &sig.Header
|
||||
|
||||
// 2. typ must be wimse-exec+jwt (constant-time compare)
|
||||
// 2. typ must be exec+jwt (preferred) or wimse-exec+jwt (legacy); constant-time compare
|
||||
typ, _ := header.ExtraHeaders["typ"].(string)
|
||||
if typ == "" {
|
||||
typ, _ = header.ExtraHeaders[jose.HeaderType].(string)
|
||||
}
|
||||
if subtle.ConstantTimeCompare([]byte(typ), []byte(ECTType)) != 1 {
|
||||
if subtle.ConstantTimeCompare([]byte(typ), []byte(ECTType)) != 1 &&
|
||||
subtle.ConstantTimeCompare([]byte(typ), []byte(ECTTypeLegacy)) != 1 {
|
||||
return nil, ErrInvalidTyp
|
||||
}
|
||||
|
||||
@@ -182,15 +183,15 @@ func Verify(compact string, opts VerifyOptions) (parsed *ParsedECT, err error) {
|
||||
return nil, ErrIATInFuture
|
||||
}
|
||||
|
||||
// 12. Required claims present (jti, exec_act, par)
|
||||
// 12. Required claims present (jti, exec_act, pred)
|
||||
if p.Jti == "" || p.ExecAct == "" {
|
||||
return nil, ErrMissingClaims
|
||||
}
|
||||
if p.Par == nil {
|
||||
p.Par = []string{}
|
||||
if p.Pred == nil {
|
||||
p.Pred = []string{}
|
||||
}
|
||||
if opts.MaxParLength > 0 && len(p.Par) > opts.MaxParLength {
|
||||
return nil, ErrParLength
|
||||
if opts.MaxPredLength > 0 && len(p.Pred) > opts.MaxPredLength {
|
||||
return nil, ErrPredLength
|
||||
}
|
||||
if opts.ValidateUUIDs {
|
||||
if !ValidUUID(p.Jti) {
|
||||
@@ -211,24 +212,14 @@ func Verify(compact string, opts VerifyOptions) (parsed *ParsedECT, err error) {
|
||||
}
|
||||
}
|
||||
|
||||
// 13. If pol or pol_decision present, both must be present and pol_decision in registry
|
||||
if p.Pol != "" || p.PolDecision != "" {
|
||||
if p.Pol == "" || p.PolDecision == "" {
|
||||
return nil, ErrPolPolDecisionPair
|
||||
}
|
||||
if !ValidPolDecision(p.PolDecision) {
|
||||
return nil, ErrInvalidPolDecision
|
||||
}
|
||||
}
|
||||
|
||||
// 14. DAG validation
|
||||
// 13. DAG validation
|
||||
if opts.Store != nil {
|
||||
if err := ValidateDAG(&p, opts.Store, opts.DAG); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 15. Replay (jti seen)
|
||||
// 14. Replay (jti seen)
|
||||
if opts.JTISeen != nil && opts.JTISeen(p.Jti) {
|
||||
return nil, ErrReplay
|
||||
}
|
||||
|
||||
@@ -11,15 +11,13 @@ func TestParse(t *testing.T) {
|
||||
key, _ := GenerateKey()
|
||||
now := time.Now()
|
||||
payload := &Payload{
|
||||
Iss: "iss",
|
||||
Aud: []string{"aud"},
|
||||
Iat: now.Unix(),
|
||||
Exp: now.Add(time.Hour).Unix(),
|
||||
Jti: "jti-parse",
|
||||
ExecAct: "act",
|
||||
Par: []string{},
|
||||
Pol: "pol",
|
||||
PolDecision: PolDecisionApproved,
|
||||
Iss: "iss",
|
||||
Aud: []string{"aud"},
|
||||
Iat: now.Unix(),
|
||||
Exp: now.Add(time.Hour).Unix(),
|
||||
Jti: "jti-parse",
|
||||
ExecAct: "act",
|
||||
Pred: []string{},
|
||||
}
|
||||
compact, err := Create(payload, key, CreateOptions{KeyID: "kid"})
|
||||
if err != nil {
|
||||
@@ -50,15 +48,13 @@ func TestVerify_Expired(t *testing.T) {
|
||||
key, _ := GenerateKey()
|
||||
now := time.Now()
|
||||
payload := &Payload{
|
||||
Iss: "iss",
|
||||
Aud: []string{"verifier"},
|
||||
Iat: now.Add(-1 * time.Hour).Unix(),
|
||||
Exp: now.Add(-1 * time.Minute).Unix(),
|
||||
Jti: "jti-exp",
|
||||
ExecAct: "act",
|
||||
Par: []string{},
|
||||
Pol: "pol",
|
||||
PolDecision: PolDecisionApproved,
|
||||
Iss: "iss",
|
||||
Aud: []string{"verifier"},
|
||||
Iat: now.Add(-1 * time.Hour).Unix(),
|
||||
Exp: now.Add(-1 * time.Minute).Unix(),
|
||||
Jti: "jti-exp",
|
||||
ExecAct: "act",
|
||||
Pred: []string{},
|
||||
}
|
||||
compact, _ := Create(payload, key, CreateOptions{KeyID: "kid"})
|
||||
resolver := func(kid string) (*ecdsa.PublicKey, error) {
|
||||
@@ -81,15 +77,13 @@ func TestVerify_Replay(t *testing.T) {
|
||||
key, _ := GenerateKey()
|
||||
now := time.Now()
|
||||
payload := &Payload{
|
||||
Iss: "iss",
|
||||
Aud: []string{"v"},
|
||||
Iat: now.Unix(),
|
||||
Exp: now.Add(time.Hour).Unix(),
|
||||
Jti: "jti-replay",
|
||||
ExecAct: "act",
|
||||
Par: []string{},
|
||||
Pol: "p",
|
||||
PolDecision: PolDecisionApproved,
|
||||
Iss: "iss",
|
||||
Aud: []string{"v"},
|
||||
Iat: now.Unix(),
|
||||
Exp: now.Add(time.Hour).Unix(),
|
||||
Jti: "jti-replay",
|
||||
ExecAct: "act",
|
||||
Pred: []string{},
|
||||
}
|
||||
compact, _ := Create(payload, key, CreateOptions{KeyID: "kid"})
|
||||
resolver := func(kid string) (*ecdsa.PublicKey, error) {
|
||||
@@ -125,7 +119,7 @@ func TestVerify_WITSubjectMismatch(t *testing.T) {
|
||||
now := time.Now()
|
||||
payload := &Payload{
|
||||
Iss: "iss", Aud: []string{"v"}, Iat: now.Unix(), Exp: now.Add(time.Hour).Unix(),
|
||||
Jti: "jti-wit", ExecAct: "act", Par: []string{}, Pol: "p", PolDecision: PolDecisionApproved,
|
||||
Jti: "jti-wit", ExecAct: "act", Pred: []string{},
|
||||
}
|
||||
compact, _ := Create(payload, key, CreateOptions{KeyID: "kid"})
|
||||
resolver := func(kid string) (*ecdsa.PublicKey, error) {
|
||||
@@ -147,7 +141,7 @@ func TestVerify_IATTooFarPast(t *testing.T) {
|
||||
now := time.Now()
|
||||
payload := &Payload{
|
||||
Iss: "iss", Aud: []string{"v"}, Iat: now.Add(-1 * time.Hour).Unix(), Exp: now.Add(time.Hour).Unix(),
|
||||
Jti: "jti-iat", ExecAct: "act", Par: []string{}, Pol: "p", PolDecision: PolDecisionApproved,
|
||||
Jti: "jti-iat", ExecAct: "act", Pred: []string{},
|
||||
}
|
||||
compact, _ := Create(payload, key, CreateOptions{KeyID: "kid"})
|
||||
resolver := func(kid string) (*ecdsa.PublicKey, error) {
|
||||
@@ -169,7 +163,7 @@ func TestVerify_IATInFuture(t *testing.T) {
|
||||
now := time.Now()
|
||||
payload := &Payload{
|
||||
Iss: "iss", Aud: []string{"v"}, Iat: now.Add(60 * time.Second).Unix(), Exp: now.Add(2 * time.Hour).Unix(),
|
||||
Jti: "jti-fut", ExecAct: "act", Par: []string{}, Pol: "p", PolDecision: PolDecisionApproved,
|
||||
Jti: "jti-fut", ExecAct: "act", Pred: []string{},
|
||||
}
|
||||
compact, _ := Create(payload, key, CreateOptions{KeyID: "kid"})
|
||||
resolver := func(kid string) (*ecdsa.PublicKey, error) {
|
||||
@@ -191,7 +185,7 @@ func TestVerify_ResolveKeyError(t *testing.T) {
|
||||
now := time.Now()
|
||||
payload := &Payload{
|
||||
Iss: "iss", Aud: []string{"v"}, Iat: now.Unix(), Exp: now.Add(time.Hour).Unix(),
|
||||
Jti: "jti-err", ExecAct: "act", Par: []string{}, Pol: "p", PolDecision: PolDecisionApproved,
|
||||
Jti: "jti-err", ExecAct: "act", Pred: []string{},
|
||||
}
|
||||
compact, _ := Create(payload, key, CreateOptions{KeyID: "kid"})
|
||||
resolver := func(kid string) (*ecdsa.PublicKey, error) {
|
||||
@@ -211,7 +205,7 @@ func TestVerify_WithDAG(t *testing.T) {
|
||||
now := time.Now()
|
||||
root := &Payload{
|
||||
Iss: "iss", Aud: []string{"v"}, Iat: now.Unix(), Exp: now.Add(time.Hour).Unix(),
|
||||
Jti: "jti-root", ExecAct: "act", Par: []string{}, Pol: "p", PolDecision: PolDecisionApproved,
|
||||
Jti: "jti-root", ExecAct: "act", Pred: []string{},
|
||||
}
|
||||
compactRoot, _ := Create(root, key, CreateOptions{KeyID: "kid"})
|
||||
resolver := func(kid string) (*ecdsa.PublicKey, error) {
|
||||
@@ -230,7 +224,7 @@ func TestVerify_WithDAG(t *testing.T) {
|
||||
_, _ = ledger.Append(compactRoot, parsed.Payload)
|
||||
child := &Payload{
|
||||
Iss: "iss", Aud: []string{"v"}, Iat: now.Unix() + 1, Exp: now.Add(time.Hour).Unix(),
|
||||
Jti: "jti-child", ExecAct: "act2", Par: []string{"jti-root"}, Pol: "p", PolDecision: PolDecisionApproved,
|
||||
Jti: "jti-child", ExecAct: "act2", Pred: []string{"jti-root"},
|
||||
}
|
||||
compactChild, _ := Create(child, key, CreateOptions{KeyID: "kid"})
|
||||
parsed2, err := Verify(compactChild, opts)
|
||||
|
||||
Reference in New Issue
Block a user