feat(survey): add IETF landscape survey (kappa, phase0, rerate), gaps update; bump wimse-ect; gitignore run logs
This commit is contained in:
147
workspace/packages/act/act/schema.json
Normal file
147
workspace/packages/act/act/schema.json
Normal file
@@ -0,0 +1,147 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "urn:ietf:params:act:schema:token",
|
||||
"title": "Agent Context Token (ACT)",
|
||||
"description": "JSON Schema for ACT Phase 1 (Authorization Mandate) and Phase 2 (Execution Record) claim sets. A token is a Phase 2 record if and only if it contains the exec_act, exec_ts, pred, and status claims; otherwise it is a Phase 1 mandate.",
|
||||
"type": "object",
|
||||
"oneOf": [
|
||||
{ "$ref": "#/$defs/phase1" },
|
||||
{ "$ref": "#/$defs/phase2" }
|
||||
],
|
||||
"$defs": {
|
||||
"actionName": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Za-z][A-Za-z0-9_-]*(\\.[A-Za-z][A-Za-z0-9_-]*)*$"
|
||||
},
|
||||
"numericDate": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"uuid": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"audience": {
|
||||
"oneOf": [
|
||||
{ "type": "string" },
|
||||
{ "type": "array", "items": { "type": "string" }, "minItems": 1 }
|
||||
]
|
||||
},
|
||||
"task": {
|
||||
"type": "object",
|
||||
"required": ["purpose"],
|
||||
"properties": {
|
||||
"purpose": { "type": "string", "minLength": 1 },
|
||||
"data_sensitivity": {
|
||||
"type": "string",
|
||||
"enum": ["public", "internal", "confidential", "restricted"]
|
||||
},
|
||||
"created_by": { "type": "string" },
|
||||
"expires_at": { "$ref": "#/$defs/numericDate" }
|
||||
},
|
||||
"additionalProperties": true
|
||||
},
|
||||
"capability": {
|
||||
"type": "object",
|
||||
"required": ["action"],
|
||||
"properties": {
|
||||
"action": { "$ref": "#/$defs/actionName" },
|
||||
"constraints": { "type": "object" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"oversight": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"requires_approval_for": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/actionName" }
|
||||
},
|
||||
"approval_ref": { "type": "string" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"delegationEntry": {
|
||||
"type": "object",
|
||||
"required": ["delegator", "jti", "sig"],
|
||||
"properties": {
|
||||
"delegator": { "type": "string" },
|
||||
"jti": { "$ref": "#/$defs/uuid" },
|
||||
"sig": { "type": "string" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"delegation": {
|
||||
"type": "object",
|
||||
"required": ["depth", "max_depth", "chain"],
|
||||
"properties": {
|
||||
"depth": { "type": "integer", "minimum": 0 },
|
||||
"max_depth": { "type": "integer", "minimum": 0 },
|
||||
"chain": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/delegationEntry" }
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"error": {
|
||||
"type": "object",
|
||||
"required": ["code"],
|
||||
"properties": {
|
||||
"code": { "type": "string" },
|
||||
"detail": { "type": "string" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"commonClaims": {
|
||||
"type": "object",
|
||||
"required": ["iss", "sub", "aud", "iat", "exp", "jti", "task", "cap"],
|
||||
"properties": {
|
||||
"iss": { "type": "string" },
|
||||
"sub": { "type": "string" },
|
||||
"aud": { "$ref": "#/$defs/audience" },
|
||||
"iat": { "$ref": "#/$defs/numericDate" },
|
||||
"exp": { "$ref": "#/$defs/numericDate" },
|
||||
"jti": { "$ref": "#/$defs/uuid" },
|
||||
"wid": { "$ref": "#/$defs/uuid" },
|
||||
"task": { "$ref": "#/$defs/task" },
|
||||
"cap": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "$ref": "#/$defs/capability" }
|
||||
},
|
||||
"oversight": { "$ref": "#/$defs/oversight" },
|
||||
"del": { "$ref": "#/$defs/delegation" }
|
||||
}
|
||||
},
|
||||
"phase1": {
|
||||
"allOf": [{ "$ref": "#/$defs/commonClaims" }],
|
||||
"not": {
|
||||
"anyOf": [
|
||||
{ "required": ["exec_act"] },
|
||||
{ "required": ["exec_ts"] },
|
||||
{ "required": ["status"] }
|
||||
]
|
||||
}
|
||||
},
|
||||
"phase2": {
|
||||
"allOf": [{ "$ref": "#/$defs/commonClaims" }],
|
||||
"required": ["exec_act", "pred", "exec_ts", "status"],
|
||||
"properties": {
|
||||
"exec_act": { "$ref": "#/$defs/actionName" },
|
||||
"pred": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/uuid" }
|
||||
},
|
||||
"inp_hash": { "type": "string" },
|
||||
"out_hash": { "type": "string" },
|
||||
"exec_ts": { "$ref": "#/$defs/numericDate" },
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["completed", "failed", "partial"]
|
||||
},
|
||||
"err": { "$ref": "#/$defs/error" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,10 +78,20 @@ def generate_vectors() -> tuple[list[TestVector], dict[str, Any]]:
|
||||
# Fixed timestamp for deterministic vectors
|
||||
base_time = 1772064000
|
||||
|
||||
# Generate key pairs for test agents
|
||||
iss_priv, iss_pub = generate_ed25519_keypair()
|
||||
sub_priv, sub_pub = generate_ed25519_keypair()
|
||||
agent_c_priv, agent_c_pub = generate_ed25519_keypair()
|
||||
# Fixed Ed25519 seeds so the published Appendix B vectors are
|
||||
# byte-for-byte reproducible. Seeds are recognizable test patterns
|
||||
# (NOT for production use): bytes 0x00..0x1f, 0x20..0x3f, 0x40..0x5f.
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
|
||||
Ed25519PrivateKey,
|
||||
)
|
||||
|
||||
def _ed25519_from_seed(seed: bytes) -> tuple[Any, Any]:
|
||||
priv = Ed25519PrivateKey.from_private_bytes(seed)
|
||||
return priv, priv.public_key()
|
||||
|
||||
iss_priv, iss_pub = _ed25519_from_seed(bytes(range(0x00, 0x20)))
|
||||
sub_priv, sub_pub = _ed25519_from_seed(bytes(range(0x20, 0x40)))
|
||||
agent_c_priv, agent_c_pub = _ed25519_from_seed(bytes(range(0x40, 0x60)))
|
||||
|
||||
# Fixed JTIs for cross-referencing
|
||||
jti_b1 = "550e8400-e29b-41d4-a716-446655440001"
|
||||
@@ -91,6 +101,14 @@ def generate_vectors() -> tuple[list[TestVector], dict[str, Any]]:
|
||||
jti_b3 = "550e8400-e29b-41d4-a716-446655440005"
|
||||
jti_b4 = "550e8400-e29b-41d4-a716-446655440006"
|
||||
jti_b5 = "550e8400-e29b-41d4-a716-446655440007"
|
||||
# Fixed JTIs for the invalid-vector cases (kept stable so the
|
||||
# published compact serializations are reproducible).
|
||||
jti_b6 = "550e8400-e29b-41d4-a716-446655440008"
|
||||
jti_b8 = "550e8400-e29b-41d4-a716-446655440009"
|
||||
jti_b9 = "550e8400-e29b-41d4-a716-44665544000a"
|
||||
jti_b10 = "550e8400-e29b-41d4-a716-44665544000b"
|
||||
jti_b12 = "550e8400-e29b-41d4-a716-44665544000c"
|
||||
jti_b13 = "550e8400-e29b-41d4-a716-44665544000d"
|
||||
wid = "a0b1c2d3-e4f5-6789-abcd-ef0123456789"
|
||||
|
||||
# Key registry
|
||||
@@ -281,7 +299,7 @@ def generate_vectors() -> tuple[list[TestVector], dict[str, Any]]:
|
||||
iss="agent-issuer", sub="agent-subject",
|
||||
aud="agent-subject",
|
||||
iat=base_time, exp=base_time + 900,
|
||||
jti=str(uuid.uuid4()),
|
||||
jti=jti_b6,
|
||||
task=TaskClaim(purpose="bad_depth"),
|
||||
cap=[Capability(action="read.data")],
|
||||
delegation=Delegation(depth=3, max_depth=2, chain=[
|
||||
@@ -315,7 +333,7 @@ def generate_vectors() -> tuple[list[TestVector], dict[str, Any]]:
|
||||
iss="agent-issuer", sub="agent-subject",
|
||||
aud="agent-subject",
|
||||
iat=base_time, exp=base_time + 900,
|
||||
jti=str(uuid.uuid4()),
|
||||
jti=jti_b8,
|
||||
task=TaskClaim(purpose="bad_exec"),
|
||||
cap=[Capability(action="read.data")],
|
||||
delegation=Delegation(depth=0, max_depth=1, chain=[]),
|
||||
@@ -346,7 +364,7 @@ def generate_vectors() -> tuple[list[TestVector], dict[str, Any]]:
|
||||
))
|
||||
|
||||
# --- B.9: DAG cycle (pred references own jti) → ACTDAGError ---
|
||||
cycle_jti = str(uuid.uuid4())
|
||||
cycle_jti = jti_b9
|
||||
cycle_record = ACTRecord(
|
||||
alg="EdDSA", kid="sub-key",
|
||||
iss="agent-issuer", sub="agent-subject",
|
||||
@@ -376,7 +394,7 @@ def generate_vectors() -> tuple[list[TestVector], dict[str, Any]]:
|
||||
iss="agent-issuer", sub="agent-subject",
|
||||
aud="agent-subject",
|
||||
iat=base_time, exp=base_time + 900,
|
||||
jti=str(uuid.uuid4()),
|
||||
jti=jti_b10,
|
||||
task=TaskClaim(purpose="missing_parent"),
|
||||
cap=[Capability(action="read.data")],
|
||||
exec_act="read.data",
|
||||
@@ -420,7 +438,7 @@ def generate_vectors() -> tuple[list[TestVector], dict[str, Any]]:
|
||||
aud="agent-subject",
|
||||
iat=base_time - 3600,
|
||||
exp=base_time - 2700, # expired 45 minutes ago
|
||||
jti=str(uuid.uuid4()),
|
||||
jti=jti_b12,
|
||||
task=TaskClaim(purpose="expired_test"),
|
||||
cap=[Capability(action="read.data")],
|
||||
)
|
||||
@@ -441,7 +459,7 @@ def generate_vectors() -> tuple[list[TestVector], dict[str, Any]]:
|
||||
iss="agent-issuer", sub="wrong-agent",
|
||||
aud="wrong-agent",
|
||||
iat=base_time, exp=base_time + 900,
|
||||
jti=str(uuid.uuid4()),
|
||||
jti=jti_b13,
|
||||
task=TaskClaim(purpose="wrong_aud_test"),
|
||||
cap=[Capability(action="read.data")],
|
||||
)
|
||||
|
||||
180
workspace/packages/act/scripts/gen_appendix_b.py
Normal file
180
workspace/packages/act/scripts/gen_appendix_b.py
Normal file
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate the Appendix B (Test Vectors) section body for
|
||||
draft-nennemann-act, in kramdown-rfc markdown.
|
||||
|
||||
The vectors are produced by act.vectors.generate_vectors() using fixed
|
||||
Ed25519 seeds and fixed JTIs, so the output is byte-for-byte
|
||||
reproducible. Run this whenever the reference implementation changes:
|
||||
|
||||
python scripts/gen_appendix_b.py > /tmp/appendix-b.md
|
||||
|
||||
then splice the result into the draft's Appendix B.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
|
||||
from act.token import decode_jws
|
||||
from act.vectors import generate_vectors
|
||||
|
||||
|
||||
def b64url(data: bytes) -> str:
|
||||
import base64
|
||||
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
|
||||
|
||||
|
||||
def fold(s: str, width: int = 64) -> str:
|
||||
return "\n".join(s[i:i + width] for i in range(0, len(s), width))
|
||||
|
||||
|
||||
def jblock(obj: object) -> str:
|
||||
return "~~~ json\n" + json.dumps(obj, indent=2, sort_keys=False) + "\n~~~"
|
||||
|
||||
|
||||
# Clean, implementation-neutral section titles (ASCII only).
|
||||
TITLES = {
|
||||
"B.1": "Valid Phase 1 ACT: root mandate, Tier 1 (Ed25519), "
|
||||
"no delegation",
|
||||
"B.2": "Valid Phase 2 ACT: completed execution (transition "
|
||||
"from B.1)",
|
||||
"B.3": "Valid Phase 2 ACT: fan-in with two predecessor tasks",
|
||||
"B.4": "Valid Phase 1 ACT: delegated mandate (depth 1)",
|
||||
"B.5": "Valid Phase 2 ACT: delegated execution record",
|
||||
"B.6": "Invalid: delegation depth exceeds maximum",
|
||||
"B.7": "Invalid: capability escalation in delegated mandate",
|
||||
"B.8": "Invalid: exec_act not present in cap",
|
||||
"B.9": "Invalid: DAG cycle (pred references own jti)",
|
||||
"B.10": "Invalid: missing predecessor task in DAG",
|
||||
"B.11": "Invalid: tampered payload",
|
||||
"B.12": "Invalid: expired token",
|
||||
"B.13": "Invalid: audience mismatch",
|
||||
"B.14": "Invalid: Phase 2 record signed by wrong key",
|
||||
"B.15": 'Invalid: unsigned token (alg "none")',
|
||||
}
|
||||
|
||||
# Expected-result text per vector id, in specification terms (no
|
||||
# implementation-specific exception names).
|
||||
EXPECTED = {
|
||||
"B.1": "Accept. The signature verifies under kid \"iss-key\" and "
|
||||
"all required Phase 1 claims are present.",
|
||||
"B.2": "Accept. Phase 2 record; the signature verifies under kid "
|
||||
"\"sub-key\" and exec_act \"read.data\" is present in cap.",
|
||||
"B.3": "Accept. Phase 2 fan-in; pred lists the two predecessor jti "
|
||||
"values from the parallel branches.",
|
||||
"B.4": "Accept. Delegated mandate at depth 1; the delegated cap "
|
||||
"(max_records=5) is a subset of the parent cap "
|
||||
"(max_records=10).",
|
||||
"B.5": "Accept. Phase 2 record for the delegated mandate; the "
|
||||
"signature verifies under kid \"agent-c-key\".",
|
||||
"B.6": "Reject. del.depth (3) exceeds del.max_depth (2).",
|
||||
"B.7": "Reject. The delegated cap (max_records=100) is not a subset "
|
||||
"of the parent cap (max_records=10); privilege reduction is "
|
||||
"violated. This vector is a delegation-creation check; no "
|
||||
"token is issued.",
|
||||
"B.8": "Reject. exec_act \"delete.everything\" does not match any "
|
||||
"cap.action.",
|
||||
"B.9": "Reject. pred references the record's own jti, forming a "
|
||||
"cycle.",
|
||||
"B.10": "Reject. pred references a jti that is not a previously "
|
||||
"verified task.",
|
||||
"B.11": "Reject. The signature does not verify; one byte of the "
|
||||
"claims segment was altered after signing.",
|
||||
"B.12": "Reject. exp is in the past (expired 45 minutes before the "
|
||||
"verification time).",
|
||||
"B.13": "Reject. aud does not include the verifier identity.",
|
||||
"B.14": "Reject. The Phase 2 record claims kid \"sub-key\" but was "
|
||||
"signed with the issuer's private key; the signature does "
|
||||
"not verify.",
|
||||
"B.15": "Reject. The JOSE header alg is \"none\"; unsigned tokens "
|
||||
"MUST NOT be accepted.",
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
vectors, ctx = generate_vectors()
|
||||
|
||||
out: list[str] = []
|
||||
out.append("# Appendix B: Test Vectors")
|
||||
out.append('{:numbered="false"}')
|
||||
out.append("")
|
||||
out.append(
|
||||
"All vectors in this appendix are produced by the reference "
|
||||
"implementation using fixed Ed25519 seeds and fixed identifiers, "
|
||||
"and are therefore reproducible. The base timestamp (iat) is "
|
||||
"1772064000 (2026-02-26T00:00:00Z). Long Base64url values are "
|
||||
"wrapped for display only; line breaks are not part of the "
|
||||
"value."
|
||||
)
|
||||
out.append("")
|
||||
|
||||
# --- Key material ---
|
||||
out.append("## B.0. Key Material")
|
||||
out.append('{:numbered="false"}')
|
||||
out.append("")
|
||||
out.append(
|
||||
"Three Ed25519 key pairs are used, identified by kid. Keys are "
|
||||
"given in JWK {{RFC7517}} form using the Ed25519 OKP encoding "
|
||||
"{{RFC8037}}; \"d\" is the private seed and \"x\" is the public "
|
||||
"key, both Base64url-encoded. These seeds are recognizable test "
|
||||
"patterns and MUST NOT be used in production."
|
||||
)
|
||||
out.append("")
|
||||
keymap = [
|
||||
("iss-key", ctx["iss_priv"], ctx["iss_pub"]),
|
||||
("sub-key", ctx["sub_priv"], ctx["sub_pub"]),
|
||||
("agent-c-key", ctx["agent_c_priv"], ctx["agent_c_pub"]),
|
||||
]
|
||||
for kid, priv, pub in keymap:
|
||||
jwk = {
|
||||
"kty": "OKP",
|
||||
"crv": "Ed25519",
|
||||
"kid": kid,
|
||||
"d": b64url(priv.private_bytes_raw()),
|
||||
"x": b64url(pub.public_bytes_raw()),
|
||||
}
|
||||
out.append(jblock(jwk))
|
||||
out.append("")
|
||||
|
||||
# --- Per-vector rendering ---
|
||||
for v in vectors:
|
||||
out.append(f"## {v.id}. {TITLES[v.id]}")
|
||||
out.append('{:numbered="false"}')
|
||||
out.append("")
|
||||
|
||||
decoded = None
|
||||
if v.compact:
|
||||
try:
|
||||
header, claims, _sig, _si = decode_jws(v.compact)
|
||||
decoded = (header, claims)
|
||||
except Exception:
|
||||
decoded = None
|
||||
|
||||
if decoded is not None:
|
||||
header, claims = decoded
|
||||
out.append("JOSE Header:")
|
||||
out.append("")
|
||||
out.append(jblock(header))
|
||||
out.append("")
|
||||
out.append("JWT Claims:")
|
||||
out.append("")
|
||||
out.append(jblock(claims))
|
||||
out.append("")
|
||||
|
||||
if v.compact:
|
||||
out.append("JWS Compact Serialization:")
|
||||
out.append("")
|
||||
out.append("~~~")
|
||||
out.append(fold(v.compact))
|
||||
out.append("~~~")
|
||||
out.append("")
|
||||
|
||||
out.append(f"Expected result: {EXPECTED[v.id]}")
|
||||
out.append("")
|
||||
|
||||
sys.stdout.write("\n".join(out) + "\n")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,9 +1,15 @@
|
||||
"""Tests for act.vectors module — Appendix B test vectors."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from act.token import decode_jws
|
||||
from act.vectors import generate_vectors, validate_vectors
|
||||
|
||||
SCHEMA_PATH = Path(__file__).resolve().parent.parent / "act" / "schema.json"
|
||||
|
||||
|
||||
class TestVectorGeneration:
|
||||
def test_generates_15_vectors(self):
|
||||
@@ -33,3 +39,32 @@ class TestVectorGeneration:
|
||||
class TestVectorValidation:
|
||||
def test_all_vectors_pass(self):
|
||||
assert validate_vectors() is True
|
||||
|
||||
|
||||
class TestVectorReproducibility:
|
||||
def test_compacts_are_deterministic(self):
|
||||
# Published Appendix B vectors must be byte-for-byte stable.
|
||||
v1, _ = generate_vectors()
|
||||
v2, _ = generate_vectors()
|
||||
a = {v.id: v.compact for v in v1}
|
||||
b = {v.id: v.compact for v in v2}
|
||||
assert a == b
|
||||
|
||||
|
||||
class TestSchemaConformance:
|
||||
def test_schema_is_well_formed(self):
|
||||
jsonschema = pytest.importorskip("jsonschema")
|
||||
schema = json.loads(SCHEMA_PATH.read_text())
|
||||
jsonschema.Draft202012Validator.check_schema(schema)
|
||||
|
||||
def test_valid_vectors_conform_to_schema(self):
|
||||
jsonschema = pytest.importorskip("jsonschema")
|
||||
schema = json.loads(SCHEMA_PATH.read_text())
|
||||
validator = jsonschema.Draft202012Validator(schema)
|
||||
vectors, _ = generate_vectors()
|
||||
valid_with_token = {"B.1", "B.2", "B.3", "B.4", "B.5"}
|
||||
for v in vectors:
|
||||
if v.id in valid_with_token:
|
||||
_h, claims, _s, _si = decode_jws(v.compact)
|
||||
errors = list(validator.iter_errors(claims))
|
||||
assert not errors, f"{v.id}: {errors}"
|
||||
|
||||
Reference in New Issue
Block a user