psi-verifier 1.3.0__py3-none-any.whl
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.
psi_verifier/__init__.py
ADDED
|
@@ -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,6 @@
|
|
|
1
|
+
psi_verifier/__init__.py,sha256=PErIQRrdh8hKssfT1kCiT7kYWXMRxDENHrpY9sHvof8,23322
|
|
2
|
+
psi_verifier-1.3.0.dist-info/licenses/LICENSE,sha256=ox5i7lVtOwvCo7QRTxi57gpK8dskLwdo2-FPOwxBK3Q,1396
|
|
3
|
+
psi_verifier-1.3.0.dist-info/METADATA,sha256=aVZCRVlIycJax3TLNmRZxuuAM_V_CAjsy3L_y1bAWDk,2835
|
|
4
|
+
psi_verifier-1.3.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
5
|
+
psi_verifier-1.3.0.dist-info/top_level.txt,sha256=qyoTzi1uy19MA3dcnLu9D89wwChyaHPOh4m6rTGkbpI,13
|
|
6
|
+
psi_verifier-1.3.0.dist-info/RECORD,,
|
|
@@ -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 @@
|
|
|
1
|
+
psi_verifier
|