psi-verifier 1.3.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,26 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 APEX Intelligence Empire / ROCKYFILMS888 PTY LTD (ABN 71 672 237 795), Victoria, Australia.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ NOTE ON SCOPE: this MIT grant covers the VERIFIER software only. The
24
+ PSI-SEAL/1 canonical seal schema is a separately copyrighted technical
25
+ specification, and the APEX PSI Sealing Engine (generator) is licensed
26
+ separately. See LICENSE-ENGINE.txt.
@@ -0,0 +1,66 @@
1
+ Metadata-Version: 2.4
2
+ Name: psi-verifier
3
+ Version: 1.3.0
4
+ Summary: MIT-licensed verifier for the PSI-SEAL/1 canonical seal schema. Verification is free forever; enforcement is on by default, and since 1.3.0 rule R11 (Ed25519 over the ASCII seal_hash) is actually checked against caller-supplied public keys.
5
+ Author: APEX Infrastructure
6
+ License: MIT
7
+ Project-URL: Homepage, https://ai-governance-standard.com/license
8
+ Project-URL: Specification, https://ai-governance-standard.com/spec
9
+ Project-URL: Schema, https://ai-governance-standard.com/.well-known/psi-schema.json
10
+ Keywords: psi,apex,verification,ai-governance,eu-ai-act,sha256,merkle
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Security :: Cryptography
14
+ Classifier: Intended Audience :: Developers
15
+ Requires-Python: >=3.8
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Provides-Extra: signature
19
+ Requires-Dist: cryptography>=41; extra == "signature"
20
+ Dynamic: license-file
21
+
22
+ # psi-verifier (Python)
23
+
24
+ > Apex PSI is not a tool and not a product. It is the verification substrate — the layer beneath proof. It does not judge. It does not certify. It only remembers.
25
+
26
+ **MIT. Free forever.** Verify any APEX PSI seal. Zero dependencies, Python 3.8+.
27
+
28
+ ```python
29
+ from psi_verifier import verify, PsiSealInvalidError
30
+
31
+ try:
32
+ result = verify(seal) # enforce=True by default (v1.2.0)
33
+ except PsiSealInvalidError as e:
34
+ print(e) # tells you exactly where to get a valid seal
35
+ ```
36
+
37
+ ## v1.2.0 — enforcement release with cross-language parity
38
+
39
+ `verify()` now **rejects** non-conformant seals instead of merely reporting them.
40
+ Invalid input raises `PsiSealInvalidError` with the canonical schema digest and a
41
+ fix URL. Report-only inspection remains available:
42
+
43
+ ```python
44
+ from psi_verifier import verify_seal, is_conformant
45
+
46
+ verify(seal, enforce=False) # legacy data only
47
+ verify_seal(seal) # ConformanceResult, never raises
48
+ is_conformant(seal) # bool
49
+ ```
50
+
51
+ ## What it checks
52
+
53
+ The PSI-SEAL/1 canonical schema, byte for byte: closed field set (R2/R3),
54
+ 64-char lowercase hex SHA-256 (R4/R5), RFC 3339 UTC with exactly three
55
+ fractional digits (R6), exact octet length (R7), domain-separated leaf
56
+ `SHA-256("PSI1:" + hash)` and promoted-odd Merkle assembly (R8/R9),
57
+ `seal_hash = SHA-256(JCS(core envelope))` (R10), and matching `schema_digest` (R12).
58
+
59
+ ## Licensing
60
+
61
+ - **This verifier: MIT.** Check seals forever, no cost, no permission.
62
+ - **The generator (sealing engine): proprietary.** Free for personal /
63
+ non-commercial use; commercial sealing is licensed under PSI-05 royalty terms.
64
+ Generate a conformant seal at <https://ai-governance-standard.com/seal>.
65
+
66
+ Schema: © 2026 APEX Infrastructure. Specification: <https://ai-governance-standard.com/spec>
@@ -0,0 +1,45 @@
1
+ # psi-verifier (Python)
2
+
3
+ > Apex PSI is not a tool and not a product. It is the verification substrate — the layer beneath proof. It does not judge. It does not certify. It only remembers.
4
+
5
+ **MIT. Free forever.** Verify any APEX PSI seal. Zero dependencies, Python 3.8+.
6
+
7
+ ```python
8
+ from psi_verifier import verify, PsiSealInvalidError
9
+
10
+ try:
11
+ result = verify(seal) # enforce=True by default (v1.2.0)
12
+ except PsiSealInvalidError as e:
13
+ print(e) # tells you exactly where to get a valid seal
14
+ ```
15
+
16
+ ## v1.2.0 — enforcement release with cross-language parity
17
+
18
+ `verify()` now **rejects** non-conformant seals instead of merely reporting them.
19
+ Invalid input raises `PsiSealInvalidError` with the canonical schema digest and a
20
+ fix URL. Report-only inspection remains available:
21
+
22
+ ```python
23
+ from psi_verifier import verify_seal, is_conformant
24
+
25
+ verify(seal, enforce=False) # legacy data only
26
+ verify_seal(seal) # ConformanceResult, never raises
27
+ is_conformant(seal) # bool
28
+ ```
29
+
30
+ ## What it checks
31
+
32
+ The PSI-SEAL/1 canonical schema, byte for byte: closed field set (R2/R3),
33
+ 64-char lowercase hex SHA-256 (R4/R5), RFC 3339 UTC with exactly three
34
+ fractional digits (R6), exact octet length (R7), domain-separated leaf
35
+ `SHA-256("PSI1:" + hash)` and promoted-odd Merkle assembly (R8/R9),
36
+ `seal_hash = SHA-256(JCS(core envelope))` (R10), and matching `schema_digest` (R12).
37
+
38
+ ## Licensing
39
+
40
+ - **This verifier: MIT.** Check seals forever, no cost, no permission.
41
+ - **The generator (sealing engine): proprietary.** Free for personal /
42
+ non-commercial use; commercial sealing is licensed under PSI-05 royalty terms.
43
+ Generate a conformant seal at <https://ai-governance-standard.com/seal>.
44
+
45
+ Schema: © 2026 APEX Infrastructure. Specification: <https://ai-governance-standard.com/spec>
@@ -0,0 +1,601 @@
1
+ """psi-verifier — MIT licensed. Free forever.
2
+
3
+ Verifies that a PSI seal conforms, byte for byte, to the copyrighted
4
+ PSI-SEAL/1 canonical schema. Verification is open to everyone; producing
5
+ conformant seals requires the licensed APEX PSI Sealing Engine.
6
+
7
+ Zero dependencies. Python 3.8+.
8
+
9
+ v1.2.0 — PARITY RELEASE: ``verify()`` rejects non-conformant seals by default
10
+ (``enforce=True``) and every rejection line is byte-identical to the
11
+ TypeScript distribution, carrying a readable rule citation.
12
+
13
+ v1.3.0 — AUTHENTICITY RELEASE: R11 is now actually checked. A seal's Ed25519
14
+ signature is verified over the ASCII ``seal_hash`` against public keys the
15
+ CALLER supplies, and a seal whose signature was never checked can no longer
16
+ report a pass. ``conformant`` still means "the bytes match the schema";
17
+ ``authentic`` is the new, separate answer to "did a trusted key sign this".
18
+ Rejection and acceptance lines stay byte-identical to the TypeScript build.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import base64
24
+ import hashlib
25
+ import json
26
+ import re
27
+ from dataclasses import dataclass, field
28
+ from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
29
+
30
+ __version__ = "1.3.0"
31
+ PSI_VERIFIER_VERSION = __version__
32
+
33
+ PSI_SCHEMA_ID = "PSI-SEAL/1.0.0"
34
+ PSI_SEAL_URL = "https://ai-governance-standard.com/seal"
35
+ PSI_SCHEMA_URL = "https://ai-governance-standard.com/.well-known/psi-schema.json"
36
+
37
+ PSI_SCHEMA_RULES: Sequence[str] = (
38
+ "R1 Envelope serialisation: RFC 8785 JSON Canonicalization Scheme (JCS), UTF-8, no BOM.",
39
+ "R2 Field set is closed. Unknown top-level fields render a seal non-conformant.",
40
+ "R3 Field order in the emitted receipt: schema, schema_digest, sealed_at, subject, hash, merkle, signature, licence.",
41
+ "R4 Digests: lowercase hexadecimal, exactly 64 characters, no 'sha256:' prefix inside the envelope.",
42
+ "R5 Hash algorithm: SHA-256 over the raw octet stream of the subject; no transport encoding, no trailing padding.",
43
+ "R6 sealed_at: RFC 3339 UTC with exactly three fractional digits and a literal 'Z' (e.g. 2026-08-17T09:00:00.000Z).",
44
+ "R7 subject.size_bytes: non-negative integer, exact octet length. subject.name: NFC-normalised UTF-8 string.",
45
+ "R8 Merkle assembly: binary tree over leaf digests in submission order; each parent = SHA-256(left_bytes || right_bytes) over 32-byte raw digests; an odd node is promoted, never duplicated.",
46
+ "R9 merkle.leaf = SHA-256 of the ASCII string 'PSI1:' || hash. Domain separation is mandatory.",
47
+ "R10 seal_hash = SHA-256(JCS(envelope minus the signature and licence members)).",
48
+ "R11 Signature suite: Ed25519 over the ASCII seal_hash; optional hybrid post-quantum LMS-W4-SHA256 (NIST SP 800-208).",
49
+ "R12 Every seal MUST carry schema and schema_digest. Only schema-conformant seals are considered PSI-compliant.",
50
+ )
51
+
52
+ PSI_RULE_CITATIONS = {
53
+ "not_object": "1.1",
54
+ "closed_field_set": "2.1",
55
+ "schema_identifier": "4.1",
56
+ "schema_digest": "4.2",
57
+ "hash_encoding": "5.1",
58
+ "sealed_at": "6.1",
59
+ "size_bytes": "7.1",
60
+ "leaf_missing": "8.1",
61
+ "leaf_mismatch": "9.1",
62
+ "seal_hash": "10.1",
63
+ "signature_absent": "11.1",
64
+ "signature_empty": "11.2",
65
+ "signature_invalid": "11.3",
66
+ "signature_unverifiable": "11.4",
67
+ "signature_alg": "11.5",
68
+ }
69
+
70
+
71
+ def reject_line(detail: str, rule: str) -> str:
72
+ """Canonical single-line rejection. Byte-identical to the TypeScript build."""
73
+ return f"PSI-SEAL v{PSI_VERIFIER_VERSION} REJECT: {detail} \u2014 rule {rule}"
74
+
75
+
76
+ _HEX64 = re.compile(r"^[0-9a-f]{64}$")
77
+ _RFC3339_MS = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$")
78
+ _ALLOWED = (
79
+ "schema",
80
+ "schema_digest",
81
+ "sealed_at",
82
+ "subject",
83
+ "hash",
84
+ "merkle",
85
+ "signature",
86
+ "licence",
87
+ )
88
+
89
+
90
+ def jcs(value: Any) -> str:
91
+ """RFC 8785 (JCS) serialisation, minimal form matching the TS verifier."""
92
+ if value is None or isinstance(value, (bool, int, float, str)):
93
+ return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
94
+ if isinstance(value, (list, tuple)):
95
+ return "[" + ",".join(jcs(v) for v in value) + "]"
96
+ if isinstance(value, dict):
97
+ keys = sorted((k for k, v in value.items() if v is not None), key=lambda k: k)
98
+ return "{" + ",".join(f"{json.dumps(k, ensure_ascii=False)}:{jcs(value[k])}" for k in keys) + "}"
99
+ raise TypeError("JCS: unsupported value")
100
+
101
+
102
+ def _sha256_hex(data: Any) -> str:
103
+ if isinstance(data, str):
104
+ data = data.encode("utf-8")
105
+ return hashlib.sha256(data).hexdigest()
106
+
107
+
108
+ def psi_schema_digest() -> str:
109
+ return _sha256_hex(jcs({"id": PSI_SCHEMA_ID, "rules": list(PSI_SCHEMA_RULES)}))
110
+
111
+
112
+ def psi_schema_digest_from_document(doc: Any) -> str:
113
+ """Digest a published schema document (e.g. /.well-known/psi-schema.json)
114
+ using this verifier's own canonicalisation. Byte-identical to the
115
+ TypeScript distribution's psiSchemaDigestFromDocument()."""
116
+ if not isinstance(doc, dict) or not isinstance(doc.get("schema"), str) or not isinstance(doc.get("rules"), list):
117
+ raise ValueError("PSI schema document must carry a string `schema` and a list of `rules`")
118
+ return _sha256_hex(jcs({"id": doc["schema"], "rules": list(doc["rules"])}))
119
+
120
+
121
+ def psi_leaf(hash_hex: str) -> str:
122
+ clean = re.sub(r"^sha256:", "", hash_hex, flags=re.I).lower()
123
+ return _sha256_hex("PSI1:" + clean)
124
+
125
+
126
+ def psi_seal_hash(envelope: Dict[str, Any]) -> str:
127
+ core = {k: v for k, v in envelope.items() if k not in ("signature", "licence")}
128
+ return _sha256_hex(jcs(core))
129
+
130
+
131
+ # ── R11 AUTHENTICITY (added v1.3.0; mirrors the TypeScript distribution) ──
132
+ # A conformant envelope is not an authenticated one. Ed25519 over the ASCII
133
+ # `seal_hash`, checked only against public keys the CALLER chose to trust.
134
+ # This module never invents trust and never reports a pass it did not compute.
135
+
136
+ _SIG_HEX = re.compile(r"^[0-9a-fA-F]{128}$")
137
+
138
+
139
+ def _decode_material(value: str, byte_length: int) -> Optional[bytes]:
140
+ """Accepts hex or standard / URL-safe base64, and requires the exact byte length."""
141
+ text = str(value or "").strip()
142
+ if len(text) == byte_length * 2 and re.fullmatch(r"[0-9a-fA-F]+", text):
143
+ return bytes.fromhex(text)
144
+ candidate = text.replace("-", "+").replace("_", "/")
145
+ candidate += "=" * (-len(candidate) % 4)
146
+ if not re.fullmatch(r"[A-Za-z0-9+/]*={0,2}", candidate):
147
+ return None
148
+ try:
149
+ raw = base64.b64decode(candidate, validate=True)
150
+ except (ValueError, TypeError):
151
+ return None
152
+ return raw if len(raw) == byte_length else None
153
+
154
+
155
+ def psi_key_fingerprint(public_key: str) -> str:
156
+ """Short readable label for a key: first 10 hex of SHA-256 over the raw key bytes."""
157
+ raw = _decode_material(public_key, 32)
158
+ if raw is None:
159
+ raise ValueError("PSI R11: public key must be 32 bytes, as 64 hex characters or base64")
160
+ return hashlib.sha256(raw).hexdigest()[:10]
161
+
162
+
163
+ def _ed25519_verifier() -> Optional[Any]:
164
+ """Return ``cryptography``'s Ed25519 public-key class, or None if it is not installed."""
165
+ try:
166
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
167
+
168
+ return Ed25519PublicKey
169
+ except Exception:
170
+ return None
171
+
172
+
173
+ def psi_verify_signature(
174
+ seal_hash: str,
175
+ signature: Optional[Dict[str, Any]],
176
+ trust_roots: Sequence[Union[str, Dict[str, Any]]] = (),
177
+ ) -> Dict[str, Any]:
178
+ """Check R11. Returns ``unverifiable`` — never ``verified`` — when there is no
179
+ trust root or this platform has no Ed25519 primitive. ``invalid`` is reserved
180
+ for a check that actually ran and said no."""
181
+ if not isinstance(signature, dict):
182
+ return {
183
+ "status": "absent",
184
+ "key_id": None,
185
+ "detail": "seal carries no signature member",
186
+ "rule": PSI_RULE_CITATIONS["signature_absent"],
187
+ }
188
+ alg = str(signature.get("alg") or "").lower()
189
+ if alg != "ed25519":
190
+ return {
191
+ "status": "invalid",
192
+ "key_id": None,
193
+ "detail": f'signature.alg is "{signature.get("alg") or ""}"; R11 requires Ed25519, so nothing here can be checked',
194
+ "rule": PSI_RULE_CITATIONS["signature_alg"],
195
+ }
196
+ value = str(signature.get("value") or "").strip()
197
+ if not value:
198
+ return {
199
+ "status": "invalid",
200
+ "key_id": None,
201
+ "detail": "signature.value is empty \u2014 no key signed this seal",
202
+ "rule": PSI_RULE_CITATIONS["signature_empty"],
203
+ }
204
+ sig = _decode_material(value, 64)
205
+ if sig is None:
206
+ return {
207
+ "status": "invalid",
208
+ "key_id": None,
209
+ "detail": "signature.value is neither 128 hex characters nor 64 base64 bytes, so it is not an Ed25519 signature",
210
+ "rule": PSI_RULE_CITATIONS["signature_invalid"],
211
+ }
212
+ roots = [{"public_key": r} if isinstance(r, str) else dict(r) for r in trust_roots]
213
+ if not roots:
214
+ return {
215
+ "status": "unverifiable",
216
+ "key_id": None,
217
+ "detail": "no trust root supplied: a signature nobody's public key was checked against proves nothing",
218
+ "rule": PSI_RULE_CITATIONS["signature_unverifiable"],
219
+ }
220
+ message = str(seal_hash).encode("utf-8")
221
+ ed25519 = _ed25519_verifier()
222
+ if ed25519 is not None:
223
+ from cryptography.exceptions import InvalidSignature
224
+
225
+ def _check(sig_bytes: bytes, msg: bytes, raw_key: bytes) -> bool:
226
+ try:
227
+ ed25519.from_public_bytes(raw_key).verify(sig_bytes, msg)
228
+ return True
229
+ except InvalidSignature:
230
+ return False
231
+ else:
232
+ # No `cryptography` wheel on this machine. Instead of refusing every
233
+ # signature, fall back to the dependency-free RFC 8032 implementation
234
+ # kept from the 1.3.0 build the website builder pushed on 17 Oct 2026 —
235
+ # slower, but a regulator with plain Python can still check a seal.
236
+ def _check(sig_bytes: bytes, msg: bytes, raw_key: bytes) -> bool:
237
+ try:
238
+ return verify_ed25519(msg, sig_bytes.hex(), raw_key.hex())
239
+ except Exception:
240
+ return False
241
+
242
+ any_key_usable = False
243
+ for root in roots:
244
+ raw = _decode_material(root.get("public_key") or "", 32)
245
+ if raw is None:
246
+ continue
247
+ any_key_usable = True
248
+ if not _check(sig, message, raw):
249
+ continue
250
+ label = root.get("key_id") or f"ed25519:{raw.hex()[:10]}"
251
+ return {
252
+ "status": "verified",
253
+ "key_id": label,
254
+ "detail": "Ed25519 signature verified over the ASCII seal_hash",
255
+ "rule": "11",
256
+ }
257
+ if not any_key_usable:
258
+ return {
259
+ "status": "unverifiable",
260
+ "key_id": None,
261
+ "detail": "this platform exposes no Ed25519 verifier, so the signature was refused rather than assumed",
262
+ "rule": PSI_RULE_CITATIONS["signature_unverifiable"],
263
+ }
264
+ return {
265
+ "status": "invalid",
266
+ "key_id": None,
267
+ "detail": f"Ed25519 signature does not verify against any of the {len(roots)} supplied trust root(s)",
268
+ "rule": PSI_RULE_CITATIONS["signature_invalid"],
269
+ }
270
+
271
+
272
+ @dataclass
273
+ class ConformanceResult:
274
+ conformant: bool
275
+ schema_id: Optional[str]
276
+ schema_digest_match: bool
277
+ seal_hash_match: bool
278
+ signature_valid: bool = False
279
+ failures: List[str] = field(default_factory=list)
280
+ signature_status: str = "absent"
281
+ authentic: bool = False
282
+ trusted_key_id: Optional[str] = None
283
+
284
+
285
+ class PsiSealInvalidError(Exception):
286
+ """Raised by ``verify()`` when enforcement is on (the default)."""
287
+
288
+ code = "PSI_SEAL_INVALID"
289
+
290
+ def __init__(self, result: ConformanceResult, canonical_digest: str) -> None:
291
+ self.result = result
292
+ self.canonical_digest = canonical_digest
293
+ super().__init__(format_rejection(result, canonical_digest))
294
+
295
+
296
+ def format_rejection(result: ConformanceResult, canonical_digest: str) -> str:
297
+ lines = [
298
+ reject_line(f"seal is not conformant to {PSI_SCHEMA_ID}", PSI_RULE_CITATIONS["not_object"]),
299
+ f"canonical schema digest: {canonical_digest}",
300
+ f"received schema: {result.schema_id or '(none)'} (digest match: {'true' if result.schema_digest_match else 'false'})",
301
+ "findings:",
302
+ ]
303
+ lines += [f" {f}" for f in result.failures]
304
+ lines += [
305
+ f"generate a conformant seal: {PSI_SEAL_URL}",
306
+ f"canonical schema: {PSI_SCHEMA_URL}",
307
+ "legacy escape hatch (report-only): enforce=false",
308
+ "verified, not asserted; trust the math, not the maker.",
309
+ ]
310
+ return "\n".join(lines)
311
+
312
+
313
+ def format_acceptance(signature_status: str = "verified", key_id: Optional[str] = None) -> str:
314
+ """Canonical acceptance line. Byte-identical to the TypeScript distribution.
315
+
316
+ Since v1.3.0 it cannot print a pass for a seal whose signature was never
317
+ checked: only ``verified`` produces ACCEPT.
318
+ """
319
+ if signature_status == "verified":
320
+ suffix = f" against {key_id}" if key_id else ""
321
+ return f"PSI-SEAL v{PSI_VERIFIER_VERSION} ACCEPT: conformant seal; Ed25519 signature verified{suffix}"
322
+ return (
323
+ f"PSI-SEAL v{PSI_VERIFIER_VERSION} PARTIAL: conformant to the schema, "
324
+ f"authenticity {signature_status} \u2014 a seal no key has signed is a formatting check, "
325
+ "not a verification"
326
+ )
327
+
328
+
329
+ # ── Ed25519 verification, RFC 8032 (pure Python, zero dependencies) ──
330
+ # Kept from the 1.3.0 the website builder pushed to GitHub on 17 Oct 2026,
331
+ # because it needs no third-party crypto library: a regulator with a plain
332
+ # Python install can check a real signature. In this merge it becomes the
333
+ # fallback primitive under psi_verify_signature, used when `cryptography` is not
334
+ # installed. The published key below is EXPORTED for callers who choose to
335
+ # trust it — it is not trusted by default, so the verifier never silently
336
+ # grades its own maker's homework.
337
+ PSI_PUBLIC_KEY_HEX = "59304685328b3cfa6ec712d66250d0f964bb9f92161e65e2e5835a873f104724"
338
+
339
+ # ── Ed25519 verification, RFC 8032 (pure Python, zero dependencies) ──
340
+ _P = 2**255 - 19
341
+ _L = 2**252 + 27742317777372353535851937790883648493
342
+ _D = -121665 * pow(121666, _P - 2, _P) % _P
343
+ _I = pow(2, (_P - 1) // 4, _P)
344
+
345
+
346
+ def _xrecover(y: int, sign: int) -> Optional[int]:
347
+ xx = (y * y - 1) * pow(_D * y * y + 1, _P - 2, _P) % _P
348
+ x = pow(xx, (_P + 3) // 8, _P)
349
+ if (x * x - xx) % _P:
350
+ x = x * _I % _P
351
+ if (x * x - xx) % _P:
352
+ return None
353
+ if x == 0 and sign:
354
+ return None
355
+ if x & 1 != sign:
356
+ x = _P - x
357
+ return x
358
+
359
+
360
+ def _padd(a, b):
361
+ x1, y1, z1, t1 = a
362
+ x2, y2, z2, t2 = b
363
+ A = (y1 - x1) * (y2 - x2) % _P
364
+ B = (y1 + x1) * (y2 + x2) % _P
365
+ C = 2 * t1 * t2 * _D % _P
366
+ Dd = 2 * z1 * z2 % _P
367
+ E, F, G, H = B - A, Dd - C, Dd + C, B + A
368
+ return (E * F % _P, G * H % _P, F * G % _P, E * H % _P)
369
+
370
+
371
+ def _pmul(s: int, p):
372
+ q = (0, 1, 1, 0)
373
+ while s:
374
+ if s & 1:
375
+ q = _padd(q, p)
376
+ p = _padd(p, p)
377
+ s >>= 1
378
+ return q
379
+
380
+
381
+ def _pdecode(b: bytes):
382
+ if len(b) != 32:
383
+ return None
384
+ y = int.from_bytes(b, "little")
385
+ sign = y >> 255
386
+ y &= (1 << 255) - 1
387
+ if y >= _P:
388
+ return None
389
+ x = _xrecover(y, sign)
390
+ if x is None:
391
+ return None
392
+ return (x, y, 1, x * y % _P)
393
+
394
+
395
+ def _pequal(a, b) -> bool:
396
+ return (a[0] * b[2] - b[0] * a[2]) % _P == 0 and (a[1] * b[2] - b[1] * a[2]) % _P == 0
397
+
398
+
399
+ _GY = 4 * pow(5, _P - 2, _P) % _P
400
+ _GX = _xrecover(_GY, 0)
401
+ _G = (_GX, _GY, 1, _GX * _GY % _P)
402
+
403
+
404
+ def verify_ed25519(message: bytes, signature_hex: str, public_key_hex: str) -> bool:
405
+ """RFC 8032 Ed25519 verification. Never raises."""
406
+ try:
407
+ sig = bytes.fromhex(signature_hex or "")
408
+ pub = bytes.fromhex(public_key_hex or "")
409
+ except ValueError:
410
+ return False
411
+ if len(sig) != 64 or len(pub) != 32:
412
+ return False
413
+ A = _pdecode(pub)
414
+ R = _pdecode(sig[:32])
415
+ if A is None or R is None:
416
+ return False
417
+ s = int.from_bytes(sig[32:], "little")
418
+ if s >= _L:
419
+ return False
420
+ k = int.from_bytes(hashlib.sha512(sig[:32] + pub + message).digest(), "little") % _L
421
+ return _pequal(_pmul(s, _G), _padd(R, _pmul(k, A)))
422
+
423
+
424
+ def verify_seal(
425
+ seal: Any,
426
+ trust_roots: Union[str, Sequence[Union[str, Dict[str, Any]]], None] = None,
427
+ require_valid_signature: bool = True,
428
+ public_key_hex: Optional[str] = None,
429
+ ) -> ConformanceResult:
430
+ """Conformance check plus the R11 authenticity verdict. Never raises.
431
+
432
+ Back-compat with v1.2.x, which took one hex key here: `verify_seal(seal,
433
+ "5930…")` positionally, or `public_key_hex="5930…"`. Both spellings are read
434
+ as one trusted key. Nothing is trusted by default — call it with no key and
435
+ a signed seal comes back `unverifiable`, never as a pass.
436
+ """
437
+ roots: List[Union[str, Dict[str, Any]]] = []
438
+ if isinstance(trust_roots, str):
439
+ roots.append(trust_roots)
440
+ elif trust_roots:
441
+ roots.extend(trust_roots)
442
+ if public_key_hex:
443
+ roots.append(public_key_hex)
444
+ if not isinstance(seal, dict):
445
+ return ConformanceResult(
446
+ False, None, False, False,
447
+ [reject_line("input is not a JSON object", PSI_RULE_CITATIONS["not_object"])],
448
+ )
449
+
450
+ failures: List[str] = []
451
+ expected = psi_schema_digest()
452
+
453
+ if seal.get("schema") != PSI_SCHEMA_ID:
454
+ failures.append(
455
+ reject_line(
456
+ f"schema identifier mismatch, expected {PSI_SCHEMA_ID}",
457
+ PSI_RULE_CITATIONS["schema_identifier"],
458
+ )
459
+ )
460
+ digest_match = seal.get("schema_digest") == expected
461
+ if not digest_match:
462
+ failures.append(reject_line("schema_digest mismatch", PSI_RULE_CITATIONS["schema_digest"]))
463
+
464
+ extra = [k for k in seal if k not in _ALLOWED]
465
+ if extra:
466
+ failures.append(
467
+ reject_line("unknown top-level fields: " + ", ".join(extra), PSI_RULE_CITATIONS["closed_field_set"])
468
+ )
469
+ if not _RFC3339_MS.match(str(seal.get("sealed_at") or "")):
470
+ failures.append(
471
+ reject_line(
472
+ "sealed_at is not RFC 3339 UTC with three fractional digits",
473
+ PSI_RULE_CITATIONS["sealed_at"],
474
+ )
475
+ )
476
+ hash_hex = str(seal.get("hash") or "")
477
+ if not _HEX64.match(hash_hex):
478
+ failures.append(
479
+ reject_line("hash is not 64 lowercase hexadecimal characters", PSI_RULE_CITATIONS["hash_encoding"])
480
+ )
481
+
482
+ subject = seal.get("subject")
483
+ size = subject.get("size_bytes") if isinstance(subject, dict) else None
484
+ if not isinstance(size, int) or isinstance(size, bool) or size < 0:
485
+ failures.append(
486
+ reject_line("subject.size_bytes is not a non-negative integer", PSI_RULE_CITATIONS["size_bytes"])
487
+ )
488
+
489
+ seal_hash_match = False
490
+ computed_seal_hash = ""
491
+ signature = seal.get("signature") if isinstance(seal.get("signature"), dict) else None
492
+ merkle = seal.get("merkle") if isinstance(seal.get("merkle"), dict) else None
493
+ if _HEX64.match(hash_hex) and merkle and merkle.get("leaf"):
494
+ if psi_leaf(hash_hex) != merkle["leaf"]:
495
+ failures.append(reject_line("merkle.leaf mismatch", PSI_RULE_CITATIONS["leaf_mismatch"]))
496
+ if signature and signature.get("seal_hash"):
497
+ computed_seal_hash = psi_seal_hash(seal)
498
+ seal_hash_match = computed_seal_hash == signature["seal_hash"]
499
+ if not seal_hash_match:
500
+ failures.append(reject_line("seal_hash mismatch", PSI_RULE_CITATIONS["seal_hash"]))
501
+ # v1.2.x put a second, coarser signature check here (one caller key, one
502
+ # boolean, rule numbers 11.1/11.2 that clashed with the published rule
503
+ # set). Removed in the 1.3.0 merge: running both emitted two rejection
504
+ # lines for one defect, and the TypeScript and Python distributions then
505
+ # stopped printing byte-identical verdicts. The single authority for R11
506
+ # is psi_verify_signature below, shared with the TypeScript package.
507
+ else:
508
+ failures.append(reject_line("merkle.leaf missing", PSI_RULE_CITATIONS["leaf_missing"]))
509
+
510
+ # R11 — authenticity, checked against the hash THIS verifier recomputed and
511
+ # only against keys the caller supplied.
512
+ check = psi_verify_signature(computed_seal_hash, signature, roots)
513
+ if check["status"] == "invalid" or (require_valid_signature and check["status"] != "verified"):
514
+ failures.append(reject_line(check["detail"], check["rule"]))
515
+
516
+ schema_id = seal.get("schema")
517
+ return ConformanceResult(
518
+ conformant=not failures,
519
+ schema_id=schema_id if isinstance(schema_id, str) else None,
520
+ schema_digest_match=digest_match,
521
+ seal_hash_match=seal_hash_match,
522
+ # Back-compat for the v1.2.x boolean: true only when the signature was
523
+ # actually checked and passed. Identical to `authentic`.
524
+ signature_valid=check["status"] == "verified",
525
+ failures=failures,
526
+ signature_status=check["status"],
527
+ authentic=check["status"] == "verified",
528
+ trusted_key_id=check["key_id"],
529
+ )
530
+
531
+
532
+ def verify(
533
+ seal: Any,
534
+ enforce: bool = True,
535
+ trust_roots: Union[str, Sequence[Union[str, Dict[str, Any]]], None] = None,
536
+ require_valid_signature: bool = True,
537
+ public_key_hex: Optional[str] = None,
538
+ ) -> ConformanceResult:
539
+ """Primary entry point. Enforced by default (since v1.1.0).
540
+
541
+ Raises ``PsiSealInvalidError`` when the seal is not PSI-SEAL/1 conformant.
542
+ From v1.3.0 an unauthenticated seal is not conformant unless
543
+ ``require_valid_signature=False`` is passed explicitly.
544
+ """
545
+ result = verify_seal(seal, trust_roots, require_valid_signature, public_key_hex)
546
+ if not result.conformant and enforce:
547
+ raise PsiSealInvalidError(result, psi_schema_digest())
548
+ return result
549
+
550
+
551
+ def is_conformant(
552
+ seal: Any,
553
+ trust_roots: Sequence[Union[str, Dict[str, Any]]] = (),
554
+ require_valid_signature: bool = True,
555
+ ) -> bool:
556
+ return verify_seal(seal, trust_roots, require_valid_signature).conformant
557
+
558
+
559
+ def merkle_root(leaves: Sequence[str]) -> str:
560
+ """Canonical Merkle root (R8) — promoted odd node, raw 32-byte concatenation."""
561
+ if not leaves:
562
+ raise ValueError("R8: at least one leaf required")
563
+ level = [bytes.fromhex(leaf.lower()) for leaf in leaves]
564
+ while len(level) > 1:
565
+ nxt = []
566
+ for i in range(0, len(level), 2):
567
+ if i + 1 == len(level):
568
+ nxt.append(level[i])
569
+ else:
570
+ nxt.append(hashlib.sha256(level[i] + level[i + 1]).digest())
571
+ level = nxt
572
+ return level[0].hex()
573
+
574
+
575
+ __all__ = [
576
+ "PSI_SCHEMA_ID",
577
+ "PSI_RULE_CITATIONS",
578
+ "PSI_VERIFIER_VERSION",
579
+ "reject_line",
580
+ "PSI_SCHEMA_RULES",
581
+ "PSI_SEAL_URL",
582
+ "PSI_SCHEMA_URL",
583
+ "ConformanceResult",
584
+ "PsiSealInvalidError",
585
+ "format_acceptance",
586
+ "format_rejection",
587
+ "is_conformant",
588
+ "jcs",
589
+ "merkle_root",
590
+ "psi_key_fingerprint",
591
+ "psi_leaf",
592
+ "psi_schema_digest",
593
+ "psi_schema_digest_from_document",
594
+ "psi_seal_hash",
595
+ "psi_verify_signature",
596
+ "verify_ed25519",
597
+ "PSI_PUBLIC_KEY_HEX",
598
+ "verify",
599
+ "verify_seal",
600
+ "__version__",
601
+ ]
@@ -0,0 +1,66 @@
1
+ Metadata-Version: 2.4
2
+ Name: psi-verifier
3
+ Version: 1.3.0
4
+ Summary: MIT-licensed verifier for the PSI-SEAL/1 canonical seal schema. Verification is free forever; enforcement is on by default, and since 1.3.0 rule R11 (Ed25519 over the ASCII seal_hash) is actually checked against caller-supplied public keys.
5
+ Author: APEX Infrastructure
6
+ License: MIT
7
+ Project-URL: Homepage, https://ai-governance-standard.com/license
8
+ Project-URL: Specification, https://ai-governance-standard.com/spec
9
+ Project-URL: Schema, https://ai-governance-standard.com/.well-known/psi-schema.json
10
+ Keywords: psi,apex,verification,ai-governance,eu-ai-act,sha256,merkle
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Security :: Cryptography
14
+ Classifier: Intended Audience :: Developers
15
+ Requires-Python: >=3.8
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Provides-Extra: signature
19
+ Requires-Dist: cryptography>=41; extra == "signature"
20
+ Dynamic: license-file
21
+
22
+ # psi-verifier (Python)
23
+
24
+ > Apex PSI is not a tool and not a product. It is the verification substrate — the layer beneath proof. It does not judge. It does not certify. It only remembers.
25
+
26
+ **MIT. Free forever.** Verify any APEX PSI seal. Zero dependencies, Python 3.8+.
27
+
28
+ ```python
29
+ from psi_verifier import verify, PsiSealInvalidError
30
+
31
+ try:
32
+ result = verify(seal) # enforce=True by default (v1.2.0)
33
+ except PsiSealInvalidError as e:
34
+ print(e) # tells you exactly where to get a valid seal
35
+ ```
36
+
37
+ ## v1.2.0 — enforcement release with cross-language parity
38
+
39
+ `verify()` now **rejects** non-conformant seals instead of merely reporting them.
40
+ Invalid input raises `PsiSealInvalidError` with the canonical schema digest and a
41
+ fix URL. Report-only inspection remains available:
42
+
43
+ ```python
44
+ from psi_verifier import verify_seal, is_conformant
45
+
46
+ verify(seal, enforce=False) # legacy data only
47
+ verify_seal(seal) # ConformanceResult, never raises
48
+ is_conformant(seal) # bool
49
+ ```
50
+
51
+ ## What it checks
52
+
53
+ The PSI-SEAL/1 canonical schema, byte for byte: closed field set (R2/R3),
54
+ 64-char lowercase hex SHA-256 (R4/R5), RFC 3339 UTC with exactly three
55
+ fractional digits (R6), exact octet length (R7), domain-separated leaf
56
+ `SHA-256("PSI1:" + hash)` and promoted-odd Merkle assembly (R8/R9),
57
+ `seal_hash = SHA-256(JCS(core envelope))` (R10), and matching `schema_digest` (R12).
58
+
59
+ ## Licensing
60
+
61
+ - **This verifier: MIT.** Check seals forever, no cost, no permission.
62
+ - **The generator (sealing engine): proprietary.** Free for personal /
63
+ non-commercial use; commercial sealing is licensed under PSI-05 royalty terms.
64
+ Generate a conformant seal at <https://ai-governance-standard.com/seal>.
65
+
66
+ Schema: © 2026 APEX Infrastructure. Specification: <https://ai-governance-standard.com/spec>
@@ -0,0 +1,9 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ psi_verifier/__init__.py
5
+ psi_verifier.egg-info/PKG-INFO
6
+ psi_verifier.egg-info/SOURCES.txt
7
+ psi_verifier.egg-info/dependency_links.txt
8
+ psi_verifier.egg-info/requires.txt
9
+ psi_verifier.egg-info/top_level.txt
@@ -0,0 +1,3 @@
1
+
2
+ [signature]
3
+ cryptography>=41
@@ -0,0 +1 @@
1
+ psi_verifier
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "psi-verifier"
7
+ version = "1.3.0"
8
+ description = "MIT-licensed verifier for the PSI-SEAL/1 canonical seal schema. Verification is free forever; enforcement is on by default, and since 1.3.0 rule R11 (Ed25519 over the ASCII seal_hash) is actually checked against caller-supplied public keys."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "APEX Infrastructure" }]
13
+ keywords = ["psi", "apex", "verification", "ai-governance", "eu-ai-act", "sha256", "merkle"]
14
+ classifiers = [
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3",
17
+ "Topic :: Security :: Cryptography",
18
+ "Intended Audience :: Developers",
19
+ ]
20
+ dependencies = []
21
+
22
+ [project.optional-dependencies]
23
+ # R11 authenticity checks need one Ed25519 primitive. Without it the verifier
24
+ # still installs and still runs — it reports `unverifiable` for every signature
25
+ # rather than assuming a pass. `pip install psi-verifier[signature]` to check.
26
+ signature = ["cryptography>=41"]
27
+
28
+ [project.urls]
29
+ Homepage = "https://ai-governance-standard.com/license"
30
+ Specification = "https://ai-governance-standard.com/spec"
31
+ Schema = "https://ai-governance-standard.com/.well-known/psi-schema.json"
32
+
33
+ [tool.setuptools]
34
+ packages = ["psi_verifier"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+