Metadata-Version: 2.4
Name: kya-os-verify
Version: 0.2.0
Summary: Standalone stdlib-only verifier for KYA-OS primitives (org.kya-os/proof@1 holder-of-key proofs, did:key resolution) with vendored cross-language conformance vectors
Project-URL: Specification, https://github.com/decentralized-identity/kya-os-mcp/blob/main/SPEC-ENTITY-CARD.md
Project-URL: Vectors, https://github.com/decentralized-identity/kya-os-mcp/tree/main/conformance/vectors
Author: KYA-OS contributors
License: MIT
License-File: LICENSE
Keywords: conformance,did,ed25519,identity,kya-os,mcp,verifiable-credentials
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Topic :: Security :: Cryptography
Requires-Python: >=3.10
Provides-Extra: dev
Requires-Dist: cryptography>=42; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Provides-Extra: fastmcp
Requires-Dist: fastmcp>=2.9; extra == 'fastmcp'
Description-Content-Type: text/markdown

# kya-os-verify

A standalone, stdlib-only Python verifier for KYA-OS's `org.kya-os/proof@1` holder-of-key proof and `did:key` DID resolution. It is a second-language implementation of the protocol reference-implemented in TypeScript by [`@kya-os/mcp`](https://github.com/decentralized-identity/kya-os-mcp), and it proves cross-language parity by running against the same signed conformance vectors, vendored from that repo at a pinned commit (`fcd38c7`, `@kya-os/mcp` 1.11.0).

`pip install kya-os-verify` gets you a package with **zero runtime dependencies** and a CLI that self-checks against those vendored vectors.

## Install

```bash
pip install kya-os-verify
kya-os-verify selftest
```

Expected output: 10 `[PASS]` lines (7 card-proof + 3 did:key vectors) and:

```
RESULT: PASS — cross-language parity confirmed
```

## CLI

```bash
kya-os-verify selftest [--json]        # run the vendored conformance vectors
kya-os-verify vectors PATH... [--json] # run one or more vector files
```

Exit codes: `0` everything matched, `1` at least one vector mismatched, `2` usage error or an unsupported vector category.

## Library quickstart

```python
from kya_os_verify import verify_card_proof

jwks = {"did:web:example.com:agents:acme#key-1": {"kty": "OKP", "crv": "Ed25519", "x": "..."}}

result = verify_card_proof(
    proof,                       # the org.kya-os/proof@1 object, e.g. from request._meta
    request,                     # {"method": ..., "params": {...}}
    resolve_key=lambda kid: jwks[kid],
    resolve_did_keys=lambda did: [k for kid, k in jwks.items() if kid.split("#")[0] == did],
    expected_audience="did:web:example.com:mcp:server",
    consume_nonce=nonce_cache.consume,   # see kya_os_verify.InMemoryNonceCache
)

if result.ok:
    print(result.did, result.level, result.warnings)
else:
    print(result.reasons)
```

`verify_card_proof` is fail-closed: it never raises on a bad proof, only on a programming error in the seams you supply. See `src/kya_os_verify/proof.py` for the full seam contract (`resolve_key`, `resolve_did_keys`, `consume_nonce`, `now_ms`, `skew_sec`, `token_cnf_jkt`, `trust_resolve_key_authority`).

`resolve_did_keys` is what proves DID membership: without it (and without `trust_resolve_key_authority=True`), every proof fails closed with `did_membership_unverifiable`, because `resolve_key` alone doesn't prove the resolved key actually belongs to the DID document. If your `resolve_key` seam already authoritatively resolves keys scoped to their DID (e.g. it queries a DID document directly), pass `trust_resolve_key_authority=True` instead of `resolve_did_keys` to skip the separate membership check.

## Reason codes

`result.reasons` carries every binding that failed, using the reference implementation's codes verbatim:

`malformed_proof`, `kid_did_mismatch`, `key_unresolvable`, `did_membership_unverifiable`, `did_keys_unresolvable`, `thumbprint_computation_failed`, `kid_not_in_did_document`, `alg_key_mismatch`, `audience_mismatch`, `request_hash_mismatch`, `invalid_window`, `ttl_too_long`, `created_in_future`, `expired`, `nonce_seam_missing`, `nonce_replayed`, `invalid_signature`, `cnf_key_mismatch`, `cnf_required_by_token`, `cnf_token_mismatch`.

