vaid-mint 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,103 @@
1
+ Metadata-Version: 2.4
2
+ Name: vaid-mint
3
+ Version: 0.1.0
4
+ Summary: The open, self-hostable reference mint (Python) for the VAID standard: mint a root VAID and mint attenuated child VAIDs (scope/capability-contained delegation).
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-mint
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,attenuation,delegation,ed25519,mint,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
+ Requires-Dist: vaid-pop>=0.1.0
22
+ Provides-Extra: test
23
+ Requires-Dist: pytest>=8.0; extra == 'test'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # vaid-mint (Python)
27
+
28
+ The Python mirror of the Rust `vaid-mint` crate: the open, self-hostable
29
+ **reference mint** for the VAID (Verifiable Agent Identity) standard.
30
+
31
+ - **`mint_root`** — mint a root/operator VAID (BYO-key with proof-of-possession,
32
+ or generate-and-discard), gated by an explicit `AuthorizationGate`.
33
+ - **`mint_child`** — **attenuated delegation**: an authenticated parent mints a
34
+ child whose authority is always a subset of its own (`child ⊆ parent`).
35
+
36
+ ## Trust model — read this before using the mint
37
+
38
+ This is a reference implementation with two deliberate, **unguarded** defaults:
39
+
40
+ 1. **`mint_root` has no authorization gate by default (`PermitAll`).** Anyone who
41
+ can call this code can mint a root VAID. Supply a real `AuthorizationGate` for
42
+ anything beyond local experimentation.
43
+ 2. **`mint_child` is intentionally ungated — attenuation *is* the authorization.**
44
+ Any holder of a valid parent VAID can mint children from it; a child can only
45
+ *narrow* scope/capabilities relative to its parent, never widen
46
+ (`child ⊆ parent`). Possession of a parent VAID is itself the authorization
47
+ boundary for delegation here. **Treat parent-VAID custody with the same care as
48
+ a credential.**
49
+
50
+ Neither of these is a security recommendation for production use — they are the
51
+ honest defaults of a self-hostable reference mint. See the sections below for
52
+ where each is enforced in code.
53
+
54
+ ```python
55
+ from vaid_mint import ReferenceIssuer, InMemoryAudit, MintService, VaidSeed
56
+
57
+ issuer = ReferenceIssuer.ephemeral(24)
58
+ mint = MintService(issuer, InMemoryAudit())
59
+ root = mint.mint_root(VaidSeed(
60
+ agent_class="orchestrator", version="1.0.0", tenant_id="acme",
61
+ scope_boundary=["data.acme"], capability_set=["read", "write"],
62
+ ))
63
+ assert issuer.verify_vaid(root)
64
+ ```
65
+
66
+ ## The split
67
+
68
+ This is the open engine of a HashiCorp-Vault-style split. Durable revocation,
69
+ KMS-backed kernel keys, and the audit-of-record are the closed managed authority
70
+ and are **not** here. `mint_root` is gated by an `AuthorizationGate` that defaults
71
+ to `PermitAll` — a reference-implementation choice, **not** a security
72
+ recommendation; production deployments should pass a real gate to `MintService`.
73
+
74
+ `mint_child` is intentionally **ungated because attenuation *is* the
75
+ authorization**: any holder of a valid parent VAID can mint children from it, and
76
+ a child can only narrow scope/capabilities relative to that parent, never widen
77
+ (`child ⊆ parent`). So **possession of a parent VAID is itself the authorization
78
+ boundary for delegation** — treat parent-VAID custody with the same care as a
79
+ credential.
80
+
81
+ ## Cross-language byte-identity
82
+
83
+ Proof-of-possession reuses the `vaid-pop` primitive verbatim. The signed VAID
84
+ **document** is proven byte-identical to the Rust mint by the vendored frozen
85
+ vector `vaid_mint/vectors/mint_v1.json` (the same `mint_v1.json` the Rust
86
+ `mint_conformance` test asserts). Run the packaged firewall:
87
+
88
+ ```
89
+ vaid-mint-conformance # exit 0 = PASS (installed mint == frozen vector)
90
+ ```
91
+
92
+ Per **Decision B** this is self-consistent within this repo (Rust == Python); it
93
+ is **not** byte-conformant against the closed substrate's (still-moving) VAID
94
+ format.
95
+
96
+ ## Install (local dev)
97
+
98
+ `vaid-mint` depends on `vaid-pop`. For a local checkout, install both editable:
99
+
100
+ ```
101
+ pip install -e python/vaid-pop
102
+ pip install -e python/vaid-mint --no-deps
103
+ ```
@@ -0,0 +1,78 @@
1
+ # vaid-mint (Python)
2
+
3
+ The Python mirror of the Rust `vaid-mint` crate: the open, self-hostable
4
+ **reference mint** for the VAID (Verifiable Agent Identity) standard.
5
+
6
+ - **`mint_root`** — mint a root/operator VAID (BYO-key with proof-of-possession,
7
+ or generate-and-discard), gated by an explicit `AuthorizationGate`.
8
+ - **`mint_child`** — **attenuated delegation**: an authenticated parent mints a
9
+ child whose authority is always a subset of its own (`child ⊆ parent`).
10
+
11
+ ## Trust model — read this before using the mint
12
+
13
+ This is a reference implementation with two deliberate, **unguarded** defaults:
14
+
15
+ 1. **`mint_root` has no authorization gate by default (`PermitAll`).** Anyone who
16
+ can call this code can mint a root VAID. Supply a real `AuthorizationGate` for
17
+ anything beyond local experimentation.
18
+ 2. **`mint_child` is intentionally ungated — attenuation *is* the authorization.**
19
+ Any holder of a valid parent VAID can mint children from it; a child can only
20
+ *narrow* scope/capabilities relative to its parent, never widen
21
+ (`child ⊆ parent`). Possession of a parent VAID is itself the authorization
22
+ boundary for delegation here. **Treat parent-VAID custody with the same care as
23
+ a credential.**
24
+
25
+ Neither of these is a security recommendation for production use — they are the
26
+ honest defaults of a self-hostable reference mint. See the sections below for
27
+ where each is enforced in code.
28
+
29
+ ```python
30
+ from vaid_mint import ReferenceIssuer, InMemoryAudit, MintService, VaidSeed
31
+
32
+ issuer = ReferenceIssuer.ephemeral(24)
33
+ mint = MintService(issuer, InMemoryAudit())
34
+ root = mint.mint_root(VaidSeed(
35
+ agent_class="orchestrator", version="1.0.0", tenant_id="acme",
36
+ scope_boundary=["data.acme"], capability_set=["read", "write"],
37
+ ))
38
+ assert issuer.verify_vaid(root)
39
+ ```
40
+
41
+ ## The split
42
+
43
+ This is the open engine of a HashiCorp-Vault-style split. Durable revocation,
44
+ KMS-backed kernel keys, and the audit-of-record are the closed managed authority
45
+ and are **not** here. `mint_root` is gated by an `AuthorizationGate` that defaults
46
+ to `PermitAll` — a reference-implementation choice, **not** a security
47
+ recommendation; production deployments should pass a real gate to `MintService`.
48
+
49
+ `mint_child` is intentionally **ungated because attenuation *is* the
50
+ authorization**: any holder of a valid parent VAID can mint children from it, and
51
+ a child can only narrow scope/capabilities relative to that parent, never widen
52
+ (`child ⊆ parent`). So **possession of a parent VAID is itself the authorization
53
+ boundary for delegation** — treat parent-VAID custody with the same care as a
54
+ credential.
55
+
56
+ ## Cross-language byte-identity
57
+
58
+ Proof-of-possession reuses the `vaid-pop` primitive verbatim. The signed VAID
59
+ **document** is proven byte-identical to the Rust mint by the vendored frozen
60
+ vector `vaid_mint/vectors/mint_v1.json` (the same `mint_v1.json` the Rust
61
+ `mint_conformance` test asserts). Run the packaged firewall:
62
+
63
+ ```
64
+ vaid-mint-conformance # exit 0 = PASS (installed mint == frozen vector)
65
+ ```
66
+
67
+ Per **Decision B** this is self-consistent within this repo (Rust == Python); it
68
+ is **not** byte-conformant against the closed substrate's (still-moving) VAID
69
+ format.
70
+
71
+ ## Install (local dev)
72
+
73
+ `vaid-mint` depends on `vaid-pop`. For a local checkout, install both editable:
74
+
75
+ ```
76
+ pip install -e python/vaid-pop
77
+ pip install -e python/vaid-mint --no-deps
78
+ ```
@@ -0,0 +1,61 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "vaid-mint"
7
+ # Independent semver. Additive-minor for new optional fields; MAJOR for any change
8
+ # to the canonical VAID document shape / signing bytes (those break cross-language
9
+ # byte-identity against mint_v1.json).
10
+ version = "0.1.0"
11
+ description = "The open, self-hostable reference mint (Python) for the VAID standard: mint a root VAID and mint attenuated child VAIDs (scope/capability-contained delegation)."
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", "delegation", "attenuation", "mint"]
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
+ # vaid-pop supplies the shared JCS→SHA-256→Ed25519 PoP primitive (mint reuses it
26
+ # for proof-of-possession, NOT reimplemented). cryptography = Ed25519; rfc8785 =
27
+ # RFC 8785 JCS, byte-identical to the Rust serde_jcs path (locked by mint_v1.json).
28
+ # Do NOT substitute another JCS library without re-proving byte-equality.
29
+ dependencies = [
30
+ "vaid-pop>=0.1.0",
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-mint-conformance` to prove the mint they got reproduces the frozen
38
+ # cross-language VAID-document vector byte-for-byte.
39
+ vaid-mint-conformance = "vaid_mint.conformance:main"
40
+
41
+ [project.urls]
42
+ Homepage = "https://github.com/solara-associates/vaid"
43
+ Repository = "https://github.com/solara-associates/vaid"
44
+ Source = "https://github.com/solara-associates/vaid/tree/main/python/vaid-mint"
45
+ Issues = "https://github.com/solara-associates/vaid/issues"
46
+
47
+ [project.optional-dependencies]
48
+ test = [
49
+ "pytest>=8.0",
50
+ ]
51
+
52
+ [tool.hatch.build.targets.wheel]
53
+ # The vendored conformance vector ships inside the wheel so a consumer's CI runs
54
+ # the firewall against the exact bytes this mint was proven against.
55
+ packages = ["vaid_mint"]
56
+
57
+ [tool.hatch.build.targets.sdist]
58
+ include = ["vaid_mint", "tests", "README.md"]
59
+
60
+ [tool.pytest.ini_options]
61
+ testpaths = ["tests"]
@@ -0,0 +1,298 @@
1
+ """Behavior parity tests — the Python mint mirrors the Rust ``vaid_mint::mint``
2
+ unit tests (attenuation matrix, proof-of-possession, the authorization gate, and
3
+ end-to-end verify). These are behavioral, not byte-identity; the frozen vector
4
+ (``test_mint_conformance.py``) covers cross-language byte-identity of the document.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import uuid
10
+
11
+ import pytest
12
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
13
+
14
+ from vaid_pop import canonical_request_signing_bytes, utc_whole_second_rfc3339
15
+
16
+ from vaid_mint import (
17
+ DenyAll,
18
+ InMemoryAudit,
19
+ MintService,
20
+ ReferenceIssuer,
21
+ VaidSeed,
22
+ build_mint_pop_payload,
23
+ build_unsigned_vaid_document,
24
+ compute_lineage_hash,
25
+ has_capability,
26
+ is_in_scope,
27
+ )
28
+ from vaid_mint.error import IdentityError, UnauthorizedError
29
+ from vaid_mint.mint_types import MintPop
30
+
31
+
32
+ # ── fixtures / helpers ──
33
+
34
+
35
+ def fixture():
36
+ audit = InMemoryAudit()
37
+ issuer = ReferenceIssuer.ephemeral(1)
38
+ return MintService(issuer, audit), audit, issuer
39
+
40
+
41
+ def holder_key() -> Ed25519PrivateKey:
42
+ return Ed25519PrivateKey.generate()
43
+
44
+
45
+ def pub_bytes(k: Ed25519PrivateKey) -> bytes:
46
+ return k.public_key().public_bytes_raw()
47
+
48
+
49
+ def make_pop(seed, registered_key, signing_key, nonce, issued_at=None) -> MintPop:
50
+ issued_at = issued_at or utc_whole_second_rfc3339()
51
+ payload = build_mint_pop_payload(
52
+ seed, public_key_der=registered_key, nonce=nonce, issued_at=issued_at
53
+ )
54
+ digest = canonical_request_signing_bytes(payload)
55
+ return MintPop(nonce=nonce, issued_at=issued_at, signature=signing_key.sign(digest))
56
+
57
+
58
+ def byo_seed(public_key_der: bytes) -> VaidSeed:
59
+ return VaidSeed(
60
+ agent_class="runner",
61
+ version="1.0.0",
62
+ tenant_id="codex",
63
+ scope_boundary=["data.x"],
64
+ capability_set=["read"],
65
+ public_key_der=public_key_der,
66
+ )
67
+
68
+
69
+ def parent_doc(tenant, scope, caps) -> dict:
70
+ vid = str(uuid.uuid4())
71
+ return build_unsigned_vaid_document(
72
+ vaid_id=vid,
73
+ agent_id=vid,
74
+ agent_class="parent",
75
+ version="1.0.0",
76
+ tenant_id=tenant,
77
+ issued_at="2026-06-04T12:00:00Z",
78
+ expires_at="2026-06-05T12:00:00Z",
79
+ public_key_der=[],
80
+ parent_vaid=None,
81
+ scope_boundary=list(scope),
82
+ lineage_hash="lineage",
83
+ capability_set=list(caps),
84
+ )
85
+
86
+
87
+ def child_seed(parent, scope, caps, child_pub) -> VaidSeed:
88
+ return VaidSeed(
89
+ agent_class="child",
90
+ version="1.0.0",
91
+ tenant_id=parent["tenant_id"],
92
+ parent_vaid=parent["vaid_id"],
93
+ scope_boundary=list(scope),
94
+ capability_set=list(caps),
95
+ public_key_der=child_pub,
96
+ )
97
+
98
+
99
+ def signed_child(parent, scope, caps, nonce):
100
+ k = holder_key()
101
+ pub = pub_bytes(k)
102
+ seed = child_seed(parent, scope, caps, pub)
103
+ return seed, make_pop(seed, pub, k, nonce)
104
+
105
+
106
+ # ── mint_root ──
107
+
108
+
109
+ def test_root_generate_and_discard_mints_and_audits():
110
+ svc, audit, _ = fixture()
111
+ seed = VaidSeed(
112
+ agent_class="researcher",
113
+ version="1.0.0",
114
+ tenant_id="codex",
115
+ scope_boundary=["data.governance"],
116
+ capability_set=["read.documents"],
117
+ )
118
+ vaid = svc.mint_root(seed)
119
+ assert vaid["agent_class"] == "researcher"
120
+ assert vaid["scope_boundary"] == ["data.governance"]
121
+ assert vaid["parent_vaid"] is None
122
+ assert len(audit.entries) == 1
123
+ assert audit.entries[0].details["delegated"] is False
124
+
125
+
126
+ def test_root_byo_key_with_valid_pop_binds_key():
127
+ svc, audit, _ = fixture()
128
+ k = holder_key()
129
+ registered = pub_bytes(k)
130
+ seed = byo_seed(registered)
131
+ pop = make_pop(seed, registered, k, "nonce-aaa")
132
+ vaid = svc.mint_root(seed, pop)
133
+ assert bytes(vaid["public_key_der"]) == registered
134
+ assert audit.entries[0].details["byo_key"] is True
135
+
136
+
137
+ def test_root_byo_key_with_pop_for_different_key_is_rejected():
138
+ svc, audit, _ = fixture()
139
+ victim, attacker = holder_key(), holder_key()
140
+ victim_pub = pub_bytes(victim)
141
+ seed = byo_seed(victim_pub)
142
+ pop = make_pop(seed, victim_pub, attacker, "nonce-bbb") # signed by attacker
143
+ with pytest.raises(IdentityError, match="does not verify"):
144
+ svc.mint_root(seed, pop)
145
+ assert audit.is_empty()
146
+
147
+
148
+ def test_root_byo_key_without_pop_is_rejected():
149
+ svc, _, _ = fixture()
150
+ seed = byo_seed(pub_bytes(holder_key()))
151
+ with pytest.raises(IdentityError, match="proof-of-possession required"):
152
+ svc.mint_root(seed, None)
153
+
154
+
155
+ def test_root_byo_key_replay_is_rejected():
156
+ svc, _, _ = fixture()
157
+ k = holder_key()
158
+ registered = pub_bytes(k)
159
+ seed = byo_seed(registered)
160
+ pop = make_pop(seed, registered, k, "nonce-replay")
161
+ svc.mint_root(seed, pop)
162
+ with pytest.raises(IdentityError, match="replay"):
163
+ svc.mint_root(seed, pop)
164
+
165
+
166
+ def test_root_mint_denied_by_gate_has_no_side_effects():
167
+ audit = InMemoryAudit()
168
+ svc = MintService(ReferenceIssuer.ephemeral(1), audit, DenyAll())
169
+ seed = VaidSeed(agent_class="x", version="1.0.0", tenant_id="codex")
170
+ with pytest.raises(UnauthorizedError, match="denied by gate"):
171
+ svc.mint_root(seed)
172
+ assert audit.is_empty()
173
+
174
+
175
+ # ── mint_child — attenuated delegation ──
176
+
177
+
178
+ def test_child_within_bounds_is_minted_with_lineage_and_delegated_audit():
179
+ svc, audit, _ = fixture()
180
+ parent = parent_doc("aifactory", ["data.aifactory"], ["read", "write"])
181
+ seed, pop = signed_child(parent, ["data.aifactory.sub"], ["read"], "ok-1")
182
+ vaid = svc.mint_child(seed, parent, pop)
183
+ assert vaid["parent_vaid"] == parent["vaid_id"]
184
+ assert audit.entries[0].details["delegated"] is True
185
+ assert audit.entries[0].details["attenuation_verified"] is True
186
+
187
+
188
+ def test_child_scope_exceeding_parent_is_denied():
189
+ svc, audit, _ = fixture()
190
+ parent = parent_doc("aifactory", ["data.aifactory"], ["read"])
191
+ seed, pop = signed_child(parent, ["data.somewhere-else"], ["read"], "deny-scope")
192
+ with pytest.raises(UnauthorizedError, match="scope_boundary exceeds"):
193
+ svc.mint_child(seed, parent, pop)
194
+ assert audit.is_empty()
195
+
196
+
197
+ def test_empty_child_scope_under_restricted_parent_is_denied():
198
+ svc, _, _ = fixture()
199
+ parent = parent_doc("aifactory", ["data.aifactory"], ["read"])
200
+ seed, pop = signed_child(parent, [], ["read"], "deny-empty-scope")
201
+ with pytest.raises(UnauthorizedError, match="scope_boundary exceeds"):
202
+ svc.mint_child(seed, parent, pop)
203
+
204
+
205
+ def test_empty_parent_scope_permits_any_child_scope():
206
+ svc, _, _ = fixture()
207
+ parent = parent_doc("aifactory", [], ["read"])
208
+ s1, p1 = signed_child(parent, ["data.anything"], ["read"], "u-1")
209
+ svc.mint_child(s1, parent, p1)
210
+ s2, p2 = signed_child(parent, [], ["read"], "u-2")
211
+ svc.mint_child(s2, parent, p2)
212
+
213
+
214
+ def test_child_caps_exceeding_parent_are_denied():
215
+ svc, _, _ = fixture()
216
+ parent = parent_doc("aifactory", ["data.aifactory"], ["read"])
217
+ seed, pop = signed_child(parent, ["data.aifactory.sub"], ["read", "write"], "deny-caps")
218
+ with pytest.raises(UnauthorizedError, match="capability_set exceeds"):
219
+ svc.mint_child(seed, parent, pop)
220
+
221
+
222
+ def test_empty_parent_caps_may_delegate_nothing_but_empty_child_caps_ok():
223
+ svc, _, _ = fixture()
224
+ parent = parent_doc("aifactory", [], [])
225
+ s1, p1 = signed_child(parent, [], ["read"], "caps-deny")
226
+ with pytest.raises(UnauthorizedError, match="capability_set exceeds"):
227
+ svc.mint_child(s1, parent, p1)
228
+ s2, p2 = signed_child(parent, [], [], "caps-ok")
229
+ svc.mint_child(s2, parent, p2)
230
+
231
+
232
+ def test_cross_tenant_child_is_denied():
233
+ svc, audit, _ = fixture()
234
+ parent = parent_doc("aifactory", ["data.aifactory"], ["read"])
235
+ k = holder_key()
236
+ pub = pub_bytes(k)
237
+ seed = child_seed(parent, ["data.aifactory.sub"], ["read"], pub)
238
+ seed.tenant_id = "codex" # forge a foreign tenant
239
+ pop = make_pop(seed, pub, k, "forge-tenant")
240
+ with pytest.raises(UnauthorizedError, match="cross-tenant delegation is denied"):
241
+ svc.mint_child(seed, parent, pop)
242
+ assert audit.is_empty()
243
+
244
+
245
+ def test_child_claiming_a_different_parent_vaid_is_denied():
246
+ svc, audit, _ = fixture()
247
+ parent = parent_doc("aifactory", ["data.aifactory"], ["read"])
248
+ k = holder_key()
249
+ pub = pub_bytes(k)
250
+ seed = child_seed(parent, ["data.aifactory.sub"], ["read"], pub)
251
+ seed.parent_vaid = str(uuid.uuid4()) # forge a different parent
252
+ pop = make_pop(seed, pub, k, "forge-parent")
253
+ with pytest.raises(UnauthorizedError, match="parent_vaid"):
254
+ svc.mint_child(seed, parent, pop)
255
+ assert audit.is_empty()
256
+
257
+
258
+ def test_mint_child_without_parent_context_is_denied():
259
+ svc, _, _ = fixture()
260
+ parent = parent_doc("aifactory", ["data.aifactory"], ["read"])
261
+ seed, pop = signed_child(parent, ["data.aifactory.sub"], ["read"], "no-parent")
262
+ with pytest.raises(UnauthorizedError, match="no verified parent VAID"):
263
+ svc.mint_child(seed, None, pop)
264
+
265
+
266
+ def test_mint_child_without_byo_key_is_denied():
267
+ svc, _, _ = fixture()
268
+ parent = parent_doc("aifactory", ["data.aifactory"], ["read"])
269
+ seed = child_seed(parent, ["data.aifactory.sub"], ["read"], None)
270
+ with pytest.raises(IdentityError, match="BYO-key required"):
271
+ svc.mint_child(seed, parent, None)
272
+
273
+
274
+ def test_rejected_attenuation_does_not_consume_the_pop_nonce():
275
+ svc, _, _ = fixture()
276
+ parent = parent_doc("aifactory", ["data.aifactory"], ["read"])
277
+ # Scope-exceeding request with nonce "N" → denied at attenuation, before insert.
278
+ s_denied, p_denied = signed_child(parent, ["data.elsewhere"], ["read"], "N")
279
+ with pytest.raises(UnauthorizedError):
280
+ svc.mint_child(s_denied, parent, p_denied)
281
+ # A VALID request reusing nonce "N" now succeeds — "N" was never consumed.
282
+ s_ok, p_ok = signed_child(parent, ["data.aifactory.sub"], ["read"], "N")
283
+ svc.mint_child(s_ok, parent, p_ok)
284
+
285
+
286
+ def test_minted_child_verifies_and_is_contained_by_parent():
287
+ audit = InMemoryAudit()
288
+ issuer = ReferenceIssuer.ephemeral(1)
289
+ svc = MintService(issuer, audit)
290
+ parent = parent_doc("aifactory", ["data.aifactory"], ["read", "write"])
291
+ seed, pop = signed_child(parent, ["data.aifactory.reports"], ["read"], "e2e")
292
+ child = svc.mint_child(seed, parent, pop)
293
+
294
+ assert issuer.verify_vaid(child)
295
+ assert all(is_in_scope(parent, s) for s in child["scope_boundary"])
296
+ assert all(has_capability(parent, c) for c in child["capability_set"])
297
+ # sanity: the derived lineage_hash on the child is self-consistent
298
+ assert child["lineage_hash"] == compute_lineage_hash(child["parent_vaid"], child["agent_id"])
@@ -0,0 +1,65 @@
1
+ """Canonical mint conformance gate (Python side of the cross-language firewall).
2
+
3
+ The vendored vector ``vaid_mint/vectors/mint_v1.json`` is byte-identical to the
4
+ Rust copy (a CI drift-check enforces that). These tests assert the Python mint
5
+ reproduces the frozen VAID-document digest + kernel signature byte-for-byte, and
6
+ that the derived fields (``lineage_hash``, ``vaid_id == agent_id``) match. A
7
+ mismatch is a BLOCKER. The Rust ``mint_conformance`` test asserts the same vector.
8
+
9
+ Per Decision B this proves self-consistency WITHIN this repo (Rust == Python),
10
+ NOT conformance against the closed substrate's VAID format.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ from importlib.resources import files
17
+
18
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import (
19
+ Ed25519PrivateKey,
20
+ Ed25519PublicKey,
21
+ )
22
+
23
+ from vaid_mint.document import canonical_vaid_signing_bytes, compute_lineage_hash
24
+
25
+
26
+ def _vector() -> dict:
27
+ data = files("vaid_mint").joinpath("vectors/mint_v1.json").read_text()
28
+ return json.loads(data)
29
+
30
+
31
+ def test_document_digest_matches_frozen_vector():
32
+ v = _vector()
33
+ digest = canonical_vaid_signing_bytes(v["input"])
34
+ assert digest.hex() == v["digest_sha256_hex"], (
35
+ "Python VAID-document digest diverged from the frozen vector — BLOCKER"
36
+ )
37
+ assert len(digest) == 32
38
+
39
+
40
+ def test_kernel_signature_matches_frozen_vector():
41
+ v = _vector()
42
+ seed = bytes.fromhex(v["ed25519"]["kernel_private_key_seed_hex"])
43
+ sk = Ed25519PrivateKey.from_private_bytes(seed)
44
+
45
+ pub = sk.public_key().public_bytes_raw()
46
+ assert pub.hex() == v["ed25519"]["kernel_public_key_hex"], "kernel pubkey diverged — BLOCKER"
47
+
48
+ digest = canonical_vaid_signing_bytes(v["input"])
49
+ sig = sk.sign(digest)
50
+ assert sig.hex() == v["ed25519"]["signature_hex"], "kernel signature diverged — BLOCKER"
51
+ assert len(sig) == 64
52
+ Ed25519PublicKey.from_public_bytes(pub).verify(sig, digest) # raises on failure
53
+
54
+
55
+ def test_lineage_hash_derivation_matches_frozen_vector():
56
+ v = _vector()
57
+ inp = v["input"]
58
+ assert compute_lineage_hash(inp["parent_vaid"], inp["agent_id"]) == inp["lineage_hash"], (
59
+ "recomputed lineage_hash diverged — BLOCKER"
60
+ )
61
+
62
+
63
+ def test_vaid_id_equals_agent_id():
64
+ v = _vector()
65
+ assert v["input"]["vaid_id"] == v["input"]["agent_id"]