- 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)
120 lines
4.3 KiB
Markdown
120 lines
4.3 KiB
Markdown
# WIMSE ECT — Go Reference Implementation
|
|
|
|
Go reference implementation of [Execution Context Tokens (ECTs)](../../draft-nennemann-wimse-execution-context-01.txt) 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
|
|
|
|
```go
|
|
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
|
|
|
|
```bash
|
|
cd refimpl/go-lang && go run ./cmd/demo
|
|
```
|
|
|
|
### Tests
|
|
|
|
```bash
|
|
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](https://github.com/go-jose/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.
|