Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions test-vectors/sep-2787/v0/MANIFEST.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"acceptance_gate": "One independent implementation reads the fixtures and produces the same canonical bytes and signature verification results for every NORMATIVE case. POLICY cases come online once the SEP names skew tolerance, alg whitelist, schema, and verifier alg-acceptance.",
"buckets": {
"normative/negative": [
"07-tampered-planner-declared",
"08-tampered-issuer-asserted",
"09-ieee754-float-in-canonical-input"
],
"normative/positive": [
"01-hs256-projection-hash-only",
"02-es256-projection-hash-only",
"03-rs256-projection-hash-only",
"04-hs256-ref",
"05-hs256-projection",
"06-rs256-projection"
],
"verifier-policy/negative": [
"10-ttl-expired-past-skew",
"11-unsupported-alg-hs512",
"12-args-commitment-missing-discriminator",
"13-hs256-envelope-against-es256-verifier"
]
},
"keys": {
"es256_private.pem": {
"sha256": "0fc948b5aa0b07495628bf44660afa843b644c896a0af4f2317633ae634d2d94",
"type": "ec_private_key_pkcs8_pem"
},
"es256_public.pem": {
"sha256": "f2c84537db1320a89b1fae26b7d955da87b6a2476c75c1f3b9c43b6f16d9d5ce",
"type": "ec_public_key_spki_pem"
},
"hs256_secret.bin": {
"sha256": "425ed4e4a36b30ea21b90e21c712c649e8214c29b7eaf68089d1039c6e55384c",
"type": "shared_secret_bytes_32"
},
"rs256_private.pem": {
"sha256": "baf843b506a2cc8d52798d2dca0ebf1c5e361d95d3060bd64d78531d0d1face5",
"type": "rsa_private_key_pkcs8_pem"
},
"rs256_public.pem": {
"sha256": "8cdd915908c3d6d83ce74eb667a5576d8424d5adfc3e626d6acca11576c9f438",
"type": "rsa_public_key_spki_pem"
}
},
"license": "Apache-2.0",
"name": "sep-2787-vectors",
"pinned_values": {
"expSecondsDefault": 300,
"iat": "2026-05-26T12:00:00Z",
"iss": "issuer://test",
"nonce": "n0_sep2787_v0_fixtures_pinned_",
"secretVersion": "v1",
"sub": "agent:archiver"
},
"provenance": "Derived from tests/test_attestation_sep2787*.py at commit 5ea7cd3 of vaaraio/vaara (tag sep2787-ref-v2, merged via vaaraio/vaara#151).",
"schema_version": "v0"
}
119 changes: 119 additions & 0 deletions test-vectors/sep-2787/v0/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# SEP-2787 Tool Call Attestation, test vectors v0 (v2 envelope)

These vectors target the v2 envelope shape: `plannerDeclared` /
`issuerAsserted` / `payloadDerived` trust-surface blocks with
`toolCalls` under `payloadDerived`, `args` carrying either an
`ArgsRef` (present `ref`, optionally `digest` + `canonicalization`)
or an `ArgsProjection` (present `projection` as a JCS-canonical
JSON string, plus `projectionDigest`), JCS canonicalisation of the
envelope, IEEE-754 float rejection at the canonicalisation boundary,
MCP camelCase field names.

The v2 envelope drops the previous `kind`-discriminated args union.
`ArgsRef` and `ArgsProjection` self-discriminate by present fields.
Hash-only identity is expressed as an `ArgsProjection` whose
projection content is `{"digest": "sha256:..."}`, which removes the
need for a separate `ArgsDigest` variant.

