strix-verify 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,31 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.pyo
5
+ *.egg-info/
6
+ .eggs/
7
+ *.egg
8
+ build/
9
+ dist/
10
+
11
+ # Environments
12
+ .venv/
13
+ venv/
14
+ .env
15
+ .env.local
16
+
17
+ # uv
18
+ .uv/
19
+ uv.lock.broken
20
+
21
+ # Tests
22
+ .pytest_cache/
23
+ pytest-cache-files-*/
24
+ .coverage
25
+ htmlcov/
26
+
27
+ # Editor / OS
28
+ .vscode/
29
+ .idea/
30
+ *.swp
31
+ .DS_Store
@@ -0,0 +1,51 @@
1
+ # Changelog
2
+
3
+ All notable changes to `strix-verify` are documented here. This project
4
+ follows semantic versioning (pre-1.0: minor bumps may add functionality or
5
+ fix correctness issues that change verification outcomes).
6
+
7
+ ## [0.2.0] — 2026-05-31
8
+
9
+ ### Fixed — Academy-form record verification (correctness)
10
+
11
+ `build_canonical_payload` previously reconstructed **only** the Console
12
+ canonical form (string `schemaVersion`/`evidenceId`, `complianceMode`-first
13
+ `regulatoryContext`). It therefore could **not** verify records signed in the
14
+ Academy form — approximately 4,450 historical Academy records — and would have
15
+ reported them as signature failures.
16
+
17
+ This release adds the SE v1 **dual-form discriminator**, matching the JS
18
+ reference verifier (`@strixgov/verifier`):
19
+
20
+ - Records with `sourceApp == "academy-platform"` now serialize `schemaVersion`
21
+ and `evidenceId` as JSON **numbers**, with a `euAiActArticle12`-first
22
+ `regulatoryContext`.
23
+ - Console records are unchanged (JSON strings, `complianceMode`-first).
24
+ - Added the `signedPayload` reconstruction-free passthrough (v1.10.0): when a
25
+ record carries the original signed bytes, they are returned verbatim.
26
+ - Canonical output now uses `ensure_ascii=False` for byte-parity with the JS
27
+ reference on non-ASCII fields.
28
+
29
+ The Console path is byte-for-byte unchanged (the prior golden-vector test
30
+ remains the canary and still passes).
31
+
32
+ ### Added — cross-language conformance
33
+
34
+ This verifier is now pinned to the JS reference against a shared,
35
+ language-neutral fixture
36
+ (`packages/strixgov-verifier/test/fixtures/se-v1-conformance-vectors.json`).
37
+ Both implementations read the same fixture and must agree byte-for-byte; a CI
38
+ lane (`SE v1 Cross-Language Conformance`) enforces this on Python 3.10–3.12 and
39
+ Node 20–24. See `tests/test_conformance_vectors.py`.
40
+
41
+ ### Notes
42
+
43
+ - No new dependencies. No API changes to `verify_evidence` / `verify_evidence_record`.
44
+ - The golden vectors were not modified; the implementation was corrected to
45
+ match them.
46
+
47
+ ## [0.1.0]
48
+
49
+ Initial release. Independent SE v1 verification (Ed25519 + SHA-256), JWKS
50
+ resolution, two-layer verification model, EU AI Act compliance-flag derivation.
51
+ Console canonical form only.
@@ -0,0 +1,248 @@
1
+ # Python verifier — feature-parity backlog
2
+
3
+ Source of truth: `packages/strixgov-verifier/src/index.mjs` (JS verifier).
4
+ This doc tracks where `strix-verify` (Python) lags the JS verifier and
5
+ which gaps are worth closing.
6
+
7
+ > **Read first:** the trust math (Ed25519 + SHA-256 + canonical-JSON)
8
+ > in Python is byte-identical to JS for the surfaces Python implements.
9
+ > The gap is feature breadth, not correctness. Mirror-parity tests on
10
+ > the JS side lock the canonical payload builders; any port has to
11
+ > preserve those bytes exactly.
12
+
13
+ ---
14
+
15
+ ## Scope
16
+
17
+ `strix-verify` (Python) is the **auditor-facing subset** of the JS
18
+ verifier — not full parity. Canonical statement of the decision,
19
+ deciding principle, and reversal triggers lives in
20
+ [`SCOPE.md`](./SCOPE.md). Read that first if the question is "should
21
+ this be ported at all?"; read this doc for "what's the state of each
22
+ capability?"
23
+
24
+ The one-line summary: in scope = anything an external AI Act auditor
25
+ would independently verify against a public proof URL. Out of scope =
26
+ runtime-integration primitives (tool gateway, wire envelope,
27
+ snapshots).
28
+
29
+ ---
30
+
31
+ ## Current parity (2026-05-18)
32
+
33
+ | Capability | JS export | Python | Status |
34
+ |---|---|---|---|
35
+ | Signed Evidence v1 verify | `verify` (`index.mjs:411`) | `verify_evidence` | Complete |
36
+ | SE v1 canonical payload | `buildCanonicalPayload` (`:275`) | `build_canonical_payload` | Complete |
37
+ | Ed25519 signature | `verifySignature` (`:383`) | `verify_signature` | Complete |
38
+ | SHA-256 hash | `verifyHash` (`:395`) | `verify_hash` | Complete |
39
+ | Evidence fetch | `fetchEvidence` (`:200`) | `fetch_evidence` | Complete (defaults aligned) |
40
+ | JWKS resolve | `fetchPublicKey` (`:158`) | `resolve_key` | Complete |
41
+ | EU AI Act compliance flags | inline | `derive_compliance_flags` | Complete |
42
+ | Redacted-kid resolution | `resolveJwkFromKid` (`:70`) | not implemented | **Gap — P1** |
43
+ | Approval artifact verify | `verifyApprovalArtifact` (`:597`) | not implemented | **Gap — P1** |
44
+ | Approval quorum verify | `verifyApprovalQuorum` (`:688`) | not implemented | **Gap — P2** |
45
+ | Actor Attestation v1 | `verifyAttestation` (`:840`) | not implemented | **Gap — P1 (post AA-1 flip)** |
46
+ | AA-1 composite status | `computeAttestationCompositeStatus` (`:977`) | not implemented | **Gap — P1 (post AA-1 flip)** |
47
+ | AA-1 + evidence combined | `verifyWithAttestations` (`:1036`) | not implemented | **Gap — P1 (post AA-1 flip)** |
48
+ | Tool-gateway receipt verify | `verifyReceipt` (`:1193`) | not implemented | **Gap — P2** |
49
+ | Receipt-chain verify | `verifyReceiptChain` (`:1264`) | not implemented | **Gap — P2** |
50
+ | Snapshot verify | `verifySnapshot` (`:1353`) | not implemented | **Gap — P3** |
51
+ | Tool-gateway proof aggregator | `verifyToolGatewayProof` (`:1434`) | not implemented | **Gap — P3** |
52
+ | Visual SVG verify | `verifyVisual` (`:1623`) | not implemented | **Gap — P3** |
53
+ | Connected-mode wire envelope | `verifyConnectedWireEnvelope` (`:1510`) | not implemented | **Non-goal** |
54
+ | CLI binary | `bin/strix-verify` | not implemented | **Gap — P1** |
55
+
56
+ ---
57
+
58
+ ## Backlog
59
+
60
+ ### P1 — auditor-facing, ship before next launch milestone
61
+
62
+ #### CLI binary
63
+
64
+ Mirror `npx @strixgov/verifier@latest 5686` as
65
+ `strix-verify 5686`. Single entry point in `pyproject.toml`
66
+ (`[project.scripts] strix-verify = "strix_verify.cli:main"`).
67
+ Behavior: same exit codes as the JS CLI, same human-readable
68
+ output, plus `--json` for machine consumption. No new
69
+ deployment-specific defaults — inherits `DEFAULT_PROOF_BASE`.
70
+
71
+ Reference: JS CLI lives at `packages/strixgov-verifier/bin/strix-verify.mjs`.
72
+
73
+ #### Redacted-kid resolution
74
+
75
+ JS `resolveJwkFromKid` (`index.mjs:70`) handles the redacted kid
76
+ format `strix-***-2026-04` by suffix-matching the `YYYY-MM`
77
+ segment against the full JWKS. Python's `resolve_key` today
78
+ does exact-string match only.
79
+
80
+ Without this, Python cannot verify any record where
81
+ `signingKeyId` came through `redactSigningKeyId()` — which is
82
+ the entire public-approval-artifact surface (Gap 5 mitigation).
83
+ SE v1 evidence records expose the unredacted kid inside the
84
+ signed `fields.*` block, so basic evidence verification still
85
+ works; this gap shows up the moment Python tries an approval
86
+ artifact.
87
+
88
+ Mirror test: JS pins this with `redaction-promotion.test.mjs`.
89
+ Port: add suffix-match fallback to `resolve_key`, then port the
90
+ same locked redacted-kid fixture into `tests/`.
91
+
92
+ #### Approval artifact verifier
93
+
94
+ Phase 3 ships `SalesApprovalArtifact` with a locked 9-field
95
+ canonical payload. Public REST surface exists today
96
+ (`GET /api/public/approval-artifact/:id`). Auditors will want
97
+ to verify approval authority independently — same trust story
98
+ as evidence records, different schema.
99
+
100
+ Implementation cost: low. Port `buildApprovalCanonicalPayload`
101
+ (`index.mjs:511`) byte-for-byte (field order is contract — see
102
+ CLAUDE.md "Files that require extra care"), reuse the existing
103
+ SE v1 signature/hash machinery.
104
+
105
+ Mirror test: golden vectors from
106
+ `apps/strix-console/tests/sales-pipeline/approval-artifact-parity.test.ts`.
107
+
108
+ #### Actor Attestation v1 verifier
109
+
110
+ Currently dormant in production
111
+ (`STRIX_ACTOR_ATTESTATION_V1=false`). When the flag flips, the
112
+ public `/api/public/verify` response starts carrying an
113
+ `actorClass` block; Python users will see records they cannot
114
+ fully verify.
115
+
116
+ Implementation cost: medium. Port:
117
+
118
+ - `buildActorAttestationPayload` (`index.mjs:805`) — sibling
119
+ artifact joined to SE v1 by `evidenceId`
120
+ - `verifyAttestation` (`:840`) — 8 rules, 11 reason codes
121
+ - `computeAttestationCompositeStatus` (`:977`) — composes
122
+ evidence verification result with attestation outcome
123
+ - `verifyWithAttestations` (`:1036`) — convenience wrapper
124
+
125
+ Contract: `docs/architecture/actor-attestation-v1.md`
126
+ contractVersion 1.0.0 (frozen). Reason codes are public API —
127
+ do not rename when porting.
128
+
129
+ Trigger: AA-1 flag flip. Track against
130
+ `scripts/aa1-postflip-validate.mjs` running GREEN in production.
131
+
132
+ ### P2 — system-integrator-facing, port if Python demand surfaces
133
+
134
+ #### Approval quorum verifier
135
+
136
+ `verifyApprovalQuorum` (`index.mjs:688`) verifies the full set
137
+ of approval artifacts for a decision plus quorum rules
138
+ (N-of-M). Auditor demand is real but narrower than single
139
+ approval verification — most external review walks one
140
+ artifact at a time. Port after single-artifact lands.
141
+
142
+ #### Tool-gateway receipt verifier (v1 + v2)
143
+
144
+ `buildReceiptCanonicalPayload` (`index.mjs:1157`) supports
145
+ schemaVersion v1 (11 fields) and v2 (14 fields with
146
+ `policyVersion` / `tenantId` / `environment`).
147
+ `verifyReceipt` (`:1193`) consumes both, dispatching on
148
+ `schemaVersion`.
149
+
150
+ Auditor demand: low today. Tool-gateway receipts live
151
+ client-side in `@strixgov/tool-gateway`; the verification path
152
+ that matters most is the hosted approval/evidence side. Defer
153
+ unless a Python-native tool-gateway consumer materializes.
154
+
155
+ Mirror test if porting:
156
+ `packages/tool-gateway/tests/verifier-parity.test.mjs` — both
157
+ v1 and v2 receipt canonical bytes must match exactly.
158
+
159
+ #### Receipt-chain verifier
160
+
161
+ `verifyReceiptChain` (`index.mjs:1264`). Walks a chain of
162
+ tool-gateway receipts and verifies the sequencing invariant.
163
+ Same demand profile as single-receipt verify. Defer.
164
+
165
+ ### P3 — unlikely to be needed in Python
166
+
167
+ #### Snapshot verifier
168
+
169
+ `verifySnapshot` (`index.mjs:1353`) +
170
+ `buildSnapshotCanonicalPayload` (`:1322`) — tool-gateway chain
171
+ snapshots (v0.2 rotation). Internal to tool-gateway operation.
172
+ External auditors do not consume these directly.
173
+
174
+ #### Tool-gateway proof aggregator
175
+
176
+ `verifyToolGatewayProof` (`index.mjs:1434`). Composite verifier
177
+ across receipts + snapshots. Same reasoning as snapshot.
178
+
179
+ #### Visual SVG verifier
180
+
181
+ `extractVisualMetadata` (`index.mjs:1581`) + `verifyVisual`
182
+ (`:1623`) parse the `<strix:*>` metadata block embedded in
183
+ signed visual artifacts. The artifact format is web-facing (a
184
+ shareable signed badge). A Python port would only matter if
185
+ someone wanted to verify the badge from a backend job. CI gate
186
+ `scripts/lint-visual-artifacts-signed.mjs` already locks the
187
+ producer side; consumer-side parity in Python is theoretical.
188
+
189
+ ---
190
+
191
+ ## Non-goals
192
+
193
+ These will not be ported even if asked.
194
+
195
+ | Capability | Why not |
196
+ |---|---|
197
+ | `verifyConnectedWireEnvelope` (`index.mjs:1510`) | The wire envelope is a runtime integration concern (replay defense between tool-gateway and hosted kernel). Auditors verify the resulting evidence record, not the wire that carried it. |
198
+ | `SUPPORTED_WIRE_VERSIONS` (`:1483`) | Same reasoning. |
199
+ | Anything in `packages/tool-gateway/` | Not part of the verifier surface. Tool-gateway has its own JS-only release cadence and the parity tests live there. |
200
+
201
+ ---
202
+
203
+ ## Mirror discipline (if you port anything)
204
+
205
+ Any canonical payload builder you port from JS MUST stay
206
+ byte-identical to the JS source. The audit story collapses if
207
+ JS and Python disagree on a single byte for the same record.
208
+
209
+ Rules:
210
+
211
+ 1. **Copy the field order exactly.** Re-ordering a canonical
212
+ payload field invalidates every previously-signed artifact.
213
+ The locked field orders are documented in CLAUDE.md under
214
+ "Files that require extra care."
215
+ 2. **Port the locked goldens.** Every JS builder has a frozen
216
+ test vector (`tests/golden-vectors/` in solo-builder-core,
217
+ or inline in the parity test files). Reuse those bytes as
218
+ Python test fixtures — do not regenerate.
219
+ 3. **Public API name-stability.** `BindingReason` codes,
220
+ AA-1 reason codes, approval status enums — these are
221
+ public contract. Port them with the same names and the
222
+ same values.
223
+ 4. **One canonicalizer.** If you find yourself
224
+ re-implementing JSON serialization, stop. Use a single
225
+ canonical-JSON helper (`payload.py` today) and route every
226
+ new artifact type through it.
227
+ 5. **No deployment-specific defaults.** The package must not
228
+ carry any single customer's URL. Hosts default to the
229
+ canonical Strix Platform host only (`www.strixgov.com`),
230
+ which mirrors the JS verifier exactly.
231
+
232
+ ---
233
+
234
+ ## References
235
+
236
+ - JS verifier: `packages/strixgov-verifier/src/index.mjs`
237
+ - JS launch incident analysis:
238
+ `docs/postmortems/2026-05-15-launch-day-9-bug-stack.md`
239
+ - Load-bearing rules for signed-evidence paths:
240
+ `docs/CONSTRAINTS-FOR-AGENTS.md`
241
+ - Signed Evidence v1 spec:
242
+ `docs/gates/SIGNED-EVIDENCE-V1-AGENT-PROMPT.md`
243
+ - Actor Attestation v1 contract:
244
+ `docs/architecture/actor-attestation-v1.md`
245
+ - Approval artifact canonical schema:
246
+ `apps/strix-console/src/lib/sales-pipeline/approval-artifact.ts`
247
+ - Tool-gateway parity test (if ever porting receipts):
248
+ `packages/tool-gateway/tests/verifier-parity.test.mjs`
@@ -0,0 +1,300 @@
1
+ Metadata-Version: 2.4
2
+ Name: strix-verify
3
+ Version: 0.2.0
4
+ Summary: Independent verification of Strix governance evidence records. Ed25519 + SHA-256 only. No Strix tooling or account required. Public source at github.com/strixgov/strix.
5
+ Project-URL: Homepage, https://strixgov.com
6
+ Project-URL: Repository, https://github.com/strixgov/strix
7
+ Project-URL: Documentation, https://strixgov.com/docs/verify
8
+ Author: Strix Platform Team
9
+ License: MIT
10
+ Keywords: ai-safety,ed25519,evidence,governance,proof,strix,verification
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Security :: Cryptography
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: cryptography>=42.0.0
22
+ Requires-Dist: httpx>=0.27.0
23
+ Requires-Dist: pydantic>=2.0.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: mypy>=1.10.0; extra == 'dev'
26
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
27
+ Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
28
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
29
+ Requires-Dist: ruff>=0.4.0; extra == 'dev'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # strix-verify
33
+
34
+ Independent verification of Strix governance evidence records.
35
+
36
+ No Strix tooling, SDK, or account required. Uses only standard cryptographic
37
+ primitives (Ed25519, SHA-256) via the Python `cryptography` library.
38
+
39
+ **strix-verify is offline-safe.** Save the JWKS once, ship it with the
40
+ evidence, verify on an air-gapped machine. No callbacks. No telemetry.
41
+ No Strix server required at runtime.
42
+
43
+ **Proof Readiness Level 4.5** — Cryptographically Signed + Externally Verifiable
44
+
45
+ **Public source:** [github.com/strixgov/strix](https://github.com/strixgov/strix)
46
+ **PyPI:** [pypi.org/project/strix-verify](https://pypi.org/project/strix-verify)
47
+
48
+ ---
49
+
50
+ ## Installation
51
+
52
+ ```bash
53
+ pip install strix-verify
54
+ ```
55
+
56
+ Or from source:
57
+
58
+ ```bash
59
+ cd python/strix-verify
60
+ pip install -e .
61
+ ```
62
+
63
+ ---
64
+
65
+ ## Quick Start
66
+
67
+ ```python
68
+ from strix_verify import verify_evidence
69
+
70
+ result = verify_evidence(
71
+ evidence_id=123,
72
+ proof_base="https://strix.example.com", # your Strix deployment
73
+ jwks_base="https://strixgov.com", # canonical JWKS surface
74
+ )
75
+
76
+ print(result.signature_valid) # SignatureStatus.VERIFIED
77
+ print(result.hash_valid) # True
78
+ print(result.compliance.article12_tamper_resistant) # True
79
+ print(result.compliance.article14_human_oversight) # True
80
+ print(result.compliance.article28_provider_obligations) # True
81
+ ```
82
+
83
+ `proof_base` is required — there is no default. The verifier is neutral
84
+ across Strix deployments and never carries a hardcoded host URL.
85
+
86
+ ### Async
87
+
88
+ ```python
89
+ import asyncio
90
+ from strix_verify import fetch_evidence_async, verify_evidence_record
91
+
92
+ async def main():
93
+ record = await fetch_evidence_async(123, proof_base="https://strix.example.com")
94
+ result = verify_evidence_record(record, jwks_base="https://strixgov.com")
95
+ return result
96
+
97
+ asyncio.run(main())
98
+ ```
99
+
100
+ ---
101
+
102
+ ## Verification Flow
103
+
104
+ 1. **Fetch** — Retrieve evidence record from the Strix proof API
105
+ 2. **Reconstruct** — Build canonical 13-field payload (locked field order)
106
+ 3. **Fetch key** — Resolve Ed25519 public key from JWKS endpoint
107
+ 4. **Verify signature** — Ed25519 signature over canonical payload
108
+ 5. **Verify hash** — SHA-256 hash of canonical payload
109
+ 6. **Derive compliance flags** — EU AI Act flags derived from outcomes (never asserted)
110
+ 7. **Return** — `VerificationResult` with all outcomes
111
+
112
+ ---
113
+
114
+ ## Two-Layer Verification Model
115
+
116
+ ### Layer 1 — Cryptographic Validity (`signature_valid`)
117
+
118
+ > "Was this record produced by the holder of the Strix signing key?"
119
+
120
+ | Status | Meaning |
121
+ |--------|---------|
122
+ | `VERIFIED` | Valid Ed25519 signature from a known key |
123
+ | `LEGACY_UNSIGNED` | Pre-signing record (migration before 0039) |
124
+ | `UNVERIFIABLE_KEY` | Signing key ID not found in JWKS or extra_keys |
125
+ | `COMPLIANCE_VIOLATION` | Signature present but invalid |
126
+ | `ERROR` | Network or unexpected failure |
127
+
128
+ ### Layer 2 — Deployment Context (`environment_match`, `tenant_match`)
129
+
130
+ > "Is this record appropriate for this deployment context?"
131
+
132
+ Optional checks against stored record fields. Per **SE-14**, verification reads
133
+ `environment` and `tenantId` from the stored evidence record — never from
134
+ environment variables. This prevents false failures when a production record is
135
+ verified in a development context.
136
+
137
+ ```python
138
+ result = verify_evidence(
139
+ evidence_id=123,
140
+ expected_environment="production", # checks stored record field
141
+ expected_tenant_id="tenant-abc", # checks stored record field
142
+ )
143
+ print(result.environment_match) # True/False/None
144
+ print(result.tenant_match) # True/False/None
145
+ ```
146
+
147
+ ---
148
+
149
+ ## Verifying a Local Record
150
+
151
+ If you already have the evidence record dict (from a database, file, etc.):
152
+
153
+ ```python
154
+ from strix_verify import verify_evidence_record
155
+
156
+ result = verify_evidence_record(
157
+ record,
158
+ jwks_base="https://strixgov.com",
159
+ )
160
+ ```
161
+
162
+ ---
163
+
164
+ ## Verifying Offline (Air-Gapped)
165
+
166
+ For audit environments that cannot reach the internet, pre-fetch the JWKS
167
+ once, save it next to the evidence, and verify with no network access at
168
+ runtime:
169
+
170
+ ```python
171
+ import json
172
+ from strix_verify import verify_evidence_record
173
+
174
+ with open("evidence-2026-04-19.json") as f:
175
+ record = json.load(f)
176
+
177
+ with open("jwks-snapshot-2026-04.json") as f:
178
+ extra_keys = json.load(f)["keys"]
179
+
180
+ result = verify_evidence_record(
181
+ record,
182
+ extra_keys=extra_keys,
183
+ jwks_base="https://unused.invalid", # never reached when key is in extra_keys
184
+ )
185
+ ```
186
+
187
+ When the signing key is already present in `extra_keys`, `jwks_base` is
188
+ never contacted. The verifier resolves the key from the pre-fetched JWK
189
+ list and does the full cryptographic check locally.
190
+
191
+ ---
192
+
193
+ ## Key Rotation Support
194
+
195
+ Historical signing keys can be provided to verify older records after key rotation:
196
+
197
+ ```python
198
+ import json, os
199
+
200
+ extra_keys = json.loads(os.environ.get("STRIX_SIGNING_JWKS_EXTRA", "[]"))
201
+
202
+ result = verify_evidence(
203
+ evidence_id=123,
204
+ extra_keys=extra_keys,
205
+ )
206
+ ```
207
+
208
+ Key ID format: `strix-{env}-{YYYY-MM}` (e.g., `strix-prod-2026-04`). EU AI Act
209
+ compliance requires a minimum 2-year key retention period.
210
+
211
+ ---
212
+
213
+ ## EU AI Act Compliance Flags
214
+
215
+ Compliance flags are **derived** from verification outcomes — never read from stored
216
+ fields (invariant CI-5). Altering the `regulatoryContext` block in a signed record
217
+ invalidates the Ed25519 signature.
218
+
219
+ | Flag | Derived From |
220
+ |------|-------------|
221
+ | `article12_tamper_resistant` | `hash_valid AND chain_valid AND signature_valid == VERIFIED` |
222
+ | `article14_human_oversight` | `signature_present` (actor fields cryptographically bound) |
223
+ | `article28_provider_obligations` | `signature_valid == VERIFIED` (evidence from known key) |
224
+
225
+ ---
226
+
227
+ ## Canonical Payload Schema
228
+
229
+ The 13-field locked-order payload that is signed and hashed:
230
+
231
+ ```
232
+ schemaVersion (always "1")
233
+ evidenceId
234
+ evidenceHash
235
+ proofChainHash
236
+ capabilityId
237
+ action
238
+ actorId
239
+ actorRole
240
+ createdAt
241
+ signingKeyId
242
+ environment
243
+ tenantId
244
+ regulatoryContext { complianceMode, euAiActArticle12, euAiActArticle14, euAiActArticle28 }
245
+ ```
246
+
247
+ **Warning:** Reordering these fields invalidates all existing signatures. This schema
248
+ is locked and versioned. See `tests/test_payload.py::test_golden_vector` for the
249
+ canonical serialization canary.
250
+
251
+ ---
252
+
253
+ ## JWKS Endpoint
254
+
255
+ Public keys are served at:
256
+
257
+ ```
258
+ GET https://strixgov.com/.well-known/strix-jwks.json
259
+ GET https://strixgov.com/.well-known/strix-jwks.json?kid=strix-prod-2026-04
260
+ ```
261
+
262
+ The endpoint follows RFC 7517. Each key is an OKP/Ed25519 JWK with a `kid` in
263
+ `strix-{env}-{YYYY-MM}` format.
264
+
265
+ ---
266
+
267
+ ## Development
268
+
269
+ ```bash
270
+ # Install with dev dependencies
271
+ pip install -e ".[dev]"
272
+
273
+ # Run tests
274
+ pytest
275
+
276
+ # Run tests with coverage
277
+ pytest --cov=strix_verify --cov-report=term-missing
278
+
279
+ # Lint
280
+ ruff check src/ tests/
281
+
282
+ # Type check
283
+ mypy src/
284
+ ```
285
+
286
+ ---
287
+
288
+ ## Architecture Notes
289
+
290
+ - **No Strix SDK dependency** — pure Python + `cryptography` + `httpx`
291
+ - **Ed25519 SPKI DER construction** matches the TypeScript implementation exactly:
292
+ 12-byte header `302a300506032b6570032100` + 32 raw key bytes
293
+ - **Never raises** on bad signatures — `verify_signature()` returns `False`
294
+ - **Sync and async** HTTP clients available for all network operations
295
+
296
+ ---
297
+
298
+ ## License
299
+
300
+ MIT — see LICENSE.