Restructure refimpl into go-lang and python subdirectories
Move Go reference implementation to refimpl/go-lang/ and add new Python reference implementation in refimpl/python/. Update build.sh with renamed draft and simplified tool paths. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
192
refimpl/go-lang/ect/create_test.go
Normal file
192
refimpl/go-lang/ect/create_test.go
Normal file
@@ -0,0 +1,192 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user