vaid-pop 0.1.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,24 @@
1
+ # Rust
2
+ /target
3
+ **/*.rs.bk
4
+ Cargo.lock.orig
5
+
6
+ # Backtraces
7
+ rustc-ice-*.txt
8
+
9
+ # Editor / OS
10
+ .DS_Store
11
+ *.swp
12
+ .idea/
13
+ .vscode/
14
+
15
+ # Python
16
+ __pycache__/
17
+ *.py[cod]
18
+ .pytest_cache/
19
+ *.egg-info/
20
+ dist/
21
+ build/
22
+ .venv/
23
+ .buildvenv/
24
+ .installtest/
@@ -0,0 +1,67 @@
1
+ Metadata-Version: 2.4
2
+ Name: vaid-pop
3
+ Version: 0.1.0
4
+ Summary: Canonical Python proof-of-possession (PoP) request signer for the VAID standard.
5
+ Project-URL: Homepage, https://github.com/solara-associates/vaid
6
+ Project-URL: Repository, https://github.com/solara-associates/vaid
7
+ Project-URL: Source, https://github.com/solara-associates/vaid/tree/main/python/vaid-pop
8
+ Project-URL: Issues, https://github.com/solara-associates/vaid/issues
9
+ Author-email: "solara.associates" <info@solara.associates>
10
+ License: Apache-2.0
11
+ Keywords: agent-identity,cryptography,ed25519,proof-of-possession,signing,vaid
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: cryptography>=42.0
20
+ Requires-Dist: rfc8785>=0.1.2
21
+ Provides-Extra: test
22
+ Requires-Dist: pytest>=8.0; extra == 'test'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # vaid-pop
26
+
27
+ Canonical Python proof-of-possession (PoP) request signer for the
28
+ [VAID](https://github.com/solara-associates/vaid) standard.
29
+
30
+ This is the **single Python definition** of the PoP signing contract. A
31
+ VAID-bound request carries a fresh, replay-protected Ed25519 signature; this
32
+ package builds the four `x-synthera-*` headers that carry it. Import it
33
+ directly — no extra runtime, no framework.
34
+
35
+ ```python
36
+ from vaid_pop import RequestSigner
37
+
38
+ signer = RequestSigner(vaid=vaid_doc, private_key=agent_key)
39
+ headers = signer.sign_headers("POST", "/vaid/mint", body_bytes)
40
+ # -> {"x-synthera-vaid": ..., "x-synthera-timestamp": ...,
41
+ # "x-synthera-nonce": ..., "x-synthera-signature": ...}
42
+ ```
43
+
44
+ > The `x-synthera-*` header names are the VAID wire contract — the prefix is the
45
+ > fixed header namespace a conforming verifier reads, not a package dependency.
46
+
47
+ ## The firewall
48
+
49
+ Cross-language byte-identity is the whole point. The signer is locked against the
50
+ frozen cross-language vector (vendored here at
51
+ `vaid_pop/vectors/operator_pop_v1.json`), which the Rust client (`vaid-client`)
52
+ and the verifier (`vaid_pop`) assert against too. The `pop-conformance` CI job
53
+ proves **Rust output == Python output == vector** byte-for-byte. A mismatch is a
54
+ hard blocker.
55
+
56
+ Contract: RFC 8785 (JCS) over the camelCase `RequestAuthPayload` → SHA-256 →
57
+ pure Ed25519 over the 32-byte digest → raw 64-byte signature.
58
+
59
+ > Standardized on `rfc8785` for JCS — the byte-for-byte JCS implementation the
60
+ > vector is proven against. Do not substitute another JCS library without
61
+ > re-proving byte-equality.
62
+
63
+ ## Relationship to other packages
64
+
65
+ - **`vaid-client`** (Rust) is the cross-language peer, proven against the same vector.
66
+ - Language-specific agent integrations depend on this package and re-export `RequestSigner`.
67
+ - Clients consume this directly to authenticate a VAID-bound request.
@@ -0,0 +1,43 @@
1
+ # vaid-pop
2
+
3
+ Canonical Python proof-of-possession (PoP) request signer for the
4
+ [VAID](https://github.com/solara-associates/vaid) standard.
5
+
6
+ This is the **single Python definition** of the PoP signing contract. A
7
+ VAID-bound request carries a fresh, replay-protected Ed25519 signature; this
8
+ package builds the four `x-synthera-*` headers that carry it. Import it
9
+ directly — no extra runtime, no framework.
10
+
11
+ ```python
12
+ from vaid_pop import RequestSigner
13
+
14
+ signer = RequestSigner(vaid=vaid_doc, private_key=agent_key)
15
+ headers = signer.sign_headers("POST", "/vaid/mint", body_bytes)
16
+ # -> {"x-synthera-vaid": ..., "x-synthera-timestamp": ...,
17
+ # "x-synthera-nonce": ..., "x-synthera-signature": ...}
18
+ ```
19
+
20
+ > The `x-synthera-*` header names are the VAID wire contract — the prefix is the
21
+ > fixed header namespace a conforming verifier reads, not a package dependency.
22
+
23
+ ## The firewall
24
+
25
+ Cross-language byte-identity is the whole point. The signer is locked against the
26
+ frozen cross-language vector (vendored here at
27
+ `vaid_pop/vectors/operator_pop_v1.json`), which the Rust client (`vaid-client`)
28
+ and the verifier (`vaid_pop`) assert against too. The `pop-conformance` CI job
29
+ proves **Rust output == Python output == vector** byte-for-byte. A mismatch is a
30
+ hard blocker.
31
+
32
+ Contract: RFC 8785 (JCS) over the camelCase `RequestAuthPayload` → SHA-256 →
33
+ pure Ed25519 over the 32-byte digest → raw 64-byte signature.
34
+
35
+ > Standardized on `rfc8785` for JCS — the byte-for-byte JCS implementation the
36
+ > vector is proven against. Do not substitute another JCS library without
37
+ > re-proving byte-equality.
38
+
39
+ ## Relationship to other packages
40
+
41
+ - **`vaid-client`** (Rust) is the cross-language peer, proven against the same vector.
42
+ - Language-specific agent integrations depend on this package and re-export `RequestSigner`.
43
+ - Clients consume this directly to authenticate a VAID-bound request.
@@ -0,0 +1,62 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "vaid-pop"
7
+ # Independent semver. Additive-minor for new optional fields; major for any
8
+ # canonicalization / field-name change (those break cross-language byte-identity
9
+ # against operator_pop_v1.json).
10
+ version = "0.1.0"
11
+ description = "Canonical Python proof-of-possession (PoP) request signer for the VAID standard."
12
+ readme = "README.md"
13
+ requires-python = ">=3.10"
14
+ license = { text = "Apache-2.0" }
15
+ authors = [{ name = "solara.associates", email = "info@solara.associates" }]
16
+ keywords = ["vaid", "agent-identity", "ed25519", "proof-of-possession", "cryptography", "signing"]
17
+ classifiers = [
18
+ "Development Status :: 3 - Alpha",
19
+ "License :: OSI Approved :: Apache Software License",
20
+ "Intended Audience :: Developers",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ ]
25
+ # cryptography = Ed25519 (no stdlib option); rfc8785 = RFC 8785 JCS, spec-for-spec
26
+ # identical to the Rust serde_jcs path so the canonical bytes agree by construction
27
+ # (locked by the vendored operator_pop_v1.json). NOTE: standardized on rfc8785 — do
28
+ # NOT substitute another JCS library without re-proving byte-equality against the
29
+ # frozen vector.
30
+ dependencies = [
31
+ "cryptography>=42.0",
32
+ "rfc8785>=0.1.2",
33
+ ]
34
+
35
+ [project.scripts]
36
+ # The packaged firewall: an external consumer who installed ONLY the wheel can run
37
+ # `vaid-pop-conformance` to prove the signer they got reproduces the frozen
38
+ # cross-language vector byte-for-byte. Ships in the wheel (it's a package module),
39
+ # unlike the repo `tests/` suite.
40
+ vaid-pop-conformance = "vaid_pop.conformance:main"
41
+
42
+ [project.urls]
43
+ Homepage = "https://github.com/solara-associates/vaid"
44
+ Repository = "https://github.com/solara-associates/vaid"
45
+ Source = "https://github.com/solara-associates/vaid/tree/main/python/vaid-pop"
46
+ Issues = "https://github.com/solara-associates/vaid/issues"
47
+
48
+ [project.optional-dependencies]
49
+ test = [
50
+ "pytest>=8.0",
51
+ ]
52
+
53
+ [tool.hatch.build.targets.wheel]
54
+ # The vendored conformance vector ships inside the wheel so a consumer's CI runs
55
+ # the firewall against the exact bytes this signer was proven against.
56
+ packages = ["vaid_pop"]
57
+
58
+ [tool.hatch.build.targets.sdist]
59
+ include = ["vaid_pop", "tests", "README.md"]
60
+
61
+ [tool.pytest.ini_options]
62
+ testpaths = ["tests"]
@@ -0,0 +1,87 @@
1
+ """Completion-record conformance gate (Python side of the cross-language firewall).
2
+
3
+ The vendored vector ``vaid_pop/vectors/completion_v1.json`` is byte-identical to
4
+ the Rust copy (a CI drift-check enforces that). Asserts the Python signer
5
+ reproduces the frozen digest + signature for a real CompletionRecord, and — since
6
+ this is the FIRST vector with an ENUM — that every AssuranceTier serializes to
7
+ exactly the frozen string (the most likely place for silent Rust/Python drift).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ from importlib.resources import files
14
+
15
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import (
16
+ Ed25519PrivateKey,
17
+ Ed25519PublicKey,
18
+ )
19
+
20
+ from vaid_pop import (
21
+ AssuranceTier,
22
+ build_completion_record,
23
+ canonical_request_signing_bytes,
24
+ )
25
+
26
+
27
+ def _vector() -> dict:
28
+ data = files("vaid_pop").joinpath("vectors/completion_v1.json").read_text()
29
+ return json.loads(data)
30
+
31
+
32
+ def _record_from_input(inp: dict) -> dict:
33
+ # Rebuild through the package builder (not the raw vector dict) so the test
34
+ # exercises the real construction path, then assert it equals the frozen input.
35
+ return build_completion_record(
36
+ vaid_id=inp["vaidId"],
37
+ request_digest_sha256=inp["requestDigestSha256"],
38
+ tenant_id=inp["tenantId"],
39
+ status=inp["status"],
40
+ result_sha256=inp["resultSha256"],
41
+ completed_at=inp["completedAt"],
42
+ signer_vaid_id=inp["signerVaidId"],
43
+ assurance_tier=inp["assuranceTier"],
44
+ record_nonce=inp["recordNonce"],
45
+ )
46
+
47
+
48
+ def test_completion_digest_matches_frozen_vector():
49
+ v = _vector()
50
+ record = _record_from_input(v["input"])
51
+ assert record == v["input"], "builder output must equal the frozen input dict"
52
+ digest = canonical_request_signing_bytes(record)
53
+ assert digest.hex() == v["digest_sha256_hex"], (
54
+ "Python completion-record digest diverged from the frozen vector — BLOCKER"
55
+ )
56
+ assert len(digest) == 32
57
+
58
+
59
+ def test_completion_signature_matches_frozen_vector():
60
+ v = _vector()
61
+ seed = bytes.fromhex(v["ed25519"]["private_key_seed_hex"])
62
+ sk = Ed25519PrivateKey.from_private_bytes(seed)
63
+
64
+ pub = sk.public_key().public_bytes_raw()
65
+ assert pub.hex() == v["ed25519"]["public_key_hex"], "public key diverged — BLOCKER"
66
+
67
+ digest = canonical_request_signing_bytes(_record_from_input(v["input"]))
68
+ sig = sk.sign(digest)
69
+ assert sig.hex() == v["ed25519"]["signature_hex"], "signature diverged — BLOCKER"
70
+ assert len(sig) == 64
71
+ Ed25519PublicKey.from_public_bytes(pub).verify(sig, digest) # raises on failure
72
+
73
+
74
+ def test_assurance_tier_strings_match_frozen_vector():
75
+ """THE ENUM DRIFT GUARD: the Python AssuranceTier values must equal the frozen
76
+ strings, in order — the field where Rust camelCase serde and Python enum values
77
+ must agree byte-for-byte inside the signed document."""
78
+ v = _vector()
79
+ frozen = v["assurance_tier_strings"]
80
+ py_values = [
81
+ AssuranceTier.SELF_REPORTED.value,
82
+ AssuranceTier.COUNTER_SIGNED.value,
83
+ AssuranceTier.THIRD_PARTY_ATTESTED.value,
84
+ ]
85
+ assert py_values == frozen, "AssuranceTier strings diverged from the frozen vector — BLOCKER"
86
+ # The declared tier in `input` is the only substantiated tier.
87
+ assert v["input"]["assuranceTier"] == AssuranceTier.SELF_REPORTED.value
@@ -0,0 +1,110 @@
1
+ """Canonical PoP conformance gate (Python side of the cross-language firewall).
2
+
3
+ The vendored vector ``vaid_pop/vectors/operator_pop_v1.json`` is byte-identical
4
+ to the canonical cross-language vector in the ``vaid`` repo (a CI drift-check
5
+ enforces that). These tests assert the Python signer reproduces the frozen
6
+ digest + Ed25519 signature byte-for-byte; a mismatch is a BLOCKER, not a
7
+ ship-anyway. The Rust client (`vaid-client`) asserts the same vector, and the
8
+ repo-level ``pop-conformance`` job proves Rust output == Python output == vector.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import base64
14
+ import hashlib
15
+ import json
16
+ from datetime import datetime, timezone
17
+ from importlib.resources import files
18
+
19
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import (
20
+ Ed25519PrivateKey,
21
+ Ed25519PublicKey,
22
+ )
23
+
24
+ from vaid_pop import (
25
+ HEADER_NONCE,
26
+ HEADER_SIGNATURE,
27
+ HEADER_TIMESTAMP,
28
+ HEADER_VAID,
29
+ RequestSigner,
30
+ canonical_request_signing_bytes,
31
+ )
32
+
33
+
34
+ def _vector() -> dict:
35
+ # Read the vector the package SHIPS — the firewall is only real if a consumer
36
+ # runs it against the copy bundled with the signer it actually installed.
37
+ data = files("vaid_pop").joinpath("vectors/operator_pop_v1.json").read_text()
38
+ return json.loads(data)
39
+
40
+
41
+ def test_canonical_digest_matches_frozen_vector():
42
+ """Python JCS + SHA-256 over the camelCase RequestAuthPayload reproduces the
43
+ Rust-frozen digest byte-for-byte."""
44
+ v = _vector()
45
+ digest = canonical_request_signing_bytes(v["input"])
46
+ assert digest.hex() == v["digest_sha256_hex"], (
47
+ "Python canonical digest diverged from the frozen vector — BLOCKER"
48
+ )
49
+ assert len(digest) == 32
50
+
51
+
52
+ def test_deterministic_signature_matches_frozen_vector():
53
+ """From the frozen seed, Python derives the same public key and produces the
54
+ same deterministic Ed25519 signature the Rust vector froze."""
55
+ v = _vector()
56
+ seed = bytes.fromhex(v["ed25519"]["private_key_seed_hex"])
57
+ sk = Ed25519PrivateKey.from_private_bytes(seed)
58
+
59
+ pub = sk.public_key().public_bytes_raw()
60
+ assert pub.hex() == v["ed25519"]["public_key_hex"], "public key diverged — BLOCKER"
61
+
62
+ digest = canonical_request_signing_bytes(v["input"])
63
+ sig = sk.sign(digest)
64
+ assert sig.hex() == v["ed25519"]["signature_hex"], "signature diverged — BLOCKER"
65
+ assert len(sig) == 64
66
+
67
+ Ed25519PublicKey.from_public_bytes(pub).verify(sig, digest) # raises on failure
68
+
69
+
70
+ def test_request_signer_produces_verifiable_pop_headers():
71
+ """The real signer path: RequestSigner.sign_headers builds the camelCase
72
+ RequestAuthPayload, signs it, and X-Synthera-Signature verifies against the
73
+ agent key over the reconstructed digest."""
74
+ v = _vector()
75
+ seed = bytes.fromhex(v["ed25519"]["private_key_seed_hex"])
76
+ sk = Ed25519PrivateKey.from_private_bytes(seed)
77
+ pub = sk.public_key()
78
+
79
+ vaid = {
80
+ "vaid_id": v["input"]["vaidId"],
81
+ "tenant_id": v["input"]["tenantId"],
82
+ "agent_id": "22222222-2222-2222-2222-222222222222",
83
+ }
84
+ signer = RequestSigner(vaid=vaid, private_key=sk)
85
+
86
+ fixed_now = datetime(2026, 6, 4, 12, 0, 0, tzinfo=timezone.utc)
87
+ body = b"" # vector bodySha256 = sha256("") = e3b0c442...
88
+ headers = signer.sign_headers(
89
+ "POST", "/vaid/mint", body, now=fixed_now, nonce=v["input"]["clientNonce"]
90
+ )
91
+
92
+ assert headers[HEADER_TIMESTAMP] == v["input"]["timestamp"]
93
+ assert headers[HEADER_NONCE] == v["input"]["clientNonce"]
94
+
95
+ payload = {
96
+ "vaidId": vaid["vaid_id"],
97
+ "method": "POST",
98
+ "path": "/vaid/mint",
99
+ "bodySha256": hashlib.sha256(body).hexdigest(),
100
+ "tenantId": vaid["tenant_id"],
101
+ "timestamp": headers[HEADER_TIMESTAMP],
102
+ "clientNonce": headers[HEADER_NONCE],
103
+ }
104
+ digest = canonical_request_signing_bytes(payload)
105
+ # The signer must reproduce the frozen digest AND signature for this input.
106
+ assert digest.hex() == v["digest_sha256_hex"]
107
+ sig = base64.b64decode(headers[HEADER_SIGNATURE])
108
+ assert sig.hex() == v["ed25519"]["signature_hex"], "header signature diverged — BLOCKER"
109
+ assert len(sig) == 64
110
+ pub.verify(sig, digest) # raises on failure → the header signature is valid
@@ -0,0 +1,43 @@
1
+ """vaid-pop — canonical Python proof-of-possession request signer.
2
+
3
+ The single Python definition of the VAID PoP signing contract. Import this
4
+ package directly to sign a VAID-bound request without pulling in a framework
5
+ (language-specific agent integrations depend on this package and re-export the
6
+ signer). Byte-identity with the Rust client (`vaid-client`) and the verifier
7
+ (`vaid_pop`) is locked by the vendored cross-language vector
8
+ ``vaid_pop/vectors/operator_pop_v1.json``.
9
+
10
+ Usage::
11
+
12
+ from vaid_pop import RequestSigner
13
+
14
+ signer = RequestSigner(vaid=vaid_doc, private_key=agent_key)
15
+ headers = signer.sign_headers("POST", "/vaid/mint", body_bytes)
16
+ """
17
+
18
+ from vaid_pop.completion import AssuranceTier, build_completion_record
19
+ from vaid_pop.signer import (
20
+ HEADER_NONCE,
21
+ HEADER_SIGNATURE,
22
+ HEADER_TIMESTAMP,
23
+ HEADER_VAID,
24
+ RequestSigner,
25
+ build_request_auth_payload,
26
+ canonical_request_signing_bytes,
27
+ utc_whole_second_rfc3339,
28
+ )
29
+
30
+ __all__ = [
31
+ "RequestSigner",
32
+ "canonical_request_signing_bytes",
33
+ "build_request_auth_payload",
34
+ "utc_whole_second_rfc3339",
35
+ "AssuranceTier",
36
+ "build_completion_record",
37
+ "HEADER_VAID",
38
+ "HEADER_TIMESTAMP",
39
+ "HEADER_NONCE",
40
+ "HEADER_SIGNATURE",
41
+ ]
42
+
43
+ __version__ = "0.1.0"
@@ -0,0 +1,72 @@
1
+ """Completion / provenance record — Python mirror of the Rust
2
+ ``vaid_pop::request_completion``.
3
+
4
+ A signed statement that a VAID-authorized action finished, over the SAME
5
+ JCS→SHA-256→Ed25519 pipeline as the request PoP (no new crypto). Byte-identity
6
+ with the Rust ``CompletionRecord`` is locked by the shared vector
7
+ ``completion_v1.json``.
8
+
9
+ SCOPE — self-signed, DECLARED metadata only
10
+ -------------------------------------------
11
+ The record carries one detached signature by ``signer_vaid_id``. That proves only
12
+ that this signer signed the record. The ``assurance_tier`` is therefore
13
+ **declared, not proven**:
14
+
15
+ * ``AssuranceTier.SELF_REPORTED`` is the only tier this repo substantiates on its
16
+ own (actor signs its own outcome; the signature verifies against the actor key).
17
+ * ``COUNTER_SIGNED`` / ``THIRD_PARTY_ATTESTED`` are NOT independently verifiable
18
+ from this repo alone — a self-reporting signer can set either and the single
19
+ signature still verifies. Provable counter-signing / third-party attestation is
20
+ a separate, not-yet-built primitive and is OUT OF SCOPE here.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from enum import Enum
26
+
27
+
28
+ class AssuranceTier(str, Enum):
29
+ """Declared assurance level. String values mirror the Rust enum's
30
+ ``rename_all = "camelCase"`` serialization EXACTLY — these strings go into the
31
+ signed document, so any divergence breaks cross-language byte-identity (this is
32
+ the enum-drift risk the conformance vector guards)."""
33
+
34
+ SELF_REPORTED = "selfReported"
35
+ COUNTER_SIGNED = "counterSigned"
36
+ THIRD_PARTY_ATTESTED = "thirdPartyAttested"
37
+
38
+
39
+ def build_completion_record(
40
+ *,
41
+ vaid_id: str,
42
+ request_digest_sha256: str,
43
+ tenant_id: str,
44
+ status: str,
45
+ result_sha256: str,
46
+ completed_at: str,
47
+ signer_vaid_id: str,
48
+ assurance_tier: AssuranceTier | str,
49
+ record_nonce: str,
50
+ ) -> dict:
51
+ """Assemble the camelCase ``CompletionRecord`` a completer signs.
52
+
53
+ Field names/encoding mirror the Rust ``CompletionRecord`` serde exactly. The
54
+ result is a plain ``dict`` (like a ``RequestAuthPayload``) ready for
55
+ ``canonical_request_signing_bytes`` → ``sign``. ``assurance_tier`` accepts the
56
+ :class:`AssuranceTier` enum or its string value; it is written as the string.
57
+
58
+ ``completed_at`` MUST be whole-second RFC 3339 ``…Z`` (the same fixed point as
59
+ ``RequestAuthPayload.timestamp``) or byte-identity breaks.
60
+ """
61
+ tier = assurance_tier.value if isinstance(assurance_tier, AssuranceTier) else assurance_tier
62
+ return {
63
+ "vaidId": vaid_id,
64
+ "requestDigestSha256": request_digest_sha256,
65
+ "tenantId": tenant_id,
66
+ "status": status,
67
+ "resultSha256": result_sha256,
68
+ "completedAt": completed_at,
69
+ "signerVaidId": signer_vaid_id,
70
+ "assuranceTier": tier,
71
+ "recordNonce": record_nonce,
72
+ }
@@ -0,0 +1,218 @@
1
+ """Packaged cross-language PoP conformance check — the firewall, shipped in the wheel.
2
+
3
+ This module is the externally-consumable form of the conformance gate. Unlike the
4
+ repo test suite under ``tests/`` (which does not ship in the wheel), this lives
5
+ *inside* the installed package, so a consumer who has only ``pip install
6
+ vaid-pop`` — no repo checkout — can prove the signer they installed reproduces
7
+ the frozen cross-language vector byte-for-byte:
8
+
9
+ python -m vaid_pop.conformance # exit 0 = PASS, 1 = BLOCKER
10
+ vaid-pop-conformance # same, via the console entry point
11
+
12
+ Programmatically::
13
+
14
+ from vaid_pop.conformance import run
15
+ run() # raises ConformanceError on any divergence; returns the vector on PASS
16
+
17
+ The vector it checks against is the one bundled with the signer
18
+ (``vaid_pop/vectors/operator_pop_v1.json``) — the firewall is only real if a
19
+ consumer runs it against the exact bytes the installed signer was proven against.
20
+ The Rust client (``vaid-client``) asserts the identical vector; the repo-level
21
+ ``pop-conformance`` job proves Rust output == Python output == vector.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import base64
27
+ import hashlib
28
+ import json
29
+ from importlib.resources import files
30
+
31
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import (
32
+ Ed25519PrivateKey,
33
+ Ed25519PublicKey,
34
+ )
35
+
36
+ from vaid_pop import (
37
+ HEADER_NONCE,
38
+ HEADER_SIGNATURE,
39
+ HEADER_TIMESTAMP,
40
+ RequestSigner,
41
+ canonical_request_signing_bytes,
42
+ )
43
+
44
+
45
+ class ConformanceError(AssertionError):
46
+ """A cross-language byte-identity divergence — a hard BLOCKER, never ship-anyway."""
47
+
48
+
49
+ def load_vector() -> dict:
50
+ """The operator-PoP conformance vector bundled with the installed package."""
51
+ data = files("vaid_pop").joinpath("vectors/operator_pop_v1.json").read_text()
52
+ return json.loads(data)
53
+
54
+
55
+ def load_completion_vector() -> dict:
56
+ """The completion-record conformance vector bundled with the installed package."""
57
+ data = files("vaid_pop").joinpath("vectors/completion_v1.json").read_text()
58
+ return json.loads(data)
59
+
60
+
61
+ def check_completion(v: dict) -> None:
62
+ """Python JCS + SHA-256 + Ed25519 over the camelCase CompletionRecord reproduce
63
+ the frozen digest + signature, and the AssuranceTier strings match (the enum
64
+ drift guard — the first vector with an enum)."""
65
+ from vaid_pop import AssuranceTier
66
+
67
+ digest = canonical_request_signing_bytes(v["input"])
68
+ if digest.hex() != v["digest_sha256_hex"]:
69
+ raise ConformanceError(
70
+ f"completion digest diverged from the frozen vector — BLOCKER\n"
71
+ f" got = {digest.hex()}\n vector = {v['digest_sha256_hex']}"
72
+ )
73
+ seed = bytes.fromhex(v["ed25519"]["private_key_seed_hex"])
74
+ sk = Ed25519PrivateKey.from_private_bytes(seed)
75
+ sig = sk.sign(digest)
76
+ if sig.hex() != v["ed25519"]["signature_hex"]:
77
+ raise ConformanceError(
78
+ f"completion signature diverged from the frozen vector — BLOCKER\n"
79
+ f" got = {sig.hex()}\n vector = {v['ed25519']['signature_hex']}"
80
+ )
81
+ py_tiers = [
82
+ AssuranceTier.SELF_REPORTED.value,
83
+ AssuranceTier.COUNTER_SIGNED.value,
84
+ AssuranceTier.THIRD_PARTY_ATTESTED.value,
85
+ ]
86
+ if py_tiers != v["assurance_tier_strings"]:
87
+ raise ConformanceError(
88
+ f"AssuranceTier strings diverged from the frozen vector — BLOCKER\n"
89
+ f" got = {py_tiers}\n vector = {v['assurance_tier_strings']}"
90
+ )
91
+
92
+
93
+ def check_digest(v: dict) -> None:
94
+ """Python JCS + SHA-256 over the camelCase RequestAuthPayload == frozen digest."""
95
+ digest = canonical_request_signing_bytes(v["input"])
96
+ if digest.hex() != v["digest_sha256_hex"]:
97
+ raise ConformanceError(
98
+ f"canonical digest diverged from the frozen vector — BLOCKER\n"
99
+ f" got = {digest.hex()}\n vector = {v['digest_sha256_hex']}"
100
+ )
101
+ if len(digest) != 32:
102
+ raise ConformanceError(f"digest is {len(digest)} bytes, expected 32")
103
+
104
+
105
+ def check_signature(v: dict) -> None:
106
+ """From the frozen seed, derive the same public key + deterministic signature."""
107
+ seed = bytes.fromhex(v["ed25519"]["private_key_seed_hex"])
108
+ sk = Ed25519PrivateKey.from_private_bytes(seed)
109
+
110
+ pub = sk.public_key().public_bytes_raw()
111
+ if pub.hex() != v["ed25519"]["public_key_hex"]:
112
+ raise ConformanceError(
113
+ f"public key diverged — BLOCKER\n"
114
+ f" got = {pub.hex()}\n vector = {v['ed25519']['public_key_hex']}"
115
+ )
116
+
117
+ digest = canonical_request_signing_bytes(v["input"])
118
+ sig = sk.sign(digest)
119
+ if sig.hex() != v["ed25519"]["signature_hex"]:
120
+ raise ConformanceError(
121
+ f"signature diverged — BLOCKER\n"
122
+ f" got = {sig.hex()}\n vector = {v['ed25519']['signature_hex']}"
123
+ )
124
+ Ed25519PublicKey.from_public_bytes(pub).verify(sig, digest) # raises on failure
125
+
126
+
127
+ def check_request_signer(v: dict) -> None:
128
+ """The real signer path: RequestSigner.sign_headers produces the frozen signature
129
+ and a header that verifies against the agent key over the reconstructed digest."""
130
+ from datetime import datetime, timezone
131
+
132
+ seed = bytes.fromhex(v["ed25519"]["private_key_seed_hex"])
133
+ sk = Ed25519PrivateKey.from_private_bytes(seed)
134
+ pub = sk.public_key()
135
+
136
+ inp = v["input"]
137
+ vaid = {"vaid_id": inp["vaidId"], "tenant_id": inp["tenantId"]}
138
+ signer = RequestSigner(vaid=vaid, private_key=sk)
139
+
140
+ now = datetime.strptime(inp["timestamp"], "%Y-%m-%dT%H:%M:%SZ").replace(
141
+ tzinfo=timezone.utc
142
+ )
143
+ body = b"" # vector bodySha256 = sha256("")
144
+ headers = signer.sign_headers(
145
+ inp["method"], inp["path"], body, now=now, nonce=inp["clientNonce"]
146
+ )
147
+
148
+ if headers[HEADER_TIMESTAMP] != inp["timestamp"]:
149
+ raise ConformanceError("header timestamp diverged — BLOCKER")
150
+ if headers[HEADER_NONCE] != inp["clientNonce"]:
151
+ raise ConformanceError("header nonce diverged — BLOCKER")
152
+
153
+ payload = {
154
+ "vaidId": vaid["vaid_id"],
155
+ "method": "POST",
156
+ "path": "/vaid/mint",
157
+ "bodySha256": hashlib.sha256(body).hexdigest(),
158
+ "tenantId": vaid["tenant_id"],
159
+ "timestamp": headers[HEADER_TIMESTAMP],
160
+ "clientNonce": headers[HEADER_NONCE],
161
+ }
162
+ digest = canonical_request_signing_bytes(payload)
163
+ sig = base64.b64decode(headers[HEADER_SIGNATURE])
164
+ if sig.hex() != v["ed25519"]["signature_hex"]:
165
+ raise ConformanceError("header signature diverged — BLOCKER")
166
+ pub.verify(sig, digest) # raises on failure → header signature is valid
167
+
168
+
169
+ def run() -> dict:
170
+ """Run all firewall checks against the bundled vector. Raises ConformanceError
171
+ on any divergence; returns the vector on PASS."""
172
+ v = load_vector()
173
+ check_digest(v)
174
+ check_signature(v)
175
+ check_request_signer(v)
176
+ check_completion(load_completion_vector())
177
+ return v
178
+
179
+
180
+ # --- pytest discovery (so `pytest --pyargs vaid_pop` runs the firewall) ---
181
+
182
+
183
+ def test_packaged_digest_matches_frozen_vector() -> None:
184
+ check_digest(load_vector())
185
+
186
+
187
+ def test_packaged_signature_matches_frozen_vector() -> None:
188
+ check_signature(load_vector())
189
+
190
+
191
+ def test_packaged_request_signer_matches_frozen_vector() -> None:
192
+ check_request_signer(load_vector())
193
+
194
+
195
+ def test_packaged_completion_matches_frozen_vector() -> None:
196
+ check_completion(load_completion_vector())
197
+
198
+
199
+ def main() -> int:
200
+ try:
201
+ v = run()
202
+ except ConformanceError as exc:
203
+ print(f"CROSS-LANGUAGE PoP FIREWALL: MISMATCH — BLOCKER\n{exc}")
204
+ return 1
205
+ c = load_completion_vector()
206
+ print(
207
+ "CROSS-LANGUAGE PoP FIREWALL: PASS — installed signer == frozen vectors, "
208
+ "byte-for-byte\n"
209
+ f" operator digest = {v['digest_sha256_hex']}\n"
210
+ f" operator signature = {v['ed25519']['signature_hex']}\n"
211
+ f" completion digest = {c['digest_sha256_hex']}\n"
212
+ f" completion signature = {c['ed25519']['signature_hex']}"
213
+ )
214
+ return 0
215
+
216
+
217
+ if __name__ == "__main__":
218
+ raise SystemExit(main())
@@ -0,0 +1,142 @@
1
+ """VAID proof-of-possession request signing — canonical Python signer.
2
+
3
+ The Rust `vaid-pop` crate (the operator-signing port and the `vaid_pop`
4
+ verifier) defines the CANONICAL contract; this is the Python **mirror**, not a
5
+ second definition. Byte-identity is locked by the shared cross-language vector
6
+ ``operator_pop_v1.json`` (vendored into this package at ``vaid_pop/vectors/`` and
7
+ drift-checked against the canonical source): if these bytes ever diverge from
8
+ Rust, the conformance test is a BLOCKER.
9
+
10
+ This module is the single source of the Python PoP signer. Language-specific
11
+ agent integrations DEPEND ON this package and re-export ``RequestSigner`` /
12
+ ``canonical_request_signing_bytes`` — they no longer carry their own copy. Any
13
+ framework-free consumer imports ``vaid_pop`` directly and pulls in nothing else.
14
+
15
+ Contract: RFC 8785 (JCS) over the camelCase ``RequestAuthPayload`` -> SHA-256 ->
16
+ 32-byte digest -> **pure Ed25519 over the digest as the raw message** -> raw
17
+ 64-byte signature. The four headers below carry it.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import base64
23
+ import hashlib
24
+ import json
25
+ import secrets
26
+ from datetime import datetime, timezone
27
+
28
+ import rfc8785
29
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
30
+
31
+ # VAID PoP wire headers — the fixed `x-synthera-*` header namespace a conforming
32
+ # verifier reads (the wire contract; the prefix is not a package dependency).
33
+ HEADER_VAID = "x-synthera-vaid"
34
+ HEADER_TIMESTAMP = "x-synthera-timestamp"
35
+ HEADER_NONCE = "x-synthera-nonce"
36
+ HEADER_SIGNATURE = "x-synthera-signature"
37
+
38
+
39
+ def canonical_request_signing_bytes(payload: dict) -> bytes:
40
+ """The 32-byte signing digest: RFC 8785 (JCS) of `payload`, then SHA-256.
41
+
42
+ Mirror of the Rust `vaid_pop::canonical_request_signing_bytes`. The
43
+ payload MUST use the camelCase `RequestAuthPayload` field names
44
+ (`vaidId`, `method`, `path`, `bodySha256`, `tenantId`, `timestamp`,
45
+ `clientNonce`) — JCS sorts keys, so order is irrelevant, but names are not.
46
+ """
47
+ return hashlib.sha256(rfc8785.dumps(payload)).digest()
48
+
49
+
50
+ def build_request_auth_payload(
51
+ *,
52
+ vaid_id: str,
53
+ method: str,
54
+ path: str,
55
+ body_sha256: str,
56
+ tenant_id: str,
57
+ timestamp: str,
58
+ client_nonce: str,
59
+ ) -> dict:
60
+ """The seven ADR-mandated fields, camelCase (mirrors `RequestAuthPayload`)."""
61
+ return {
62
+ "vaidId": vaid_id,
63
+ "method": method,
64
+ "path": path,
65
+ "bodySha256": body_sha256,
66
+ "tenantId": tenant_id,
67
+ "timestamp": timestamp,
68
+ "clientNonce": client_nonce,
69
+ }
70
+
71
+
72
+ def utc_whole_second_rfc3339(now: datetime | None = None) -> str:
73
+ """Whole-second UTC RFC 3339 with `Z`.
74
+
75
+ This is the chrono-serde fixed point: the verifier parses the
76
+ `x-synthera-timestamp` header into a `DateTime<Utc>` and re-serializes it
77
+ when it recomputes the canonical bytes; a whole-second `...Z` string parses
78
+ and re-serializes to itself, so the client's signed-payload timestamp matches
79
+ the server's. Sub-second precision would risk a re-serialization mismatch.
80
+ """
81
+ now = now or datetime.now(timezone.utc)
82
+ return now.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
83
+
84
+
85
+ class RequestSigner:
86
+ """Signs requests with the agent's per-agent Ed25519 key (build-3 PoP).
87
+
88
+ The agent's VAID document (as minted) and its private key are INPUTS here;
89
+ durable provisioning/injection of the key is the agent template's job,
90
+ not this client's.
91
+ """
92
+
93
+ def __init__(self, *, vaid: dict, private_key: Ed25519PrivateKey) -> None:
94
+ # The VAID document is snake_case (`Vaid` has no serde rename), unlike
95
+ # the camelCase `RequestAuthPayload`. Extract identity by snake_case keys.
96
+ try:
97
+ self._vaid_id = str(vaid["vaid_id"])
98
+ self._tenant_id = str(vaid["tenant_id"])
99
+ except KeyError as exc:
100
+ raise ValueError(f"VAID document missing required field {exc}") from exc
101
+ self._private_key = private_key
102
+ # X-Synthera-Vaid = base64 of the VAID document JSON. The verifier
103
+ # re-deserializes by field and recomputes the VAID's own canonical bytes
104
+ # for signature verification, so a compact re-encoding is fine.
105
+ self._vaid_header = base64.b64encode(
106
+ json.dumps(vaid, separators=(",", ":")).encode("utf-8")
107
+ ).decode("ascii")
108
+
109
+ def sign_headers(
110
+ self,
111
+ method: str,
112
+ path: str,
113
+ body: bytes,
114
+ *,
115
+ now: datetime | None = None,
116
+ nonce: str | None = None,
117
+ ) -> dict[str, str]:
118
+ """Produce the four PoP headers for `(method, path, body)`.
119
+
120
+ `now`/`nonce` are injectable for deterministic tests; in production they
121
+ default to the current UTC second and a fresh 128-bit random nonce.
122
+ """
123
+ timestamp = utc_whole_second_rfc3339(now)
124
+ nonce = nonce or secrets.token_hex(16)
125
+ body_sha256 = hashlib.sha256(body).hexdigest()
126
+ payload = build_request_auth_payload(
127
+ vaid_id=self._vaid_id,
128
+ method=method.upper(),
129
+ path=path,
130
+ body_sha256=body_sha256,
131
+ tenant_id=self._tenant_id,
132
+ timestamp=timestamp,
133
+ client_nonce=nonce,
134
+ )
135
+ digest = canonical_request_signing_bytes(payload)
136
+ signature = self._private_key.sign(digest) # pure Ed25519, raw 64 bytes
137
+ return {
138
+ HEADER_VAID: self._vaid_header,
139
+ HEADER_TIMESTAMP: timestamp,
140
+ HEADER_NONCE: nonce,
141
+ HEADER_SIGNATURE: base64.b64encode(signature).decode("ascii"),
142
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "_comment": "Completion-record conformance vector (v1). Load-bearing. `input` is a real CompletionRecord (camelCase). NOTE: this is the FIRST vector containing an ENUM (`assuranceTier`) — the most likely place for silent Rust/Python string divergence, so an explicit enum round-trip test accompanies it. A conforming implementation MUST produce `digest_sha256_hex` from `input` via JCS (RFC 8785) -> SHA-256 and reproduce `ed25519.signature_hex` byte-for-byte. SCOPE: self-signed DECLARED metadata only — the single signature substantiates `assuranceTier=selfReported` and nothing above it; counterSigned / thirdPartyAttested are unverified claims here (a separate, not-yet-built primitive would substantiate them). Same JCS→SHA-256→Ed25519 primitive as operator_pop_v1.json.",
3
+ "assurance_tier_strings": [
4
+ "selfReported",
5
+ "counterSigned",
6
+ "thirdPartyAttested"
7
+ ],
8
+ "digest_sha256_hex": "98a51b7e34cbfc8d57a58f7c8fd6e353ad50936ab9f26293a10374c755cd784d",
9
+ "ed25519": {
10
+ "_comment": "Deterministic test key (same RFC 8032 seed as operator_pop_v1.json).",
11
+ "private_key_seed_hex": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
12
+ "public_key_hex": "03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8",
13
+ "signature_hex": "19657ec3ea3c660807e6153b66ff6e6d534c0aa798344614d162790564103be6a393eca15da2db52d0a7572f1d014a68834217c221895de46b8d322b2a3a150e"
14
+ },
15
+ "input": {
16
+ "assuranceTier": "selfReported",
17
+ "completedAt": "2026-06-04T12:00:05Z",
18
+ "recordNonce": "fedcba9876543210fedcba9876543210",
19
+ "requestDigestSha256": "ee474ba87d703ebeacf663d7d6a2f15319bdef285c5b702e336d0f4af5b61327",
20
+ "resultSha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
21
+ "signerVaidId": "11111111-1111-1111-1111-111111111111",
22
+ "status": "succeeded",
23
+ "tenantId": "synthera-control-plane",
24
+ "vaidId": "11111111-1111-1111-1111-111111111111"
25
+ },
26
+ "scheme": "JCS(RFC8785) -> SHA-256 -> pure Ed25519 over the 32-byte digest as raw message; raw 64-byte signature; raw 32-byte Ed25519 public key"
27
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "_comment": "Operator-signing conformance vector (v1). Load-bearing. `input` is a real RequestAuthPayload (#[serde(rename_all=camelCase)]), not a hand-written generic object. A conforming verifier MUST produce `digest_sha256_hex` from `input` via JCS (RFC 8785) -> SHA-256, and (given `ed25519.private_key_seed_hex`) reproduce `ed25519.signature_hex` byte-for-byte (Ed25519 is deterministic, RFC 8032). A mismatch in any field name, ordering, encoding, or signature is a canonicalization break.",
3
+ "scheme": "JCS(RFC8785) -> SHA-256 -> pure Ed25519 over the 32-byte digest as raw message; raw 64-byte signature; raw 32-byte Ed25519 public key",
4
+ "input": {
5
+ "vaidId": "11111111-1111-1111-1111-111111111111",
6
+ "method": "POST",
7
+ "path": "/vaid/mint",
8
+ "bodySha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
9
+ "tenantId": "synthera-control-plane",
10
+ "timestamp": "2026-06-04T12:00:00Z",
11
+ "clientNonce": "0123456789abcdef0123456789abcdef"
12
+ },
13
+ "digest_sha256_hex": "ee474ba87d703ebeacf663d7d6a2f15319bdef285c5b702e336d0f4af5b61327",
14
+ "ed25519": {
15
+ "_comment": "Deterministic test key (RFC 8032 Ed25519 private seed). Any conforming implementation derives the same public key and produces the same signature over digest_sha256_hex. The signature verifies under vaid_pop::verify_signed_payload.",
16
+ "private_key_seed_hex": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
17
+ "public_key_hex": "03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8",
18
+ "signature_hex": "77e79744c362d352ce678992a3e3934fa57c33c3f307f6ffbe6ffc4ec5e726e096844fd8552db819b21a3e49e3b7796e23d1e10d4a03699285df4871f62e1502"
19
+ }
20
+ }