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
230 lines
8.8 KiB
Python
230 lines
8.8 KiB
Python
"""Tests for act.delegation module."""
|
|
|
|
import time
|
|
import uuid
|
|
|
|
import pytest
|
|
|
|
from act.crypto import generate_ed25519_keypair, sign, verify, compute_sha256
|
|
from act.delegation import (
|
|
create_delegated_mandate,
|
|
verify_capability_subset,
|
|
verify_delegation_chain,
|
|
)
|
|
from act.errors import (
|
|
ACTDelegationError,
|
|
ACTPrivilegeEscalationError,
|
|
)
|
|
from act.token import (
|
|
ACTMandate,
|
|
Capability,
|
|
Delegation,
|
|
DelegationEntry,
|
|
TaskClaim,
|
|
_b64url_decode,
|
|
encode_jws,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def parent_setup():
|
|
iss_priv, iss_pub = generate_ed25519_keypair()
|
|
mandate = ACTMandate(
|
|
alg="EdDSA", kid="iss-key",
|
|
iss="agent-a", sub="agent-b", aud="agent-b",
|
|
iat=1772064000, exp=1772064900,
|
|
jti="parent-jti-1",
|
|
task=TaskClaim(purpose="parent_task"),
|
|
cap=[
|
|
Capability(action="read.data", constraints={"max_records": 10}),
|
|
Capability(action="write.result"),
|
|
],
|
|
delegation=Delegation(depth=0, max_depth=3, chain=[]),
|
|
)
|
|
sig = sign(iss_priv, mandate.signing_input())
|
|
compact = encode_jws(mandate, sig)
|
|
return mandate, compact, iss_priv, iss_pub
|
|
|
|
|
|
class TestCreateDelegatedMandate:
|
|
def test_basic_delegation(self, parent_setup):
|
|
mandate, compact, priv, _ = parent_setup
|
|
delegated, _ = create_delegated_mandate(
|
|
parent_mandate=mandate, parent_compact=compact,
|
|
delegator_private_key=priv,
|
|
sub="agent-c", kid="key-b", iss="agent-a", aud="agent-c",
|
|
iat=1772064010, exp=1772064600,
|
|
jti="child-jti-1",
|
|
cap=[Capability(action="read.data", constraints={"max_records": 5})],
|
|
task=TaskClaim(purpose="child_task"),
|
|
)
|
|
assert delegated.delegation.depth == 1
|
|
assert len(delegated.delegation.chain) == 1
|
|
assert delegated.delegation.chain[0].delegator == "agent-a"
|
|
|
|
def test_depth_exceeded(self, parent_setup):
|
|
mandate, compact, priv, _ = parent_setup
|
|
# Set parent to max depth
|
|
mandate.delegation = Delegation(depth=3, max_depth=3, chain=[
|
|
DelegationEntry(delegator="x", jti="j", sig="s")
|
|
for _ in range(3)
|
|
])
|
|
with pytest.raises(ACTDelegationError, match="exceeds max_depth"):
|
|
create_delegated_mandate(
|
|
parent_mandate=mandate, parent_compact=compact,
|
|
delegator_private_key=priv,
|
|
sub="c", kid="k", iss="a", aud="c",
|
|
iat=1, exp=2, jti="j",
|
|
cap=[Capability(action="read.data", constraints={"max_records": 5})],
|
|
task=TaskClaim(purpose="t"),
|
|
)
|
|
|
|
def test_no_del_claim(self):
|
|
priv, _ = generate_ed25519_keypair()
|
|
mandate = ACTMandate(
|
|
alg="EdDSA", kid="k", iss="a", sub="b", aud="b",
|
|
iat=1, exp=2,
|
|
task=TaskClaim(purpose="t"),
|
|
cap=[Capability(action="x.y")],
|
|
delegation=None, # no del claim
|
|
)
|
|
with pytest.raises(ACTDelegationError, match="not permitted"):
|
|
create_delegated_mandate(
|
|
parent_mandate=mandate, parent_compact="compact",
|
|
delegator_private_key=priv,
|
|
sub="c", kid="k", iss="a", aud="c",
|
|
iat=1, exp=2, jti="j",
|
|
cap=[Capability(action="x.y")],
|
|
task=TaskClaim(purpose="t"),
|
|
)
|
|
|
|
def test_max_depth_reduction(self, parent_setup):
|
|
mandate, compact, priv, _ = parent_setup
|
|
delegated, _ = create_delegated_mandate(
|
|
parent_mandate=mandate, parent_compact=compact,
|
|
delegator_private_key=priv,
|
|
sub="c", kid="k", iss="a", aud="c",
|
|
iat=1, exp=2, jti="j",
|
|
cap=[Capability(action="read.data", constraints={"max_records": 5})],
|
|
task=TaskClaim(purpose="t"),
|
|
max_depth=2,
|
|
)
|
|
assert delegated.delegation.max_depth == 2
|
|
|
|
def test_max_depth_escalation(self, parent_setup):
|
|
mandate, compact, priv, _ = parent_setup
|
|
with pytest.raises(ACTDelegationError, match="exceeds parent max_depth"):
|
|
create_delegated_mandate(
|
|
parent_mandate=mandate, parent_compact=compact,
|
|
delegator_private_key=priv,
|
|
sub="c", kid="k", iss="a", aud="c",
|
|
iat=1, exp=2, jti="j",
|
|
cap=[Capability(action="read.data", constraints={"max_records": 5})],
|
|
task=TaskClaim(purpose="t"),
|
|
max_depth=10,
|
|
)
|
|
|
|
|
|
class TestCapabilitySubset:
|
|
def test_valid_subset(self):
|
|
parent = [Capability(action="read.data", constraints={"max_records": 10})]
|
|
child = [Capability(action="read.data", constraints={"max_records": 5})]
|
|
verify_capability_subset(parent, child)
|
|
|
|
def test_extra_action(self):
|
|
parent = [Capability(action="read.data")]
|
|
child = [Capability(action="delete.data")]
|
|
with pytest.raises(ACTPrivilegeEscalationError):
|
|
verify_capability_subset(parent, child)
|
|
|
|
def test_numeric_escalation(self):
|
|
parent = [Capability(action="read.data", constraints={"max_records": 10})]
|
|
child = [Capability(action="read.data", constraints={"max_records": 100})]
|
|
with pytest.raises(ACTPrivilegeEscalationError):
|
|
verify_capability_subset(parent, child)
|
|
|
|
def test_sensitivity_escalation(self):
|
|
parent = [Capability(action="read.data",
|
|
constraints={"data_sensitivity": "confidential"})]
|
|
child = [Capability(action="read.data",
|
|
constraints={"data_sensitivity": "internal"})]
|
|
with pytest.raises(ACTPrivilegeEscalationError):
|
|
verify_capability_subset(parent, child)
|
|
|
|
def test_sensitivity_more_restrictive(self):
|
|
parent = [Capability(action="read.data",
|
|
constraints={"data_sensitivity": "internal"})]
|
|
child = [Capability(action="read.data",
|
|
constraints={"data_sensitivity": "restricted"})]
|
|
verify_capability_subset(parent, child) # should pass
|
|
|
|
def test_missing_constraint(self):
|
|
parent = [Capability(action="read.data",
|
|
constraints={"max_records": 10, "scope": "local"})]
|
|
child = [Capability(action="read.data",
|
|
constraints={"max_records": 5})]
|
|
with pytest.raises(ACTPrivilegeEscalationError, match="missing"):
|
|
verify_capability_subset(parent, child)
|
|
|
|
def test_domain_specific_identical(self):
|
|
parent = [Capability(action="read.data",
|
|
constraints={"custom": "value_a"})]
|
|
child = [Capability(action="read.data",
|
|
constraints={"custom": "value_a"})]
|
|
verify_capability_subset(parent, child)
|
|
|
|
def test_domain_specific_different(self):
|
|
parent = [Capability(action="read.data",
|
|
constraints={"custom": "value_a"})]
|
|
child = [Capability(action="read.data",
|
|
constraints={"custom": "value_b"})]
|
|
with pytest.raises(ACTPrivilegeEscalationError, match="identical"):
|
|
verify_capability_subset(parent, child)
|
|
|
|
|
|
class TestVerifyDelegationChain:
|
|
def test_chain_sig_verification(self, parent_setup):
|
|
mandate, compact, priv, pub = parent_setup
|
|
delegated, _ = create_delegated_mandate(
|
|
parent_mandate=mandate, parent_compact=compact,
|
|
delegator_private_key=priv,
|
|
sub="c", kid="k", iss="agent-a", aud="c",
|
|
iat=1, exp=2, jti="j",
|
|
cap=[Capability(action="read.data", constraints={"max_records": 5})],
|
|
task=TaskClaim(purpose="t"),
|
|
)
|
|
|
|
# Verify the chain
|
|
def resolve_key(delegator_id):
|
|
return pub
|
|
|
|
def resolve_compact(jti):
|
|
if jti == "parent-jti-1":
|
|
return compact
|
|
return None
|
|
|
|
verify_delegation_chain(delegated, resolve_key, resolve_compact)
|
|
|
|
def test_no_delegation(self):
|
|
mandate = ACTMandate(
|
|
alg="EdDSA", kid="k", iss="a", sub="b", aud="b",
|
|
iat=1, exp=2,
|
|
task=TaskClaim(purpose="t"),
|
|
cap=[Capability(action="x.y")],
|
|
)
|
|
verify_delegation_chain(mandate, lambda x: None) # no-op
|
|
|
|
def test_depth_exceeds_max(self):
|
|
mandate = ACTMandate(
|
|
alg="EdDSA", kid="k", iss="a", sub="b", aud="b",
|
|
iat=1, exp=2,
|
|
task=TaskClaim(purpose="t"),
|
|
cap=[Capability(action="x.y")],
|
|
delegation=Delegation(depth=5, max_depth=3, chain=[
|
|
DelegationEntry(delegator="x", jti="j", sig="s")
|
|
for _ in range(5)
|
|
]),
|
|
)
|
|
with pytest.raises(ACTDelegationError, match="exceeds"):
|
|
verify_delegation_chain(mandate, lambda x: None)
|