One warning code (non-fatal, in `result.warnings`): `cnf_present_but_token_unfused` — the proof carried a sender-constraint `cnf`, but no token `cnf.jkt` was supplied to fuse against, so the result is a valid `L3-minus` rather than `L3`.

## Documented divergences from the TypeScript reference

- **`es256_unsupported`**: Python's standard library has no P-256 (ES256) implementation. A proof with `alg: "ES256"` passes the schema and the alg/key-type binding, but signature verification fails closed with this Python-only reason code. Every vendored conformance vector is EdDSA, so vector parity is unaffected.
- **Unsupported vector categories refuse loudly.** The upstream vector set also ships `audit-integrity`, `delegation-chain`, `did-web-resolution`, `entity-card`, `signed-proof`, and `status-list` categories. This package implements only `card-proof` and `did-key-resolution`; running any other category raises `UnsupportedCategoryError` rather than silently skipping it.
- **Non-integer numbers in request params fail closed.** JCS canonicalization here (`jcs.py`) rejects any `float` value rather than attempting ECMAScript-style number formatting, so a request whose params contain a non-integer JSON number (e.g. `{"amount": 12.5}`) always yields `request_hash_mismatch`, even for an otherwise-valid proof. The TypeScript reference canonicalizes JS numbers and accepts the same proof. No vendored conformance vector exercises this path (all vectors use integer or string values); see `tests/test_proof.py::test_float_in_request_params_fails_closed_instead_of_raising`.

## Provenance

The vectors in `src/kya_os_verify/vectors/` are vendored byte-for-byte from `decentralized-identity/kya-os-mcp` at commit `fcd38c78502dbfb234b41c3ae8361ddeb3d7f83d` via `scripts/sync-vectors.sh`. CI re-runs that script and fails on any diff, so the vendored copies can never silently drift from the pin.

- Protocol spec: [SPEC-ENTITY-CARD.md, section 8](https://github.com/decentralized-identity/kya-os-mcp/blob/main/SPEC-ENTITY-CARD.md)
- Conformance vectors: [conformance/vectors](https://github.com/decentralized-identity/kya-os-mcp/tree/main/conformance/vectors)

## FastMCP integration

`pip install 'kya-os-verify[fastmcp]'` adds `with_kya_os()` (alias `with_KYA_OS`), a middleware for [FastMCP](https://github.com/PrefectHQ/fastmcp) (the PrefectHQ package) that gates every `tools/call` on a valid `org.kya-os/proof@1`:

```python
from fastmcp import FastMCP
from kya_os_verify.fastmcp import with_KYA_OS

mcp = FastMCP("my-server")
# ... register tools ...
with_KYA_OS(mcp, audience="did:web:my-server.example")
```

It is opt-in and zero-impact on any server that never calls it: nothing in `kya_os_verify.fastmcp` runs unless `with_kya_os`/`with_KYA_OS` is attached, and importing `kya_os_verify` (the core package) never imports `fastmcp`.

**Which FastMCP?** This targets [`fastmcp`](https://github.com/PrefectHQ/fastmcp) (PrefectHQ's package, `pip install fastmcp`), the one with a middleware system. It is a *different* project from the official MCP Python SDK's bundled `mcp.server.fastmcp.FastMCP`, which does not support middleware and cannot use `with_kya_os`.

**Key resolution**, in precedence order:
1. `resolve_key=` (and optionally `resolve_did_keys=`) — an explicit seam, for a server backed by its own key store.
2. `jwks={"keys": [...]}` — a static JWKS; keys are looked up by `kid`, and DID membership is checked against the same set unless `resolve_did_keys` overrides it.
3. Neither supplied — zero-config `did:key` mode: the caller's `did:key` is resolved directly (self-certifying: membership is true by construction, since the DID *is* derived from the key).

**Nonce cache caveat**: the default `InMemoryNonceCache` is per-middleware-instance and in-memory, so replay protection is scoped to a single server process. A multi-instance deployment needs a shared store (Redis, etc.) behind the same `consume(nonce, did) -> bool` seam, passed as `nonce_cache=`.

See `examples/fastmcp-server/` for a runnable end-to-end demo.

## License

MIT. See `LICENSE`.
