xoonya 2.7.0 → 2.9.0
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.
- package/CROSS_RUNTIME_POSTURE.md +32 -2
- package/README.md +14 -1
- package/RELEASE_NOTES.md +26 -0
- package/SERVICE_CONTRACT.md +4 -0
- package/XNYA_CONFORMANCE_MARK.md +78 -0
- package/XNYA_REFERENCE_IMPLEMENTATIONS.md +157 -0
- package/XOONYA_NATIVE_PROOF_FORMAT.md +2 -0
- package/XOONYA_NATIVE_STANDARD_GOVERNANCE.md +4 -1
- package/bin/xoonya-release.js +19 -1
- package/bin/xoonya.js +1 -0
- package/lib/release_history.js +26 -0
- package/package.json +13 -4
- package/proof_sdk.js +605 -2
- package/reference/python/README.md +35 -0
- package/reference/python/requirements-optional.txt +1 -0
- package/reference/python/verify_vectors.py +209 -0
- package/reference/python/xnya_reference.py +810 -0
- package/reference/rust/Cargo.toml +11 -0
- package/reference/rust/README.md +28 -0
- package/reference/rust/src/lib.rs +87 -0
- package/scripts/verify_all.js +35 -0
- package/scripts/verify_external_proof_import_gate.js +84 -0
- package/scripts/verify_native_hybrid_design_gate.js +108 -0
- package/scripts/verify_reference_promotion_gate.js +74 -0
- package/scripts/verify_reference_python.js +49 -0
- package/scripts/verify_reference_python_crypto.js +87 -0
- package/scripts/verify_reference_rust_scaffold.js +50 -0
- package/scripts/verify_release_candidate.js +19 -0
- package/scripts/verify_service.js +24 -0
- package/scripts/verify_tooling.js +23 -0
- package/server.js +20 -0
- package/vectors/adversarial-vectors.json +27 -0
|
@@ -0,0 +1,810 @@
|
|
|
1
|
+
"""Independent Python candidate reference helpers for XNYA atom/payload vectors.
|
|
2
|
+
|
|
3
|
+
This module intentionally does not import or shell out to the JavaScript
|
|
4
|
+
implementation. It is a vector-parity scaffold, not yet a full
|
|
5
|
+
XNYA-compatible reference implementation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
import base64
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
XNYA_MAGIC = bytes.fromhex("584e5941")
|
|
18
|
+
VERSION_V1 = 0x01
|
|
19
|
+
VERSION_V2 = 0x02
|
|
20
|
+
VALID_RECORD_TYPES = {0x01, 0x02, 0x03}
|
|
21
|
+
ZERO_HASH32 = bytes(32)
|
|
22
|
+
IDENTITY_PREFIXES = {"01", "02", "03", "04", "05", "06", "07"}
|
|
23
|
+
|
|
24
|
+
XNYA_DS_AGENT_V1 = b"xoonya:agent:v1:"
|
|
25
|
+
XNYA_DS_AGENT_V2 = b"xoonya:agent:v2:"
|
|
26
|
+
XNYA_DS_PAYLOAD_V1 = b"xoonya:payload:v1:"
|
|
27
|
+
XNYA_DS_PAYLOAD_V2 = b"xoonya:payload:v2:"
|
|
28
|
+
XNYA_DS_PAYLOAD_PROFILE_V1 = b"xoonya:payload-profile:v1:"
|
|
29
|
+
XNYA_DS_LINE_RATE_FRAME_V1 = b"xoonya:line-rate-frame:v1:"
|
|
30
|
+
CANONICAL_EVENT_BUNDLE_VERSION = "canonical-event-bundle/v1"
|
|
31
|
+
CANONICAL_RECEIPT_VERSION = "receipt/v1"
|
|
32
|
+
PROOF_BUNDLE_VERSION = "proof-bundle/v1"
|
|
33
|
+
XOONYA_NATIVE_SHA256_METHOD = "xoonya-native-sha256-v1"
|
|
34
|
+
XOONYA_NATIVE_SIGNED_METHOD = "xoonya-native-signed-v1"
|
|
35
|
+
XOONYA_NATIVE_ANCHORED_METHOD = "xoonya-native-anchored-v1"
|
|
36
|
+
XOONYA_NATIVE_HYBRID_METHOD = "xoonya-native-hybrid-v1"
|
|
37
|
+
XOONYA_NATIVE_PROOF_FORMAT_VERSION = "xoonya-native-proof/v1"
|
|
38
|
+
LEGACY_OPRETURN_METHOD = "legacy-opreturn-v1"
|
|
39
|
+
DETACHED_ASSERTION_METHOD = "detached-assertion-v1"
|
|
40
|
+
EXTERNAL_ASSERTION_METHODS = {
|
|
41
|
+
"detached-assertion-v1",
|
|
42
|
+
"jwt-assertion-v1",
|
|
43
|
+
"did-vc-v1",
|
|
44
|
+
"sd-jwt-vc-v1",
|
|
45
|
+
"signed-envelope-v1",
|
|
46
|
+
"partner-proof-envelope-v1",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
FIELD_TYPES = {
|
|
50
|
+
"bytes32": (1, 32),
|
|
51
|
+
"bytes20": (2, 20),
|
|
52
|
+
"uint64": (3, 8),
|
|
53
|
+
"utf8": (4, None),
|
|
54
|
+
"bool": (5, 1),
|
|
55
|
+
}
|
|
56
|
+
FIELD_TYPES_BY_ID = {spec[0]: (name, spec[1]) for name, spec in FIELD_TYPES.items()}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class XnyaReferenceError(ValueError):
|
|
60
|
+
def __init__(self, code: str, message: str):
|
|
61
|
+
super().__init__(message)
|
|
62
|
+
self.code = code
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class ParsedAtom:
|
|
67
|
+
atom_version: str
|
|
68
|
+
spec_version: str
|
|
69
|
+
byte_length: int
|
|
70
|
+
record_type_hex: str
|
|
71
|
+
agent_id: str
|
|
72
|
+
payload_hash_sha256: str
|
|
73
|
+
atom_hash_sha256: str
|
|
74
|
+
atom_hex: str
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def sha256(data: bytes) -> bytes:
|
|
78
|
+
return hashlib.sha256(data).digest()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def sha256_hex(data: bytes) -> str:
|
|
82
|
+
return sha256(data).hex()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def hash160_hex(data: bytes) -> str:
|
|
86
|
+
return hashlib.new("ripemd160", sha256(data)).hexdigest()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def canonicalize(value: Any) -> str:
|
|
90
|
+
if isinstance(value, list):
|
|
91
|
+
return "[" + ",".join(canonicalize(item) for item in value) + "]"
|
|
92
|
+
if isinstance(value, dict):
|
|
93
|
+
return "{" + ",".join(
|
|
94
|
+
json.dumps(str(key), separators=(",", ":"), ensure_ascii=False) + ":" + canonicalize(value[key])
|
|
95
|
+
for key in sorted(value.keys())
|
|
96
|
+
) + "}"
|
|
97
|
+
return json.dumps(value, separators=(",", ":"), ensure_ascii=False)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def canonical_json_sha256_hex(value: Any) -> str:
|
|
101
|
+
return sha256_hex(canonicalize(value).encode("utf-8"))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def normalize_string(value: Any) -> str:
|
|
105
|
+
return str("" if value is None else value).strip()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def normalize_string_array(values: Any, label: str) -> list[str] | None:
|
|
109
|
+
if values is None:
|
|
110
|
+
return None
|
|
111
|
+
if not isinstance(values, list):
|
|
112
|
+
raise ValueError(f"{label} must be an array")
|
|
113
|
+
normalized = [normalize_string(value) for value in values]
|
|
114
|
+
normalized = [value for value in normalized if value]
|
|
115
|
+
return normalized or None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def canonicalize_event(event: dict[str, Any]) -> dict[str, Any]:
|
|
119
|
+
if not isinstance(event, dict):
|
|
120
|
+
raise ValueError("event must be an object")
|
|
121
|
+
normalized = {
|
|
122
|
+
"version": normalize_string(event.get("version") or CANONICAL_EVENT_BUNDLE_VERSION),
|
|
123
|
+
"domain_id": normalize_string(event.get("domain_id")),
|
|
124
|
+
"sequence": normalize_string(event.get("sequence")),
|
|
125
|
+
"actor_id": normalize_string(event.get("actor_id")),
|
|
126
|
+
"adapter_source": normalize_string(event.get("adapter_source")),
|
|
127
|
+
"event_type": normalize_string(event.get("event_type")),
|
|
128
|
+
"event_hash": normalize_string(event.get("event_hash")).lower(),
|
|
129
|
+
}
|
|
130
|
+
for key, value in normalized.items():
|
|
131
|
+
if not value:
|
|
132
|
+
raise ValueError(f"event.{key} required")
|
|
133
|
+
if normalized["version"] != CANONICAL_EVENT_BUNDLE_VERSION:
|
|
134
|
+
raise ValueError(f"event.version must be {CANONICAL_EVENT_BUNDLE_VERSION}")
|
|
135
|
+
assert_hex(normalized["event_hash"], "event_hash_invalid", "event.event_hash must be 32 byte hex")
|
|
136
|
+
|
|
137
|
+
timestamp_ns = normalize_string(event.get("timestamp_ns"))
|
|
138
|
+
if timestamp_ns:
|
|
139
|
+
normalized["timestamp_ns"] = timestamp_ns
|
|
140
|
+
parent_refs = normalize_string_array(event.get("parent_refs"), "event.parent_refs")
|
|
141
|
+
if parent_refs:
|
|
142
|
+
normalized["parent_refs"] = parent_refs
|
|
143
|
+
capability_refs = normalize_string_array(event.get("capability_refs"), "event.capability_refs")
|
|
144
|
+
if capability_refs:
|
|
145
|
+
normalized["capability_refs"] = capability_refs
|
|
146
|
+
attestation_ref = normalize_string(event.get("attestation_ref"))
|
|
147
|
+
if attestation_ref:
|
|
148
|
+
normalized["attestation_ref"] = attestation_ref
|
|
149
|
+
semantic_slots = event.get("semantic_slots")
|
|
150
|
+
if semantic_slots is not None:
|
|
151
|
+
if not isinstance(semantic_slots, dict):
|
|
152
|
+
raise ValueError("event.semantic_slots must be an object")
|
|
153
|
+
normalized["semantic_slots"] = semantic_slots
|
|
154
|
+
return normalized
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def hash_event(event: dict[str, Any]) -> str:
|
|
158
|
+
return canonical_json_sha256_hex(canonicalize_event(event))
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def build_canonical_receipt_signing_material(receipt: dict[str, Any]) -> dict[str, Any]:
|
|
162
|
+
return {
|
|
163
|
+
"agent_identity": receipt.get("agent_identity"),
|
|
164
|
+
"domain_id": receipt.get("domain_id"),
|
|
165
|
+
"event_commitment": receipt.get("event_commitment"),
|
|
166
|
+
"receipt_id": receipt.get("receipt_id"),
|
|
167
|
+
"sequence": receipt.get("sequence"),
|
|
168
|
+
"version": receipt.get("version"),
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def create_legacy_receipt(identity_material_hex: str, payload: bytes) -> dict[str, Any]:
|
|
173
|
+
material = bytes.fromhex(assert_hex(identity_material_hex, "non_hex", "identityMaterialHex must be hex"))
|
|
174
|
+
agent_id = hash160_hex(material)
|
|
175
|
+
payload_hash = sha256_hex(payload) if payload else ZERO_HASH32.hex()
|
|
176
|
+
opreturn_hex = "50524654" + "01" + "01" + agent_id + payload_hash
|
|
177
|
+
return {
|
|
178
|
+
"agent_id": agent_id,
|
|
179
|
+
"opreturn_hex": opreturn_hex,
|
|
180
|
+
"payload_hash": payload_hash,
|
|
181
|
+
"spec_version": "v1",
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def verify_legacy_receipt(opreturn_hex: str, payload: bytes) -> dict[str, Any]:
|
|
186
|
+
normalized = str(opreturn_hex or "").strip().lower()
|
|
187
|
+
try:
|
|
188
|
+
bytes.fromhex(normalized)
|
|
189
|
+
except ValueError:
|
|
190
|
+
return {"valid": False, "reason": "non_hex", "agent_id": None}
|
|
191
|
+
if len(normalized) != 116:
|
|
192
|
+
return {"valid": False, "reason": "invalid_length", "agent_id": None}
|
|
193
|
+
if normalized[:8] != "50524654":
|
|
194
|
+
return {"valid": False, "reason": "bad_magic", "agent_id": None}
|
|
195
|
+
if normalized[8:10] != "01":
|
|
196
|
+
return {"valid": False, "reason": "bad_version", "agent_id": None}
|
|
197
|
+
if normalized[10:12] not in {"01", "02", "03"}:
|
|
198
|
+
return {"valid": False, "reason": "bad_type", "agent_id": None}
|
|
199
|
+
expected_payload = sha256_hex(payload) if payload else ZERO_HASH32.hex()
|
|
200
|
+
if normalized[52:116] != expected_payload:
|
|
201
|
+
return {"valid": False, "reason": "payload_mismatch", "agent_id": normalized[12:52]}
|
|
202
|
+
return {"valid": True, "reason": None, "agent_id": normalized[12:52]}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def select_receipt_algorithm(options: dict[str, Any], compatibility: dict[str, Any] | None) -> str:
|
|
206
|
+
requested = normalize_string(options.get("algorithm"))
|
|
207
|
+
if requested:
|
|
208
|
+
return requested
|
|
209
|
+
if compatibility:
|
|
210
|
+
return LEGACY_OPRETURN_METHOD
|
|
211
|
+
if options.get("signature") is not None:
|
|
212
|
+
return DETACHED_ASSERTION_METHOD
|
|
213
|
+
return XOONYA_NATIVE_SHA256_METHOD
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def build_native_receipt_signature(receipt: dict[str, Any], canonical_event: dict[str, Any]) -> str:
|
|
217
|
+
material = {
|
|
218
|
+
"method": XOONYA_NATIVE_SHA256_METHOD,
|
|
219
|
+
"receipt_signing_material": build_canonical_receipt_signing_material(receipt),
|
|
220
|
+
"event_commitment": hash_event(canonical_event),
|
|
221
|
+
}
|
|
222
|
+
return XOONYA_NATIVE_SHA256_METHOD + ":" + sha256_hex(canonicalize(material).encode("utf-8"))
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _base64url_decode(value: str) -> bytes:
|
|
226
|
+
normalized = normalize_string(value)
|
|
227
|
+
padded = normalized + ("=" * ((4 - (len(normalized) % 4)) % 4))
|
|
228
|
+
return base64.urlsafe_b64decode(padded.encode("ascii"))
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _spki_der_from_pem(public_key_pem: str) -> bytes:
|
|
232
|
+
pem = str(public_key_pem or "").replace("\r\n", "\n").strip()
|
|
233
|
+
begin = "-----BEGIN PUBLIC KEY-----"
|
|
234
|
+
end = "-----END PUBLIC KEY-----"
|
|
235
|
+
if begin not in pem or end not in pem:
|
|
236
|
+
raise ValueError("native public key must be SPKI PEM")
|
|
237
|
+
body = pem.split(begin, 1)[1].split(end, 1)[0]
|
|
238
|
+
return base64.b64decode("".join(body.split()))
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _ed25519_crypto_available() -> bool:
|
|
242
|
+
try:
|
|
243
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey # noqa: F401
|
|
244
|
+
from cryptography.hazmat.primitives.serialization import load_pem_public_key # noqa: F401
|
|
245
|
+
except Exception:
|
|
246
|
+
return False
|
|
247
|
+
return True
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def build_native_signed_proof_material(receipt: dict[str, Any], canonical_event: dict[str, Any], proof_descriptor: dict[str, Any]) -> dict[str, Any]:
|
|
251
|
+
return {
|
|
252
|
+
"identity_ref": proof_descriptor.get("identity_ref"),
|
|
253
|
+
"method": XOONYA_NATIVE_SIGNED_METHOD,
|
|
254
|
+
"public_key_ref": proof_descriptor.get("public_key_ref"),
|
|
255
|
+
"receipt_signing_material": build_canonical_receipt_signing_material(receipt),
|
|
256
|
+
"event_commitment": hash_event(canonical_event),
|
|
257
|
+
"signature_suite": proof_descriptor.get("signature_suite"),
|
|
258
|
+
"standard_version": XOONYA_NATIVE_PROOF_FORMAT_VERSION,
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def verify_native_signed_receipt_proof(receipt: dict[str, Any], ceb: dict[str, Any]) -> dict[str, Any]:
|
|
263
|
+
proof = receipt.get("native_proof")
|
|
264
|
+
if not isinstance(proof, dict):
|
|
265
|
+
return {"valid": False, "reason": "native_proof_missing"}
|
|
266
|
+
if normalize_string(proof.get("method")) != XOONYA_NATIVE_SIGNED_METHOD:
|
|
267
|
+
return {"valid": False, "reason": "native_proof_method_mismatch"}
|
|
268
|
+
signature_suite = normalize_string(proof.get("signature_suite"))
|
|
269
|
+
if signature_suite != "ed25519":
|
|
270
|
+
return {"valid": False, "reason": "native_signature_suite_crypto_unavailable_in_python_candidate"}
|
|
271
|
+
identity_ref = normalize_string(proof.get("identity_ref"))
|
|
272
|
+
if not identity_ref:
|
|
273
|
+
return {"valid": False, "reason": "native_identity_ref_missing"}
|
|
274
|
+
public_key_material = proof.get("public_key_material")
|
|
275
|
+
if not isinstance(public_key_material, dict):
|
|
276
|
+
return {"valid": False, "reason": "native_public_key_material_missing"}
|
|
277
|
+
if normalize_string(public_key_material.get("format")) != "spki-pem":
|
|
278
|
+
return {"valid": False, "reason": "native_public_key_format_mismatch"}
|
|
279
|
+
try:
|
|
280
|
+
der = _spki_der_from_pem(public_key_material.get("value", ""))
|
|
281
|
+
except Exception as error:
|
|
282
|
+
return {"valid": False, "reason": str(error)}
|
|
283
|
+
expected_public_key_ref = "ed25519-spki-sha256:" + sha256_hex(der)
|
|
284
|
+
if normalize_string(proof.get("public_key_ref")) != expected_public_key_ref:
|
|
285
|
+
return {"valid": False, "reason": "native_public_key_ref_mismatch"}
|
|
286
|
+
proof_material = proof.get("proof_material")
|
|
287
|
+
if not isinstance(proof_material, dict):
|
|
288
|
+
return {"valid": False, "reason": "native_proof_material_missing"}
|
|
289
|
+
signature = normalize_string(proof_material.get("signature"))
|
|
290
|
+
if not signature:
|
|
291
|
+
return {"valid": False, "reason": "native_signature_missing"}
|
|
292
|
+
if normalize_string(receipt.get("signature")) != XOONYA_NATIVE_SIGNED_METHOD + ":ed25519:" + signature:
|
|
293
|
+
return {"valid": False, "reason": "signature_native_signed_mismatch"}
|
|
294
|
+
signing_material = build_native_signed_proof_material(receipt, ceb, {
|
|
295
|
+
"identity_ref": identity_ref,
|
|
296
|
+
"public_key_ref": expected_public_key_ref,
|
|
297
|
+
"signature_suite": "ed25519",
|
|
298
|
+
})
|
|
299
|
+
signing_bytes = canonicalize(signing_material).encode("utf-8")
|
|
300
|
+
if normalize_string(proof_material.get("signing_material_sha256")).lower() != sha256_hex(signing_bytes):
|
|
301
|
+
return {"valid": False, "reason": "native_signing_material_hash_mismatch"}
|
|
302
|
+
if not _ed25519_crypto_available():
|
|
303
|
+
return {
|
|
304
|
+
"valid": False,
|
|
305
|
+
"reason": "native_signature_crypto_dependency_missing",
|
|
306
|
+
"dependency": "cryptography",
|
|
307
|
+
}
|
|
308
|
+
try:
|
|
309
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
|
310
|
+
from cryptography.hazmat.primitives.serialization import load_pem_public_key
|
|
311
|
+
|
|
312
|
+
public_key = load_pem_public_key(public_key_material["value"].encode("utf-8"))
|
|
313
|
+
if not isinstance(public_key, Ed25519PublicKey):
|
|
314
|
+
return {"valid": False, "reason": "native_public_key_suite_mismatch"}
|
|
315
|
+
public_key.verify(_base64url_decode(signature), signing_bytes)
|
|
316
|
+
except Exception:
|
|
317
|
+
return {"valid": False, "reason": "native_signature_invalid"}
|
|
318
|
+
return {
|
|
319
|
+
"valid": True,
|
|
320
|
+
"verification_state": "verified_native_signature",
|
|
321
|
+
"signature_suite": "ed25519",
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def assert_hex(value: str, code: str, message: str) -> str:
|
|
326
|
+
normalized = str(value or "").strip().lower()
|
|
327
|
+
if not normalized or len(normalized) % 2 != 0:
|
|
328
|
+
raise XnyaReferenceError(code, message)
|
|
329
|
+
try:
|
|
330
|
+
bytes.fromhex(normalized)
|
|
331
|
+
except ValueError as exc:
|
|
332
|
+
raise XnyaReferenceError(code, message) from exc
|
|
333
|
+
return normalized
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def normalize_identity_material(identity_material_hex: str) -> bytes:
|
|
337
|
+
material_hex = assert_hex(
|
|
338
|
+
identity_material_hex,
|
|
339
|
+
"identity_unsupported_type_prefix",
|
|
340
|
+
"identityMaterialHex has unsupported identity type prefix",
|
|
341
|
+
)
|
|
342
|
+
prefix = material_hex[:2]
|
|
343
|
+
if prefix not in IDENTITY_PREFIXES:
|
|
344
|
+
raise XnyaReferenceError(
|
|
345
|
+
"identity_unsupported_type_prefix",
|
|
346
|
+
"identityMaterialHex has unsupported identity type prefix",
|
|
347
|
+
)
|
|
348
|
+
if len(material_hex) == 2:
|
|
349
|
+
raise XnyaReferenceError(
|
|
350
|
+
"identity_missing_raw_bytes",
|
|
351
|
+
"identityMaterialHex must include raw identity bytes",
|
|
352
|
+
)
|
|
353
|
+
return bytes.fromhex(material_hex)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def hash_payload(payload: bytes | None, domain: bytes) -> bytes:
|
|
357
|
+
if not payload:
|
|
358
|
+
return ZERO_HASH32
|
|
359
|
+
digest = sha256(domain + payload)
|
|
360
|
+
if digest == ZERO_HASH32:
|
|
361
|
+
raise XnyaReferenceError("payload_hash_mismatch", "payload hash cannot equal absent sentinel")
|
|
362
|
+
return digest
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def create_atom_v1(identity_material_hex: str, payload: bytes | None, record_type_hex: str = "01") -> dict[str, Any]:
|
|
366
|
+
material = normalize_identity_material(identity_material_hex)
|
|
367
|
+
record_type = int(record_type_hex, 16)
|
|
368
|
+
if record_type not in VALID_RECORD_TYPES:
|
|
369
|
+
raise XnyaReferenceError("atom_unsupported_record_type", "atomHex has unsupported record type")
|
|
370
|
+
agent_id = bytes.fromhex(hash160_hex(XNYA_DS_AGENT_V1 + material))
|
|
371
|
+
payload_hash = hash_payload(payload, XNYA_DS_PAYLOAD_V1)
|
|
372
|
+
atom = XNYA_MAGIC + bytes([VERSION_V1, record_type]) + agent_id + payload_hash
|
|
373
|
+
return {
|
|
374
|
+
"atom_hex": atom.hex(),
|
|
375
|
+
"atom_version": "xoonya-native-atom/v1",
|
|
376
|
+
"spec_version": "v1",
|
|
377
|
+
"byte_length": len(atom),
|
|
378
|
+
"record_type_hex": f"{record_type:02x}",
|
|
379
|
+
"agent_id": agent_id.hex(),
|
|
380
|
+
"payload_hash_sha256": payload_hash.hex(),
|
|
381
|
+
"atom_hash_sha256": sha256_hex(atom),
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def create_atom_v2(identity_material_hex: str, payload: bytes | None, record_type_hex: str = "01") -> dict[str, Any]:
|
|
386
|
+
material = normalize_identity_material(identity_material_hex)
|
|
387
|
+
record_type = int(record_type_hex, 16)
|
|
388
|
+
if record_type not in VALID_RECORD_TYPES:
|
|
389
|
+
raise XnyaReferenceError("atom_unsupported_record_type", "atomHex has unsupported record type")
|
|
390
|
+
agent_id = sha256(XNYA_DS_AGENT_V2 + material)
|
|
391
|
+
payload_hash = hash_payload(payload, XNYA_DS_PAYLOAD_V2)
|
|
392
|
+
atom = XNYA_MAGIC + bytes([VERSION_V2, record_type]) + agent_id + payload_hash
|
|
393
|
+
return {
|
|
394
|
+
"atom_hex": atom.hex(),
|
|
395
|
+
"atom_version": "xoonya-native-atom/v2",
|
|
396
|
+
"spec_version": "v2",
|
|
397
|
+
"byte_length": len(atom),
|
|
398
|
+
"record_type_hex": f"{record_type:02x}",
|
|
399
|
+
"agent_id": agent_id.hex(),
|
|
400
|
+
"payload_hash_sha256": payload_hash.hex(),
|
|
401
|
+
"atom_hash_sha256": sha256_hex(atom),
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def parse_atom(atom_hex: str) -> ParsedAtom:
|
|
406
|
+
hex_value = str(atom_hex or "").strip().lower()
|
|
407
|
+
try:
|
|
408
|
+
atom = bytes.fromhex(hex_value)
|
|
409
|
+
except ValueError as exc:
|
|
410
|
+
raise XnyaReferenceError("atom_hex_not_hex", "atomHex must be hex") from exc
|
|
411
|
+
if len(atom) not in (58, 70):
|
|
412
|
+
raise XnyaReferenceError("atom_unsupported_length", "atomHex must be a supported Xoonya native atom")
|
|
413
|
+
if atom[:4] != XNYA_MAGIC:
|
|
414
|
+
raise XnyaReferenceError("atom_bad_magic", "atomHex has bad Xoonya native atom magic")
|
|
415
|
+
record_type = atom[5]
|
|
416
|
+
if record_type not in VALID_RECORD_TYPES:
|
|
417
|
+
raise XnyaReferenceError("atom_unsupported_record_type", "atomHex has unsupported record type")
|
|
418
|
+
version = atom[4]
|
|
419
|
+
if version == VERSION_V1 and len(atom) == 58:
|
|
420
|
+
return ParsedAtom(
|
|
421
|
+
atom_version="xoonya-native-atom/v1",
|
|
422
|
+
spec_version="v1",
|
|
423
|
+
byte_length=58,
|
|
424
|
+
record_type_hex=f"{record_type:02x}",
|
|
425
|
+
agent_id=atom[6:26].hex(),
|
|
426
|
+
payload_hash_sha256=atom[26:58].hex(),
|
|
427
|
+
atom_hash_sha256=sha256_hex(atom),
|
|
428
|
+
atom_hex=hex_value,
|
|
429
|
+
)
|
|
430
|
+
if version == VERSION_V2 and len(atom) == 70:
|
|
431
|
+
return ParsedAtom(
|
|
432
|
+
atom_version="xoonya-native-atom/v2",
|
|
433
|
+
spec_version="v2",
|
|
434
|
+
byte_length=70,
|
|
435
|
+
record_type_hex=f"{record_type:02x}",
|
|
436
|
+
agent_id=atom[6:38].hex(),
|
|
437
|
+
payload_hash_sha256=atom[38:70].hex(),
|
|
438
|
+
atom_hash_sha256=sha256_hex(atom),
|
|
439
|
+
atom_hex=hex_value,
|
|
440
|
+
)
|
|
441
|
+
raise XnyaReferenceError("atom_version_length_mismatch", "atomHex/version length mismatch")
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def verify_atom(atom_hex: str, *, identity_material_hex: str | None = None, payload: bytes | None = None) -> dict[str, Any]:
|
|
445
|
+
parsed = parse_atom(atom_hex)
|
|
446
|
+
if identity_material_hex is not None:
|
|
447
|
+
material = normalize_identity_material(identity_material_hex)
|
|
448
|
+
expected_agent_id = (
|
|
449
|
+
hash160_hex(XNYA_DS_AGENT_V1 + material)
|
|
450
|
+
if parsed.spec_version == "v1"
|
|
451
|
+
else sha256_hex(XNYA_DS_AGENT_V2 + material)
|
|
452
|
+
)
|
|
453
|
+
if parsed.agent_id != expected_agent_id:
|
|
454
|
+
return {"valid": False, "reason": "agent_id_mismatch", "error_code": "agent_id_mismatch"}
|
|
455
|
+
if payload is not None:
|
|
456
|
+
expected_payload_hash = (
|
|
457
|
+
hash_payload(payload, XNYA_DS_PAYLOAD_V1)
|
|
458
|
+
if parsed.spec_version == "v1"
|
|
459
|
+
else hash_payload(payload, XNYA_DS_PAYLOAD_V2)
|
|
460
|
+
).hex()
|
|
461
|
+
if parsed.payload_hash_sha256 != expected_payload_hash:
|
|
462
|
+
return {"valid": False, "reason": "payload_hash_mismatch", "error_code": "payload_hash_mismatch"}
|
|
463
|
+
return {"valid": True, "reason": None, "error_code": None, "parsed": parsed}
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _encode_field(field: dict[str, Any]) -> tuple[int, int, bytes]:
|
|
467
|
+
field_id = int(field.get("field_id", field.get("id")))
|
|
468
|
+
field_type = str(field.get("field_type", field.get("type"))).strip()
|
|
469
|
+
if field_id < 1 or field_id > 0xFFFF:
|
|
470
|
+
raise ValueError("payload profile field_id must be uint16")
|
|
471
|
+
if field_type not in FIELD_TYPES:
|
|
472
|
+
raise ValueError("payload profile field_type unsupported")
|
|
473
|
+
field_type_id, fixed_len = FIELD_TYPES[field_type]
|
|
474
|
+
if field_type in ("bytes32", "bytes20"):
|
|
475
|
+
value = bytes.fromhex(str(field.get("value_hex", field.get("value"))).strip().lower())
|
|
476
|
+
if len(value) != fixed_len:
|
|
477
|
+
raise ValueError("payload profile fixed field length mismatch")
|
|
478
|
+
elif field_type == "uint64":
|
|
479
|
+
n = int(field.get("value"))
|
|
480
|
+
if n < 0 or n > 0xFFFFFFFFFFFFFFFF:
|
|
481
|
+
raise ValueError("payload profile uint64 out of range")
|
|
482
|
+
value = n.to_bytes(8, "big")
|
|
483
|
+
elif field_type == "utf8":
|
|
484
|
+
value = str(field.get("value", "")).encode("utf-8")
|
|
485
|
+
if not value or len(value) > 1024:
|
|
486
|
+
raise ValueError("payload profile utf8 length unsupported")
|
|
487
|
+
elif field_type == "bool":
|
|
488
|
+
raw = field.get("value")
|
|
489
|
+
if not isinstance(raw, bool):
|
|
490
|
+
raise ValueError("payload profile bool must be boolean")
|
|
491
|
+
value = b"\x01" if raw else b"\x00"
|
|
492
|
+
return field_id, field_type_id, value
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def encode_payload_profile(input_value: dict[str, Any]) -> dict[str, Any]:
|
|
496
|
+
schema_ref = str(input_value.get("schema_ref", input_value.get("schemaRef", "xoonya.payload.schema.generic.v1"))).strip()
|
|
497
|
+
schema_hash = sha256_hex(f"xoonya:payload-schema:v1:{schema_ref}".encode("utf-8"))
|
|
498
|
+
encoded_fields = sorted((_encode_field(field) for field in input_value["fields"]), key=lambda item: (item[0], item[1]))
|
|
499
|
+
canonical = bytearray()
|
|
500
|
+
canonical.extend(b"XNYP")
|
|
501
|
+
canonical.append(1)
|
|
502
|
+
canonical.extend(bytes.fromhex(schema_hash))
|
|
503
|
+
canonical.append(len(encoded_fields))
|
|
504
|
+
for field_id, field_type_id, value in encoded_fields:
|
|
505
|
+
canonical.extend(field_id.to_bytes(2, "big"))
|
|
506
|
+
canonical.append(field_type_id)
|
|
507
|
+
canonical.extend(len(value).to_bytes(2, "big"))
|
|
508
|
+
canonical.extend(value)
|
|
509
|
+
canonical_bytes = bytes(canonical)
|
|
510
|
+
return {
|
|
511
|
+
"profile": "xoonya-native-payload-profile/v1",
|
|
512
|
+
"schema_ref": schema_ref,
|
|
513
|
+
"schema_hash_sha256": schema_hash,
|
|
514
|
+
"field_count": len(encoded_fields),
|
|
515
|
+
"canonical_bytes_hex": canonical_bytes.hex(),
|
|
516
|
+
"payload_hash_sha256": sha256_hex(XNYA_DS_PAYLOAD_PROFILE_V1 + canonical_bytes),
|
|
517
|
+
"field_order": [field_id for field_id, _, _ in encoded_fields],
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def parse_payload_profile(canonical_bytes_hex: str) -> dict[str, Any]:
|
|
522
|
+
canonical = bytes.fromhex(canonical_bytes_hex)
|
|
523
|
+
if len(canonical) < 38:
|
|
524
|
+
raise ValueError("payload profile bytes too short")
|
|
525
|
+
if canonical[:4] != b"XNYP":
|
|
526
|
+
raise ValueError("payload profile magic mismatch")
|
|
527
|
+
if canonical[4] != 1:
|
|
528
|
+
raise ValueError("payload profile version unsupported")
|
|
529
|
+
field_count = canonical[37]
|
|
530
|
+
offset = 38
|
|
531
|
+
fields = []
|
|
532
|
+
for _ in range(field_count):
|
|
533
|
+
if offset + 5 > len(canonical):
|
|
534
|
+
raise ValueError("payload profile truncated field header")
|
|
535
|
+
field_id = int.from_bytes(canonical[offset : offset + 2], "big")
|
|
536
|
+
field_type_id = canonical[offset + 2]
|
|
537
|
+
length = int.from_bytes(canonical[offset + 3 : offset + 5], "big")
|
|
538
|
+
offset += 5
|
|
539
|
+
value = canonical[offset : offset + length]
|
|
540
|
+
offset += length
|
|
541
|
+
if field_type_id not in FIELD_TYPES_BY_ID:
|
|
542
|
+
raise ValueError("payload profile field type unsupported")
|
|
543
|
+
field_type, fixed_len = FIELD_TYPES_BY_ID[field_type_id]
|
|
544
|
+
if fixed_len is not None and len(value) != fixed_len:
|
|
545
|
+
raise ValueError("payload profile fixed field length mismatch")
|
|
546
|
+
fields.append({"field_id": field_id, "field_type": field_type, "field_type_id": field_type_id, "value_hex": value.hex()})
|
|
547
|
+
if offset != len(canonical):
|
|
548
|
+
raise ValueError("payload profile trailing bytes")
|
|
549
|
+
sorted_fields = sorted(fields, key=lambda item: (item["field_id"], item["field_type_id"]))
|
|
550
|
+
if fields != sorted_fields:
|
|
551
|
+
raise ValueError("payload profile fields not canonical order")
|
|
552
|
+
return {
|
|
553
|
+
"profile": "xoonya-native-payload-profile/v1",
|
|
554
|
+
"schema_hash_sha256": canonical[5:37].hex(),
|
|
555
|
+
"field_count": field_count,
|
|
556
|
+
"fields": fields,
|
|
557
|
+
"canonical_bytes_hex": canonical.hex(),
|
|
558
|
+
"payload_hash_sha256": sha256_hex(XNYA_DS_PAYLOAD_PROFILE_V1 + canonical),
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def line_rate_frame_checksum(atom: bytes) -> str:
|
|
563
|
+
return sha256_hex(XNYA_DS_LINE_RATE_FRAME_V1 + atom)[:4]
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def normalize_anchor_refs(anchor_refs: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
567
|
+
if not isinstance(anchor_refs, list) or not anchor_refs:
|
|
568
|
+
raise ValueError("native anchors must be a non-empty array")
|
|
569
|
+
normalized = []
|
|
570
|
+
for index, anchor in enumerate(anchor_refs):
|
|
571
|
+
if not isinstance(anchor, dict):
|
|
572
|
+
raise ValueError(f"native anchors[{index}] must be an object")
|
|
573
|
+
entry = {
|
|
574
|
+
"anchor_id": normalize_string(anchor.get("anchor_id") or anchor.get("id")),
|
|
575
|
+
"anchor_kind": normalize_string(anchor.get("anchor_kind") or anchor.get("kind")),
|
|
576
|
+
"anchor_ref": normalize_string(anchor.get("anchor_ref") or anchor.get("ref")),
|
|
577
|
+
"anchor_hash_sha256": normalize_string(anchor.get("anchor_hash_sha256") or anchor.get("hash_sha256") or anchor.get("anchor_hash")).lower(),
|
|
578
|
+
}
|
|
579
|
+
if not entry["anchor_id"]:
|
|
580
|
+
raise ValueError(f"native anchors[{index}].anchor_id required")
|
|
581
|
+
if not entry["anchor_kind"]:
|
|
582
|
+
raise ValueError(f"native anchors[{index}].anchor_kind required")
|
|
583
|
+
if not entry["anchor_ref"]:
|
|
584
|
+
raise ValueError(f"native anchors[{index}].anchor_ref required")
|
|
585
|
+
assert_hex(entry["anchor_hash_sha256"], "anchor_hash_invalid", f"native anchors[{index}].anchor_hash_sha256 must be 32 byte hex")
|
|
586
|
+
for key in ("network", "finality_state", "observed_at_iso", "provider_ref", "inclusion_proof_ref"):
|
|
587
|
+
value = normalize_string(anchor.get(key))
|
|
588
|
+
if value:
|
|
589
|
+
entry[key] = value
|
|
590
|
+
normalized.append(entry)
|
|
591
|
+
return sorted(normalized, key=canonicalize)
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def build_native_anchor_binding_material(receipt: dict[str, Any], canonical_event: dict[str, Any], anchor_refs: list[dict[str, Any]]) -> dict[str, Any]:
|
|
595
|
+
return {
|
|
596
|
+
"anchor_refs": normalize_anchor_refs(anchor_refs),
|
|
597
|
+
"base_method": XOONYA_NATIVE_SHA256_METHOD,
|
|
598
|
+
"event_commitment": hash_event(canonical_event),
|
|
599
|
+
"method": XOONYA_NATIVE_ANCHORED_METHOD,
|
|
600
|
+
"receipt_signing_material": build_canonical_receipt_signing_material(receipt),
|
|
601
|
+
"standard_version": "xoonya-native-proof/v1",
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def build_native_anchored_receipt_proof(receipt: dict[str, Any], canonical_event: dict[str, Any], anchors: list[dict[str, Any]]) -> dict[str, Any]:
|
|
606
|
+
anchor_refs = normalize_anchor_refs(anchors)
|
|
607
|
+
binding_material = build_native_anchor_binding_material(receipt, canonical_event, anchor_refs)
|
|
608
|
+
binding_hash = sha256_hex(canonicalize(binding_material).encode("utf-8"))
|
|
609
|
+
anchor_root_hash = canonical_json_sha256_hex(anchor_refs)
|
|
610
|
+
return {
|
|
611
|
+
"method": XOONYA_NATIVE_ANCHORED_METHOD,
|
|
612
|
+
"base_method": XOONYA_NATIVE_SHA256_METHOD,
|
|
613
|
+
"anchor_refs": anchor_refs,
|
|
614
|
+
"proof_material": {
|
|
615
|
+
"anchor_root_sha256": anchor_root_hash,
|
|
616
|
+
"binding_material_sha256": binding_hash,
|
|
617
|
+
},
|
|
618
|
+
"compact_signature": XOONYA_NATIVE_ANCHORED_METHOD + ":" + binding_hash,
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def emit_receipt(event: dict[str, Any], options: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
623
|
+
options = options or {}
|
|
624
|
+
canonical_event = canonicalize_event(event)
|
|
625
|
+
event_commitment = hash_event(canonical_event)
|
|
626
|
+
agent_identity = normalize_string(options.get("agent_identity") or options.get("agentIdentity") or canonical_event["actor_id"])
|
|
627
|
+
if not agent_identity:
|
|
628
|
+
raise ValueError("agent_identity required")
|
|
629
|
+
compatibility = None
|
|
630
|
+
legacy_identity = options.get("legacy_identity_material_hex")
|
|
631
|
+
if legacy_identity:
|
|
632
|
+
legacy_receipt = create_legacy_receipt(str(legacy_identity).strip().lower(), canonicalize(canonical_event).encode("utf-8"))
|
|
633
|
+
compatibility = {"legacy_receipt": legacy_receipt}
|
|
634
|
+
receipt_id = sha256_hex(canonicalize({
|
|
635
|
+
"agent_identity": agent_identity,
|
|
636
|
+
"domain_id": canonical_event["domain_id"],
|
|
637
|
+
"event_commitment": event_commitment,
|
|
638
|
+
"sequence": canonical_event["sequence"],
|
|
639
|
+
"version": CANONICAL_RECEIPT_VERSION,
|
|
640
|
+
}).encode("utf-8"))
|
|
641
|
+
receipt = {
|
|
642
|
+
"version": CANONICAL_RECEIPT_VERSION,
|
|
643
|
+
"receipt_id": receipt_id,
|
|
644
|
+
"agent_identity": agent_identity,
|
|
645
|
+
"domain_id": canonical_event["domain_id"],
|
|
646
|
+
"sequence": canonical_event["sequence"],
|
|
647
|
+
"event_commitment": event_commitment,
|
|
648
|
+
"algorithm": select_receipt_algorithm(options, compatibility),
|
|
649
|
+
}
|
|
650
|
+
if compatibility:
|
|
651
|
+
receipt["compatibility"] = compatibility
|
|
652
|
+
signature = normalize_string(options.get("signature"))
|
|
653
|
+
native_proof = options.get("native_proof") if isinstance(options.get("native_proof"), dict) else None
|
|
654
|
+
if not signature and receipt["algorithm"] == XOONYA_NATIVE_ANCHORED_METHOD:
|
|
655
|
+
native_proof = build_native_anchored_receipt_proof(receipt, canonical_event, options.get("anchors") or options.get("anchor_refs") or [])
|
|
656
|
+
signature = native_proof.pop("compact_signature")
|
|
657
|
+
if not signature and receipt["algorithm"] == XOONYA_NATIVE_SHA256_METHOD:
|
|
658
|
+
signature = build_native_receipt_signature(receipt, canonical_event)
|
|
659
|
+
if not signature and compatibility:
|
|
660
|
+
signature = compatibility["legacy_receipt"]["opreturn_hex"]
|
|
661
|
+
if not signature:
|
|
662
|
+
raise ValueError("signature or legacy_identity_material_hex required")
|
|
663
|
+
if native_proof:
|
|
664
|
+
receipt["native_proof"] = native_proof
|
|
665
|
+
receipt["signature"] = signature
|
|
666
|
+
return receipt
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def build_proof_bundle(input_value: dict[str, Any]) -> dict[str, Any]:
|
|
670
|
+
ceb = canonicalize_event(input_value.get("ceb") or input_value.get("event"))
|
|
671
|
+
anchors = input_value.get("anchors") if isinstance(input_value.get("anchors"), list) else None
|
|
672
|
+
receipt_options = input_value.get("receipt_options") if isinstance(input_value.get("receipt_options"), dict) else {}
|
|
673
|
+
if anchors and not any(key in receipt_options for key in ("anchors", "native_anchor_refs", "anchor_refs")):
|
|
674
|
+
receipt_options = {**receipt_options, "anchors": anchors}
|
|
675
|
+
receipt = input_value.get("receipt") if isinstance(input_value.get("receipt"), dict) else emit_receipt(ceb, receipt_options)
|
|
676
|
+
bundle = {
|
|
677
|
+
"version": PROOF_BUNDLE_VERSION,
|
|
678
|
+
"ceb": ceb,
|
|
679
|
+
"receipt": receipt,
|
|
680
|
+
}
|
|
681
|
+
if anchors is not None:
|
|
682
|
+
bundle["anchors"] = anchors
|
|
683
|
+
for key in ("attestations", "evidence_refs", "batch_refs"):
|
|
684
|
+
if isinstance(input_value.get(key), list):
|
|
685
|
+
bundle[key] = input_value[key]
|
|
686
|
+
if isinstance(input_value.get("metadata"), dict):
|
|
687
|
+
bundle["metadata"] = input_value["metadata"]
|
|
688
|
+
return bundle
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def _verify_native_anchored(receipt: dict[str, Any], ceb: dict[str, Any], bundle: dict[str, Any]) -> dict[str, Any]:
|
|
692
|
+
proof = receipt.get("native_proof")
|
|
693
|
+
if not isinstance(proof, dict):
|
|
694
|
+
return {"valid": False, "reason": "native_proof_missing"}
|
|
695
|
+
if normalize_string(proof.get("method")) != XOONYA_NATIVE_ANCHORED_METHOD:
|
|
696
|
+
return {"valid": False, "reason": "native_proof_method_mismatch"}
|
|
697
|
+
if normalize_string(proof.get("base_method")) != XOONYA_NATIVE_SHA256_METHOD:
|
|
698
|
+
return {"valid": False, "reason": "native_anchor_base_method_unsupported"}
|
|
699
|
+
if not isinstance(bundle.get("anchors"), list):
|
|
700
|
+
return {"valid": False, "reason": "native_anchor_bundle_missing"}
|
|
701
|
+
proof_anchors = normalize_anchor_refs(proof.get("anchor_refs"))
|
|
702
|
+
bundle_anchors = normalize_anchor_refs(bundle.get("anchors"))
|
|
703
|
+
if canonicalize(proof_anchors) != canonicalize(bundle_anchors):
|
|
704
|
+
return {"valid": False, "reason": "native_anchor_bundle_mismatch"}
|
|
705
|
+
proof_material = proof.get("proof_material")
|
|
706
|
+
if not isinstance(proof_material, dict):
|
|
707
|
+
return {"valid": False, "reason": "native_anchor_proof_material_missing"}
|
|
708
|
+
expected_anchor_root = canonical_json_sha256_hex(proof_anchors)
|
|
709
|
+
if normalize_string(proof_material.get("anchor_root_sha256")).lower() != expected_anchor_root:
|
|
710
|
+
return {"valid": False, "reason": "native_anchor_root_mismatch"}
|
|
711
|
+
binding_material = build_native_anchor_binding_material(receipt, ceb, proof_anchors)
|
|
712
|
+
binding_hash = sha256_hex(canonicalize(binding_material).encode("utf-8"))
|
|
713
|
+
if normalize_string(proof_material.get("binding_material_sha256")).lower() != binding_hash:
|
|
714
|
+
return {"valid": False, "reason": "native_anchor_binding_hash_mismatch"}
|
|
715
|
+
if normalize_string(receipt.get("signature")) != XOONYA_NATIVE_ANCHORED_METHOD + ":" + binding_hash:
|
|
716
|
+
return {"valid": False, "reason": "signature_native_anchored_mismatch"}
|
|
717
|
+
return {"valid": True, "verification_state": "verified_native_anchor_binding", "anchor_count": len(proof_anchors), "anchor_root_sha256": expected_anchor_root}
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
def verify_proof_bundle(bundle: dict[str, Any]) -> dict[str, Any]:
|
|
721
|
+
try:
|
|
722
|
+
if not isinstance(bundle, dict):
|
|
723
|
+
return {"valid": False, "reason": "bundle_not_object"}
|
|
724
|
+
if normalize_string(bundle.get("version")) != PROOF_BUNDLE_VERSION:
|
|
725
|
+
return {"valid": False, "reason": "bad_bundle_version"}
|
|
726
|
+
ceb = canonicalize_event(bundle.get("ceb"))
|
|
727
|
+
receipt = bundle.get("receipt")
|
|
728
|
+
if not isinstance(receipt, dict):
|
|
729
|
+
return {"valid": False, "reason": "receipt_not_object"}
|
|
730
|
+
if normalize_string(receipt.get("version")) != CANONICAL_RECEIPT_VERSION:
|
|
731
|
+
return {"valid": False, "reason": "bad_receipt_version"}
|
|
732
|
+
if normalize_string(receipt.get("domain_id")) != ceb["domain_id"]:
|
|
733
|
+
return {"valid": False, "reason": "receipt_domain_mismatch"}
|
|
734
|
+
if normalize_string(receipt.get("sequence")) != ceb["sequence"]:
|
|
735
|
+
return {"valid": False, "reason": "receipt_sequence_mismatch"}
|
|
736
|
+
expected_commitment = hash_event(ceb)
|
|
737
|
+
if normalize_string(receipt.get("event_commitment")).lower() != expected_commitment:
|
|
738
|
+
return {"valid": False, "reason": "event_commitment_mismatch"}
|
|
739
|
+
expected_receipt_id = sha256_hex(canonicalize({
|
|
740
|
+
"agent_identity": normalize_string(receipt.get("agent_identity")),
|
|
741
|
+
"domain_id": ceb["domain_id"],
|
|
742
|
+
"event_commitment": expected_commitment,
|
|
743
|
+
"sequence": ceb["sequence"],
|
|
744
|
+
"version": CANONICAL_RECEIPT_VERSION,
|
|
745
|
+
}).encode("utf-8"))
|
|
746
|
+
if normalize_string(receipt.get("receipt_id")).lower() != expected_receipt_id:
|
|
747
|
+
return {"valid": False, "reason": "receipt_id_mismatch"}
|
|
748
|
+
if not normalize_string(receipt.get("agent_identity")):
|
|
749
|
+
return {"valid": False, "reason": "agent_identity_missing"}
|
|
750
|
+
if not normalize_string(receipt.get("signature")):
|
|
751
|
+
return {"valid": False, "reason": "signature_missing"}
|
|
752
|
+
algorithm = normalize_string(receipt.get("algorithm"))
|
|
753
|
+
if not algorithm:
|
|
754
|
+
return {"valid": False, "reason": "verification_method_missing"}
|
|
755
|
+
if algorithm == "none":
|
|
756
|
+
return {"valid": False, "reason": "verification_method_none"}
|
|
757
|
+
if algorithm == XOONYA_NATIVE_HYBRID_METHOD:
|
|
758
|
+
return {"valid": False, "reason": "native_hybrid_design_gated"}
|
|
759
|
+
if algorithm == LEGACY_OPRETURN_METHOD:
|
|
760
|
+
legacy_receipt = (receipt.get("compatibility") or {}).get("legacy_receipt")
|
|
761
|
+
if not isinstance(legacy_receipt, dict):
|
|
762
|
+
return {"valid": False, "reason": "legacy_receipt_missing"}
|
|
763
|
+
legacy_verification = verify_legacy_receipt(legacy_receipt.get("opreturn_hex"), canonicalize(ceb).encode("utf-8"))
|
|
764
|
+
if not legacy_verification["valid"]:
|
|
765
|
+
return {"valid": False, "reason": "legacy_receipt_" + legacy_verification["reason"]}
|
|
766
|
+
if normalize_string(receipt.get("signature")).lower() != normalize_string(legacy_receipt.get("opreturn_hex")).lower():
|
|
767
|
+
return {"valid": False, "reason": "signature_legacy_mismatch"}
|
|
768
|
+
state = "verified_legacy_opreturn"
|
|
769
|
+
elif algorithm == XOONYA_NATIVE_SHA256_METHOD:
|
|
770
|
+
expected_signature = build_native_receipt_signature(receipt, ceb)
|
|
771
|
+
if normalize_string(receipt.get("signature")) != expected_signature:
|
|
772
|
+
return {"valid": False, "reason": "signature_native_mismatch"}
|
|
773
|
+
state = "verified_native_digest"
|
|
774
|
+
elif algorithm == XOONYA_NATIVE_ANCHORED_METHOD:
|
|
775
|
+
anchored = _verify_native_anchored(receipt, ceb, bundle)
|
|
776
|
+
if not anchored["valid"]:
|
|
777
|
+
return {"valid": False, "reason": anchored["reason"]}
|
|
778
|
+
state = anchored["verification_state"]
|
|
779
|
+
elif algorithm == XOONYA_NATIVE_SIGNED_METHOD:
|
|
780
|
+
signed = verify_native_signed_receipt_proof(receipt, ceb)
|
|
781
|
+
if not signed["valid"]:
|
|
782
|
+
return {
|
|
783
|
+
"valid": False,
|
|
784
|
+
"reason": signed["reason"],
|
|
785
|
+
**({"dependency": signed["dependency"]} if signed.get("dependency") else {}),
|
|
786
|
+
}
|
|
787
|
+
state = signed["verification_state"]
|
|
788
|
+
elif algorithm in EXTERNAL_ASSERTION_METHODS:
|
|
789
|
+
signature = normalize_string(receipt.get("signature"))
|
|
790
|
+
if signature.startswith((
|
|
791
|
+
XOONYA_NATIVE_SHA256_METHOD + ":",
|
|
792
|
+
XOONYA_NATIVE_SIGNED_METHOD + ":",
|
|
793
|
+
XOONYA_NATIVE_ANCHORED_METHOD + ":",
|
|
794
|
+
XOONYA_NATIVE_HYBRID_METHOD + ":",
|
|
795
|
+
)):
|
|
796
|
+
return {"valid": False, "reason": "signature_method_mismatch"}
|
|
797
|
+
state = "accepted_detached_assertion" if algorithm == DETACHED_ASSERTION_METHOD else "accepted_external_proof_envelope"
|
|
798
|
+
else:
|
|
799
|
+
return {"valid": False, "reason": "verification_method_unsupported"}
|
|
800
|
+
return {
|
|
801
|
+
"valid": True,
|
|
802
|
+
"reason": None,
|
|
803
|
+
"version": PROOF_BUNDLE_VERSION,
|
|
804
|
+
"event_commitment": expected_commitment,
|
|
805
|
+
"receipt_id": expected_receipt_id,
|
|
806
|
+
"verification_method": algorithm,
|
|
807
|
+
"verification_state": state,
|
|
808
|
+
}
|
|
809
|
+
except Exception as error:
|
|
810
|
+
return {"valid": False, "reason": str(error)}
|