181 lines
6.5 KiB
Python
181 lines
6.5 KiB
Python
#!/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())
|