@passioncode-ai/fabric-agent-adapter 0.39.1 → 0.40.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/CHANGELOG.md +79 -0
- package/README.md +9 -3
- package/package.json +1 -1
- package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +15 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +64 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/config.example.json +15 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/knowledge.example.json +13 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support_agent.py +264 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +93 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +3 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +128 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/usage.md +2 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +10 -183
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +204 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-auth.mjs +286 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-backups.mjs +327 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-base.mjs +47 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-fixture.mjs +79 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +840 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-shapes.mjs +340 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +42 -1237
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ceremonies.mjs +1 -16
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-crypto.mjs +41 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +13 -45
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +387 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +8 -67
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-events.mjs +52 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +231 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-jsvalue.mjs +67 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +202 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-logs.mjs +136 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +335 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +2 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-paths.mjs +69 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-platform.mjs +164 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-profiles.mjs +1 -32
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +122 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +177 -2541
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-share.mjs +1 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +400 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-time.mjs +46 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-usage.mjs +458 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_auth.py +402 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_backups.py +399 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_base.py +78 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +64 -1398
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_fixture.py +116 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +902 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_shapes.py +457 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ceremonies.py +36 -38
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_crypto.py +289 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +15 -180
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +586 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +39 -400
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +137 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +402 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +1 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_jsvalue.py +310 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_kit.py +243 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_lock.py +308 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_logs.py +202 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +431 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +21 -43
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_paths.py +102 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_platform.py +338 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_profiles.py +31 -38
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +173 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +298 -3450
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_share.py +8 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +412 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_time.py +120 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +697 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +1526 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +3 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +116 -1392
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +270 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +508 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_hosts.py +265 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mail.py +155 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +335 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/host_hooks.py +1 -0
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ceremonies.py
CHANGED
|
@@ -62,8 +62,8 @@ def _finite(value: float) -> bool:
|
|
|
62
62
|
def _register_ok(kind: str, fields: Dict[str, Any]) -> bool:
|
|
63
63
|
if kind == ENROLL_TOKEN_TYPE:
|
|
64
64
|
return (_matches(_ESTATE_SCOPE, fields["scope"]) and _matches(_SHA256, fields["genesis"]) and fields["purpose"] == ENROLL_PURPOSE
|
|
65
|
-
and (fields["ceiling"] is None or _matches(_SHA256, fields["ceiling"])) and _finite(
|
|
66
|
-
and _finite(
|
|
65
|
+
and (fields["ceiling"] is None or _matches(_SHA256, fields["ceiling"])) and _finite(_ftime.parse_rfc3339_ms(fields["notBefore"]))
|
|
66
|
+
and _finite(_ftime.parse_rfc3339_ms(fields["expiresAt"])) and _matches(_NONCE32, fields["nonce"]) and _is_one(fields["uses"]))
|
|
67
67
|
return _matches(_SHA256, fields["digest"]) and _matches(_NONCE32, fields["nonce"])
|
|
68
68
|
|
|
69
69
|
|
|
@@ -77,7 +77,7 @@ def signing_statement(kind: str, fields: Any) -> Optional[bytes]:
|
|
|
77
77
|
if not _register_ok(kind, fields):
|
|
78
78
|
return None
|
|
79
79
|
try:
|
|
80
|
-
return
|
|
80
|
+
return _jsv.jcs(dict(fields, type=kind)).encode("utf-8")
|
|
81
81
|
except (TypeError, ValueError):
|
|
82
82
|
return None
|
|
83
83
|
|
|
@@ -100,7 +100,7 @@ def verify_enroll_token(token: Any, *, record: Any, genesis: Any, at: Any) -> Di
|
|
|
100
100
|
clock (an aware datetime or epoch seconds). The first failure answers: no-clock, malformed, wrong-estate, the passkey
|
|
101
101
|
(no-trusted-authority, revoked-authority, duplicate-authority, unknown-authority), the assertion, then the window
|
|
102
102
|
(too-long-lived, not-yet-valid, expired). Success: ``{"ok": True, "nonce", "expiresAt", "signedBy"}``."""
|
|
103
|
-
now =
|
|
103
|
+
now = _ftime.clock_ms(at)
|
|
104
104
|
if not _finite(now):
|
|
105
105
|
return {"ok": False, "problem": "no-clock"}
|
|
106
106
|
fields = _statement_fields(token)
|
|
@@ -112,31 +112,31 @@ def verify_enroll_token(token: Any, *, record: Any, genesis: Any, at: Any) -> Di
|
|
|
112
112
|
return {"ok": False, "problem": "malformed"}
|
|
113
113
|
if fields["scope"] != "estate:%s" % record["estate"] or not isinstance(genesis, str) or fields["genesis"] != genesis:
|
|
114
114
|
return {"ok": False, "problem": "wrong-estate"}
|
|
115
|
-
authorities =
|
|
115
|
+
authorities = _jsv.objects(record.get("authorities"))
|
|
116
116
|
if not authorities:
|
|
117
117
|
return {"ok": False, "problem": "no-trusted-authority"}
|
|
118
118
|
credential_id = signature["credentialId"]
|
|
119
|
-
if any(entry.get("kind") == "authority" and
|
|
119
|
+
if any(entry.get("kind") == "authority" and _jsv.same(entry.get("ref", _jsv.MISSING), credential_id) for entry in _jsv.objects(record.get("revocations"))):
|
|
120
120
|
return {"ok": False, "problem": "revoked-authority"}
|
|
121
|
-
matches = [entry for entry in authorities if
|
|
121
|
+
matches = [entry for entry in authorities if _jsv.same(entry.get("credentialId", _jsv.MISSING), credential_id)]
|
|
122
122
|
if len(matches) > 1:
|
|
123
123
|
return {"ok": False, "problem": "duplicate-authority"}
|
|
124
124
|
if not matches:
|
|
125
125
|
return {"ok": False, "problem": "unknown-authority"}
|
|
126
126
|
authority = matches[0]
|
|
127
127
|
origins = authority.get("origins")
|
|
128
|
-
rp_id =
|
|
128
|
+
rp_id = _jsv.show(authority.get("rpId", _jsv.MISSING))
|
|
129
129
|
if not _estate.is_signing_rp_id(rp_id):
|
|
130
130
|
return {"ok": False, "problem": "wrong-rp"}
|
|
131
|
-
credential = {"credentialId": credential_id, "alg":
|
|
131
|
+
credential = {"credentialId": credential_id, "alg": _jsv.show(authority.get("alg", _jsv.MISSING)),
|
|
132
132
|
"publicKey": authority["publicKey"] if isinstance(authority.get("publicKey"), dict) else {}, "rpId": rp_id,
|
|
133
133
|
"origins": [o for o in origins if isinstance(o, str) and _estate.origin_within(o, rp_id)] if isinstance(origins, list) else []}
|
|
134
134
|
challenge = hashlib.sha256(signing_statement(ENROLL_TOKEN_TYPE, fields)).digest()
|
|
135
135
|
assertion = _estate.verify_assertion(signature, credential, challenge)
|
|
136
136
|
if not assertion["ok"]:
|
|
137
137
|
return {"ok": False, "problem": assertion["problem"]}
|
|
138
|
-
not_before =
|
|
139
|
-
expires_at =
|
|
138
|
+
not_before = _ftime.parse_rfc3339_ms(fields["notBefore"])
|
|
139
|
+
expires_at = _ftime.parse_rfc3339_ms(fields["expiresAt"])
|
|
140
140
|
if expires_at <= not_before or expires_at - not_before > MAX_ENROLL_TOKEN_SECONDS * 1000:
|
|
141
141
|
return {"ok": False, "problem": "too-long-lived"}
|
|
142
142
|
if now < not_before - _estate.CLOCK_SKEW_SECONDS * 1000:
|
|
@@ -177,8 +177,8 @@ def _token_findings(value: Dict[str, Any]) -> List[Dict[str, str]]:
|
|
|
177
177
|
flag("/statement/purpose", "an enroll token's purpose is agent-host")
|
|
178
178
|
if not _is_one(statement.get("uses")):
|
|
179
179
|
flag("/statement/uses", "an enroll token is used once: uses is 1, and the hub binds its nonce on the first use")
|
|
180
|
-
not_before =
|
|
181
|
-
expires_at =
|
|
180
|
+
not_before = _ftime.parse_rfc3339_ms(statement.get("notBefore"))
|
|
181
|
+
expires_at = _ftime.parse_rfc3339_ms(statement.get("expiresAt"))
|
|
182
182
|
rest = {name: v for name, v in statement.items() if name != "type"}
|
|
183
183
|
named = not extra and not missing and statement.get("purpose") == ENROLL_PURPOSE and _is_one(statement.get("uses")) and _finite(not_before) and _finite(expires_at)
|
|
184
184
|
if named and signing_statement(ENROLL_TOKEN_TYPE, rest) is None:
|
|
@@ -201,20 +201,21 @@ _PROOF = re.compile(r"[A-Za-z0-9_-]{85}[AQgw]\Z")
|
|
|
201
201
|
|
|
202
202
|
|
|
203
203
|
def _b64url(text: str) -> bytes:
|
|
204
|
+
"""Strict base64url decode: a length the alphabet cannot produce raises, as the ceremony's readers expect."""
|
|
204
205
|
return base64.urlsafe_b64decode(text + "=" * (-len(text) % 4))
|
|
205
206
|
|
|
206
207
|
|
|
207
208
|
def _es256_raw(octets: bytes, message: bytes, raw: bytes) -> bool:
|
|
208
209
|
"""ECDSA P-256 over SHA-256 with a 64-byte r || s signature, low-S only (relay-auth/1's form)."""
|
|
209
|
-
if len(octets) != 65 or octets[0] != 4 or not
|
|
210
|
+
if len(octets) != 65 or octets[0] != 4 or not _crypto.is_valid_p256_key(octets) or len(raw) != 64:
|
|
210
211
|
return False
|
|
211
212
|
r, s = int.from_bytes(raw[:32], "big"), int.from_bytes(raw[32:], "big")
|
|
212
|
-
n =
|
|
213
|
+
n = _crypto._P256_N
|
|
213
214
|
if not (1 <= r < n and 1 <= s <= n // 2):
|
|
214
215
|
return False
|
|
215
216
|
e = int.from_bytes(hashlib.sha256(message).digest(), "big")
|
|
216
217
|
w = pow(s, -1, n)
|
|
217
|
-
point =
|
|
218
|
+
point = _crypto._shamir(e * w % n, r * w % n, (int.from_bytes(octets[1:33], "big"), int.from_bytes(octets[33:], "big")))
|
|
218
219
|
return point is not None and point[0] % n == r
|
|
219
220
|
|
|
220
221
|
|
|
@@ -227,7 +228,7 @@ def _proves(key: Dict[str, Any], message: str, proof: Any) -> bool:
|
|
|
227
228
|
signature = _b64url(proof)
|
|
228
229
|
if key.get("alg") == "es256":
|
|
229
230
|
return _es256_raw(octets, message.encode("utf-8"), signature)
|
|
230
|
-
return
|
|
231
|
+
return _crypto.ed25519_verify(octets, message.encode("utf-8"), signature)
|
|
231
232
|
|
|
232
233
|
|
|
233
234
|
def redeem_enroll_token(token: Any, machine_key: Any, proof: Any, ledger: Any) -> Dict[str, Any]:
|
|
@@ -244,11 +245,11 @@ def redeem_enroll_token(token: Any, machine_key: Any, proof: Any, ledger: Any) -
|
|
|
244
245
|
if not _proves(machine_key, join, proof):
|
|
245
246
|
return {"ok": False, "problem": "bad-proof"}
|
|
246
247
|
fingerprint = _estate.device_key_fingerprint(machine_key)
|
|
247
|
-
entry = ledger.get(nonce,
|
|
248
|
+
entry = ledger.get(nonce, _jsv.MISSING)
|
|
248
249
|
if isinstance(entry, dict) and entry.get("state") == "revoked":
|
|
249
250
|
return {"ok": False, "problem": "revoked"}
|
|
250
|
-
if entry is not
|
|
251
|
-
bound =
|
|
251
|
+
if entry is not _jsv.MISSING:
|
|
252
|
+
bound = _jsv.show(entry.get("fingerprint", _jsv.MISSING)) if isinstance(entry, dict) else _jsv.show(entry)
|
|
252
253
|
return {"ok": False, "problem": "reused", "log": {"nonce": nonce, "presented": fingerprint, "boundTo": bound}}
|
|
253
254
|
return {"ok": True, "record": {"nonce": nonce, "state": "redeemed", "fingerprint": fingerprint}}
|
|
254
255
|
|
|
@@ -268,13 +269,10 @@ _KEY_MEMBERS = ["alg", "kid", "publicKey", "use"]
|
|
|
268
269
|
_ALGORITHMS = {"sig": ("es256", "ed25519"), "kem": ("ecdh-p256",)}
|
|
269
270
|
|
|
270
271
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if isinstance(a, dict) or isinstance(b, dict):
|
|
276
|
-
return isinstance(a, dict) and isinstance(b, dict) and len(a) == len(b) and all(k in b and _json_equal(a[k], b[k]) for k in a)
|
|
277
|
-
return _estate._same(a, b)
|
|
272
|
+
from fabric_jsvalue import json_equal as _json_equal # noqa: E402,F401 - moved (FAA-65)
|
|
273
|
+
import fabric_time as _ftime # noqa: E402 - the core (FAA-65)
|
|
274
|
+
import fabric_jsvalue as _jsv # noqa: E402 - the core (FAA-65)
|
|
275
|
+
import fabric_crypto as _crypto # noqa: E402 - the core (FAA-65)
|
|
278
276
|
|
|
279
277
|
|
|
280
278
|
def _is_role(value: Any) -> bool:
|
|
@@ -337,7 +335,7 @@ def pairing_commit(side: Any) -> Dict[str, Any]:
|
|
|
337
335
|
problem = _side_problem(side)
|
|
338
336
|
if problem:
|
|
339
337
|
return {"ok": False, "problem": problem}
|
|
340
|
-
return {"ok": True, "commit": "sha256:" + _sha_hex(
|
|
338
|
+
return {"ok": True, "commit": "sha256:" + _sha_hex(_jsv.jcs(dict({"type": COMMIT_TYPE, "role": side["role"]}, **_covered(side))))}
|
|
341
339
|
|
|
342
340
|
|
|
343
341
|
def _message(value: Any, step: str) -> Optional[Dict[str, Any]]:
|
|
@@ -354,7 +352,7 @@ def check_reveal(commit: Any, reveal: Any, expected: Optional[str] = None) -> Di
|
|
|
354
352
|
c, r = _message(commit, "commit"), _message(reveal, "reveal")
|
|
355
353
|
if c is None or r is None:
|
|
356
354
|
return {"ok": False, "problem": "malformed"}
|
|
357
|
-
if not
|
|
355
|
+
if not _jsv.same(r["role"], c["role"]) or (expected is not None and not _jsv.same(r["role"], expected)):
|
|
358
356
|
return {"ok": False, "problem": "role-mismatch"}
|
|
359
357
|
made = pairing_commit({k: v for k, v in r.items() if k not in ("schema", "step")})
|
|
360
358
|
if not made["ok"]:
|
|
@@ -369,14 +367,14 @@ def comparison_code(new_reveal: Any, trusted_reveal: Any) -> Dict[str, Any]:
|
|
|
369
367
|
a, b = _message(new_reveal, "reveal"), _message(trusted_reveal, "reveal")
|
|
370
368
|
if a is None or b is None:
|
|
371
369
|
return {"ok": False, "problem": "malformed"}
|
|
372
|
-
if not
|
|
370
|
+
if not _jsv.same(a["role"], "new") or not _jsv.same(b["role"], "trusted"):
|
|
373
371
|
return {"ok": False, "problem": "role-mismatch" if _is_role(a["role"]) and _is_role(b["role"]) else "bad-role"}
|
|
374
372
|
problem = _side_problem(a) or _side_problem(b)
|
|
375
373
|
if problem:
|
|
376
374
|
return {"ok": False, "problem": problem}
|
|
377
375
|
if machine_of_keys(a["keys"]) == machine_of_keys(b["keys"]):
|
|
378
376
|
return {"ok": False, "problem": "same-machine"}
|
|
379
|
-
digest = hashlib.sha256(
|
|
377
|
+
digest = hashlib.sha256(_jsv.jcs({"type": CODE_TYPE, "new": _covered(a), "trusted": _covered(b)}).encode("utf-8")).digest()
|
|
380
378
|
return {"ok": True, "code": str(int.from_bytes(digest[:5], "big") % 10 ** CODE_DIGITS).zfill(CODE_DIGITS)}
|
|
381
379
|
|
|
382
380
|
|
|
@@ -384,7 +382,7 @@ def estate_fingerprint(digest: Any) -> Dict[str, Any]:
|
|
|
384
382
|
"""The estate's fingerprint from revision 1's digest: five groups of five digits a person compares."""
|
|
385
383
|
if not _matches(_SHA256, digest):
|
|
386
384
|
return {"ok": False, "problem": "malformed"}
|
|
387
|
-
f = hashlib.sha256(
|
|
385
|
+
f = hashlib.sha256(_jsv.jcs({"type": FINGERPRINT_TYPE, "digest": digest}).encode("utf-8")).digest()
|
|
388
386
|
groups = [str(int.from_bytes(f[5 * i:5 * i + 5], "big") % 10 ** FINGERPRINT_GROUP_DIGITS).zfill(FINGERPRINT_GROUP_DIGITS) for i in range(FINGERPRINT_GROUPS)]
|
|
389
387
|
return {"ok": True, "fingerprint": " ".join(groups)}
|
|
390
388
|
|
|
@@ -403,8 +401,8 @@ def checked_comparison_code(new_commit: Any, new_reveal: Any, trusted_commit: An
|
|
|
403
401
|
def rendezvous_verdict(opened_at: Any, messages: Any, at: Any) -> Dict[str, Any]:
|
|
404
402
|
"""A one-time rendezvous after the messages it carried, at ``at`` (an aware datetime or epoch seconds): open,
|
|
405
403
|
complete, or burned with the reason and the index of the message that burned it."""
|
|
406
|
-
opened =
|
|
407
|
-
now =
|
|
404
|
+
opened = _ftime.parse_rfc3339_ms(opened_at)
|
|
405
|
+
now = _ftime.clock_ms(at)
|
|
408
406
|
if not _finite(opened) or not _finite(now) or not isinstance(messages, list):
|
|
409
407
|
return {"state": "burned", "reason": "malformed", "index": -1}
|
|
410
408
|
if now > opened + RENDEZVOUS_SECONDS * 1000 or now < opened - _estate.CLOCK_SKEW_SECONDS * 1000:
|
|
@@ -443,21 +441,21 @@ def _machine_of(record: Dict[str, Any], machine_id: Any) -> Optional[Dict[str, A
|
|
|
443
441
|
machines = record.get("machines")
|
|
444
442
|
if not isinstance(machines, list):
|
|
445
443
|
return None
|
|
446
|
-
return next((m for m in machines if isinstance(m, dict) and
|
|
444
|
+
return next((m for m in machines if isinstance(m, dict) and _jsv.same(m.get("id", _jsv.MISSING), machine_id)), None)
|
|
447
445
|
|
|
448
446
|
|
|
449
447
|
def _machine_revoked(record: Dict[str, Any], machine_id: Any) -> bool:
|
|
450
448
|
revocations = record.get("revocations")
|
|
451
449
|
return isinstance(revocations, list) and any(isinstance(e, dict) and e.get("kind") == "machine"
|
|
452
|
-
and
|
|
450
|
+
and _jsv.same(e.get("ref", _jsv.MISSING), machine_id) for e in revocations)
|
|
453
451
|
|
|
454
452
|
|
|
455
453
|
def join_listed(record: Any, new_reveal: Any, trusted_reveal: Any, genesis: Any) -> Dict[str, Any]:
|
|
456
454
|
"""What the new machine checks before it pins and applies: the estate is the one the code bound, and the record
|
|
457
455
|
lists it with exactly the keys it revealed and the trusted machine with its revealed #sig key, revoking neither."""
|
|
458
456
|
a, b = _message(new_reveal, "reveal"), _message(trusted_reveal, "reveal")
|
|
459
|
-
if (not isinstance(record, dict) or a is None or b is None or not
|
|
460
|
-
or not
|
|
457
|
+
if (not isinstance(record, dict) or a is None or b is None or not _jsv.same(a["role"], "new")
|
|
458
|
+
or not _jsv.same(b["role"], "trusted") or _side_problem(a) or _side_problem(b)):
|
|
461
459
|
return {"ok": False, "problem": "malformed"}
|
|
462
460
|
if not isinstance(genesis, str) or b["genesis"] != genesis:
|
|
463
461
|
return {"ok": False, "problem": "other-estate"}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""The kit's cryptography, once (FAA-65, layer L1): what the contract's verifiers need and nothing more.
|
|
2
|
+
|
|
3
|
+
- ``ed25519_verify(public_key, message, signature)`` (RFC 8032) and ``is_small_order_key`` with
|
|
4
|
+
``SMALL_ORDER_ENCODINGS``: a key of small order verifies forged signatures and is refused (DEC-0045).
|
|
5
|
+
- ``is_valid_p256_key`` and ``es256_verify`` (ECDSA P-256 over SHA-256, DER or raw signatures) for passkeys.
|
|
6
|
+
- base64url without padding (``b64url``, ``b64url_bytes``, ``node_base64``) and ``sha256``.
|
|
7
|
+
|
|
8
|
+
Pure Python over integers: slow next to OpenSSL, constant inputs only, verification only. Nothing here signs.
|
|
9
|
+
Standard library only; imports only ``fabric_jsvalue``.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import base64
|
|
14
|
+
import hashlib
|
|
15
|
+
import re
|
|
16
|
+
from typing import Any, Optional, Tuple
|
|
17
|
+
import secrets
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# The small-order points of edwards25519 and their non-canonical encodings, sign bit cleared (the contract's
|
|
21
|
+
# src/ed25519.ts; libsodium's has_small_order list).
|
|
22
|
+
SMALL_ORDER_ENCODINGS = (
|
|
23
|
+
"0000000000000000000000000000000000000000000000000000000000000000", "0100000000000000000000000000000000000000000000000000000000000000",
|
|
24
|
+
"26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05", "c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a",
|
|
25
|
+
"ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", "edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
|
|
26
|
+
"eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def is_small_order_key(raw: bytes) -> bool:
|
|
31
|
+
"""Whether a 32-byte Ed25519 public key is a point of small order, in any encoding (CT-29)."""
|
|
32
|
+
if len(raw) != 32:
|
|
33
|
+
return True
|
|
34
|
+
return (raw[:31] + bytes([raw[31] & 0x7F])).hex() in SMALL_ORDER_ENCODINGS
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
_ED_P = 2 ** 255 - 19
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
_ED_L = 2 ** 252 + 27742317777372353535851937790883648493
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_ED_D = (-121665 * pow(121666, _ED_P - 2, _ED_P)) % _ED_P
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
_ED_SQRT_M1 = pow(2, (_ED_P - 1) // 4, _ED_P)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _ed_add(a: Tuple[int, int, int, int], b: Tuple[int, int, int, int]) -> Tuple[int, int, int, int]:
|
|
50
|
+
x1, y1, z1, t1 = a
|
|
51
|
+
x2, y2, z2, t2 = b
|
|
52
|
+
aa = (y1 - x1) * (y2 - x2) % _ED_P
|
|
53
|
+
bb = (y1 + x1) * (y2 + x2) % _ED_P
|
|
54
|
+
cc = 2 * t1 * t2 * _ED_D % _ED_P
|
|
55
|
+
dd = 2 * z1 * z2 % _ED_P
|
|
56
|
+
e, f, g, h = bb - aa, dd - cc, dd + cc, bb + aa
|
|
57
|
+
return (e * f % _ED_P, g * h % _ED_P, f * g % _ED_P, e * h % _ED_P)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _ed_mul(scalar: int, point: Tuple[int, int, int, int]) -> Tuple[int, int, int, int]:
|
|
61
|
+
result = (0, 1, 1, 0)
|
|
62
|
+
while scalar:
|
|
63
|
+
if scalar & 1:
|
|
64
|
+
result = _ed_add(result, point)
|
|
65
|
+
point = _ed_add(point, point)
|
|
66
|
+
scalar >>= 1
|
|
67
|
+
return result
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _ed_decode_point(data: bytes) -> Optional[Tuple[int, int, int, int]]:
|
|
71
|
+
if len(data) != 32:
|
|
72
|
+
return None
|
|
73
|
+
y = int.from_bytes(data, "little")
|
|
74
|
+
sign = y >> 255
|
|
75
|
+
y &= (1 << 255) - 1
|
|
76
|
+
if y >= _ED_P:
|
|
77
|
+
return None
|
|
78
|
+
x2 = (y * y - 1) * pow(_ED_D * y * y + 1, _ED_P - 2, _ED_P) % _ED_P
|
|
79
|
+
if x2 == 0:
|
|
80
|
+
if sign:
|
|
81
|
+
return None
|
|
82
|
+
return (0, y, 1, 0)
|
|
83
|
+
x = pow(x2, (_ED_P + 3) // 8, _ED_P)
|
|
84
|
+
if (x * x - x2) % _ED_P:
|
|
85
|
+
x = x * _ED_SQRT_M1 % _ED_P
|
|
86
|
+
if (x * x - x2) % _ED_P:
|
|
87
|
+
return None
|
|
88
|
+
if x & 1 != sign:
|
|
89
|
+
x = _ED_P - x
|
|
90
|
+
return (x, y, 1, x * y % _ED_P)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _ed_encode_point(point: Tuple[int, int, int, int]) -> bytes:
|
|
94
|
+
x, y, z, _ = point
|
|
95
|
+
zi = pow(z, _ED_P - 2, _ED_P)
|
|
96
|
+
x, y = x * zi % _ED_P, y * zi % _ED_P
|
|
97
|
+
return (y | ((x & 1) << 255)).to_bytes(32, "little")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
_ED_G = _ed_decode_point(bytes.fromhex("5866666666666666666666666666666666666666666666666666666666666666"))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
_ED_IDENTITY = (1).to_bytes(32, "little")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def ed25519_verify(public_key: bytes, message: bytes, signature: bytes) -> bool:
|
|
107
|
+
"""RFC 8032 §5.1.7, cofactorless: [S]B == R + [k]A, compared by R's encoding. False, never an exception."""
|
|
108
|
+
if len(public_key) != 32 or len(signature) != 64:
|
|
109
|
+
return False
|
|
110
|
+
s = int.from_bytes(signature[32:], "little")
|
|
111
|
+
if s >= _ED_L:
|
|
112
|
+
return False
|
|
113
|
+
a = _ed_decode_point(public_key)
|
|
114
|
+
if a is None or _ed_encode_point(_ed_mul(8, a)) == _ED_IDENTITY:
|
|
115
|
+
return False # a small-order key would accept R = [S]B for a quarter or more of all messages; OpenSSL refuses it
|
|
116
|
+
k = int.from_bytes(hashlib.sha512(signature[:32] + public_key + message).digest(), "little") % _ED_L
|
|
117
|
+
neg_a = ((-a[0]) % _ED_P, a[1], a[2], (-a[3]) % _ED_P)
|
|
118
|
+
return secrets.compare_digest(_ed_encode_point(_ed_add(_ed_mul(s, _ED_G), _ed_mul(k, neg_a))), signature[:32])
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def b64url_bytes(text: str) -> bytes:
|
|
122
|
+
"""Buffer.from(text, "base64url") for text of the base64url alphabet: a dangling sixth bit group is ignored."""
|
|
123
|
+
return b64url_decode(text)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def b64url(data: bytes) -> str:
|
|
127
|
+
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def node_base64(text: str) -> bytes:
|
|
131
|
+
"""Buffer.from(text, "base64"): either alphabet, other characters skipped, stops at the first "=", a dangling
|
|
132
|
+
sixth bit group ignored."""
|
|
133
|
+
kept = []
|
|
134
|
+
for ch in text:
|
|
135
|
+
if ch == "=":
|
|
136
|
+
break
|
|
137
|
+
if ch.isascii() and (ch.isalnum() or ch in "+/-_"):
|
|
138
|
+
kept.append({"-": "+", "_": "/"}.get(ch, ch))
|
|
139
|
+
body = "".join(kept)
|
|
140
|
+
if len(body) % 4 == 1:
|
|
141
|
+
body = body[:-1]
|
|
142
|
+
return base64.b64decode(body + "=" * (-len(body) % 4))
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def sha256(data: bytes) -> bytes:
|
|
146
|
+
return hashlib.sha256(data).digest()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
_P256_P = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
_P256_A = _P256_P - 3
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
_P256_B = 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
_P256_N = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
_P256_GX = 0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
_P256_GY = 0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def is_valid_p256_key(raw: bytes) -> bool:
|
|
168
|
+
"""SEC 1 §3.2.2 for an uncompressed point: 0x04 ‖ X ‖ Y, X and Y below p, on y² = x³ − 3x + b. The cofactor is 1."""
|
|
169
|
+
if len(raw) != 65 or raw[0] != 0x04:
|
|
170
|
+
return False
|
|
171
|
+
x = int.from_bytes(raw[1:33], "big")
|
|
172
|
+
y = int.from_bytes(raw[33:], "big")
|
|
173
|
+
if x >= _P256_P or y >= _P256_P:
|
|
174
|
+
return False
|
|
175
|
+
return (y * y - (x * x * x + _P256_A * x + _P256_B)) % _P256_P == 0
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _jacobian_double(p: Tuple[int, int, int]) -> Tuple[int, int, int]:
|
|
179
|
+
x, y, z = p
|
|
180
|
+
if y == 0 or z == 0:
|
|
181
|
+
return (0, 1, 0)
|
|
182
|
+
ysq = y * y % _P256_P
|
|
183
|
+
s = 4 * x * ysq % _P256_P
|
|
184
|
+
zsq = z * z % _P256_P
|
|
185
|
+
m = 3 * (x - zsq) * (x + zsq) % _P256_P # a = −3
|
|
186
|
+
nx = (m * m - 2 * s) % _P256_P
|
|
187
|
+
ny = (m * (s - nx) - 8 * ysq * ysq) % _P256_P
|
|
188
|
+
nz = 2 * y * z % _P256_P
|
|
189
|
+
return (nx, ny, nz)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _jacobian_add(p: Tuple[int, int, int], q: Tuple[int, int, int]) -> Tuple[int, int, int]:
|
|
193
|
+
if p[2] == 0:
|
|
194
|
+
return q
|
|
195
|
+
if q[2] == 0:
|
|
196
|
+
return p
|
|
197
|
+
z1z1 = p[2] * p[2] % _P256_P
|
|
198
|
+
z2z2 = q[2] * q[2] % _P256_P
|
|
199
|
+
u1 = p[0] * z2z2 % _P256_P
|
|
200
|
+
u2 = q[0] * z1z1 % _P256_P
|
|
201
|
+
s1 = p[1] * q[2] * z2z2 % _P256_P
|
|
202
|
+
s2 = q[1] * p[2] * z1z1 % _P256_P
|
|
203
|
+
if u1 == u2:
|
|
204
|
+
return _jacobian_double(p) if s1 == s2 else (0, 1, 0)
|
|
205
|
+
h = (u2 - u1) % _P256_P
|
|
206
|
+
r = (s2 - s1) % _P256_P
|
|
207
|
+
hh = h * h % _P256_P
|
|
208
|
+
hhh = h * hh % _P256_P
|
|
209
|
+
v = u1 * hh % _P256_P
|
|
210
|
+
nx = (r * r - hhh - 2 * v) % _P256_P
|
|
211
|
+
ny = (r * (v - nx) - s1 * hhh) % _P256_P
|
|
212
|
+
nz = h * p[2] * q[2] % _P256_P
|
|
213
|
+
return (nx, ny, nz)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _shamir(u1: int, u2: int, q: Tuple[int, int]) -> Optional[Tuple[int, int]]:
|
|
217
|
+
"""u1·G + u2·Q in affine form, or None at infinity."""
|
|
218
|
+
g = (_P256_GX, _P256_GY, 1)
|
|
219
|
+
qj = (q[0], q[1], 1)
|
|
220
|
+
both = _jacobian_add(g, qj)
|
|
221
|
+
out = (0, 1, 0)
|
|
222
|
+
for bit in range(max(u1.bit_length(), u2.bit_length()) - 1, -1, -1):
|
|
223
|
+
out = _jacobian_double(out)
|
|
224
|
+
a, b = (u1 >> bit) & 1, (u2 >> bit) & 1
|
|
225
|
+
if a and b:
|
|
226
|
+
out = _jacobian_add(out, both)
|
|
227
|
+
elif a:
|
|
228
|
+
out = _jacobian_add(out, g)
|
|
229
|
+
elif b:
|
|
230
|
+
out = _jacobian_add(out, qj)
|
|
231
|
+
if out[2] == 0:
|
|
232
|
+
return None
|
|
233
|
+
zinv = pow(out[2], -1, _P256_P)
|
|
234
|
+
return (out[0] * zinv * zinv % _P256_P, out[1] * zinv * zinv * zinv % _P256_P)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _der_integer(data: bytes, at: int) -> Tuple[Optional[int], int]:
|
|
238
|
+
if at + 2 > len(data) or data[at] != 0x02:
|
|
239
|
+
return None, at
|
|
240
|
+
length = data[at + 1]
|
|
241
|
+
start = at + 2
|
|
242
|
+
if length == 0 or length > 33 or start + length > len(data):
|
|
243
|
+
return None, at
|
|
244
|
+
body = data[start:start + length]
|
|
245
|
+
if body[0] & 0x80:
|
|
246
|
+
return None, at # negative
|
|
247
|
+
if length > 1 and body[0] == 0 and not body[1] & 0x80:
|
|
248
|
+
return None, at # not minimal
|
|
249
|
+
return int.from_bytes(body, "big"), start + length
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _der_signature(signature: bytes) -> Optional[Tuple[int, int]]:
|
|
253
|
+
"""An ASN.1 DER Ecdsa-Sig-Value (RFC 3279 §2.2.3), strictly: one SEQUENCE of two minimal positive INTEGERs."""
|
|
254
|
+
if len(signature) < 8 or signature[0] != 0x30:
|
|
255
|
+
return None
|
|
256
|
+
if signature[1] & 0x80:
|
|
257
|
+
return None # a P-256 signature fits a short-form length
|
|
258
|
+
if signature[1] != len(signature) - 2:
|
|
259
|
+
return None
|
|
260
|
+
r, at = _der_integer(signature, 2)
|
|
261
|
+
if r is None:
|
|
262
|
+
return None
|
|
263
|
+
s, at = _der_integer(signature, at)
|
|
264
|
+
if s is None or at != len(signature):
|
|
265
|
+
return None
|
|
266
|
+
return r, s
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def es256_verify(x: bytes, y: bytes, message: bytes, signature: bytes) -> bool:
|
|
270
|
+
"""ECDSA P-256 with SHA-256 over `message`, the signature in DER (FIPS 186-5 §6.4.2). False, never an exception."""
|
|
271
|
+
if not is_valid_p256_key(b"\x04" + x + y):
|
|
272
|
+
return False
|
|
273
|
+
parsed = _der_signature(signature)
|
|
274
|
+
if parsed is None:
|
|
275
|
+
return False
|
|
276
|
+
r, s = parsed
|
|
277
|
+
if not (1 <= r < _P256_N and 1 <= s < _P256_N):
|
|
278
|
+
return False
|
|
279
|
+
e = int.from_bytes(sha256(message), "big")
|
|
280
|
+
w = pow(s, -1, _P256_N)
|
|
281
|
+
point = _shamir(e * w % _P256_N, r * w % _P256_N, (int.from_bytes(x, "big"), int.from_bytes(y, "big")))
|
|
282
|
+
return point is not None and point[0] % _P256_N == r
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def b64url_decode(segment: str) -> bytes:
|
|
286
|
+
"""Base64url without padding, as Node reads it: a dangling sixth bit group is ignored."""
|
|
287
|
+
if len(segment) % 4 == 1:
|
|
288
|
+
segment = segment[:-1]
|
|
289
|
+
return base64.urlsafe_b64decode(segment + "=" * (-len(segment) % 4))
|