kya-os-verify 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ __pycache__/
2
+ .claude/
3
+ .venv/
4
+ *.egg-info/
5
+ dist/
6
+ drafts/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 KYA-OS contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,129 @@
1
+ Metadata-Version: 2.4
2
+ Name: kya-os-verify
3
+ Version: 0.2.0
4
+ 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
5
+ Project-URL: Specification, https://github.com/decentralized-identity/kya-os-mcp/blob/main/SPEC-ENTITY-CARD.md
6
+ Project-URL: Vectors, https://github.com/decentralized-identity/kya-os-mcp/tree/main/conformance/vectors
7
+ Author: KYA-OS contributors
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: conformance,did,ed25519,identity,kya-os,mcp,verifiable-credentials
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Topic :: Security :: Cryptography
17
+ Requires-Python: >=3.10
18
+ Provides-Extra: dev
19
+ Requires-Dist: cryptography>=42; extra == 'dev'
20
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
21
+ Requires-Dist: pytest>=8; extra == 'dev'
22
+ Provides-Extra: fastmcp
23
+ Requires-Dist: fastmcp>=2.9; extra == 'fastmcp'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # kya-os-verify
27
+
28
+ 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).
29
+
30
+ `pip install kya-os-verify` gets you a package with **zero runtime dependencies** and a CLI that self-checks against those vendored vectors.
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pip install kya-os-verify
36
+ kya-os-verify selftest
37
+ ```
38
+
39
+ Expected output: 10 `[PASS]` lines (7 card-proof + 3 did:key vectors) and:
40
+
41
+ ```
42
+ RESULT: PASS — cross-language parity confirmed
43
+ ```
44
+
45
+ ## CLI
46
+
47
+ ```bash
48
+ kya-os-verify selftest [--json] # run the vendored conformance vectors
49
+ kya-os-verify vectors PATH... [--json] # run one or more vector files
50
+ ```
51
+
52
+ Exit codes: `0` everything matched, `1` at least one vector mismatched, `2` usage error or an unsupported vector category.
53
+
54
+ ## Library quickstart
55
+
56
+ ```python
57
+ from kya_os_verify import verify_card_proof
58
+
59
+ jwks = {"did:web:example.com:agents:acme#key-1": {"kty": "OKP", "crv": "Ed25519", "x": "..."}}
60
+
61
+ result = verify_card_proof(
62
+ proof, # the org.kya-os/proof@1 object, e.g. from request._meta
63
+ request, # {"method": ..., "params": {...}}
64
+ resolve_key=lambda kid: jwks[kid],
65
+ resolve_did_keys=lambda did: [k for kid, k in jwks.items() if kid.split("#")[0] == did],
66
+ expected_audience="did:web:example.com:mcp:server",
67
+ consume_nonce=nonce_cache.consume, # see kya_os_verify.InMemoryNonceCache
68
+ )
69
+
70
+ if result.ok:
71
+ print(result.did, result.level, result.warnings)
72
+ else:
73
+ print(result.reasons)
74
+ ```
75
+
76
+ `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`).
77
+
78
+ `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.
79
+
80
+ ## Reason codes
81
+
82
+ `result.reasons` carries every binding that failed, using the reference implementation's codes verbatim:
83
+
84
+ `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`.
85
+
86
+ 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`.
87
+
88
+ ## Documented divergences from the TypeScript reference
89
+
90
+ - **`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.
91
+ - **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.
92
+ - **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`.
93
+
94
+ ## Provenance
95
+
96
+ 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.
97
+
98
+ - Protocol spec: [SPEC-ENTITY-CARD.md, section 8](https://github.com/decentralized-identity/kya-os-mcp/blob/main/SPEC-ENTITY-CARD.md)
99
+ - Conformance vectors: [conformance/vectors](https://github.com/decentralized-identity/kya-os-mcp/tree/main/conformance/vectors)
100
+
101
+ ## FastMCP integration
102
+
103
+ `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`:
104
+
105
+ ```python
106
+ from fastmcp import FastMCP
107
+ from kya_os_verify.fastmcp import with_KYA_OS
108
+
109
+ mcp = FastMCP("my-server")
110
+ # ... register tools ...
111
+ with_KYA_OS(mcp, audience="did:web:my-server.example")
112
+ ```
113
+
114
+ 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`.
115
+
116
+ **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`.
117
+
118
+ **Key resolution**, in precedence order:
119
+ 1. `resolve_key=` (and optionally `resolve_did_keys=`) — an explicit seam, for a server backed by its own key store.
120
+ 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.
121
+ 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).
122
+
123
+ **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=`.
124
+
125
+ See `examples/fastmcp-server/` for a runnable end-to-end demo.
126
+
127
+ ## License
128
+
129
+ MIT. See `LICENSE`.
@@ -0,0 +1,104 @@
1
+ # kya-os-verify
2
+
3
+ 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).
4
+
5
+ `pip install kya-os-verify` gets you a package with **zero runtime dependencies** and a CLI that self-checks against those vendored vectors.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install kya-os-verify
11
+ kya-os-verify selftest
12
+ ```
13
+
14
+ Expected output: 10 `[PASS]` lines (7 card-proof + 3 did:key vectors) and:
15
+
16
+ ```
17
+ RESULT: PASS — cross-language parity confirmed
18
+ ```
19
+
20
+ ## CLI
21
+
22
+ ```bash
23
+ kya-os-verify selftest [--json] # run the vendored conformance vectors
24
+ kya-os-verify vectors PATH... [--json] # run one or more vector files
25
+ ```
26
+
27
+ Exit codes: `0` everything matched, `1` at least one vector mismatched, `2` usage error or an unsupported vector category.
28
+
29
+ ## Library quickstart
30
+
31
+ ```python
32
+ from kya_os_verify import verify_card_proof
33
+
34
+ jwks = {"did:web:example.com:agents:acme#key-1": {"kty": "OKP", "crv": "Ed25519", "x": "..."}}
35
+
36
+ result = verify_card_proof(
37
+ proof, # the org.kya-os/proof@1 object, e.g. from request._meta
38
+ request, # {"method": ..., "params": {...}}
39
+ resolve_key=lambda kid: jwks[kid],
40
+ resolve_did_keys=lambda did: [k for kid, k in jwks.items() if kid.split("#")[0] == did],
41
+ expected_audience="did:web:example.com:mcp:server",
42
+ consume_nonce=nonce_cache.consume, # see kya_os_verify.InMemoryNonceCache
43
+ )
44
+
45
+ if result.ok:
46
+ print(result.did, result.level, result.warnings)
47
+ else:
48
+ print(result.reasons)
49
+ ```
50
+
51
+ `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`).
52
+
53
+ `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.
54
+
55
+ ## Reason codes
56
+
57
+ `result.reasons` carries every binding that failed, using the reference implementation's codes verbatim:
58
+
59
+ `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`.
60
+
61
+ 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`.
62
+
63
+ ## Documented divergences from the TypeScript reference
64
+
65
+ - **`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.
66
+ - **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.
67
+ - **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`.
68
+
69
+ ## Provenance
70
+
71
+ 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.
72
+
73
+ - Protocol spec: [SPEC-ENTITY-CARD.md, section 8](https://github.com/decentralized-identity/kya-os-mcp/blob/main/SPEC-ENTITY-CARD.md)
74
+ - Conformance vectors: [conformance/vectors](https://github.com/decentralized-identity/kya-os-mcp/tree/main/conformance/vectors)
75
+
76
+ ## FastMCP integration
77
+
78
+ `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`:
79
+
80
+ ```python
81
+ from fastmcp import FastMCP
82
+ from kya_os_verify.fastmcp import with_KYA_OS
83
+
84
+ mcp = FastMCP("my-server")
85
+ # ... register tools ...
86
+ with_KYA_OS(mcp, audience="did:web:my-server.example")
87
+ ```
88
+
89
+ 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`.
90
+
91
+ **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`.
92
+
93
+ **Key resolution**, in precedence order:
94
+ 1. `resolve_key=` (and optionally `resolve_did_keys=`) — an explicit seam, for a server backed by its own key store.
95
+ 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.
96
+ 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).
97
+
98
+ **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=`.
99
+
100
+ See `examples/fastmcp-server/` for a runnable end-to-end demo.
101
+
102
+ ## License
103
+
104
+ MIT. See `LICENSE`.
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "kya-os-verify"
7
+ version = "0.2.0"
8
+ description = "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"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "KYA-OS contributors" }]
13
+ keywords = ["kya-os", "mcp", "did", "identity", "verifiable-credentials", "conformance", "ed25519"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Topic :: Security :: Cryptography",
21
+ ]
22
+
23
+ [project.urls]
24
+ Specification = "https://github.com/decentralized-identity/kya-os-mcp/blob/main/SPEC-ENTITY-CARD.md"
25
+ Vectors = "https://github.com/decentralized-identity/kya-os-mcp/tree/main/conformance/vectors"
26
+
27
+ [project.scripts]
28
+ kya-os-verify = "kya_os_verify.cli:main"
29
+
30
+ [project.optional-dependencies]
31
+ fastmcp = ["fastmcp>=2.9"]
32
+ dev = ["pytest>=8", "pytest-asyncio>=0.23", "cryptography>=42"]
33
+
34
+ [tool.pytest.ini_options]
35
+ asyncio_mode = "auto"
36
+
37
+ [tool.hatch.build.targets.wheel]
38
+ packages = ["src/kya_os_verify"]
39
+
40
+ [tool.hatch.build.targets.sdist]
41
+ include = ["/src", "/README.md", "/LICENSE"]
@@ -0,0 +1,43 @@
1
+ """kya-os-verify: standalone verifier for KYA-OS primitives.
2
+
3
+ A pure-stdlib, second-language implementation of the org.kya-os/proof@1
4
+ verification path defined by the KYA-OS protocol (SPEC-ENTITY-CARD.md
5
+ section 8) and reference-implemented in TypeScript by @kya-os/mcp. It runs
6
+ the same signed conformance vectors as the TypeScript harness, vendored at a
7
+ pinned upstream commit, proving cross-language JCS (RFC 8785) and Ed25519
8
+ signature parity.
9
+ """
10
+
11
+ __version__ = "0.2.0"
12
+
13
+ from .didkey import did_key_to_jwk
14
+ from .nonce import NONCE_RETENTION_SEC, InMemoryNonceCache
15
+ from .proof import (
16
+ DEFAULT_SKEW_SEC,
17
+ MAX_TTL_SEC,
18
+ PROOF_META_KEY,
19
+ PROOF_PROFILE_V1,
20
+ VerifyResult,
21
+ request_hash,
22
+ verify_card_proof,
23
+ verify_http_sig,
24
+ )
25
+ from .vectors import UnsupportedCategoryError, run_selftest, run_vector_file
26
+
27
+ __all__ = [
28
+ "__version__",
29
+ "VerifyResult",
30
+ "verify_card_proof",
31
+ "verify_http_sig",
32
+ "request_hash",
33
+ "did_key_to_jwk",
34
+ "InMemoryNonceCache",
35
+ "NONCE_RETENTION_SEC",
36
+ "run_vector_file",
37
+ "run_selftest",
38
+ "UnsupportedCategoryError",
39
+ "PROOF_META_KEY",
40
+ "PROOF_PROFILE_V1",
41
+ "MAX_TTL_SEC",
42
+ "DEFAULT_SKEW_SEC",
43
+ ]
@@ -0,0 +1,78 @@
1
+ """`kya-os-verify` console entry point: selftest and vectors subcommands."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import sys
7
+ from typing import Optional, Sequence
8
+
9
+ from . import __version__
10
+ from .vectors import UnsupportedCategoryError, run_selftest, run_vector_file
11
+
12
+
13
+ def _print_report(report: dict, *, as_json: bool) -> None:
14
+ if as_json:
15
+ print(json.dumps(report))
16
+ return
17
+ for r in report["results"]:
18
+ mark = "[PASS]" if r["matched"] else "[FAIL]"
19
+ print(f"{mark} {r['id']}: {r['detail']}")
20
+ if report["allMatched"]:
21
+ print("RESULT: PASS — cross-language parity confirmed")
22
+ else:
23
+ mismatched = sum(1 for r in report["results"] if not r["matched"])
24
+ print(f"RESULT: FAIL — {mismatched} of {len(report['results'])} vector(s) mismatched")
25
+
26
+
27
+ def _cmd_selftest(args: argparse.Namespace) -> int:
28
+ report = run_selftest()
29
+ _print_report(report, as_json=args.json)
30
+ return 0 if report["allMatched"] else 1
31
+
32
+
33
+ def _cmd_vectors(args: argparse.Namespace) -> int:
34
+ results: list = []
35
+ for path in args.paths:
36
+ try:
37
+ results.extend(run_vector_file(path))
38
+ except UnsupportedCategoryError as exc:
39
+ print(f"error: {exc}", file=sys.stderr)
40
+ return 2
41
+ if not results:
42
+ # No silent caps: a file with zero vectors must not print
43
+ # "RESULT: PASS" — that reads as full coverage when nothing ran.
44
+ print("error: no vectors found in the given file(s)", file=sys.stderr)
45
+ return 2
46
+ report = {"allMatched": all(r["matched"] for r in results), "results": results}
47
+ _print_report(report, as_json=args.json)
48
+ return 0 if report["allMatched"] else 1
49
+
50
+
51
+ def _build_parser() -> argparse.ArgumentParser:
52
+ parser = argparse.ArgumentParser(prog="kya-os-verify")
53
+ parser.add_argument("--version", action="version", version=f"kya-os-verify {__version__}")
54
+ sub = parser.add_subparsers(dest="command")
55
+
56
+ selftest = sub.add_parser("selftest", help="run all vendored conformance vectors")
57
+ selftest.add_argument("--json", action="store_true")
58
+ selftest.set_defaults(func=_cmd_selftest)
59
+
60
+ vectors = sub.add_parser("vectors", help="run one or more vector files")
61
+ vectors.add_argument("paths", nargs="+")
62
+ vectors.add_argument("--json", action="store_true")
63
+ vectors.set_defaults(func=_cmd_vectors)
64
+
65
+ return parser
66
+
67
+
68
+ def main(argv: Optional[Sequence[str]] = None) -> int:
69
+ parser = _build_parser()
70
+ args = parser.parse_args(argv)
71
+ if not getattr(args, "command", None):
72
+ parser.print_usage(sys.stderr)
73
+ return 2
74
+ return args.func(args)
75
+
76
+
77
+ if __name__ == "__main__":
78
+ sys.exit(main())
@@ -0,0 +1,48 @@
1
+ """did:key resolution to an Ed25519 JWK.
2
+
3
+ Supports only the `z` (base58btc) multibase prefix and the Ed25519
4
+ multicodec (0xed01), which is the only key type the vendored
5
+ did-key-resolution.json vectors and the card-proof vectors exercise.
6
+ Anything else (malformed multibase, wrong method, wrong multicodec) raises
7
+ ValueError: fail-closed, no silent partial resolution.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from .encoding import b64url_nopad
12
+
13
+ _B58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
14
+ _ED25519_MULTICODEC = b"\xed\x01"
15
+
16
+
17
+ def _base58btc_decode(value: str) -> bytes:
18
+ num = 0
19
+ for char in value:
20
+ index = _B58_ALPHABET.find(char)
21
+ if index == -1:
22
+ raise ValueError(f"did:key: invalid base58btc character {char!r}")
23
+ num = num * 58 + index
24
+ decoded = num.to_bytes((num.bit_length() + 7) // 8, "big") if num else b""
25
+ n_leading_zeros = len(value) - len(value.lstrip("1"))
26
+ return b"\x00" * n_leading_zeros + decoded
27
+
28
+
29
+ def did_key_to_jwk(did: str) -> dict:
30
+ """Resolve a did:key DID to an Ed25519 OKP JWK. Raises ValueError otherwise."""
31
+ prefix = "did:key:z"
32
+ if not did.startswith(prefix):
33
+ raise ValueError(f"did:key: not a did:key with z multibase: {did!r}")
34
+ try:
35
+ raw = _base58btc_decode(did[len(prefix) :])
36
+ except ValueError:
37
+ raise
38
+ except Exception as exc:
39
+ raise ValueError(f"did:key: base58btc decode failed: {exc}") from exc
40
+ if len(raw) != 34 or raw[:2] != _ED25519_MULTICODEC:
41
+ raise ValueError("did:key: not an Ed25519 (0xed01) multicodec key")
42
+ key_bytes = raw[2:]
43
+ return {
44
+ "kty": "OKP",
45
+ "crv": "Ed25519",
46
+ "x": b64url_nopad(key_bytes),
47
+ "kid": f"{did}#keys-1",
48
+ }
@@ -0,0 +1,104 @@
1
+ """Ed25519 signature verification (RFC 8032), pure Python stdlib.
2
+
3
+ Ported unchanged from decentralized-identity/kya-os-mcp
4
+ conformance/verify.py (MIT), lines 27-121 at origin/main commit
5
+ fcd38c78502dbfb234b41c3ae8361ddeb3d7f83d. Verification only (no signing).
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+
11
+ _p = 2 ** 255 - 19
12
+ _L = 2 ** 252 + 27742317777372353535851937790883648493
13
+
14
+
15
+ def _modp_inv(x):
16
+ return pow(x, _p - 2, _p)
17
+
18
+
19
+ _d = -121665 * _modp_inv(121666) % _p
20
+ _modp_sqrt_m1 = pow(2, (_p - 1) // 4, _p)
21
+
22
+
23
+ def _sha512_modL(s):
24
+ return int.from_bytes(hashlib.sha512(s).digest(), "little") % _L
25
+
26
+
27
+ def _point_add(P, Q):
28
+ A = (P[1] - P[0]) * (Q[1] - Q[0]) % _p
29
+ B = (P[1] + P[0]) * (Q[1] + Q[0]) % _p
30
+ C = 2 * P[3] * Q[3] * _d % _p
31
+ D = 2 * P[2] * Q[2] % _p
32
+ E, F, G, H = B - A, D - C, D + C, B + A
33
+ return (E * F % _p, G * H % _p, F * G % _p, E * H % _p)
34
+
35
+
36
+ def _point_mul(s, P):
37
+ Q = (0, 1, 1, 0) # neutral element
38
+ while s > 0:
39
+ if s & 1:
40
+ Q = _point_add(Q, P)
41
+ P = _point_add(P, P)
42
+ s >>= 1
43
+ return Q
44
+
45
+
46
+ def _point_equal(P, Q):
47
+ if (P[0] * Q[2] - Q[0] * P[2]) % _p != 0:
48
+ return False
49
+ if (P[1] * Q[2] - Q[1] * P[2]) % _p != 0:
50
+ return False
51
+ return True
52
+
53
+
54
+ def _recover_x(y, sign):
55
+ if y >= _p:
56
+ return None
57
+ x2 = (y * y - 1) * _modp_inv(_d * y * y + 1) % _p
58
+ if x2 == 0:
59
+ return None if sign else 0
60
+ x = pow(x2, (_p + 3) // 8, _p)
61
+ if (x * x - x2) % _p != 0:
62
+ x = x * _modp_sqrt_m1 % _p
63
+ if (x * x - x2) % _p != 0:
64
+ return None
65
+ if (x & 1) != sign:
66
+ x = _p - x
67
+ return x
68
+
69
+
70
+ _g_y = 4 * _modp_inv(5) % _p
71
+ _g_x = _recover_x(_g_y, 0)
72
+ _G = (_g_x, _g_y, 1, _g_x * _g_y % _p)
73
+
74
+
75
+ def _point_decompress(s):
76
+ if len(s) != 32:
77
+ return None
78
+ y = int.from_bytes(s, "little")
79
+ sign = y >> 255
80
+ y &= (1 << 255) - 1
81
+ x = _recover_x(y, sign)
82
+ if x is None:
83
+ return None
84
+ return (x, y, 1, x * y % _p)
85
+
86
+
87
+ def ed25519_verify(public_key: bytes, message: bytes, signature: bytes) -> bool:
88
+ """Verify an Ed25519 signature (RFC 8032). Fail-closed on any malformed input."""
89
+ if len(public_key) != 32 or len(signature) != 64:
90
+ return False
91
+ A = _point_decompress(public_key)
92
+ if A is None:
93
+ return False
94
+ Rs = signature[:32]
95
+ R = _point_decompress(Rs)
96
+ if R is None:
97
+ return False
98
+ S = int.from_bytes(signature[32:], "little")
99
+ if S >= _L:
100
+ return False
101
+ h = _sha512_modL(Rs + public_key + message)
102
+ sB = _point_mul(S, _G)
103
+ hA = _point_mul(h, A)
104
+ return _point_equal(sB, _point_add(R, hA))
@@ -0,0 +1,20 @@
1
+ """base64url / base64 helpers for the wire encodings this protocol uses."""
2
+ from __future__ import annotations
3
+
4
+ import base64
5
+
6
+
7
+ def b64url_decode(value: str) -> bytes:
8
+ """Decode base64url, re-padding as needed (the wire format omits padding)."""
9
+ padded = value + "=" * (-len(value) % 4)
10
+ return base64.urlsafe_b64decode(padded)
11
+
12
+
13
+ def b64url_nopad(data: bytes) -> str:
14
+ """Encode as base64url without padding."""
15
+ return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
16
+
17
+
18
+ def std_b64(data: bytes) -> str:
19
+ """Encode as standard (non-url-safe) base64, with padding."""
20
+ return base64.b64encode(data).decode("ascii")