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:
2026-04-03 10:55:58 +02:00
parent ba044f6626
commit 884d2dc836
33 changed files with 416 additions and 481 deletions

View File

@@ -10,9 +10,9 @@ from typing import Optional
import jwt
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey
from ect.types import Payload, valid_pol_decision
from ect.types import ECT_TYPE, Payload
from ect.validate import (
DEFAULT_MAX_PAR_LENGTH,
DEFAULT_MAX_PRED_LENGTH,
validate_ext,
validate_hash_format,
valid_uuid,
@@ -25,7 +25,7 @@ class CreateOptions:
iat_max_age_sec: int = 900 # 15 min
default_expiry_sec: int = 600 # 10 min
validate_uuids: bool = False
max_par_length: int = 0 # 0 = no limit; use DEFAULT_MAX_PAR_LENGTH for 100
max_pred_length: int = 0 # 0 = no limit; use DEFAULT_MAX_PRED_LENGTH for 100
def default_create_options() -> CreateOptions:
@@ -46,22 +46,14 @@ def _validate_payload(p: Payload, opts: CreateOptions) -> None:
raise ValueError("ect: jti must be UUID format")
if p.wid and not valid_uuid(p.wid):
raise ValueError("ect: wid must be UUID format when set")
max_par = opts.max_par_length or 0
if max_par > 0 and len(p.par) > max_par:
raise ValueError("ect: par exceeds max length")
max_pred = opts.max_pred_length or 0
if max_pred > 0 and len(p.pred) > max_pred:
raise ValueError("ect: pred exceeds max length")
if p.inp_hash:
validate_hash_format(p.inp_hash)
if p.out_hash:
validate_hash_format(p.out_hash)
validate_ext(p.ext)
# pol/pol_decision OPTIONAL; if either set, both must be present and valid
if p.pol or p.pol_decision:
if not p.pol or not p.pol_decision:
raise ValueError("ect: pol and pol_decision must both be present when either is set")
if not valid_pol_decision(p.pol_decision):
raise ValueError(
"ect: pol_decision must be approved, rejected, or pending_human_review"
)
# compensation in ext per spec
if p.ext and p.ext.get("compensation_reason") and not p.ext.get("compensation_required"):
raise ValueError("ect: ext.compensation_reason requires ext.compensation_required true")
@@ -73,8 +65,7 @@ def create(
opts: CreateOptions,
) -> str:
"""Build and sign an ECT. Payload must have required claims; iat/exp can be 0 for defaults.
create() may modify the payload in place (iat, exp, sub, par) when filling defaults;
pass a copy if the original must stay unchanged.
create() works on a deep copy so the caller's payload is not modified.
"""
if not opts.key_id:
raise ValueError("ect: KeyID required")
@@ -87,16 +78,14 @@ def create(
payload.iat = now
if payload.exp == 0:
payload.exp = now + (opts.default_expiry_sec or 600)
if not payload.sub:
payload.sub = payload.iss
if payload.par is None:
payload.par = []
if payload.pred is None:
payload.pred = []
_validate_payload(payload, opts)
claims = payload.to_claims()
headers = {
"typ": "wimse-exec+jwt",
"typ": ECT_TYPE,
"alg": "ES256",
"kid": opts.key_id,
}