aevum-spiffe 0.4.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,49 @@
1
+ # Python
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ *.pyd
6
+ .venv/
7
+ *.egg-info/
8
+
9
+ # Build
10
+ dist/
11
+ build/
12
+ site/
13
+
14
+ # Tools
15
+ .mypy_cache/
16
+ .ruff_cache/
17
+ .pytest_cache/
18
+ .hypothesis/
19
+ .cache/
20
+
21
+ # IDE
22
+ .vscode/
23
+ .idea/
24
+ *.swp
25
+ *.swo
26
+
27
+ # OS
28
+ .DS_Store
29
+ Thumbs.db
30
+
31
+ # Verify scripts (run locally, never commit)
32
+ verify_*.py
33
+ scripts/verify_*.py
34
+
35
+ # Aevum development — never commit (Phase 0+)
36
+ aevum_principles.key
37
+ signed_principles_draft.yaml
38
+ tools/sign_principles.py
39
+
40
+ # Private keys — never commit
41
+ *.key
42
+ *.pem
43
+
44
+ # OpenSSF Scorecard output (Phase 0+)
45
+ results.sarif
46
+ verify_phase3.py
47
+ verify_phase7.py
48
+ verify_phase8.py
49
+ verify_phase*.py
@@ -0,0 +1,107 @@
1
+ Metadata-Version: 2.4
2
+ Name: aevum-spiffe
3
+ Version: 0.4.0
4
+ Summary: Aevum — SPIFFE/SPIRE cryptographic agent identity complication.
5
+ Project-URL: Homepage, https://aevum.build
6
+ Project-URL: Repository, https://github.com/aevum-labs/aevum
7
+ License: Apache-2.0
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Typing :: Typed
13
+ Requires-Python: >=3.11
14
+ Requires-Dist: aevum-core
15
+ Provides-Extra: dev
16
+ Requires-Dist: mypy>=1.10; extra == 'dev'
17
+ Requires-Dist: pytest>=8.0; extra == 'dev'
18
+ Requires-Dist: ruff>=0.9; extra == 'dev'
19
+ Provides-Extra: spiffe
20
+ Requires-Dist: spiffe>=0.2.3; extra == 'spiffe'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # aevum-spiffe
24
+
25
+ SPIFFE/SPIRE agent identity complication for Aevum.
26
+
27
+ Provides cryptographically-attested agent identity via JWT-SVIDs from the
28
+ SPIFFE Workload API. When `on_approved()` is called, emits a `spiffe.attested`
29
+ AuditEvent recording the SPIFFE ID and SVID metadata in the sigchain.
30
+
31
+ Requires SPIRE or a compatible SPIFFE Workload API (Vault SPIFFE secrets
32
+ engine, KUDO, etc.) to be running at attestation time.
33
+
34
+ ```bash
35
+ pip install aevum-spiffe[spiffe]
36
+ ```
37
+
38
+ ```python
39
+ from aevum.core import Engine
40
+ from aevum.spiffe import SpiffeComplication
41
+
42
+ engine = Engine()
43
+ comp = SpiffeComplication(
44
+ socket_path="unix:///run/spire/sockets/agent.sock", # optional
45
+ audience=["aevum"], # optional
46
+ )
47
+ engine.install_complication(comp)
48
+ engine.approve_complication("aevum-spiffe")
49
+ comp.on_approved(engine) # emits spiffe.attested into the sigchain
50
+ ```
51
+
52
+ The chain now contains a signed `spiffe.attested` event:
53
+
54
+ ```json
55
+ {
56
+ "event_type": "spiffe.attested",
57
+ "actor": "aevum-spiffe",
58
+ "payload": {
59
+ "spiffe_id": "spiffe://example.org/billing-agent",
60
+ "trust_domain": "example.org",
61
+ "audience": ["aevum"],
62
+ "svid_type": "jwt",
63
+ "source": "workload-api",
64
+ "socket": "unix:///run/spire/sockets/agent.sock",
65
+ "expiry": "2026-05-06T15:00:00+00:00"
66
+ }
67
+ }
68
+ ```
69
+
70
+ ## Lifecycle note
71
+
72
+ The Aevum Engine does not call lifecycle hooks automatically at approval time.
73
+ After `engine.approve_complication("aevum-spiffe")`, callers must invoke
74
+ `comp.on_approved(engine)` explicitly to trigger attestation. This is the
75
+ correct pattern for all complications that need to act at approval time.
76
+
77
+ ## Downstream use
78
+
79
+ Other complications can read the attested SPIFFE ID:
80
+
81
+ ```python
82
+ spiffe_comp = engine.get_active_complication_by_capability("spiffe-identity")
83
+ spiffe_id = spiffe_comp.get_actor_spiffe_id() if spiffe_comp else None
84
+ payload = {"actor_spiffe_id": spiffe_id, ...}
85
+ ```
86
+
87
+ ## Without SPIRE
88
+
89
+ If the SPIFFE socket is unavailable or py-spiffe is not installed,
90
+ `on_approved()` logs a warning and continues without attestation.
91
+ Engine startup is never blocked.
92
+
93
+ ## Trust boundary
94
+
95
+ The SPIFFE ID in the `spiffe.attested` event is cryptographically attested
96
+ by SPIRE's attestation plugins. It is NOT caller-asserted (unlike the `actor`
97
+ field). An auditor can verify the attestation by checking the SVID's parent
98
+ trust chain against the SPIFFE trust bundle.
99
+
100
+ The JWT token itself is NOT stored in the AuditEvent — it expires (typically
101
+ 1 hour) and is large. Only the SPIFFE ID string and metadata are recorded.
102
+
103
+ ## See also
104
+
105
+ - [ADR-006: SPIFFE integration](../../docs/adrs/adr-006-spiffe-integration.md)
106
+ - [py-spiffe](https://github.com/HewlettPackard/py-spiffe)
107
+ - [SPIFFE specification](https://spiffe.io)
@@ -0,0 +1,85 @@
1
+ # aevum-spiffe
2
+
3
+ SPIFFE/SPIRE agent identity complication for Aevum.
4
+
5
+ Provides cryptographically-attested agent identity via JWT-SVIDs from the
6
+ SPIFFE Workload API. When `on_approved()` is called, emits a `spiffe.attested`
7
+ AuditEvent recording the SPIFFE ID and SVID metadata in the sigchain.
8
+
9
+ Requires SPIRE or a compatible SPIFFE Workload API (Vault SPIFFE secrets
10
+ engine, KUDO, etc.) to be running at attestation time.
11
+
12
+ ```bash
13
+ pip install aevum-spiffe[spiffe]
14
+ ```
15
+
16
+ ```python
17
+ from aevum.core import Engine
18
+ from aevum.spiffe import SpiffeComplication
19
+
20
+ engine = Engine()
21
+ comp = SpiffeComplication(
22
+ socket_path="unix:///run/spire/sockets/agent.sock", # optional
23
+ audience=["aevum"], # optional
24
+ )
25
+ engine.install_complication(comp)
26
+ engine.approve_complication("aevum-spiffe")
27
+ comp.on_approved(engine) # emits spiffe.attested into the sigchain
28
+ ```
29
+
30
+ The chain now contains a signed `spiffe.attested` event:
31
+
32
+ ```json
33
+ {
34
+ "event_type": "spiffe.attested",
35
+ "actor": "aevum-spiffe",
36
+ "payload": {
37
+ "spiffe_id": "spiffe://example.org/billing-agent",
38
+ "trust_domain": "example.org",
39
+ "audience": ["aevum"],
40
+ "svid_type": "jwt",
41
+ "source": "workload-api",
42
+ "socket": "unix:///run/spire/sockets/agent.sock",
43
+ "expiry": "2026-05-06T15:00:00+00:00"
44
+ }
45
+ }
46
+ ```
47
+
48
+ ## Lifecycle note
49
+
50
+ The Aevum Engine does not call lifecycle hooks automatically at approval time.
51
+ After `engine.approve_complication("aevum-spiffe")`, callers must invoke
52
+ `comp.on_approved(engine)` explicitly to trigger attestation. This is the
53
+ correct pattern for all complications that need to act at approval time.
54
+
55
+ ## Downstream use
56
+
57
+ Other complications can read the attested SPIFFE ID:
58
+
59
+ ```python
60
+ spiffe_comp = engine.get_active_complication_by_capability("spiffe-identity")
61
+ spiffe_id = spiffe_comp.get_actor_spiffe_id() if spiffe_comp else None
62
+ payload = {"actor_spiffe_id": spiffe_id, ...}
63
+ ```
64
+
65
+ ## Without SPIRE
66
+
67
+ If the SPIFFE socket is unavailable or py-spiffe is not installed,
68
+ `on_approved()` logs a warning and continues without attestation.
69
+ Engine startup is never blocked.
70
+
71
+ ## Trust boundary
72
+
73
+ The SPIFFE ID in the `spiffe.attested` event is cryptographically attested
74
+ by SPIRE's attestation plugins. It is NOT caller-asserted (unlike the `actor`
75
+ field). An auditor can verify the attestation by checking the SVID's parent
76
+ trust chain against the SPIFFE trust bundle.
77
+
78
+ The JWT token itself is NOT stored in the AuditEvent — it expires (typically
79
+ 1 hour) and is large. Only the SPIFFE ID string and metadata are recorded.
80
+
81
+ ## See also
82
+
83
+ - [ADR-006: SPIFFE integration](../../docs/adrs/adr-006-spiffe-integration.md)
84
+ - [py-spiffe](https://github.com/HewlettPackard/py-spiffe)
85
+ - [SPIFFE specification](https://spiffe.io)
@@ -0,0 +1,61 @@
1
+ [project]
2
+ name = "aevum-spiffe"
3
+ version = "0.4.0"
4
+ description = "Aevum — SPIFFE/SPIRE cryptographic agent identity complication."
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = { text = "Apache-2.0" }
8
+ classifiers = [
9
+ "Development Status :: 3 - Alpha",
10
+ "Intended Audience :: Developers",
11
+ "License :: OSI Approved :: Apache Software License",
12
+ "Programming Language :: Python :: 3.11",
13
+ "Typing :: Typed",
14
+ ]
15
+ dependencies = [
16
+ "aevum-core",
17
+ ]
18
+
19
+ [project.optional-dependencies]
20
+ spiffe = ["spiffe>=0.2.3"]
21
+ dev = [
22
+ "pytest>=8.0",
23
+ "mypy>=1.10",
24
+ "ruff>=0.9",
25
+ ]
26
+
27
+ [project.entry-points."aevum.complications"]
28
+ spiffe = "aevum.spiffe.complication:SpiffeComplication"
29
+
30
+ [project.urls]
31
+ Homepage = "https://aevum.build"
32
+ Repository = "https://github.com/aevum-labs/aevum"
33
+
34
+ [build-system]
35
+ requires = ["hatchling"]
36
+ build-backend = "hatchling.build"
37
+
38
+ [tool.hatch.build.targets.wheel]
39
+ packages = ["src/aevum"]
40
+
41
+ [tool.uv.sources]
42
+ aevum-core = { workspace = true }
43
+
44
+ [tool.pytest.ini_options]
45
+ testpaths = ["tests"]
46
+ addopts = "--tb=short"
47
+ pythonpath = ["src", "../../packages/aevum-core/src"]
48
+
49
+ [tool.mypy]
50
+ strict = true
51
+ python_version = "3.11"
52
+ mypy_path = "src"
53
+ explicit_package_bases = true
54
+ ignore_missing_imports = true
55
+
56
+ [tool.ruff]
57
+ line-length = 100
58
+
59
+ [tool.ruff.lint]
60
+ select = ["E", "F", "UP", "B", "SIM", "I", "ANN"]
61
+ ignore = ["ANN401"]
@@ -0,0 +1,23 @@
1
+ """
2
+ aevum.spiffe — SPIFFE/SPIRE cryptographic agent identity complication.
3
+
4
+ Provides cryptographically-attested agent identity via SPIFFE JWT-SVIDs.
5
+ Emits a spiffe.attested AuditEvent when on_approved() is called, recording
6
+ the SPIFFE ID and SVID metadata (not the JWT itself).
7
+
8
+ Requires: pip install aevum-spiffe[spiffe] (installs py-spiffe 0.2.3+)
9
+
10
+ Without py-spiffe installed: importing this module succeeds but
11
+ SpiffeComplication.on_approved() will warn and skip gracefully.
12
+
13
+ Usage:
14
+ from aevum.spiffe import SpiffeComplication
15
+ engine.install_complication(comp)
16
+ engine.approve_complication("aevum-spiffe")
17
+ comp.on_approved(engine) # emit spiffe.attested event
18
+ """
19
+
20
+ from aevum.spiffe.complication import SpiffeComplication
21
+
22
+ __version__ = "0.4.0"
23
+ __all__ = ["SpiffeComplication"]
@@ -0,0 +1,184 @@
1
+ """
2
+ SpiffeComplication — SPIFFE agent identity via JWT-SVIDs.
3
+
4
+ ADR-006 implementation. See docs/adrs/adr-006-spiffe-integration.md.
5
+
6
+ Lifecycle note: the Engine has no on_approved callback mechanism. Callers
7
+ must invoke comp.on_approved(engine) explicitly after
8
+ engine.approve_complication("aevum-spiffe") to trigger attestation.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ import os
15
+ import sys
16
+ from typing import Any
17
+
18
+ log = logging.getLogger(__name__)
19
+
20
+ _DEFAULT_SOCKET = "unix:///tmp/spire-agent/public/api.sock"
21
+ _DEFAULT_AUDIENCE = ["aevum"]
22
+
23
+ # Module-level sentinel. Not a top-level import — py-spiffe is imported lazily
24
+ # inside _fetch_svid(). This name exists so tests can patch
25
+ # aevum.spiffe.complication.WorkloadApiClient without triggering a real import.
26
+ WorkloadApiClient: Any = None
27
+
28
+
29
+ class SpiffeComplication:
30
+ """
31
+ SPIFFE/SPIRE agent identity complication.
32
+
33
+ When on_approved() is called, fetches a JWT-SVID from the SPIFFE Workload
34
+ API and emits a spiffe.attested AuditEvent recording the SPIFFE ID and
35
+ metadata. The JWT token itself is never stored.
36
+
37
+ Failure modes (all non-fatal):
38
+ - py-spiffe not installed: warn and skip
39
+ - SPIFFE socket unavailable: warn and skip
40
+ - Invalid SVID: warn and skip
41
+
42
+ The complication never prevents Engine startup or operation.
43
+
44
+ Engine integration (no automatic lifecycle hook):
45
+ engine.install_complication(comp)
46
+ engine.approve_complication("aevum-spiffe")
47
+ comp.on_approved(engine) # caller must invoke this explicitly
48
+ """
49
+
50
+ name: str = "aevum-spiffe"
51
+ version: str = "0.4.0"
52
+ capabilities: list[str] = ["spiffe-identity"]
53
+
54
+ def __init__(
55
+ self,
56
+ socket_path: str | None = None,
57
+ audience: list[str] | None = None,
58
+ ) -> None:
59
+ self._socket = socket_path or os.environ.get(
60
+ "AEVUM_SPIFFE_SOCKET", _DEFAULT_SOCKET
61
+ )
62
+ self._audience = audience or list(_DEFAULT_AUDIENCE)
63
+ self._spiffe_id: str | None = None
64
+ self._trust_domain: str | None = None
65
+ self._attested: bool = False
66
+
67
+ def manifest(self) -> dict[str, Any]:
68
+ return {
69
+ "name": self.name,
70
+ "version": self.version,
71
+ "description": "SPIFFE/SPIRE cryptographic agent identity via JWT-SVIDs",
72
+ "capabilities": list(self.capabilities),
73
+ "classification_max": 0,
74
+ "functions": ["commit"],
75
+ "auth": {"scopes_required": [], "public_key": None},
76
+ "schema_version": "1.0",
77
+ }
78
+
79
+ # ── Lifecycle ─────────────────────────────────────────────────────────────
80
+
81
+ def on_approved(self, engine: Any) -> None:
82
+ """
83
+ Call after engine.approve_complication("aevum-spiffe") to trigger
84
+ SPIFFE attestation and emit the spiffe.attested event.
85
+
86
+ The Engine does not call this automatically; callers must invoke it.
87
+ """
88
+ self._attest_and_emit(engine)
89
+
90
+ def _attest_and_emit(self, engine: Any) -> None:
91
+ try:
92
+ spiffe_id, trust_domain, expiry = self._fetch_svid()
93
+ except ImportError:
94
+ log.warning(
95
+ "aevum-spiffe: py-spiffe not installed. "
96
+ "Install with: pip install aevum-spiffe[spiffe]. "
97
+ "Continuing without agent identity attestation."
98
+ )
99
+ return
100
+ except Exception as exc:
101
+ log.warning(
102
+ "aevum-spiffe: SPIFFE attestation failed (%s). "
103
+ "Is SPIRE running at %s? "
104
+ "Continuing without agent identity attestation.",
105
+ exc,
106
+ self._socket,
107
+ )
108
+ return
109
+
110
+ self._spiffe_id = spiffe_id
111
+ self._trust_domain = trust_domain
112
+ self._attested = True
113
+
114
+ self._write_attested_event(engine, spiffe_id, trust_domain, expiry)
115
+ log.info("aevum-spiffe: attested as %s", spiffe_id)
116
+
117
+ def _fetch_svid(self) -> tuple[str, str, str]:
118
+ """
119
+ Fetch a JWT-SVID from the SPIFFE Workload API.
120
+ Returns (spiffe_id, trust_domain, expiry_iso8601).
121
+ Raises ImportError if py-spiffe is not installed.
122
+ Raises Exception if attestation fails.
123
+ """
124
+ import datetime
125
+
126
+ # Honour the module-level name so tests can patch it; fall back to lazy import.
127
+ _module = sys.modules.get(__name__)
128
+ wac = getattr(_module, "WorkloadApiClient", None) if _module else None
129
+ if wac is None:
130
+ from spiffe import WorkloadApiClient as _wac # noqa: PLC0415
131
+ wac = _wac
132
+
133
+ with wac(workload_api_address=self._socket) as client:
134
+ svid = client.fetch_jwt_svid(audiences=self._audience)
135
+
136
+ spiffe_id = str(svid.spiffe_id)
137
+ trust_domain = svid.spiffe_id.trust_domain.name
138
+ expiry_dt = datetime.datetime.fromtimestamp(
139
+ svid.expiry, tz=datetime.UTC
140
+ )
141
+ expiry = expiry_dt.isoformat()
142
+ return spiffe_id, trust_domain, expiry
143
+
144
+ def _write_attested_event(
145
+ self,
146
+ engine: Any,
147
+ spiffe_id: str,
148
+ trust_domain: str,
149
+ expiry: str,
150
+ ) -> None:
151
+ try:
152
+ engine._ledger.append(
153
+ event_type="spiffe.attested",
154
+ payload={
155
+ "spiffe_id": spiffe_id,
156
+ "trust_domain": trust_domain,
157
+ "audience": self._audience,
158
+ "svid_type": "jwt",
159
+ "source": "workload-api",
160
+ "socket": self._socket,
161
+ "expiry": expiry,
162
+ },
163
+ actor="aevum-spiffe",
164
+ )
165
+ except Exception as exc:
166
+ log.error("aevum-spiffe: failed to write spiffe.attested event: %s", exc)
167
+
168
+ # ── Public API for downstream complications ───────────────────────────────
169
+
170
+ def get_actor_spiffe_id(self) -> str | None:
171
+ """
172
+ Return the attested SPIFFE ID, or None if not yet attested.
173
+
174
+ Downstream complications can call this to add actor_spiffe_id to
175
+ their event payloads:
176
+ spiffe_comp = engine.get_active_complication_by_capability("spiffe-identity")
177
+ spiffe_id = spiffe_comp.get_actor_spiffe_id() if spiffe_comp else None
178
+ """
179
+ return self._spiffe_id
180
+
181
+ @property
182
+ def is_attested(self) -> bool:
183
+ """True if SPIFFE attestation succeeded."""
184
+ return self._attested
File without changes
@@ -0,0 +1,225 @@
1
+ """
2
+ Tests for SpiffeComplication.
3
+ Uses mocked SPIFFE WorkloadApiClient — no real SPIRE deployment required.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import datetime
8
+ import sys
9
+ import unittest.mock
10
+
11
+
12
+ def _make_mock_svid(spiffe_id_str: str = "spiffe://example.org/billing") -> unittest.mock.MagicMock:
13
+ """Build a minimal mock JWT-SVID."""
14
+ mock_svid = unittest.mock.MagicMock()
15
+ mock_spiffe_id = unittest.mock.MagicMock()
16
+ mock_spiffe_id.__str__ = lambda self: spiffe_id_str
17
+ mock_trust_domain = unittest.mock.MagicMock()
18
+ mock_trust_domain.name = "example.org"
19
+ mock_spiffe_id.trust_domain = mock_trust_domain
20
+ mock_svid.spiffe_id = mock_spiffe_id
21
+ mock_svid.expiry = datetime.datetime(
22
+ 2026, 5, 6, 14, 0, 0, tzinfo=datetime.UTC
23
+ ).timestamp()
24
+ return mock_svid
25
+
26
+
27
+ def _make_mock_client(svid: unittest.mock.MagicMock) -> unittest.mock.MagicMock:
28
+ """Build a mock WorkloadApiClient context manager."""
29
+ client = unittest.mock.MagicMock()
30
+ client.__enter__ = unittest.mock.MagicMock(return_value=client)
31
+ client.__exit__ = unittest.mock.MagicMock(return_value=False)
32
+ client.fetch_jwt_svid = unittest.mock.MagicMock(return_value=svid)
33
+ return client
34
+
35
+
36
+ def _make_mock_engine() -> unittest.mock.MagicMock:
37
+ """Minimal engine mock with a ledger."""
38
+ engine = unittest.mock.MagicMock()
39
+ engine._ledger.append = unittest.mock.MagicMock(return_value=None)
40
+ return engine
41
+
42
+
43
+ class TestSpiffeComplicationWithMockSPIRE:
44
+
45
+ def test_on_approved_emits_spiffe_attested(self) -> None:
46
+ """Successful attestation must emit a spiffe.attested event."""
47
+ from aevum.spiffe import SpiffeComplication
48
+
49
+ mock_svid = _make_mock_svid()
50
+ mock_client = _make_mock_client(mock_svid)
51
+ mock_engine = _make_mock_engine()
52
+
53
+ comp = SpiffeComplication(socket_path="unix:///fake/socket")
54
+
55
+ with unittest.mock.patch(
56
+ "aevum.spiffe.complication.WorkloadApiClient",
57
+ return_value=mock_client,
58
+ ):
59
+ comp.on_approved(mock_engine)
60
+
61
+ assert comp.is_attested is True
62
+ assert comp.get_actor_spiffe_id() == "spiffe://example.org/billing"
63
+
64
+ mock_engine._ledger.append.assert_called_once()
65
+ call_kwargs = mock_engine._ledger.append.call_args.kwargs
66
+ assert call_kwargs["event_type"] == "spiffe.attested"
67
+ assert call_kwargs["payload"]["spiffe_id"] == "spiffe://example.org/billing"
68
+ assert call_kwargs["payload"]["trust_domain"] == "example.org"
69
+ assert call_kwargs["actor"] == "aevum-spiffe"
70
+
71
+ def test_missing_spiffe_library_degrades_gracefully(self) -> None:
72
+ """Missing py-spiffe must warn and not raise."""
73
+ from aevum.spiffe import SpiffeComplication
74
+
75
+ mock_engine = _make_mock_engine()
76
+ comp = SpiffeComplication()
77
+
78
+ with unittest.mock.patch.dict(sys.modules, {"spiffe": None}):
79
+ comp.on_approved(mock_engine)
80
+
81
+ assert comp.is_attested is False
82
+ assert comp.get_actor_spiffe_id() is None
83
+ mock_engine._ledger.append.assert_not_called()
84
+
85
+ def test_socket_unavailable_degrades_gracefully(self) -> None:
86
+ """Unreachable SPIFFE socket must warn and not raise."""
87
+ from aevum.spiffe import SpiffeComplication
88
+
89
+ mock_engine = _make_mock_engine()
90
+ comp = SpiffeComplication(socket_path="unix:///nonexistent/socket")
91
+
92
+ bad_client = unittest.mock.MagicMock()
93
+ bad_client.__enter__ = unittest.mock.MagicMock(
94
+ side_effect=Exception("Connection refused")
95
+ )
96
+ bad_client.__exit__ = unittest.mock.MagicMock(return_value=False)
97
+
98
+ with unittest.mock.patch(
99
+ "aevum.spiffe.complication.WorkloadApiClient",
100
+ return_value=bad_client,
101
+ ):
102
+ comp.on_approved(mock_engine)
103
+
104
+ assert comp.is_attested is False
105
+ mock_engine._ledger.append.assert_not_called()
106
+
107
+ def test_get_actor_spiffe_id_before_attestation(self) -> None:
108
+ """get_actor_spiffe_id() returns None before attestation."""
109
+ from aevum.spiffe import SpiffeComplication
110
+
111
+ comp = SpiffeComplication()
112
+ assert comp.get_actor_spiffe_id() is None
113
+
114
+ def test_spiffe_id_format_preserved(self) -> None:
115
+ """SPIFFE ID must be stored verbatim as returned by the SVID."""
116
+ from aevum.spiffe import SpiffeComplication
117
+
118
+ spiffe_id = "spiffe://production.example.com/service/billing-agent-v2"
119
+ mock_svid = _make_mock_svid(spiffe_id)
120
+ mock_client = _make_mock_client(mock_svid)
121
+ mock_engine = _make_mock_engine()
122
+
123
+ comp = SpiffeComplication()
124
+
125
+ with unittest.mock.patch(
126
+ "aevum.spiffe.complication.WorkloadApiClient",
127
+ return_value=mock_client,
128
+ ):
129
+ comp.on_approved(mock_engine)
130
+
131
+ assert comp.get_actor_spiffe_id() == spiffe_id
132
+
133
+ call_kwargs = mock_engine._ledger.append.call_args.kwargs
134
+ assert call_kwargs["payload"]["spiffe_id"] == spiffe_id
135
+ assert call_kwargs["payload"]["trust_domain"] == "example.org"
136
+
137
+ def test_audience_passed_to_workload_api(self) -> None:
138
+ """Custom audience must be forwarded to fetch_jwt_svid."""
139
+ from aevum.spiffe import SpiffeComplication
140
+
141
+ mock_svid = _make_mock_svid()
142
+ mock_client = _make_mock_client(mock_svid)
143
+ mock_engine = _make_mock_engine()
144
+
145
+ comp = SpiffeComplication(audience=["billing-service", "audit"])
146
+
147
+ with unittest.mock.patch(
148
+ "aevum.spiffe.complication.WorkloadApiClient",
149
+ return_value=mock_client,
150
+ ):
151
+ comp.on_approved(mock_engine)
152
+
153
+ mock_client.fetch_jwt_svid.assert_called_once_with(
154
+ audiences=["billing-service", "audit"]
155
+ )
156
+
157
+ def test_svid_jwt_not_stored(self) -> None:
158
+ """The JWT token itself must NOT appear in the spiffe.attested payload."""
159
+ from aevum.spiffe import SpiffeComplication
160
+
161
+ mock_svid = _make_mock_svid()
162
+ mock_svid.token = "eyJhbGciOiJFZERTQSJ9.SENSITIVE.CONTENT"
163
+ mock_client = _make_mock_client(mock_svid)
164
+ mock_engine = _make_mock_engine()
165
+
166
+ comp = SpiffeComplication()
167
+
168
+ with unittest.mock.patch(
169
+ "aevum.spiffe.complication.WorkloadApiClient",
170
+ return_value=mock_client,
171
+ ):
172
+ comp.on_approved(mock_engine)
173
+
174
+ call_kwargs = mock_engine._ledger.append.call_args.kwargs
175
+ payload_str = str(call_kwargs["payload"])
176
+ assert "eyJ" not in payload_str, "JWT token must not appear in payload"
177
+ assert "SENSITIVE" not in payload_str, "JWT token must not appear in payload"
178
+
179
+ def test_manifest_passes_validation(self) -> None:
180
+ """manifest() must satisfy ManifestValidator (all required fields present)."""
181
+ from aevum.spiffe import SpiffeComplication
182
+
183
+ sys.path.insert(0, "../../packages/aevum-core/src")
184
+ from aevum.core.complications.manifest_validator import ManifestValidator
185
+
186
+ comp = SpiffeComplication()
187
+ errors = ManifestValidator().validate(comp.manifest())
188
+ assert errors == [], f"Manifest validation errors: {errors}"
189
+
190
+
191
+ class TestSpiffeComplicationWithEngine:
192
+
193
+ def test_spiffe_attested_appears_in_sigchain(self) -> None:
194
+ """spiffe.attested must be a verifiable, signed event in the chain."""
195
+ # Engine lives in aevum-core; pythonpath configured in pyproject.toml
196
+ from aevum.core import Engine
197
+
198
+ from aevum.spiffe import SpiffeComplication
199
+
200
+ mock_svid = _make_mock_svid()
201
+ mock_client = _make_mock_client(mock_svid)
202
+
203
+ comp = SpiffeComplication()
204
+ engine = Engine()
205
+
206
+ with unittest.mock.patch(
207
+ "aevum.spiffe.complication.WorkloadApiClient",
208
+ return_value=mock_client,
209
+ ):
210
+ engine.install_complication(comp)
211
+ engine.approve_complication("aevum-spiffe")
212
+ # Engine has no automatic on_approved hook; caller invokes it.
213
+ comp.on_approved(engine)
214
+
215
+ entries = engine.get_ledger_entries()
216
+ event_types = [e["event_type"] for e in entries]
217
+
218
+ assert "spiffe.attested" in event_types, (
219
+ f"spiffe.attested not in chain. Events: {event_types}"
220
+ )
221
+ assert engine.verify_sigchain() is True, "Chain must be intact after attestation"
222
+
223
+ attested = next(e for e in entries if e["event_type"] == "spiffe.attested")
224
+ assert attested["payload"]["spiffe_id"] == "spiffe://example.org/billing"
225
+ assert attested["actor"] == "aevum-spiffe"