Files
ietf-draft-analyzer/workspace/packages/ect/tests/test_verify.py
Christian Nennemann 3a139dfc7e feat: ACT/ECT strategy, package restructure, draft -01/-02 prep
Strategic work for IETF submission of draft-nennemann-act-01 and
draft-nennemann-wimse-ect-02:

Package restructure:
- move ACT and ECT refimpls to workspace/packages/{act,ect}/
- ietf-act and ietf-ect distribution names (sibling packages)
- cross-spec interop test plan (INTEROP-TEST-PLAN.md)

ACT draft -01 revisions:
- rename 'par' claim to 'pred' (align with ECT)
- rename 'Agent Compact Token' to 'Agent Context Token' (semantic
  alignment with ECT family)
- add Applicability section (MCP, OpenAI, LangGraph, A2A, CrewAI)
- add DAG vs Linear Delegation Chains section (differentiator vs
  txn-tokens-for-agents actchain, Agentic JWT, AIP/IBCTs)
- add Related Work: AIP, SentinelAgent, Agentic JWT, txn-tokens-for-agents,
  HDP, SCITT-AI-agent-execution
- pin SCITT arch to -22, note AUTH48 status

Outreach drafts:
- Emirdag liaison email (SCITT-AI coordination)
- OAuth ML response on txn-tokens-for-agents-06

Strategy document:
- STRATEGY.md with phased action plan, risk register, timeline

Submodule:
- update workspace/drafts/ietf-wimse-ect pointer to -02 commit
2026-04-12 07:33:08 +02:00

195 lines
6.5 KiB
Python

