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.
- vaid_mint-0.1.0/.gitignore +24 -0
- vaid_mint-0.1.0/PKG-INFO +103 -0
- vaid_mint-0.1.0/README.md +78 -0
- vaid_mint-0.1.0/pyproject.toml +61 -0
- vaid_mint-0.1.0/tests/test_mint_behavior.py +298 -0
- vaid_mint-0.1.0/tests/test_mint_conformance.py +65 -0
- vaid_mint-0.1.0/vaid_mint/__init__.py +77 -0
- vaid_mint-0.1.0/vaid_mint/audit.py +47 -0
- vaid_mint-0.1.0/vaid_mint/authz.py +46 -0
- vaid_mint-0.1.0/vaid_mint/conformance.py +143 -0
- vaid_mint-0.1.0/vaid_mint/document.py +103 -0
- vaid_mint-0.1.0/vaid_mint/error.py +32 -0
- vaid_mint-0.1.0/vaid_mint/issuer.py +171 -0
- vaid_mint-0.1.0/vaid_mint/mint.py +232 -0
- vaid_mint-0.1.0/vaid_mint/mint_types.py +64 -0
- vaid_mint-0.1.0/vaid_mint/vectors/mint_v1.json +64 -0
|
@@ -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/
|
vaid_mint-0.1.0/PKG-INFO
ADDED
|
@@ -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"]
|