Conformance suite
Every Ratify SDK ships with a test that loads the canonical fixtures from testvectors/v1/
and runs them through every protocol operation. The current conformance corpus is:
- 59 wire-format / verifier fixtures — each is one JSON file. Cover delegation signing, challenge signing, full bundle verification (positive and negative), revocation, scope intersection, and all seven built-in constraint types.
- 10 cross-SDK byte-equivalence vectors (bundled in
cross_sdk_vectors.json, added in alpha.7) — pin canonical bytes forVerificationReceipt,TransactionReceipt,PolicyVerdict,SessionToken, and the canonicalVerifierContexthash so a verifier in one language can byte-for-byte verify a primitive signed in another.
If all 59 wire fixtures + the 10 cross-SDK vectors pass in your SDK, your SDK is byte-for-byte interoperable with every other Ratify SDK on the planet. That is the contract.
What’s in the suite
Section titled “What’s in the suite” testvectors/v1/ ├── happy_path_depth_{1,2,3}.json (positive verify, chain depth 1–3) ├── reject_*.json (negative verify — every rejection branch) ├── constraint_geo_*.json (geo_circle / polygon / bbox, inside / outside / antimeridian) ├── constraint_time_window_*.json (non-wrap / wrapping windows) ├── constraint_max_{amount,rate,speed_mps}_*.json ├── constraint_unknown_denied.json (fail-closed on unknown constraint type) ├── key_rotation_valid.json (KeyRotationStatement) ├── session_token_*.json (v1.1 fast path) ├── stream_bundle_*.json (v1.1 stream binding) ├── verification_receipt_*.json (alpha.7) ├── transaction_receipt_*.json (alpha.7) └── cross_sdk_vectors.json (10 alpha.7 byte-equivalence vectors)Each fixture is a deterministic JSON file with:
- Inputs. Keys (seeded so they’re reproducible), cert/bundle/list shape, verifier context.
- Expected canonical bytes. What
delegationSignBytesorchallengeSignBytesshould produce (hex-encoded). Any SDK that produces different bytes here has a non-byte-identical canonicalizer and is rejected. - Expected verification outcome. For verify fixtures: the exact
VerifyResultstruct the verifier should return, includingidentity_status,granted_scope, anderror_reason.
The fixtures are generated by cmd/ratify-testvectors in the protocol repo. The Go reference
is authoritative; every other SDK must produce identical output.
Running the suite
Section titled “Running the suite”# Gocd ratify-protocolgo test ./...
# TypeScriptcd sdks/typescriptnpm run test:conformance
# Pythoncd sdks/pythonpytest
# Rustcd sdks/rustcargo testExpected output: all fixtures pass. Any failure means the SDK has drifted — file a bug.
What the rejection fixtures cover
Section titled “What the rejection fixtures cover”Half the value is in the rejection paths. The reject_* and constraint_*_denied fixtures
exercise every branch of the verifier:
- Tampered cert bodies (sig stops verifying) →
identity_status: invalid,error_reason: bad_*_sig: … - Expired / not-yet-valid certs →
expired - Out-of-scope request →
scope_denied - Broken chain (subject ≠ next issuer) →
invalid(chain_broken) - Chain depth exceeding
MaxDelegationChainDepth→invalid(chain_too_deep) - Sub-delegation without
identity:delegate→delegation_not_authorized - Stale or future-dated challenge →
invalid(stale_challenge/ negative-age) SessionContextmismatch (cross-verifier forwarding) →invalid- Constraint violations →
constraint_denied,constraint_unverifiable,constraint_unknown - Sensitive scope introduced by wildcard →
scope_denied
Each rejection is checked for the exact identity_status value AND the leading
error_reason prefix. The strictness is what makes integrators able to write deterministic
error-handling code: switch on identity_status for the enum, then match the
error_reason: prefix when you need the specific sub-cause.
Hybrid signature corner cases
Section titled “Hybrid signature corner cases”Dedicated reject_ed25519_only_corrupted.json and reject_mldsa65_only_corrupted.json
fixtures pin the defense-in-depth posture:
Ed25519 valid + ML-DSA-65 valid → ✓ authorized_agent Ed25519 valid + ML-DSA-65 corrupted → ✗ invalid (bad_*_sig) Ed25519 corrupted + ML-DSA-65 valid → ✗ invalid (bad_*_sig)Both components must verify. There is no degradation path that accepts one signature and silently ignores the other.
Regenerating the fixtures
Section titled “Regenerating the fixtures”cd ratify-protocolgo run ./cmd/ratify-testvectors -out /tmp/regendiff -rq testvectors/v1/ /tmp/regen/# expected: identicalThe CI workflow runs this exact diff. If a contributor’s change causes the regenerated fixtures
to differ from the committed ones, the change is non-deterministic (map iteration, RNG without
a seed, time.Now() without a fixed clock) — and the PR fails.
This is how the project guarantees that fixture bytes are stable across rebuilds, across runners, across machines. Anyone can reproduce the bytes from the deterministic seeds.
Adding a new SDK
Section titled “Adding a new SDK”To add a new language SDK (Swift, Java, C, etc.):
- Open a new-SDK coordination issue.
- Implement the SDK following SPEC.md.
- Implement a conformance test that loads
testvectors/v1/*.jsonand runs every fixture. - Every fixture must pass before the SDK is merged. No exceptions, no negotiated drift.
See docs/SDKS.md in
the protocol repo for the full conformance contract.
Where to next
Section titled “Where to next”- SDK packages — current versions and install commands
- Versioning — when fixture bytes can change
- Specification — the normative protocol