aot-ev1 0.1.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.
aot_ev1/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ """AOT-EV1 — deterministic evidence layer (see SPEC.md)."""
2
+
3
+ __version__ = "0.1.0"
4
+ __all__ = ["__version__"]
aot_ev1/anchor.py ADDED
@@ -0,0 +1,16 @@
1
+ """Optional anchoring hook — optional, not part of core.
2
+
3
+ AOT-EV1 deliberately ships without any anchoring (no BTC timestamps, no
4
+ network calls). A future extra may implement :func:`anchor_bundle`; until
5
+ then it is an explicit stub so callers fail loudly instead of assuming that
6
+ an evidence bundle has been anchored.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any, NoReturn
12
+
13
+
14
+ def anchor_bundle(*args: Any, **kwargs: Any) -> NoReturn:
15
+ """Anchor a bundle (optional, not part of core) — not implemented."""
16
+ raise NotImplementedError("optional, not part of core")
aot_ev1/bundle.py ADDED
@@ -0,0 +1,196 @@
1
+ """Deterministic ``.aotev.zip`` bundles (SPEC.md §6, §7)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import io
7
+ import zipfile
8
+ from pathlib import Path
9
+ from typing import Iterable, Mapping
10
+
11
+ from .canonical import canonical_bytes, check_canonical_payload, evidence_hash_hex
12
+ from .errors import AOTEV1Error
13
+ from .jcs_strict import MAX_SAFE_INT
14
+ from .schema_lite import validate_payload
15
+
16
+ __all__ = [
17
+ "MANIFEST_NAME",
18
+ "PAYLOAD_NAME",
19
+ "MAX_ENTRIES",
20
+ "MAX_UNCOMPRESSED",
21
+ "ZIP_DATE_TIME",
22
+ "ZIP_EXTERNAL_ATTR",
23
+ "signature_file",
24
+ "bundle_hash",
25
+ "build_manifest",
26
+ "bundle_bytes",
27
+ "write_bundle",
28
+ "read_signed_dir",
29
+ ]
30
+
31
+ MANIFEST_NAME = "manifest.json"
32
+ PAYLOAD_NAME = "payload.json"
33
+ MAX_ENTRIES = 32
34
+ MAX_UNCOMPRESSED = 8 * 1024 * 1024
35
+ ZIP_DATE_TIME = (1980, 1, 1, 0, 0, 0)
36
+ ZIP_EXTERNAL_ATTR = 0o100644 << 16
37
+
38
+ _SIGNATURE_SUFFIX = {"ed25519": "ed25519", "eip712": "eip712"}
39
+ _SIGNATURE_SIZE = {"ed25519": 64, "eip712": 65}
40
+
41
+
42
+ def signature_file(role: str, scheme: str) -> str:
43
+ try:
44
+ suffix = _SIGNATURE_SUFFIX[scheme]
45
+ except KeyError:
46
+ raise AOTEV1Error(f"unsupported signature scheme: {scheme!r}") from None
47
+ if role not in ("issuer", "auditor"):
48
+ raise AOTEV1Error(f"unsupported signature role: {role!r}")
49
+ return f"signatures/{role}.{suffix}"
50
+
51
+
52
+ def bundle_hash(entries: Iterable[tuple[str, bytes]]) -> bytes:
53
+ """``sha256`` over ``name + b"\\x00" + sha256(content)`` sorted by name."""
54
+ parts = []
55
+ for name, content in sorted(entries, key=lambda item: item[0]):
56
+ parts.append(name.encode("ascii") + b"\x00" + hashlib.sha256(content).digest())
57
+ return hashlib.sha256(b"".join(parts)).digest()
58
+
59
+
60
+ def _signature_entries(
61
+ payload: dict, signatures: Mapping[str, bytes]
62
+ ) -> list[tuple[str, bytes]]:
63
+ for role in signatures:
64
+ if role not in ("issuer", "auditor"):
65
+ raise AOTEV1Error(f"unsupported signature role: {role!r}")
66
+ if "issuer" not in signatures:
67
+ raise AOTEV1Error("missing issuer signature")
68
+ if payload["auditor"] is None and "auditor" in signatures:
69
+ raise AOTEV1Error("auditor signature given but payload.auditor is null")
70
+ if payload["auditor"] is not None and "auditor" not in signatures:
71
+ raise AOTEV1Error("missing auditor signature")
72
+
73
+ entries: list[tuple[str, bytes]] = []
74
+ for role in ("issuer", "auditor"):
75
+ if role not in signatures:
76
+ continue
77
+ identity = payload[role]
78
+ scheme = identity["scheme"]
79
+ signature = bytes(signatures[role])
80
+ expected = _SIGNATURE_SIZE[scheme]
81
+ if len(signature) != expected:
82
+ raise AOTEV1Error(
83
+ f"{role} signature must be {expected} bytes for {scheme}, got {len(signature)}"
84
+ )
85
+ entries.append((signature_file(role, scheme), signature))
86
+ return entries
87
+
88
+
89
+ def build_manifest(
90
+ payload: dict,
91
+ data_entries: list[tuple[str, bytes]],
92
+ created_at: int,
93
+ ) -> dict:
94
+ signatures = []
95
+ for role in ("issuer", "auditor"):
96
+ identity = payload[role]
97
+ if identity is None:
98
+ continue
99
+ signatures.append(
100
+ {
101
+ "role": role,
102
+ "scheme": identity["scheme"],
103
+ "key_id": identity["key_id"],
104
+ "file": signature_file(role, identity["scheme"]),
105
+ }
106
+ )
107
+ payload_bytes = dict(data_entries)[PAYLOAD_NAME]
108
+ return {
109
+ "v": "1",
110
+ "created_at": created_at,
111
+ "evidence_hash": evidence_hash_hex(payload),
112
+ "payload_sha256": hashlib.sha256(payload_bytes).hexdigest(),
113
+ "entries": [
114
+ {"name": name, "sha256": hashlib.sha256(content).hexdigest(), "size": len(content)}
115
+ for name, content in sorted(data_entries, key=lambda item: item[0])
116
+ ],
117
+ "signatures": signatures,
118
+ "bundle_hash": bundle_hash(data_entries).hex(),
119
+ }
120
+
121
+
122
+ def _zip_info(name: str) -> zipfile.ZipInfo:
123
+ info = zipfile.ZipInfo(name, date_time=ZIP_DATE_TIME)
124
+ info.compress_type = zipfile.ZIP_STORED
125
+ info.create_system = 0
126
+ info.external_attr = ZIP_EXTERNAL_ATTR
127
+ info.flag_bits = 0
128
+ info.extra = b""
129
+ info.comment = b""
130
+ return info
131
+
132
+
133
+ def _write_zip(records: list[tuple[str, bytes]]) -> bytes:
134
+ buffer = io.BytesIO()
135
+ with zipfile.ZipFile(buffer, "w", compression=zipfile.ZIP_STORED, allowZip64=False) as archive:
136
+ for name, content in records:
137
+ info = _zip_info(name)
138
+ info.file_size = len(content)
139
+ archive.writestr(info, content)
140
+ return buffer.getvalue()
141
+
142
+
143
+ def bundle_bytes(
144
+ payload: dict,
145
+ signatures: Mapping[str, bytes],
146
+ created_at: int | None = None,
147
+ ) -> bytes:
148
+ """Build deterministic zip bytes for ``payload`` and its signatures."""
149
+ validate_payload(payload)
150
+ payload_bytes = canonical_bytes(payload)
151
+ if created_at is None:
152
+ created_at = payload["valid_from"]
153
+ if not isinstance(created_at, int) or isinstance(created_at, bool):
154
+ raise AOTEV1Error("created_at must be an integer")
155
+ if created_at < 0 or created_at > MAX_SAFE_INT:
156
+ raise AOTEV1Error("created_at outside the safe integer range")
157
+
158
+ data_entries = [(PAYLOAD_NAME, payload_bytes)]
159
+ data_entries.extend(_signature_entries(payload, signatures))
160
+ if len(data_entries) + 1 > MAX_ENTRIES:
161
+ raise AOTEV1Error(f"bundle exceeds {MAX_ENTRIES} entries")
162
+ if sum(len(content) for _, content in data_entries) > MAX_UNCOMPRESSED:
163
+ raise AOTEV1Error(f"bundle exceeds {MAX_UNCOMPRESSED} uncompressed bytes")
164
+
165
+ manifest = build_manifest(payload, data_entries, created_at)
166
+ records = [(MANIFEST_NAME, canonical_bytes(manifest))]
167
+ records.extend(sorted(data_entries, key=lambda item: item[0]))
168
+ return _write_zip(records)
169
+
170
+
171
+ def write_bundle(
172
+ path: str | Path,
173
+ payload: dict,
174
+ signatures: Mapping[str, bytes],
175
+ created_at: int | None = None,
176
+ ) -> str:
177
+ """Write ``bundle_bytes`` to ``path`` and return the sha256 hex digest."""
178
+ data = bundle_bytes(payload, signatures, created_at)
179
+ target = Path(path)
180
+ target.parent.mkdir(parents=True, exist_ok=True)
181
+ target.write_bytes(data)
182
+ return hashlib.sha256(data).hexdigest()
183
+
184
+
185
+ def read_signed_dir(directory: str | Path) -> tuple[dict, dict[str, bytes]]:
186
+ """Read ``payload.json`` + ``signatures/`` produced by ``aotev sign``."""
187
+ root = Path(directory)
188
+ payload = check_canonical_payload((root / PAYLOAD_NAME).read_bytes())
189
+ signatures: dict[str, bytes] = {}
190
+ for role in ("issuer", "auditor"):
191
+ identity = payload[role]
192
+ if identity is None:
193
+ continue
194
+ path = root / signature_file(role, identity["scheme"])
195
+ signatures[role] = path.read_bytes()
196
+ return payload, signatures
aot_ev1/canonical.py ADDED
@@ -0,0 +1,119 @@
1
+ """Payload bytes, canonical form and ``evidence_hash`` (SPEC.md §1, §3)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ from typing import Any, NoReturn
8
+
9
+ from .errors import CanonicalError
10
+ from .jcs_strict import MAX_SAFE_INT, canonical_bytes, canonical_text
11
+
12
+ __all__ = [
13
+ "EVIDENCE_DOMAIN",
14
+ "canonical_bytes",
15
+ "canonical_text",
16
+ "evidence_hash",
17
+ "evidence_hash_hex",
18
+ "parse_payload",
19
+ "parse_json",
20
+ "check_canonical_payload",
21
+ ]
22
+
23
+ EVIDENCE_DOMAIN = b"AOT-EV1"
24
+
25
+
26
+ def _reject_float(token: str) -> NoReturn:
27
+ raise CanonicalError(f"floats are not allowed: {token}")
28
+
29
+
30
+ def _reject_constant(token: str) -> NoReturn:
31
+ raise CanonicalError(f"non-finite numbers are not allowed: {token}")
32
+
33
+
34
+ def _parse_int(token: str) -> int:
35
+ if token == "-0":
36
+ raise CanonicalError("negative zero is not allowed")
37
+ value = int(token)
38
+ if value < -MAX_SAFE_INT or value > MAX_SAFE_INT:
39
+ raise CanonicalError(f"integer outside safe range: {token}")
40
+ return value
41
+
42
+
43
+ def _pairs_hook(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
44
+ result: dict[str, Any] = {}
45
+ for key, value in pairs:
46
+ if key in result:
47
+ raise CanonicalError(f"duplicate object key: {key!r}")
48
+ result[key] = value
49
+ return result
50
+
51
+
52
+ def _check_node(value: Any) -> None:
53
+ if isinstance(value, str):
54
+ for ch in value:
55
+ if 0xD800 <= ord(ch) <= 0xDFFF:
56
+ raise CanonicalError("lone surrogate in string")
57
+ elif isinstance(value, list):
58
+ for item in value:
59
+ _check_node(item)
60
+ elif isinstance(value, dict):
61
+ for key, item in value.items():
62
+ if not isinstance(key, str):
63
+ raise CanonicalError(f"object key is not a string: {type(key).__name__}")
64
+ for ch in key:
65
+ if 0xD800 <= ord(ch) <= 0xDFFF:
66
+ raise CanonicalError("lone surrogate in object key")
67
+ _check_node(item)
68
+
69
+
70
+ def parse_json(data: bytes | str) -> Any:
71
+ """Parse strict JSON: no floats, no ``-0``, no duplicate keys."""
72
+ if isinstance(data, bytes):
73
+ try:
74
+ text = data.decode("utf-8")
75
+ except UnicodeDecodeError as exc:
76
+ raise CanonicalError(f"payload is not valid UTF-8: {exc}") from exc
77
+ else:
78
+ text = data
79
+ if text.startswith("\ufeff"):
80
+ raise CanonicalError("byte-order mark is not allowed")
81
+ try:
82
+ value = json.loads(
83
+ text,
84
+ object_pairs_hook=_pairs_hook,
85
+ parse_float=_reject_float,
86
+ parse_constant=_reject_constant,
87
+ parse_int=_parse_int,
88
+ )
89
+ except CanonicalError:
90
+ raise
91
+ except ValueError as exc:
92
+ raise CanonicalError(f"invalid JSON: {exc}") from exc
93
+ _check_node(value)
94
+ return value
95
+
96
+
97
+ def parse_payload(data: bytes | str) -> dict[str, Any]:
98
+ """Parse ``payload.json`` bytes and require a top-level object."""
99
+ value = parse_json(data)
100
+ if not isinstance(value, dict):
101
+ raise CanonicalError("payload must be a JSON object")
102
+ return value
103
+
104
+
105
+ def evidence_hash(payload: Any) -> bytes:
106
+ """``sha256(b"AOT-EV1" + canonical_bytes)`` (SPEC.md §1)."""
107
+ return hashlib.sha256(EVIDENCE_DOMAIN + canonical_bytes(payload)).digest()
108
+
109
+
110
+ def evidence_hash_hex(payload: Any) -> str:
111
+ return evidence_hash(payload).hex()
112
+
113
+
114
+ def check_canonical_payload(raw: bytes) -> dict[str, Any]:
115
+ """Step 6 helper: parse ``raw`` and require it to be canonical bytes."""
116
+ payload = parse_payload(raw)
117
+ if canonical_bytes(payload) != raw:
118
+ raise CanonicalError("payload.json is not stored in canonical bytes")
119
+ return payload
aot_ev1/cli.py ADDED
@@ -0,0 +1,206 @@
1
+ """Command line interface for AOT-EV1 (see SPEC.md)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Sequence
9
+
10
+ from . import __version__
11
+ from .canonical import canonical_bytes, evidence_hash_hex, parse_payload
12
+ from .errors import AOTEV1Error, exit_code_for
13
+ from .bundle import read_signed_dir, write_bundle
14
+ from .verify import verify_bundle
15
+ from .schema_lite import validate_payload
16
+ from .sign import eip712_types, sign_evidence
17
+
18
+ __all__ = ["main"]
19
+
20
+
21
+ def _read_input(path: str | None) -> bytes:
22
+ if path is None or path == "-":
23
+ return sys.stdin.buffer.read()
24
+ with open(path, "rb") as handle:
25
+ return handle.read()
26
+
27
+
28
+ def _cmd_canonical(args: argparse.Namespace) -> int:
29
+ raw = _read_input(args.path)
30
+ payload = parse_payload(raw)
31
+ data = canonical_bytes(payload)
32
+ sys.stdout.write(f"canonical_bytes: {data.hex()}\n")
33
+ sys.stdout.write(f"canonical_json: {data.decode('ascii')}\n")
34
+ sys.stdout.write(f"evidence_hash: {evidence_hash_hex(payload)}\n")
35
+ return 0
36
+
37
+
38
+ def _cmd_sign(args: argparse.Namespace) -> int:
39
+ payload = parse_payload(_read_input(args.payload))
40
+ validate_payload(payload)
41
+ out = Path(args.out)
42
+ out.mkdir(parents=True, exist_ok=True)
43
+ if payload["issuer"]["scheme"] == "ed25519":
44
+ issuer_file = "signatures/issuer.ed25519"
45
+ else:
46
+ issuer_file = "signatures/issuer.eip712"
47
+ (out / "signatures").mkdir(exist_ok=True)
48
+ (out / "payload.json").write_bytes(canonical_bytes(payload))
49
+ (out / issuer_file).write_bytes(sign_evidence(payload, "issuer", args.issuer_key, args.chain_id))
50
+ sys.stdout.write(f"issuer: {issuer_file} ({payload['issuer']['key_id']})\n")
51
+ if payload["auditor"] is not None:
52
+ if not args.auditor_key:
53
+ raise AOTEV1Error("payload has an auditor but --auditor-key was not given")
54
+ auditor_file = (
55
+ "signatures/auditor.ed25519"
56
+ if payload["auditor"]["scheme"] == "ed25519"
57
+ else "signatures/auditor.eip712"
58
+ )
59
+ (out / auditor_file).write_bytes(
60
+ sign_evidence(payload, "auditor", args.auditor_key, args.chain_id)
61
+ )
62
+ sys.stdout.write(f"auditor: {auditor_file} ({payload['auditor']['key_id']})\n")
63
+ elif args.auditor_key:
64
+ raise AOTEV1Error("--auditor-key given but payload.auditor is null")
65
+ sys.stdout.write(f"payload: payload.json\nevidence_hash: {evidence_hash_hex(payload)}\n")
66
+ return 0
67
+
68
+
69
+ def _print_types() -> None:
70
+ from .sign import PAYLOAD_FIELDS
71
+
72
+ sys.stdout.write("payload fields:\n")
73
+ for name, kind in PAYLOAD_FIELDS:
74
+ sys.stdout.write(f" {name}: {kind}\n")
75
+ sys.stdout.write("eip712 types:\n")
76
+ for type_name, fields in eip712_types().items():
77
+ rendered = ", ".join(f"{item['name']} {item['type']}" for item in fields)
78
+ sys.stdout.write(f" {type_name}({rendered})\n")
79
+
80
+
81
+ def _cmd_inspect(args: argparse.Namespace) -> int:
82
+ if args.types:
83
+ _print_types()
84
+ if not args.path:
85
+ return 0
86
+ if not args.path:
87
+ raise AOTEV1Error("inspect needs a bundle path or --types")
88
+ import json
89
+ import zipfile
90
+
91
+ from .bundle import MANIFEST_NAME, PAYLOAD_NAME
92
+ from .canonical import parse_json
93
+
94
+ try:
95
+ with zipfile.ZipFile(args.path) as archive:
96
+ manifest = parse_json(archive.read(MANIFEST_NAME))
97
+ payload_bytes = archive.read(PAYLOAD_NAME)
98
+ payload = parse_json(payload_bytes)
99
+ except (zipfile.BadZipFile, KeyError, OSError) as exc:
100
+ raise AOTEV1Error(f"cannot inspect bundle: {exc}") from exc
101
+ sys.stdout.write(f"evidence_hash: {manifest['evidence_hash']}\n")
102
+ sys.stdout.write(f"created_at: {manifest['created_at']}\n")
103
+ sys.stdout.write(f"bundle_hash: {manifest['bundle_hash']}\n")
104
+ sys.stdout.write("entries:\n")
105
+ for entry in manifest["entries"]:
106
+ sys.stdout.write(f" {entry['name']} {entry['size']} {entry['sha256']}\n")
107
+ sys.stdout.write("signatures:\n")
108
+ for signature in manifest["signatures"]:
109
+ sys.stdout.write(
110
+ f" {signature['role']} {signature['scheme']} {signature['key_id']} {signature['file']}\n"
111
+ )
112
+ sys.stdout.write("payload:\n")
113
+ sys.stdout.write(json.dumps(payload, indent=2, ensure_ascii=True, sort_keys=True) + "\n")
114
+ return 0
115
+
116
+
117
+ def _cmd_bundle(args: argparse.Namespace) -> int:
118
+ payload, signatures = read_signed_dir(args.directory)
119
+ out = Path(args.out) if args.out else Path(f"{evidence_hash_hex(payload)}.aotev.zip")
120
+ digest = write_bundle(out, payload, signatures, args.created_at)
121
+ sys.stdout.write(f"bundle: {out}\n")
122
+ sys.stdout.write(f"evidence_hash: {evidence_hash_hex(payload)}\n")
123
+ sys.stdout.write(f"bundle_sha256: {digest}\n")
124
+ return 0
125
+
126
+
127
+ def _cmd_verify(args: argparse.Namespace) -> int:
128
+ result = verify_bundle(
129
+ args.bundle,
130
+ now=args.now,
131
+ chain_path=args.chain,
132
+ chain_id=args.chain_id,
133
+ )
134
+ for step in result.steps:
135
+ state = "ok" if step.ok else (step.error or "failed")
136
+ sys.stdout.write(f"step {step.number:2d}: {state:<14} {step.name}\n")
137
+ sys.stdout.write(f"status: {result.status}\n")
138
+ if result.bundle_sha256:
139
+ sys.stdout.write(f"bundle_sha256: {result.bundle_sha256}\n")
140
+ if result.evidence_hash:
141
+ sys.stdout.write(f"evidence_hash: {result.evidence_hash}\n")
142
+ sys.stdout.write(f"chain: {result.chain}\n")
143
+ if result.error is not None:
144
+ sys.stdout.write(f"error: {result.error}\n")
145
+ return result.exit_code
146
+
147
+
148
+ def build_parser() -> argparse.ArgumentParser:
149
+ parser = argparse.ArgumentParser(
150
+ prog="aotev",
151
+ description="AOT-EV1 — deterministic evidence layer for ERC-8325.",
152
+ epilog="Exit codes: 0 valid, 2 broken (steps 1-10), 3 expired/revoked, 4 chain error.",
153
+ )
154
+ parser.add_argument("--version", action="version", version=f"aot-ev1 {__version__}")
155
+ sub = parser.add_subparsers(dest="command", required=True)
156
+
157
+ canonical = sub.add_parser("canonical", help="print canonical payload bytes and evidence_hash")
158
+ canonical.add_argument("path", nargs="?", help="payload JSON file (default: stdin)")
159
+ canonical.set_defaults(func=_cmd_canonical)
160
+
161
+ sign = sub.add_parser("sign", help="sign a payload and write payload.json + signatures/")
162
+ sign.add_argument("--payload", required=True, help="payload JSON file")
163
+ sign.add_argument("--issuer-key", required=True, help="Ed25519 seed/secret hex or 0x EVM private key")
164
+ sign.add_argument("--auditor-key", help="auditor key, required when payload.auditor is set")
165
+ sign.add_argument("--chain-id", type=int, default=1, help="EIP-712 chainId (default: 1)")
166
+ sign.add_argument("--out", required=True, help="output directory")
167
+ sign.set_defaults(func=_cmd_sign)
168
+
169
+ inspect = sub.add_parser("inspect", help="inspect a payload bundle or print type information")
170
+ inspect.add_argument("path", nargs="?", help="bundle file")
171
+ inspect.add_argument("--types", action="store_true", help="print payload and EIP-712 type tables")
172
+ inspect.set_defaults(func=_cmd_inspect)
173
+
174
+ bundle = sub.add_parser("bundle", help="pack a signed payload directory into a deterministic zip")
175
+ bundle.add_argument("directory", help="directory with payload.json and signatures/")
176
+ bundle.add_argument("-o", "--out", help="output zip path (default: <evidence_hash>.aotev.zip)")
177
+ bundle.add_argument("--created-at", type=int, help="manifest created_at (default: payload.valid_from)")
178
+ bundle.set_defaults(func=_cmd_bundle)
179
+
180
+ verify = sub.add_parser("verify", help="verify a bundle offline (12 checks)")
181
+ verify.add_argument("bundle", help="bundle file")
182
+ verify.add_argument("--chain", help="previous bundle for chain continuity (optional)")
183
+ verify.add_argument("--now", type=int, help="verification time in unix seconds (default: system clock)")
184
+ verify.add_argument("--chain-id", type=int, default=1, help="EIP-712 chainId (default: 1)")
185
+ verify.set_defaults(func=_cmd_verify)
186
+ return parser
187
+
188
+
189
+ def main(argv: Sequence[str] | None = None) -> int:
190
+ parser = build_parser()
191
+ args = parser.parse_args(argv)
192
+ try:
193
+ return int(args.func(args))
194
+ except AOTEV1Error as exc:
195
+ sys.stderr.write(f"error: {exc}\n")
196
+ return exit_code_for(exc)
197
+ except ImportError as exc:
198
+ sys.stderr.write(f"error: {exc}\n")
199
+ return 2
200
+ except OSError as exc:
201
+ sys.stderr.write(f"error: {exc}\n")
202
+ return 2
203
+
204
+
205
+ if __name__ == "__main__": # pragma: no cover
206
+ raise SystemExit(main())
@@ -0,0 +1,49 @@
1
+ """Strict Ed25519 verification profile (D2, 2026-11-02).
2
+
3
+ Rejects non-canonical/small-order public keys, S >= L, and non-canonical R.
4
+ Used for both agent (sig_A) and notary (sig_N) signatures.
5
+ """
6
+ import nacl.bindings
7
+ from nacl.exceptions import BadSignatureError
8
+ from nacl.signing import VerifyKey
9
+
10
+ L = 2**252 + 27742317777372353535851937790883648493
11
+
12
+ # Canonical encodings of the eight small-order points (identity, order 2, etc.).
13
+ SMALL_ORDER_POINTS = {
14
+ bytes.fromhex("00" * 32),
15
+ bytes.fromhex("01" + "00" * 31),
16
+ bytes.fromhex("ec" + "ff" * 31),
17
+ bytes.fromhex("ed" + "ff" * 31),
18
+ bytes.fromhex("26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05"),
19
+ bytes.fromhex("c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a"),
20
+ bytes.fromhex("c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa"),
21
+ bytes.fromhex("26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc85"),
22
+ }
23
+
24
+
25
+ def _canonical_point(enc: bytes) -> bool:
26
+ if len(enc) != 32:
27
+ return False
28
+ try:
29
+ return bool(nacl.bindings.crypto_core_ed25519_is_valid_point(enc))
30
+ except Exception:
31
+ return False
32
+
33
+
34
+ def verify_strict(pubkey: bytes, message: bytes, signature: bytes) -> bool:
35
+ if len(pubkey) != 32 or len(signature) != 64:
36
+ return False
37
+ pubkey = bytes(pubkey)
38
+ if pubkey in SMALL_ORDER_POINTS or not _canonical_point(pubkey):
39
+ return False
40
+ s = int.from_bytes(signature[32:], "little")
41
+ if s >= L:
42
+ return False
43
+ if not _canonical_point(signature[:32]):
44
+ return False
45
+ try:
46
+ VerifyKey(pubkey).verify(bytes(message), bytes(signature))
47
+ return True
48
+ except (BadSignatureError, ValueError, TypeError):
49
+ return False