Apache-2.0. Derived from `tests/test_attestation_sep2787*.py` at
vaaraio/vaara@5ea7cd3 (tag `sep2787-ref-v2`, merged via
vaaraio/vaara#151). SEP maintainers own the final normative artifact
location.

## Layout

`normative/` cases bind the wire format. A conformant SEP-2787
implementation MUST reproduce or reject these as documented.

- `positive/`. Canonical bytes, signature input bytes, and signed
envelopes across HS256, ES256, RS256, and across both
args-commitment shapes. Six cases: three hash-only-identity
`ArgsProjection` round-trips (one per alg), one `ArgsRef`
round-trip, two `ArgsProjection` round-trips carrying real
projection content.
- `negative/`. Tampering on the plannerDeclared and issuerAsserted
blocks (signature verification fails on the recanonicalised body),
and IEEE-754 float rejection at the canonicalisation boundary.
Three cases.

`verifier-policy/` cases depend on validator policy that the SEP has
not yet specified. They are not pass/fail against the wire format
alone.

- TTL expiry past `iat + exp + skew`. Skew tolerance is policy.
- Unsupported alg (HS512) rejection. Alg whitelist is policy.
- Schema rejection of an `args` object that carries neither `ref` nor
`projection`, so neither commitment branch can be reconstructed.
Schema is policy.
- HS256 envelope against an ES256-only verifier. Alg-acceptance is
policy.

Once the SEP names skew tolerance, the alg whitelist, the canonical
schema, and the verifier alg-acceptance rule, these cases become
normative.

## Per-case files

Each case directory contains up to five files.

- `unsigned_envelope.json`. The envelope body before signing, with the
three trust-surface blocks `plannerDeclared`, `issuerAsserted`,
`payloadDerived`, plus `version` and `alg`.
- `canonical_signing_input.bin`. The RFC 8785 (JCS) canonical encoding
of the body the signature was actually computed over. For positive
cases this equals the recanonicalisation of the body present in
`signed_envelope.json`. For tampered cases (07, 08) it is the
pre-tamper canonical, so the signature still verifies against these
bytes; recanonicalising the present (tampered) body produces
different bytes against which the signature does not verify, and
that is how the walker detects tampering.
- `canonical_signing_input.hex`. Same bytes as `canonical_signing_input.bin`,
hex-encoded for human inspection and diff-friendly review.
- `signed_envelope.json`. The signed envelope including the `signature`
field. Omitted for the float-rejection case where canonicalisation
itself is the rejection point.
- `expected.json`. Machine-readable expected outcome: verification
result, verifying material, rejection dimension (for negative cases),
policy dependency (for verifier-policy cases), determinism flag.
Time-sensitive policy cases (case 10) carry `verify_at_epoch`
(integer UNIX seconds) so the verifier's clock is fixture data,
not ambient wall clock. A second implementation replays the same
inputs and gets the same pass/fail regardless of when it runs.

## Determinism

HS256 and RS256 (PKCS1v15) are deterministic. A second implementation
re-signing `canonical_signing_input.bin` with the corresponding key
reproduces `signed_envelope.signature` exactly.

ES256 signing is randomised. The ES256 case stores one valid signature.
A second implementation verifies it against `keys/es256_public.pem`
rather than reproducing the hex bit-for-bit.

## Keys

`keys/hs256_secret.bin` is 32 raw bytes. `keys/es256_private.pem` and
`keys/es256_public.pem` are PKCS8 and SPKI PEM. `keys/rs256_private.pem`
and `keys/rs256_public.pem` are PKCS8 and SPKI PEM. ES256 signatures
are raw r||s (64 bytes), not ASN.1 DER.

## Independent walker

`_check_independent.py` reads the fixtures from disk and walks the
conformance dimensions with no reference to any implementation. Imports
stdlib plus `cryptography` and `rfc8785` only. Output is tagged
NORMATIVE or POLICY per bucket.

Run it with:

```
pip install cryptography rfc8785
python _check_independent.py
```

## Acceptance gate

One independent implementation reads these fixtures and produces the
same canonical bytes and signature verification results for every
NORMATIVE case. POLICY cases come online once the SEP specifies the
relevant validator-policy paragraphs.
192 changes: 192 additions & 0 deletions test-vectors/sep-2787/v0/_check_independent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
"""Independent walker for the SEP-2787 v0 test vectors.

Reads fixtures from disk with no reference to any implementation.
Imports stdlib plus `cryptography` and `rfc8785` only.

Usage:
pip install cryptography rfc8785
python _check_independent.py

NORMATIVE cases gate the exit code. POLICY cases are reported but do
not affect it, because they depend on validator policy the SEP has not
yet specified. Apache-2.0.
"""
from __future__ import annotations
import hashlib, hmac, json, sys
from datetime import datetime
from pathlib import Path

import rfc8785
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec, padding
from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature

HERE = Path(__file__).resolve().parent
KEYS = HERE / "keys"
DEFAULT_ALG_WHITELIST = {"HS256", "ES256", "RS256"}
DEFAULT_SKEW_SECONDS = 30
ARGS_DISCRIMINATOR_FIELDS = {"ref", "projection"}


def load_hs(): return (KEYS / "hs256_secret.bin").read_bytes()
def load_es(): return serialization.load_pem_public_key((KEYS / "es256_public.pem").read_bytes())
def load_rs(): return serialization.load_pem_public_key((KEYS / "rs256_public.pem").read_bytes())


def verify_hs256(payload, sig_hex, secret):
expected = hmac.new(secret, payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig_hex)


def verify_es256(payload, sig_hex, pub):
if len(sig_hex) != 128:
return False
try:
raw = bytes.fromhex(sig_hex)
except ValueError:
return False
der = encode_dss_signature(int.from_bytes(raw[:32], "big"),
int.from_bytes(raw[32:], "big"))
try:
pub.verify(der, payload, ec.ECDSA(hashes.SHA256()))
return True
except InvalidSignature:
return False


def verify_rs256(payload, sig_hex, pub):
try:
sig = bytes.fromhex(sig_hex)
except ValueError:
return False
try:
pub.verify(sig, payload, padding.PKCS1v15(), hashes.SHA256())
return True
except InvalidSignature:
return False


def body_of(signed):
return {k: v for k, v in signed.items() if k != "signature"}


def report(cid, ok, detail=""):
print(f" [{'PASS' if ok else 'FAIL'}] {cid}{(' ' + detail) if detail else ''}")
return ok


def has_float(v):
if isinstance(v, float):
return True
if isinstance(v, dict):
return any(has_float(x) for x in v.values())
if isinstance(v, list):
return any(has_float(x) for x in v)
return False


def walk_norm_positive():
print("\n== NORMATIVE / positive ==")
out = []
for cd in sorted((HERE / "normative" / "positive").iterdir()):
if not cd.is_dir():
continue
expected = json.loads((cd / "expected.json").read_text())
signed = json.loads((cd / "signed_envelope.json").read_text())
stored = (cd / "canonical_signing_input.bin").read_bytes()
recomputed = rfc8785.dumps(body_of(signed))
if recomputed != stored:
out.append(report(cd.name, False, "canonical bytes mismatch"))
continue
alg = expected["alg"]
sig = signed["signature"]
if alg == "HS256":
ok = verify_hs256(stored, sig, load_hs())
elif alg == "ES256":
ok = verify_es256(stored, sig, load_es())
elif alg == "RS256":
ok = verify_rs256(stored, sig, load_rs())
else:
ok = False
out.append(report(cd.name, ok, f"{alg} verification"))
return out


def walk_norm_negative():
print("\n== NORMATIVE / negative ==")
out = []
for cd in sorted((HERE / "normative" / "negative").iterdir()):
if not cd.is_dir():
continue
cid = cd.name
if cid == "09-ieee754-float-in-canonical-input":
body = json.loads((cd / "unsigned_envelope.json").read_text())
out.append(report(cid, has_float(body), "IEEE-754 float at boundary"))
continue
signed = json.loads((cd / "signed_envelope.json").read_text())
stored = (cd / "canonical_signing_input.bin").read_bytes()
recomputed = rfc8785.dumps(body_of(signed))
sig_ok_on_stored = verify_hs256(stored, signed["signature"], load_hs())
sig_ok_on_present = verify_hs256(recomputed, signed["signature"], load_hs())
tamper_detected = (
sig_ok_on_stored and not sig_ok_on_present and recomputed != stored
)
out.append(report(cid, tamper_detected,
"present body recanonicalises to bytes that do not verify"))
return out


def walk_policy():
print("\n== VERIFIER-POLICY / negative ==")
out = []
for cd in sorted((HERE / "verifier-policy" / "negative").iterdir()):
if not cd.is_dir():
continue
cid = cd.name
expected = json.loads((cd / "expected.json").read_text())
signed = json.loads((cd / "signed_envelope.json").read_text())
if cid == "10-ttl-expired-past-skew":
iat = signed["issuerAsserted"]["iat"]
iat_e = datetime.fromisoformat(iat.replace("Z", "+00:00")).timestamp()
deadline = iat_e + signed["issuerAsserted"]["expSeconds"] + DEFAULT_SKEW_SECONDS
verify_at = expected["verify_at_epoch"]
rejected = verify_at > deadline
reason = (f"verify_at_epoch > iat+exp+skew "
f"(default skew={DEFAULT_SKEW_SECONDS}s)")
elif cid == "11-unsupported-alg-hs512":
rejected = signed["alg"] not in DEFAULT_ALG_WHITELIST
reason = f"alg {signed['alg']!r} not in default whitelist"
elif cid == "12-args-commitment-missing-discriminator":
calls = signed["payloadDerived"]["toolCalls"]
missing = [i for i, c in enumerate(calls)
if not (set(c["args"]) & ARGS_DISCRIMINATOR_FIELDS)]
rejected = bool(missing)
allowed = sorted(ARGS_DISCRIMINATOR_FIELDS)
reason = (f"toolCalls indices {missing} have args with neither ref "
f"nor projection (allowed discriminators: {allowed})")
elif cid == "13-hs256-envelope-against-es256-verifier":
rejected = signed["alg"] != "ES256"
reason = f"envelope alg {signed['alg']!r} != verifier policy ES256_only"
else:
rejected = False
reason = "unknown policy case"
out.append(report(cid, rejected, reason))
return out


def main():
p, n, pol = walk_norm_positive(), walk_norm_negative(), walk_policy()
print("\n== Summary ==")
print(f" NORMATIVE positive: {sum(p)}/{len(p)} pass")
print(f" NORMATIVE negative: {sum(n)}/{len(n)} pass")
print(f" POLICY negative: {sum(pol)}/{len(pol)} match default policy")
if all(p + n):
print("\nNORMATIVE: ALL PASS")
sys.exit(0)
print("\nNORMATIVE: FAILURES PRESENT")
sys.exit(1)


if __name__ == "__main__":
main()
5 changes: 5 additions & 0 deletions test-vectors/sep-2787/v0/keys/es256_private.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgO4Qv59j2Vx4rWGMd
q3F/h1kOH1qedikuF2wB+VA8zBChRANCAAQEQVpmiQ4lL0HekqDdc1lmRPp5Nlz/
+KG6KaxlBjdRmcu+nANK6Z9D0ULZT6+sWOBMGXZiByGd3xadisuilaFZ
-----END PRIVATE KEY-----
4 changes: 4 additions & 0 deletions test-vectors/sep-2787/v0/keys/es256_public.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEBEFaZokOJS9B3pKg3XNZZkT6eTZc
//ihuimsZQY3UZnLvpwDSumfQ9FC2U+vrFjgTBl2Ygchnd8WnYrLopWhWQ==
-----END PUBLIC KEY-----
1 change: 1 addition & 0 deletions test-vectors/sep-2787/v0/keys/hs256_secret.bin
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
Loading
Loading