odingard-guardian 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.
@@ -0,0 +1,63 @@
1
+ """Guardian SDK for Python — Human Authority Infrastructure for Autonomous AI.
2
+
3
+ The agent proposes and the human decides. Every action gets a deterministic
4
+ ALLOW / ASK / DENY decision before it runs, and every execution produces a
5
+ tamper-evident receipt you can verify offline.
6
+ """
7
+
8
+ from ._canonical import CanonicalJsonError, canonical_json, canonical_sha256
9
+ from .client import (
10
+ DEFAULT_BASE_URL,
11
+ AsyncGuardianClient,
12
+ ExecuteResult,
13
+ GuardianClient,
14
+ PortableReceiptResult,
15
+ ReceiptsResult,
16
+ RequestResult,
17
+ build_proposal,
18
+ )
19
+ from .guard import (
20
+ Guard,
21
+ GuardianApprovalPending,
22
+ GuardianDenied,
23
+ GuardianExecutionFailed,
24
+ ResumeResult,
25
+ to_arguments_record,
26
+ )
27
+ from .lifecycle import GuardianDecision, classify_request_result
28
+ from .receipts import (
29
+ afetch_receipt_verification_keys,
30
+ averify_receipt_online,
31
+ fetch_receipt_verification_keys,
32
+ verify_receipt_offline,
33
+ verify_receipt_online,
34
+ )
35
+
36
+ __version__ = "0.1.0"
37
+
38
+ __all__ = [
39
+ "AsyncGuardianClient",
40
+ "CanonicalJsonError",
41
+ "DEFAULT_BASE_URL",
42
+ "ExecuteResult",
43
+ "Guard",
44
+ "GuardianApprovalPending",
45
+ "GuardianClient",
46
+ "GuardianDecision",
47
+ "GuardianDenied",
48
+ "GuardianExecutionFailed",
49
+ "PortableReceiptResult",
50
+ "ReceiptsResult",
51
+ "RequestResult",
52
+ "ResumeResult",
53
+ "afetch_receipt_verification_keys",
54
+ "averify_receipt_online",
55
+ "build_proposal",
56
+ "canonical_json",
57
+ "canonical_sha256",
58
+ "classify_request_result",
59
+ "fetch_receipt_verification_keys",
60
+ "to_arguments_record",
61
+ "verify_receipt_offline",
62
+ "verify_receipt_online",
63
+ ]
@@ -0,0 +1,151 @@
1
+ """guardian-canonical-json.v1 — byte-identical to the TypeScript implementation.
2
+
3
+ Mirrors ``packages/contracts/src/canonical-json.ts`` in the Guardian monorepo:
4
+
5
+ * object keys sorted by UTF-16 code units (JavaScript ``Array.prototype.sort``);
6
+ * strings escaped exactly like ``JSON.stringify``;
7
+ * numbers rendered exactly like JavaScript ``Number.prototype.toString``
8
+ (``-0`` becomes ``0``, non-finite numbers are rejected);
9
+ * depth limit 64, node budget 100 000, cycles rejected.
10
+
11
+ Golden vectors generated from the TypeScript implementation live in
12
+ ``tests/fixtures/canonical-json.json``.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import hashlib
18
+ import math
19
+ from decimal import Decimal
20
+ from typing import Any
21
+
22
+ __all__ = ["canonical_json", "canonical_sha256", "sha256_text", "CanonicalJsonError"]
23
+
24
+ _MAX_NODES = 100_000
25
+ _MAX_DEPTH = 64
26
+
27
+
28
+ class CanonicalJsonError(TypeError):
29
+ """The value cannot be represented in guardian-canonical-json.v1."""
30
+
31
+
32
+ _ESCAPES = {
33
+ '"': '\\"',
34
+ "\\": "\\\\",
35
+ "\b": "\\b",
36
+ "\f": "\\f",
37
+ "\n": "\\n",
38
+ "\r": "\\r",
39
+ "\t": "\\t",
40
+ }
41
+
42
+
43
+ def _js_string(value: str) -> str:
44
+ out = ['"']
45
+ for ch in value:
46
+ code = ord(ch)
47
+ if ch in _ESCAPES:
48
+ out.append(_ESCAPES[ch])
49
+ elif code < 0x20 or 0xD800 <= code <= 0xDFFF:
50
+ # Control characters and lone surrogates: lowercase \\uXXXX.
51
+ out.append(f"\\u{code:04x}")
52
+ else:
53
+ out.append(ch)
54
+ out.append('"')
55
+ return "".join(out)
56
+
57
+
58
+ def _js_number(value: float | int) -> str:
59
+ """ECMAScript Number::toString(10) for a finite double."""
60
+ number = float(value)
61
+ if not math.isfinite(number):
62
+ raise CanonicalJsonError("Canonical JSON does not permit non-finite numbers")
63
+ if number == 0:
64
+ return "0" # also normalizes -0
65
+ sign = "-" if number < 0 else ""
66
+ # repr() is the shortest round-trip representation — the same digits
67
+ # JavaScript selects. Re-render them with JavaScript's layout rules.
68
+ decimal = Decimal(repr(abs(number)))
69
+ _, digit_tuple, exponent = decimal.as_tuple()
70
+ digits = "".join(str(d) for d in digit_tuple).lstrip("0")
71
+ trailing = len(digits) - len(digits.rstrip("0"))
72
+ digits = digits.rstrip("0")
73
+ exponent = int(exponent) + trailing
74
+ k = len(digits)
75
+ n = exponent + k # value = 0.digits × 10^n
76
+ if k <= n <= 21:
77
+ body = digits + "0" * (n - k)
78
+ elif 0 < n <= 21:
79
+ body = digits[:n] + "." + digits[n:]
80
+ elif -6 < n <= 0:
81
+ body = "0." + "0" * (-n) + digits
82
+ else:
83
+ e = n - 1
84
+ mantissa = digits if k == 1 else digits[0] + "." + digits[1:]
85
+ body = f"{mantissa}e{'+' if e >= 0 else '-'}{abs(e)}"
86
+ return sign + body
87
+
88
+
89
+ def _utf16_key(key: str) -> bytes:
90
+ return key.encode("utf-16-be", "surrogatepass")
91
+
92
+
93
+ def _serialize(value: Any, ancestors: set[int], depth: int, budget: list[int]) -> str:
94
+ budget[0] -= 1
95
+ if budget[0] < 0:
96
+ raise CanonicalJsonError("Canonical JSON exceeds maximum node count")
97
+ if depth > _MAX_DEPTH:
98
+ raise CanonicalJsonError("Canonical JSON exceeds maximum depth")
99
+ if value is None:
100
+ return "null"
101
+ if value is True:
102
+ return "true"
103
+ if value is False:
104
+ return "false"
105
+ if isinstance(value, str):
106
+ return _js_string(value)
107
+ if isinstance(value, (int, float)):
108
+ return _js_number(value)
109
+ if isinstance(value, (list, tuple)):
110
+ marker = id(value)
111
+ if marker in ancestors:
112
+ raise CanonicalJsonError("Canonical JSON rejects cycles")
113
+ ancestors.add(marker)
114
+ try:
115
+ return "[" + ",".join(_serialize(v, ancestors, depth + 1, budget) for v in value) + "]"
116
+ finally:
117
+ ancestors.discard(marker)
118
+ if isinstance(value, dict):
119
+ marker = id(value)
120
+ if marker in ancestors:
121
+ raise CanonicalJsonError("Canonical JSON rejects cycles")
122
+ if any(not isinstance(k, str) for k in value):
123
+ raise CanonicalJsonError("Canonical JSON accepts only string keys")
124
+ ancestors.add(marker)
125
+ try:
126
+ keys = sorted(value.keys(), key=_utf16_key)
127
+ return (
128
+ "{"
129
+ + ",".join(
130
+ _js_string(k) + ":" + _serialize(value[k], ancestors, depth + 1, budget)
131
+ for k in keys
132
+ )
133
+ + "}"
134
+ )
135
+ finally:
136
+ ancestors.discard(marker)
137
+ raise CanonicalJsonError(f"Canonical JSON rejects {type(value).__name__}")
138
+
139
+
140
+ def canonical_json(value: Any) -> str:
141
+ """Serialize ``value`` as guardian-canonical-json.v1."""
142
+ return _serialize(value, set(), 0, [_MAX_NODES])
143
+
144
+
145
+ def canonical_sha256(value: Any) -> str:
146
+ """Hex SHA-256 of the canonical JSON (UTF-8)."""
147
+ return hashlib.sha256(canonical_json(value).encode("utf-8", "surrogatepass")).hexdigest()
148
+
149
+
150
+ def sha256_text(value: str) -> str:
151
+ return hashlib.sha256(value.encode("utf-8")).hexdigest()
@@ -0,0 +1,352 @@
1
+ """Evidence-chain verification — a faithful port of ``verifyEvidenceChain``.
2
+
3
+ Mirrors ``packages/evidence-primitives/src/evidence.ts`` in the Guardian
4
+ monorepo. Every record's payload digest and record hash is re-derived with
5
+ guardian-canonical-json.v1, the hash links and sequence are checked, and the
6
+ event order must follow the authority lifecycle (decision → capability →
7
+ adapter authentication → execution → effect), including the approval state
8
+ machine. Any malformed input fails closed.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import re
14
+ from typing import Any, Optional
15
+
16
+ from ._canonical import canonical_sha256
17
+
18
+ _CONTEXT_KEYS = ("tenantId", "principalId", "agentId", "requestId", "correlationId")
19
+ _RECORD_KEYS = {
20
+ "schemaVersion",
21
+ "id",
22
+ "tenantId",
23
+ "principalId",
24
+ "agentId",
25
+ "requestId",
26
+ "correlationId",
27
+ "eventTimeEpochMs",
28
+ "event",
29
+ "sequence",
30
+ "previousRecordHash",
31
+ "payloadDigest",
32
+ "recordHash",
33
+ }
34
+ _SHA256 = re.compile(r"^[a-f0-9]{64}$")
35
+
36
+ _APPROVAL_TRANSITIONS = {
37
+ "pending": ("approved", "denied", "revoked", "expired", "superseded"),
38
+ "approved": ("denied", "revoked", "expired", "superseded", "consumed"),
39
+ }
40
+ _HUMAN_DECISION_FOR_STATE = {"approved": "approve", "denied": "deny", "revoked": "revoke"}
41
+ _DECISION_FROM_STATES = {
42
+ "approve": ("pending",),
43
+ "deny": ("pending", "approved"),
44
+ "revoke": ("pending", "approved"),
45
+ }
46
+ _EXECUTION_EVENT_TYPES = {
47
+ "EXECUTION_ATTEMPTED",
48
+ "EXECUTION_ABORTED",
49
+ "EFFECT_PREPARED",
50
+ "EFFECT_COMMIT_AUTHORIZED",
51
+ "EFFECT_OUTCOME_UNKNOWN",
52
+ "EFFECT_UNRESOLVED",
53
+ "EFFECT_VERIFIED",
54
+ "EXECUTION_FAILED",
55
+ }
56
+ _PRE_COMMIT_FAILURE_CODES = {
57
+ "SENDER_ACCOUNT_MISMATCH",
58
+ "CREDENTIAL_UNAVAILABLE",
59
+ "PREPARE_REJECTED",
60
+ "PREPARE_AMBIGUOUS",
61
+ "READBACK_UNAVAILABLE",
62
+ "PREPARED_CONTENT_MISMATCH",
63
+ "COMMIT_NOT_AUTHORIZED",
64
+ "AUTHORITY_REVOKED_BEFORE_COMMIT",
65
+ "COMMIT_DEADLINE_EXPIRED",
66
+ }
67
+
68
+
69
+ def hash_evidence_payload(event: Any) -> str:
70
+ return canonical_sha256({"domain": "odingard.guardian.evidence.payload.v1", "event": event})
71
+
72
+
73
+ def hash_evidence_record_core(core: Any) -> str:
74
+ return canonical_sha256({"domain": "odingard.guardian.evidence.record.v1", "record": core})
75
+
76
+
77
+ def _invalid(reason: str, index: Optional[int]) -> dict:
78
+ return {"valid": False, "reasonCode": reason, "recordIndex": index}
79
+
80
+
81
+ def _is_int(value: Any) -> bool:
82
+ return isinstance(value, int) and not isinstance(value, bool)
83
+
84
+
85
+ def _well_formed(record: Any) -> bool:
86
+ if not isinstance(record, dict) or set(record.keys()) != _RECORD_KEYS:
87
+ return False
88
+ if record["schemaVersion"] != 1:
89
+ return False
90
+ for key in ("id", *_CONTEXT_KEYS):
91
+ if not isinstance(record[key], str) or not record[key]:
92
+ return False
93
+ if not _is_int(record["sequence"]) or record["sequence"] < 1:
94
+ return False
95
+ if not _is_int(record["eventTimeEpochMs"]) or record["eventTimeEpochMs"] < 0:
96
+ return False
97
+ if record["previousRecordHash"] is not None and not (
98
+ isinstance(record["previousRecordHash"], str) and _SHA256.match(record["previousRecordHash"])
99
+ ):
100
+ return False
101
+ for key in ("payloadDigest", "recordHash"):
102
+ if not isinstance(record[key], str) or not _SHA256.match(record[key]):
103
+ return False
104
+ event = record["event"]
105
+ return (
106
+ isinstance(event, dict)
107
+ and isinstance(event.get("eventType"), str)
108
+ and isinstance(event.get("payload"), dict)
109
+ )
110
+
111
+
112
+ class _ApprovalTracker:
113
+ def __init__(self) -> None:
114
+ self.approval_id: Optional[str] = None
115
+ self.state: Optional[str] = None
116
+ self.version = 0
117
+ self.state_digests: dict = {}
118
+ self.decisions: dict = {}
119
+
120
+ def human_approved_current_version(self) -> bool:
121
+ decision = self.decisions.get(self.version)
122
+ return self.state == "approved" and decision is not None and decision[0] == "approve"
123
+
124
+ def accept(self, event: dict) -> bool:
125
+ event_type = event["eventType"]
126
+ if event_type not in (
127
+ "APPROVAL_STATE_REFERENCED",
128
+ "APPROVAL_DECISION_RECORDED",
129
+ "APPROVAL_PRESENTED",
130
+ "APPROVAL_REJECTED",
131
+ ):
132
+ return True
133
+ payload = event["payload"]
134
+ if self.approval_id is not None and payload.get("approvalId") != self.approval_id:
135
+ return False
136
+ self.approval_id = payload.get("approvalId")
137
+ if event_type == "APPROVAL_PRESENTED":
138
+ return self.version >= 1
139
+ if event_type == "APPROVAL_REJECTED":
140
+ return True
141
+ digest = hash_evidence_payload(event)
142
+ version = payload.get("version")
143
+ if event_type == "APPROVAL_DECISION_RECORDED":
144
+ existing = self.decisions.get(version)
145
+ if existing is not None:
146
+ return existing[1] == digest and version in (self.version, self.version + 1)
147
+ if (
148
+ self.version < 1
149
+ or version != self.version + 1
150
+ or self.state is None
151
+ or self.state not in _DECISION_FROM_STATES.get(payload.get("decision"), ())
152
+ ):
153
+ return False
154
+ self.decisions[version] = (payload.get("decision"), digest)
155
+ return True
156
+ existing_state = self.state_digests.get(version)
157
+ if existing_state is not None:
158
+ return existing_state == digest and version == self.version
159
+ if version != self.version + 1:
160
+ return False
161
+ new_state = payload.get("state")
162
+ if self.state is None:
163
+ lifecycle_valid = new_state == "pending"
164
+ else:
165
+ lifecycle_valid = new_state in _APPROVAL_TRANSITIONS.get(self.state, ())
166
+ required = _HUMAN_DECISION_FOR_STATE.get(new_state)
167
+ recorded = self.decisions.get(version)
168
+ recorded_decision = recorded[0] if recorded else None
169
+ if not lifecycle_valid or (
170
+ recorded_decision is not None if required is None else recorded_decision != required
171
+ ):
172
+ return False
173
+ self.state_digests[version] = digest
174
+ self.version = version
175
+ self.state = new_state
176
+ return True
177
+
178
+
179
+ def verify_evidence_chain(chain: Any) -> dict:
180
+ """Verify an evidence chain. Returns ``{"valid": True, ...}`` or a reason code."""
181
+ try:
182
+ if not isinstance(chain, list) or len(chain) > 10_000:
183
+ return _invalid("MALFORMED_CHAIN", None)
184
+ if not all(_well_formed(r) for r in chain):
185
+ return _invalid("MALFORMED_CHAIN", None)
186
+ if not chain:
187
+ return {"valid": True, "count": 0, "lastRecordHash": None, "context": None}
188
+ first = chain[0]
189
+ if first["sequence"] != 1 or first["previousRecordHash"] is not None:
190
+ return _invalid("INVALID_GENESIS", 0)
191
+ ids = [r["id"] for r in chain]
192
+ if len(set(ids)) != len(ids):
193
+ return _invalid("DUPLICATE_EVIDENCE_ID", None)
194
+ sequences = [r["sequence"] for r in chain]
195
+ if len(set(sequences)) != len(sequences):
196
+ return _invalid("DUPLICATE_SEQUENCE", None)
197
+ if any(seq != i + 1 for i, seq in enumerate(sorted(sequences))):
198
+ return _invalid("SEQUENCE_GAP", None)
199
+
200
+ bound_action_digest = None
201
+ recorded_decision = None
202
+ last_decision_index = -1
203
+ approval = _ApprovalTracker()
204
+ issued_capabilities: set = set()
205
+ authenticated_adapters: set = set()
206
+ capability_used = False
207
+ execution_start = None
208
+ execution_key = None
209
+ prepared = False
210
+ prepared_index = -1
211
+ commit_authorized = False
212
+ outcome_unknown = False
213
+ unresolved = False
214
+ terminal = False
215
+
216
+ for index, record in enumerate(chain):
217
+ event = record["event"]
218
+ event_type = event["eventType"]
219
+ payload = event["payload"]
220
+ if record["sequence"] != index + 1:
221
+ return _invalid("RECORD_REORDERED", index)
222
+ if any(record[k] != first[k] for k in _CONTEXT_KEYS):
223
+ return _invalid("CONTEXT_MISMATCH", index)
224
+ if index > 0 and record["eventTimeEpochMs"] < chain[index - 1]["eventTimeEpochMs"]:
225
+ return _invalid("NON_MONOTONIC_EVENT_TIME", index)
226
+ if hash_evidence_payload(event) != record["payloadDigest"]:
227
+ return _invalid("PAYLOAD_DIGEST_MISMATCH", index)
228
+ if "actionDigest" in payload:
229
+ if bound_action_digest is None:
230
+ bound_action_digest = payload["actionDigest"]
231
+ elif payload["actionDigest"] != bound_action_digest:
232
+ return _invalid("ACTION_DIGEST_MISMATCH", index)
233
+ if event_type == "DECISION_RECORDED":
234
+ recorded_decision = payload.get("decision")
235
+ last_decision_index = index
236
+ if not approval.accept(event):
237
+ return _invalid("INVALID_EVENT_ORDER", index)
238
+ approval_state = approval.state
239
+ approval_satisfied = approval_state in ("approved", "consumed")
240
+
241
+ if event_type == "CAPABILITY_STATE_REFERENCED":
242
+ capability_id = payload.get("capabilityId")
243
+ state = payload.get("state")
244
+ if state == "issued":
245
+ human_approved = (
246
+ recorded_decision == "ASK" and approval.human_approved_current_version()
247
+ )
248
+ if recorded_decision != "ALLOW" and not human_approved:
249
+ return _invalid("INVALID_EVENT_ORDER", index)
250
+ issued_capabilities.add(capability_id)
251
+ elif capability_id not in issued_capabilities:
252
+ return _invalid("INVALID_EVENT_ORDER", index)
253
+ elif state == "used" and recorded_decision == "ASK" and approval_state != "consumed":
254
+ return _invalid("INVALID_EVENT_ORDER", index)
255
+ elif state == "used":
256
+ capability_used = True
257
+
258
+ if event_type == "ADAPTER_AUTHENTICATION_RECORDED":
259
+ if not issued_capabilities:
260
+ return _invalid("INVALID_EVENT_ORDER", index)
261
+ authenticated_adapters.add(payload.get("adapterId"))
262
+
263
+ if event_type in _EXECUTION_EVENT_TYPES and (execution_start == "aborted" or terminal):
264
+ return _invalid("INVALID_EVENT_ORDER", index)
265
+
266
+ if event_type in ("EXECUTION_ATTEMPTED", "EXECUTION_ABORTED"):
267
+ if (
268
+ execution_start is not None
269
+ or not capability_used
270
+ or payload.get("adapterId") not in authenticated_adapters
271
+ or recorded_decision is None
272
+ or recorded_decision == "DENY"
273
+ or (recorded_decision == "ASK" and not approval_satisfied)
274
+ ):
275
+ return _invalid("INVALID_EVENT_ORDER", index)
276
+ execution_key = f"{payload.get('adapterId')}:{payload.get('attemptReference')}"
277
+ execution_start = "attempted" if event_type == "EXECUTION_ATTEMPTED" else "aborted"
278
+
279
+ if event_type in _EXECUTION_EVENT_TYPES and event_type not in (
280
+ "EXECUTION_ATTEMPTED",
281
+ "EXECUTION_ABORTED",
282
+ ):
283
+ if "adapterId" not in payload or "attemptReference" not in payload:
284
+ return _invalid("INVALID_EVENT_ORDER", index)
285
+ attempt = f"{payload['adapterId']}:{payload['attemptReference']}"
286
+ if execution_start != "attempted" or attempt != execution_key:
287
+ return _invalid("INVALID_EVENT_ORDER", index)
288
+
289
+ if event_type == "EFFECT_PREPARED":
290
+ if prepared:
291
+ return _invalid("INVALID_EVENT_ORDER", index)
292
+ prepared = True
293
+ prepared_index = index
294
+
295
+ if event_type == "EFFECT_COMMIT_AUTHORIZED":
296
+ allows_commit = recorded_decision == "ALLOW" or (
297
+ recorded_decision == "ASK" and approval_state == "consumed"
298
+ )
299
+ if (
300
+ commit_authorized
301
+ or not prepared
302
+ or last_decision_index <= prepared_index
303
+ or not allows_commit
304
+ ):
305
+ return _invalid("INVALID_EVENT_ORDER", index)
306
+ commit_authorized = True
307
+
308
+ if event_type == "EFFECT_OUTCOME_UNKNOWN":
309
+ if not commit_authorized or outcome_unknown or unresolved:
310
+ return _invalid("INVALID_EVENT_ORDER", index)
311
+ outcome_unknown = True
312
+
313
+ if event_type == "EFFECT_UNRESOLVED":
314
+ if not commit_authorized or unresolved:
315
+ return _invalid("INVALID_EVENT_ORDER", index)
316
+ unresolved = True
317
+
318
+ if event_type == "EFFECT_VERIFIED":
319
+ if not commit_authorized:
320
+ return _invalid("INVALID_EVENT_ORDER", index)
321
+ terminal = True
322
+
323
+ if event_type == "EXECUTION_FAILED":
324
+ code = payload.get("failureCode")
325
+ if (outcome_unknown or unresolved) and code not in (
326
+ "EFFECT_MISMATCH",
327
+ "DUPLICATE_EFFECT_DETECTED",
328
+ ):
329
+ return _invalid("INVALID_EVENT_ORDER", index)
330
+ if code in ("COMMIT_REJECTED", "COMMIT_NOT_ISSUED") and (
331
+ not commit_authorized or outcome_unknown or unresolved
332
+ ):
333
+ return _invalid("INVALID_EVENT_ORDER", index)
334
+ if commit_authorized and code in _PRE_COMMIT_FAILURE_CODES:
335
+ return _invalid("INVALID_EVENT_ORDER", index)
336
+ terminal = True
337
+
338
+ expected_previous = None if index == 0 else chain[index - 1]["recordHash"]
339
+ if record["previousRecordHash"] != expected_previous:
340
+ return _invalid("BROKEN_LINK", index)
341
+ core = {k: v for k, v in record.items() if k != "recordHash"}
342
+ if hash_evidence_record_core(core) != record["recordHash"]:
343
+ return _invalid("RECORD_HASH_MISMATCH", index)
344
+
345
+ return {
346
+ "valid": True,
347
+ "count": len(chain),
348
+ "lastRecordHash": chain[-1]["recordHash"],
349
+ "context": {k: first[k] for k in _CONTEXT_KEYS},
350
+ }
351
+ except Exception: # noqa: BLE001 — fail closed on anything unexpected
352
+ return _invalid("MALFORMED_CHAIN", None)