package ect import ( "crypto/ecdsa" "encoding/json" "os" "testing" "time" ) func TestCreateRoundtrip(t *testing.T) { key, err := GenerateKey() if err != nil { t.Fatal(err) } 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, } compact, err := Create(payload, key, CreateOptions{KeyID: "agent-a-key-1"}) if err != nil { t.Fatal(err) } if compact == "" { t.Fatal("expected non-empty compact JWS") } // Verify with same key resolver := func(kid string) (*ecdsa.PublicKey, error) { if kid != "agent-a-key-1" { return nil, nil } return &key.PublicKey, nil } opts := VerifyOptions{ VerifierID: "spiffe://example.com/agent/b", ResolveKey: resolver, Now: now, IATMaxAge: 15 * time.Minute, IATMaxFuture: 30 * time.Second, } parsed, err := Verify(compact, opts) if err != nil { t.Fatal(err) } if parsed.Payload.Jti != payload.Jti || parsed.Payload.ExecAct != payload.ExecAct { t.Errorf("payload mismatch: got jti=%q exec_act=%q", parsed.Payload.Jti, parsed.Payload.ExecAct) } } func TestDefaultCreateOptions(t *testing.T) { opts := DefaultCreateOptions() if opts.KeyID != "" { t.Errorf("KeyID: got %q", opts.KeyID) } if opts.DefaultExpiry != 10*time.Minute { t.Errorf("DefaultExpiry: got %v", opts.DefaultExpiry) } } 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} if _, err := Create(nil, key, CreateOptions{KeyID: "k"}); err == nil { t.Error("expected error for nil payload") } if _, err := Create(payload, nil, CreateOptions{KeyID: "k"}); err == nil { t.Error("expected error for nil key") } if _, err := Create(payload, key, CreateOptions{KeyID: ""}); err == nil { t.Error("expected error for empty KeyID") } } func TestCreate_OptionalPol(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-nopol", ExecAct: "act", Par: []string{}, } compact, err := Create(payload, key, CreateOptions{KeyID: "kid"}) if err != nil { t.Fatal(err) } if compact == "" { t.Fatal("expected token without pol") } } 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{}, } _, err := Create(payload, key, CreateOptions{KeyID: "kid", DefaultExpiry: 5 * time.Minute}) if err != nil { t.Fatal(err) } if payload.Exp <= payload.Iat { t.Error("exp should be after iat") } } 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{}, Ext: map[string]interface{}{"compensation_reason": "rollback", "compensation_required": false}, } _, err := Create(payload, key, CreateOptions{KeyID: "k"}) if err == nil { t.Fatal("expected error when ext has compensation_reason without compensation_required true") } } func TestCreate_ValidationErrors(t *testing.T) { key, _ := GenerateKey() tests := []struct { 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}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if _, err := Create(tt.p, key, CreateOptions{KeyID: "k"}); err == nil { t.Error("expected validation error") } }) } } func TestCreateWithTestVector(t *testing.T) { // testdata at module root (go-lang/testdata/); test cwd is package dir (ect/) data, err := os.ReadFile("../testdata/valid_root_ect_payload.json") if err != nil { data, err = os.ReadFile("testdata/valid_root_ect_payload.json") } if err != nil { t.Skipf("test vector not found: %v", err) return } var p Payload if err := json.Unmarshal(data, &p); err != nil { t.Fatal(err) } key, err := GenerateKey() if err != nil { t.Fatal(err) } // Override timestamps for verification now := time.Now() p.Iat = now.Unix() p.Exp = now.Add(10 * time.Minute).Unix() compact, err := Create(&p, key, CreateOptions{KeyID: "test-kid"}) if err != nil { t.Fatal(err) } resolver := func(kid string) (*ecdsa.PublicKey, error) { if kid != "test-kid" { return nil, nil } return &key.PublicKey, nil } _, err = Verify(compact, VerifyOptions{ VerifierID: p.Aud[0], ResolveKey: resolver, Now: now, IATMaxAge: 15 * time.Minute, IATMaxFuture: 30 * time.Second, }) if err != nil { t.Fatal(err) } }