Go ValidateHashFormat was still validating the old -00 format (algorithm:base64url with sha-256/sha-384/sha-512 prefix). Updated to validate plain base64url without prefix per -01 spec and RFC 9449. Python was already updated but uncommitted. Both refimpls now match.
WIMSE ECT — Go Reference Implementation
Go reference implementation of Execution Context Tokens (ECTs) for WIMSE. Implements ECT creation (ES256), verification (Section 7), DAG validation (Section 6), and an in-memory audit ledger (Section 9).
Layout
go-lang/
├── go.mod, go.sum
├── ect/ # library
│ ├── types.go # Payload, Audience, constants
│ ├── audience.go # aud marshal/unmarshal
│ ├── create.go # Create(), GenerateKey()
│ ├── verify.go # Parse(), Verify(), VerifyOptions
│ ├── dag.go # ValidateDAG(), ECTStore
│ ├── ledger.go # Ledger, MemoryLedger
│ ├── config.go # Config, LoadConfigFromEnv()
│ ├── jti_cache.go # JTICache for replay protection
│ ├── errors.go # Sentinel errors (ErrExpired, ErrReplay, etc.)
│ ├── validate.go # ValidateExt, ValidUUID, ValidateHashFormat
│ └── *_test.go
├── testdata/
│ └── valid_root_ect_payload.json
└── cmd/demo/ # two-agent workflow demo
└── main.go
Usage
Library
import "github.com/nennemann/ect-refimpl/go-lang/ect"
// Production: load config from environment
cfg := ect.LoadConfigFromEnv()
key, _ := ect.GenerateKey()
payload := &ect.Payload{
Iss: "spiffe://example.com/agent/a",
Aud: ect.Audience{"spiffe://example.com/agent/b"},
Iat: time.Now().Unix(),
Exp: time.Now().Add(10*time.Minute).Unix(),
Jti: "550e8400-e29b-41d4-a716-446655440000",
ExecAct: "review_spec",
Pred: []string{},
Ext: map[string]interface{}{
"pol": "policy_v1",
"pol_decision": "approved",
},
}
compact, err := ect.Create(payload, key, cfg.CreateOptions("agent-a-key"))
// Verify with optional replay cache
store := ect.NewMemoryLedger()
var jtiCache ect.JTICache
if cfg.JTIReplaySize > 0 {
jtiCache = ect.NewJTICache(cfg.JTIReplaySize, cfg.JTIReplayTTL)
}
opts := cfg.VerifyOptions()
opts.VerifierID = "spiffe://example.com/agent/b"
opts.ResolveKey = resolver
opts.Store = store
if jtiCache != nil {
opts.JTISeen = jtiCache.Seen
}
parsed, err := ect.Verify(compact, opts)
if err != nil { ... }
if jtiCache != nil {
jtiCache.Add(parsed.Payload.Jti)
}
store.Append(compact, parsed.Payload)
Demo
cd refimpl/go-lang && go run ./cmd/demo
Tests
cd refimpl/go-lang && go test ./ect/... -cover
Unit tests are in ect/*_test.go. Coverage target: ~90% (run go test ./ect/... -coverprofile=cover.out && go tool cover -func=cover.out). Remaining uncovered lines are mostly Parse/Verify error paths that require custom JWS or multi-sig tokens.
draft-01 claim changes
| -00 (previous) | -01 (current) | Notes |
|---|---|---|
par |
pred |
Predecessor task IDs |
pol, pol_decision |
removed (use ect_ext) |
Policy claims moved to extension object |
sub |
not defined | Standard JWT claim, not part of ECT spec |
typ: wimse-exec+jwt |
typ: exec+jwt (preferred) |
Both accepted for backward compat |
MaxParLength |
MaxPredLength |
Renamed to match pred claim |
Production configuration (environment)
| Variable | Default | Description |
|---|---|---|
ECT_IAT_MAX_AGE_MINUTES |
15 | Max age of iat (minutes). |
ECT_IAT_MAX_FUTURE_SEC |
30 | Max iat in future / clock skew (seconds). |
ECT_DEFAULT_EXPIRY_MIN |
10 | Default token expiry when exp is zero (minutes). |
ECT_JTI_REPLAY_CACHE_SIZE |
0 | Max JTIs to remember for replay check; 0 = disabled. |
ECT_JTI_REPLAY_TTL_MIN |
60 | TTL for JTI cache entries (minutes). |
Replay cache (multi-instance)
JTICache is in-memory only. For multiple verifier instances (e.g. behind a load balancer), use a shared store (Redis, database) so every instance sees the same "seen" JTIs. Implement JTISeen as a function that checks (and optionally records) the JTI in that store (e.g. with TTL). Pass it in VerifyOptions.JTISeen. See refimpl/README for an overview.
Dependencies
- go-jose/v4 for JWS (ES256) and JWK handling.
License
Same as the Internet-Draft (IETF Trust). Code under Revised BSD per BCP 78/79.