"""Tests for verify module."""
import time
import pytest
from ect import (
Payload,
create,
generate_key,
CreateOptions,
parse,
verify,
VerifyOptions,
default_verify_options,
)
def test_parse():
key = generate_key()
now = int(time.time())
p = Payload(
iss="iss", aud=["a"], iat=now, exp=now + 3600,
jti="jti-parse", exec_act="act", pred=[],
)
compact = create(p, key, CreateOptions(key_id="kid"))
parsed = parse(compact)
assert parsed.payload.jti == "jti-parse"
assert parsed.raw == compact
def test_default_verify_options():
opts = default_verify_options()
assert opts.dag is not None
assert opts.iat_max_age_sec == 900
def test_verify_expired():
key = generate_key()
now = int(time.time())
p = Payload(
iss="iss", aud=["v"], iat=now - 3600, exp=now - 60,
jti="jti-exp", exec_act="act", pred=[],
)
compact = create(p, key, CreateOptions(key_id="kid"))
resolver = lambda kid: key.public_key() if kid == "kid" else None
with pytest.raises(ValueError, match="expired"):
verify(compact, VerifyOptions(verifier_id="v", resolve_key=resolver, now=now))
def test_verify_replay():
key = generate_key()
now = int(time.time())
p = Payload(
iss="iss", aud=["v"], iat=now, exp=now + 3600,
jti="jti-replay", exec_act="act", pred=[],
)
compact = create(p, key, CreateOptions(key_id="kid"))
resolver = lambda kid: key.public_key() if kid == "kid" else None
with pytest.raises(ValueError, match="replay"):
verify(compact, VerifyOptions(
verifier_id="v", resolve_key=resolver, now=now,
jti_seen=lambda j: j == "jti-replay",
))
def test_verify_invalid_typ():
import jwt as jwt_lib
with pytest.raises((ValueError, jwt_lib.exceptions.DecodeError)):
verify("not-a-jws", VerifyOptions())
def test_verify_audience_mismatch():
key = generate_key()
now = int(time.time())
p = Payload(
iss="iss", aud=["other"], iat=now, exp=now + 3600,
jti="jti-a", exec_act="act", pred=[],
)
compact = create(p, key, CreateOptions(key_id="kid"))
resolver = lambda kid: key.public_key() if kid == "kid" else None
with pytest.raises(ValueError, match="audience"):
verify(compact, VerifyOptions(verifier_id="verifier", resolve_key=resolver, now=now))
def test_verify_wit_subject_mismatch():
key = generate_key()
now = int(time.time())
p = Payload(
iss="wrong-iss", aud=["v"], iat=now, exp=now + 3600,
jti="jti-w", exec_act="act", pred=[],
)
compact = create(p, key, CreateOptions(key_id="kid"))
resolver = lambda kid: key.public_key() if kid == "kid" else None
with pytest.raises(ValueError, match="WIT subject"):
verify(compact, VerifyOptions(
verifier_id="v", resolve_key=resolver, now=now, wit_subject="correct-iss",
))
def test_verify_iat_too_old():
key = generate_key()
now = int(time.time())
p = Payload(
iss="iss", aud=["v"], iat=now - 2000, exp=now + 3600,
jti="jti-old", exec_act="act", pred=[],
)
compact = create(p, key, CreateOptions(key_id="kid"))
resolver = lambda kid: key.public_key() if kid == "kid" else None
with pytest.raises(ValueError, match="iat"):
verify(compact, VerifyOptions(
verifier_id="v", resolve_key=resolver, now=now, iat_max_age_sec=900,
))
def test_verify_unknown_key():
key = generate_key()
now = int(time.time())
p = Payload(
iss="iss", aud=["v"], iat=now, exp=now + 3600,
jti="jti-k", exec_act="act", pred=[],
)
compact = create(p, key, CreateOptions(key_id="kid"))
resolver = lambda kid: None # unknown key
with pytest.raises(ValueError, match="unknown key"):
verify(compact, VerifyOptions(verifier_id="v", resolve_key=resolver, now=now))
def test_verify_resolve_key_required():
key = generate_key()
now = int(time.time())
p = Payload(
iss="iss", aud=["v"], iat=now, exp=now + 3600,
jti="jti-r", exec_act="act", pred=[],
)
compact = create(p, key, CreateOptions(key_id="kid"))
with pytest.raises(ValueError, match="ResolveKey"):
verify(compact, VerifyOptions(verifier_id="v", resolve_key=None))
def test_verify_with_dag():
from ect import MemoryLedger
key = generate_key()
ledger = MemoryLedger()
now = int(time.time())
root = Payload(
iss="iss", aud=["v"], iat=now, exp=now + 3600,
jti="jti-root", exec_act="act", pred=[],
)
compact_root = create(root, key, CreateOptions(key_id="kid"))
resolver = lambda kid: key.public_key() if kid == "kid" else None
opts = VerifyOptions(verifier_id="v", resolve_key=resolver, store=ledger, now=now)
parsed = verify(compact_root, opts)
ledger.append(compact_root, parsed.payload)
child = Payload(
iss="iss", aud=["v"], iat=now + 1, exp=now + 3600,
jti="jti-child", exec_act="act2", pred=["jti-root"],
)
compact_child = create(child, key, CreateOptions(key_id="kid"))
parsed2 = verify(compact_child, opts)
assert parsed2.payload.jti == "jti-child"
def test_on_verify_attempt_callback():
"""Observability: on_verify_attempt is called with jti and error (or None)."""
key = generate_key()
now = int(time.time())
p = Payload(iss="i", aud=["v"], iat=now, exp=now + 3600, jti="jti-obs", exec_act="a", pred=[])
compact = create(p, key, CreateOptions(key_id="kid"))
resolver = lambda k: key.public_key() if k == "kid" else None
seen = []
def hook(jti, err):
seen.append((jti, err))
opts = VerifyOptions(verifier_id="v", resolve_key=resolver, on_verify_attempt=hook)
result = verify(compact, opts)
assert result.payload.jti == "jti-obs"
assert len(seen) == 1
assert seen[0][0] == "jti-obs"
assert seen[0][1] is None
def test_on_verify_attempt_called_on_failure():
key = generate_key()
now = int(time.time())
p = Payload(iss="i", aud=["v"], iat=now, exp=now - 1, jti="jti-fail", exec_act="a", pred=[])
compact = create(p, key, CreateOptions(key_id="kid"))
resolver = lambda k: key.public_key() if k == "kid" else None
seen = []
opts = VerifyOptions(verifier_id="v", resolve_key=resolver, now=now, on_verify_attempt=lambda jti, err: seen.append((jti, err)))
with pytest.raises(ValueError, match="expired"):
verify(compact, opts)
assert len(seen) == 1
assert seen[0][0] == "jti-fail"
assert seen[0][1] is not None