@passioncode-ai/fabric-agent-adapter 0.39.1 → 0.41.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 +203 -0
- package/README.md +18 -6
- package/SECURITY.md +27 -6
- package/bin/fabric-agent-adapter.js +71 -6
- package/package.json +2 -2
- package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +42 -66
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +4 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/operator-channel-and-repository.md +55 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +9 -3
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/verification.md +15 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +181 -26
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/check_dashboard_link.py +66 -5
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +81 -23
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +42 -33
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +21 -9
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +9 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +104 -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 +465 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +21 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +93 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +2 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +3 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +5 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +5 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +129 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +3 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +5 -1
- 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_dashboard_layout.mjs +48 -19
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_link.py +66 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +11 -184
- 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/dashboard_kit.py +53 -19
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-auth.mjs +288 -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-link.mjs +145 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +848 -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 +7 -18
- 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 +15 -46
- 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 +11 -68
- 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 +50 -16
- 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 +235 -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 +146 -32
- 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 +404 -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 +408 -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 +91 -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_link.py +162 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +882 -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 -39
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_crypto.py +336 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +113 -222
- 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 +44 -405
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +140 -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_hub.py +6 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +119 -77
- 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 +285 -109
- 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 +32 -39
- 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_secrets.py +1 -1
- 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 +437 -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 +1621 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +25 -7
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +29 -60
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +4 -2
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +3 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/operator-channel-and-repository.md +55 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/check_dashboard_link.py +66 -5
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +3 -3
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/native-host-hooks.md +2 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +26 -13
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +122 -1392
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +278 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +539 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_hosts.py +324 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mail.py +173 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +491 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/host_hooks.py +11 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/hosts/context.mjs +25 -2
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/hosts/openclaw/index.mjs +4 -2
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/hosts/opencode/fabric.mjs +2 -1
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ceremonies.py
CHANGED
|
@@ -29,7 +29,6 @@ from pathlib import Path
|
|
|
29
29
|
from typing import Any, Dict, List, Optional
|
|
30
30
|
|
|
31
31
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
32
|
-
import fabric_delegation as _delegation # noqa: E402 - beside this file: RFC 8785
|
|
33
32
|
import fabric_estate as _estate # noqa: E402 - beside this file: passkey assertions, times, show
|
|
34
33
|
|
|
35
34
|
ENROLL_TOKEN_SCHEMA = "enroll-token/1"
|
|
@@ -62,8 +61,8 @@ def _finite(value: float) -> bool:
|
|
|
62
61
|
def _register_ok(kind: str, fields: Dict[str, Any]) -> bool:
|
|
63
62
|
if kind == ENROLL_TOKEN_TYPE:
|
|
64
63
|
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(
|
|
64
|
+
and (fields["ceiling"] is None or _matches(_SHA256, fields["ceiling"])) and _finite(_ftime.parse_rfc3339_ms(fields["notBefore"]))
|
|
65
|
+
and _finite(_ftime.parse_rfc3339_ms(fields["expiresAt"])) and _matches(_NONCE32, fields["nonce"]) and _is_one(fields["uses"]))
|
|
67
66
|
return _matches(_SHA256, fields["digest"]) and _matches(_NONCE32, fields["nonce"])
|
|
68
67
|
|
|
69
68
|
|
|
@@ -77,7 +76,7 @@ def signing_statement(kind: str, fields: Any) -> Optional[bytes]:
|
|
|
77
76
|
if not _register_ok(kind, fields):
|
|
78
77
|
return None
|
|
79
78
|
try:
|
|
80
|
-
return
|
|
79
|
+
return _jsv.jcs(dict(fields, type=kind)).encode("utf-8")
|
|
81
80
|
except (TypeError, ValueError):
|
|
82
81
|
return None
|
|
83
82
|
|
|
@@ -100,7 +99,7 @@ def verify_enroll_token(token: Any, *, record: Any, genesis: Any, at: Any) -> Di
|
|
|
100
99
|
clock (an aware datetime or epoch seconds). The first failure answers: no-clock, malformed, wrong-estate, the passkey
|
|
101
100
|
(no-trusted-authority, revoked-authority, duplicate-authority, unknown-authority), the assertion, then the window
|
|
102
101
|
(too-long-lived, not-yet-valid, expired). Success: ``{"ok": True, "nonce", "expiresAt", "signedBy"}``."""
|
|
103
|
-
now =
|
|
102
|
+
now = _ftime.clock_ms(at)
|
|
104
103
|
if not _finite(now):
|
|
105
104
|
return {"ok": False, "problem": "no-clock"}
|
|
106
105
|
fields = _statement_fields(token)
|
|
@@ -112,31 +111,31 @@ def verify_enroll_token(token: Any, *, record: Any, genesis: Any, at: Any) -> Di
|
|
|
112
111
|
return {"ok": False, "problem": "malformed"}
|
|
113
112
|
if fields["scope"] != "estate:%s" % record["estate"] or not isinstance(genesis, str) or fields["genesis"] != genesis:
|
|
114
113
|
return {"ok": False, "problem": "wrong-estate"}
|
|
115
|
-
authorities =
|
|
114
|
+
authorities = _jsv.objects(record.get("authorities"))
|
|
116
115
|
if not authorities:
|
|
117
116
|
return {"ok": False, "problem": "no-trusted-authority"}
|
|
118
117
|
credential_id = signature["credentialId"]
|
|
119
|
-
if any(entry.get("kind") == "authority" and
|
|
118
|
+
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
119
|
return {"ok": False, "problem": "revoked-authority"}
|
|
121
|
-
matches = [entry for entry in authorities if
|
|
120
|
+
matches = [entry for entry in authorities if _jsv.same(entry.get("credentialId", _jsv.MISSING), credential_id)]
|
|
122
121
|
if len(matches) > 1:
|
|
123
122
|
return {"ok": False, "problem": "duplicate-authority"}
|
|
124
123
|
if not matches:
|
|
125
124
|
return {"ok": False, "problem": "unknown-authority"}
|
|
126
125
|
authority = matches[0]
|
|
127
126
|
origins = authority.get("origins")
|
|
128
|
-
rp_id =
|
|
127
|
+
rp_id = _jsv.show(authority.get("rpId", _jsv.MISSING))
|
|
129
128
|
if not _estate.is_signing_rp_id(rp_id):
|
|
130
129
|
return {"ok": False, "problem": "wrong-rp"}
|
|
131
|
-
credential = {"credentialId": credential_id, "alg":
|
|
130
|
+
credential = {"credentialId": credential_id, "alg": _jsv.show(authority.get("alg", _jsv.MISSING)),
|
|
132
131
|
"publicKey": authority["publicKey"] if isinstance(authority.get("publicKey"), dict) else {}, "rpId": rp_id,
|
|
133
132
|
"origins": [o for o in origins if isinstance(o, str) and _estate.origin_within(o, rp_id)] if isinstance(origins, list) else []}
|
|
134
133
|
challenge = hashlib.sha256(signing_statement(ENROLL_TOKEN_TYPE, fields)).digest()
|
|
135
134
|
assertion = _estate.verify_assertion(signature, credential, challenge)
|
|
136
135
|
if not assertion["ok"]:
|
|
137
136
|
return {"ok": False, "problem": assertion["problem"]}
|
|
138
|
-
not_before =
|
|
139
|
-
expires_at =
|
|
137
|
+
not_before = _ftime.parse_rfc3339_ms(fields["notBefore"])
|
|
138
|
+
expires_at = _ftime.parse_rfc3339_ms(fields["expiresAt"])
|
|
140
139
|
if expires_at <= not_before or expires_at - not_before > MAX_ENROLL_TOKEN_SECONDS * 1000:
|
|
141
140
|
return {"ok": False, "problem": "too-long-lived"}
|
|
142
141
|
if now < not_before - _estate.CLOCK_SKEW_SECONDS * 1000:
|
|
@@ -177,8 +176,8 @@ def _token_findings(value: Dict[str, Any]) -> List[Dict[str, str]]:
|
|
|
177
176
|
flag("/statement/purpose", "an enroll token's purpose is agent-host")
|
|
178
177
|
if not _is_one(statement.get("uses")):
|
|
179
178
|
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 =
|
|
179
|
+
not_before = _ftime.parse_rfc3339_ms(statement.get("notBefore"))
|
|
180
|
+
expires_at = _ftime.parse_rfc3339_ms(statement.get("expiresAt"))
|
|
182
181
|
rest = {name: v for name, v in statement.items() if name != "type"}
|
|
183
182
|
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
183
|
if named and signing_statement(ENROLL_TOKEN_TYPE, rest) is None:
|
|
@@ -201,20 +200,21 @@ _PROOF = re.compile(r"[A-Za-z0-9_-]{85}[AQgw]\Z")
|
|
|
201
200
|
|
|
202
201
|
|
|
203
202
|
def _b64url(text: str) -> bytes:
|
|
203
|
+
"""Strict base64url decode: a length the alphabet cannot produce raises, as the ceremony's readers expect."""
|
|
204
204
|
return base64.urlsafe_b64decode(text + "=" * (-len(text) % 4))
|
|
205
205
|
|
|
206
206
|
|
|
207
207
|
def _es256_raw(octets: bytes, message: bytes, raw: bytes) -> bool:
|
|
208
208
|
"""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
|
|
209
|
+
if len(octets) != 65 or octets[0] != 4 or not _crypto.is_valid_p256_key(octets) or len(raw) != 64:
|
|
210
210
|
return False
|
|
211
211
|
r, s = int.from_bytes(raw[:32], "big"), int.from_bytes(raw[32:], "big")
|
|
212
|
-
n =
|
|
212
|
+
n = _crypto._P256_N
|
|
213
213
|
if not (1 <= r < n and 1 <= s <= n // 2):
|
|
214
214
|
return False
|
|
215
215
|
e = int.from_bytes(hashlib.sha256(message).digest(), "big")
|
|
216
216
|
w = pow(s, -1, n)
|
|
217
|
-
point =
|
|
217
|
+
point = _crypto._shamir(e * w % n, r * w % n, (int.from_bytes(octets[1:33], "big"), int.from_bytes(octets[33:], "big")))
|
|
218
218
|
return point is not None and point[0] % n == r
|
|
219
219
|
|
|
220
220
|
|
|
@@ -227,7 +227,7 @@ def _proves(key: Dict[str, Any], message: str, proof: Any) -> bool:
|
|
|
227
227
|
signature = _b64url(proof)
|
|
228
228
|
if key.get("alg") == "es256":
|
|
229
229
|
return _es256_raw(octets, message.encode("utf-8"), signature)
|
|
230
|
-
return
|
|
230
|
+
return _crypto.ed25519_verify(octets, message.encode("utf-8"), signature)
|
|
231
231
|
|
|
232
232
|
|
|
233
233
|
def redeem_enroll_token(token: Any, machine_key: Any, proof: Any, ledger: Any) -> Dict[str, Any]:
|
|
@@ -244,11 +244,11 @@ def redeem_enroll_token(token: Any, machine_key: Any, proof: Any, ledger: Any) -
|
|
|
244
244
|
if not _proves(machine_key, join, proof):
|
|
245
245
|
return {"ok": False, "problem": "bad-proof"}
|
|
246
246
|
fingerprint = _estate.device_key_fingerprint(machine_key)
|
|
247
|
-
entry = ledger.get(nonce,
|
|
247
|
+
entry = ledger.get(nonce, _jsv.MISSING)
|
|
248
248
|
if isinstance(entry, dict) and entry.get("state") == "revoked":
|
|
249
249
|
return {"ok": False, "problem": "revoked"}
|
|
250
|
-
if entry is not
|
|
251
|
-
bound =
|
|
250
|
+
if entry is not _jsv.MISSING:
|
|
251
|
+
bound = _jsv.show(entry.get("fingerprint", _jsv.MISSING)) if isinstance(entry, dict) else _jsv.show(entry)
|
|
252
252
|
return {"ok": False, "problem": "reused", "log": {"nonce": nonce, "presented": fingerprint, "boundTo": bound}}
|
|
253
253
|
return {"ok": True, "record": {"nonce": nonce, "state": "redeemed", "fingerprint": fingerprint}}
|
|
254
254
|
|
|
@@ -268,13 +268,10 @@ _KEY_MEMBERS = ["alg", "kid", "publicKey", "use"]
|
|
|
268
268
|
_ALGORITHMS = {"sig": ("es256", "ed25519"), "kem": ("ecdh-p256",)}
|
|
269
269
|
|
|
270
270
|
|
|
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)
|
|
271
|
+
from fabric_jsvalue import json_equal as _json_equal # noqa: E402,F401 - moved (FAA-65)
|
|
272
|
+
import fabric_time as _ftime # noqa: E402 - the core (FAA-65)
|
|
273
|
+
import fabric_jsvalue as _jsv # noqa: E402 - the core (FAA-65)
|
|
274
|
+
import fabric_crypto as _crypto # noqa: E402 - the core (FAA-65)
|
|
278
275
|
|
|
279
276
|
|
|
280
277
|
def _is_role(value: Any) -> bool:
|
|
@@ -337,7 +334,7 @@ def pairing_commit(side: Any) -> Dict[str, Any]:
|
|
|
337
334
|
problem = _side_problem(side)
|
|
338
335
|
if problem:
|
|
339
336
|
return {"ok": False, "problem": problem}
|
|
340
|
-
return {"ok": True, "commit": "sha256:" + _sha_hex(
|
|
337
|
+
return {"ok": True, "commit": "sha256:" + _sha_hex(_jsv.jcs(dict({"type": COMMIT_TYPE, "role": side["role"]}, **_covered(side))))}
|
|
341
338
|
|
|
342
339
|
|
|
343
340
|
def _message(value: Any, step: str) -> Optional[Dict[str, Any]]:
|
|
@@ -354,7 +351,7 @@ def check_reveal(commit: Any, reveal: Any, expected: Optional[str] = None) -> Di
|
|
|
354
351
|
c, r = _message(commit, "commit"), _message(reveal, "reveal")
|
|
355
352
|
if c is None or r is None:
|
|
356
353
|
return {"ok": False, "problem": "malformed"}
|
|
357
|
-
if not
|
|
354
|
+
if not _jsv.same(r["role"], c["role"]) or (expected is not None and not _jsv.same(r["role"], expected)):
|
|
358
355
|
return {"ok": False, "problem": "role-mismatch"}
|
|
359
356
|
made = pairing_commit({k: v for k, v in r.items() if k not in ("schema", "step")})
|
|
360
357
|
if not made["ok"]:
|
|
@@ -369,14 +366,14 @@ def comparison_code(new_reveal: Any, trusted_reveal: Any) -> Dict[str, Any]:
|
|
|
369
366
|
a, b = _message(new_reveal, "reveal"), _message(trusted_reveal, "reveal")
|
|
370
367
|
if a is None or b is None:
|
|
371
368
|
return {"ok": False, "problem": "malformed"}
|
|
372
|
-
if not
|
|
369
|
+
if not _jsv.same(a["role"], "new") or not _jsv.same(b["role"], "trusted"):
|
|
373
370
|
return {"ok": False, "problem": "role-mismatch" if _is_role(a["role"]) and _is_role(b["role"]) else "bad-role"}
|
|
374
371
|
problem = _side_problem(a) or _side_problem(b)
|
|
375
372
|
if problem:
|
|
376
373
|
return {"ok": False, "problem": problem}
|
|
377
374
|
if machine_of_keys(a["keys"]) == machine_of_keys(b["keys"]):
|
|
378
375
|
return {"ok": False, "problem": "same-machine"}
|
|
379
|
-
digest = hashlib.sha256(
|
|
376
|
+
digest = hashlib.sha256(_jsv.jcs({"type": CODE_TYPE, "new": _covered(a), "trusted": _covered(b)}).encode("utf-8")).digest()
|
|
380
377
|
return {"ok": True, "code": str(int.from_bytes(digest[:5], "big") % 10 ** CODE_DIGITS).zfill(CODE_DIGITS)}
|
|
381
378
|
|
|
382
379
|
|
|
@@ -384,7 +381,7 @@ def estate_fingerprint(digest: Any) -> Dict[str, Any]:
|
|
|
384
381
|
"""The estate's fingerprint from revision 1's digest: five groups of five digits a person compares."""
|
|
385
382
|
if not _matches(_SHA256, digest):
|
|
386
383
|
return {"ok": False, "problem": "malformed"}
|
|
387
|
-
f = hashlib.sha256(
|
|
384
|
+
f = hashlib.sha256(_jsv.jcs({"type": FINGERPRINT_TYPE, "digest": digest}).encode("utf-8")).digest()
|
|
388
385
|
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
386
|
return {"ok": True, "fingerprint": " ".join(groups)}
|
|
390
387
|
|
|
@@ -403,8 +400,8 @@ def checked_comparison_code(new_commit: Any, new_reveal: Any, trusted_commit: An
|
|
|
403
400
|
def rendezvous_verdict(opened_at: Any, messages: Any, at: Any) -> Dict[str, Any]:
|
|
404
401
|
"""A one-time rendezvous after the messages it carried, at ``at`` (an aware datetime or epoch seconds): open,
|
|
405
402
|
complete, or burned with the reason and the index of the message that burned it."""
|
|
406
|
-
opened =
|
|
407
|
-
now =
|
|
403
|
+
opened = _ftime.parse_rfc3339_ms(opened_at)
|
|
404
|
+
now = _ftime.clock_ms(at)
|
|
408
405
|
if not _finite(opened) or not _finite(now) or not isinstance(messages, list):
|
|
409
406
|
return {"state": "burned", "reason": "malformed", "index": -1}
|
|
410
407
|
if now > opened + RENDEZVOUS_SECONDS * 1000 or now < opened - _estate.CLOCK_SKEW_SECONDS * 1000:
|
|
@@ -443,21 +440,21 @@ def _machine_of(record: Dict[str, Any], machine_id: Any) -> Optional[Dict[str, A
|
|
|
443
440
|
machines = record.get("machines")
|
|
444
441
|
if not isinstance(machines, list):
|
|
445
442
|
return None
|
|
446
|
-
return next((m for m in machines if isinstance(m, dict) and
|
|
443
|
+
return next((m for m in machines if isinstance(m, dict) and _jsv.same(m.get("id", _jsv.MISSING), machine_id)), None)
|
|
447
444
|
|
|
448
445
|
|
|
449
446
|
def _machine_revoked(record: Dict[str, Any], machine_id: Any) -> bool:
|
|
450
447
|
revocations = record.get("revocations")
|
|
451
448
|
return isinstance(revocations, list) and any(isinstance(e, dict) and e.get("kind") == "machine"
|
|
452
|
-
and
|
|
449
|
+
and _jsv.same(e.get("ref", _jsv.MISSING), machine_id) for e in revocations)
|
|
453
450
|
|
|
454
451
|
|
|
455
452
|
def join_listed(record: Any, new_reveal: Any, trusted_reveal: Any, genesis: Any) -> Dict[str, Any]:
|
|
456
453
|
"""What the new machine checks before it pins and applies: the estate is the one the code bound, and the record
|
|
457
454
|
lists it with exactly the keys it revealed and the trusted machine with its revealed #sig key, revoking neither."""
|
|
458
455
|
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
|
|
456
|
+
if (not isinstance(record, dict) or a is None or b is None or not _jsv.same(a["role"], "new")
|
|
457
|
+
or not _jsv.same(b["role"], "trusted") or _side_problem(a) or _side_problem(b)):
|
|
461
458
|
return {"ok": False, "problem": "malformed"}
|
|
462
459
|
if not isinstance(genesis, str) or b["genesis"] != genesis:
|
|
463
460
|
return {"ok": False, "problem": "other-estate"}
|
|
@@ -0,0 +1,336 @@
|
|
|
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, the signature in DER) for passkeys.
|
|
6
|
+
- ``ed25519_sign`` and ``ed25519_seed_public_key`` (RFC 8032), which ``fabric_package`` signs with, over ``ed_mul_secret``.
|
|
7
|
+
- base64url without padding (``b64url``, ``b64url_bytes``, ``node_base64``) and ``sha256``.
|
|
8
|
+
|
|
9
|
+
Pure Python over integers, slow next to OpenSSL. Verification multiplies public values only and takes the short path;
|
|
10
|
+
a secret scalar goes through ``ed_mul_secret``, a ladder that runs the same 256 steps whatever its bits are (FAA-78,
|
|
11
|
+
finding 45). Python's integers do not promise constant-time arithmetic, so that removes the branch on the secret, not
|
|
12
|
+
every timing difference: the signer stays a local, one-shot command line, never a service that signs for others.
|
|
13
|
+
Standard library only; imports no other kit module.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import base64
|
|
18
|
+
import hashlib
|
|
19
|
+
import secrets
|
|
20
|
+
from typing import Optional, Tuple
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# The small-order points of edwards25519 and their non-canonical encodings, sign bit cleared (the contract's
|
|
24
|
+
# src/ed25519.ts; libsodium's has_small_order list).
|
|
25
|
+
SMALL_ORDER_ENCODINGS = (
|
|
26
|
+
"0000000000000000000000000000000000000000000000000000000000000000", "0100000000000000000000000000000000000000000000000000000000000000",
|
|
27
|
+
"26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05", "c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a",
|
|
28
|
+
"ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", "edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
|
|
29
|
+
"eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def is_small_order_key(raw: bytes) -> bool:
|
|
34
|
+
"""Whether a 32-byte Ed25519 public key is a point of small order, in any encoding (CT-29)."""
|
|
35
|
+
if len(raw) != 32:
|
|
36
|
+
return True
|
|
37
|
+
return (raw[:31] + bytes([raw[31] & 0x7F])).hex() in SMALL_ORDER_ENCODINGS
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
_ED_P = 2 ** 255 - 19
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_ED_L = 2 ** 252 + 27742317777372353535851937790883648493
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
_ED_D = (-121665 * pow(121666, _ED_P - 2, _ED_P)) % _ED_P
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
_ED_SQRT_M1 = pow(2, (_ED_P - 1) // 4, _ED_P)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _ed_add(a: Tuple[int, int, int, int], b: Tuple[int, int, int, int]) -> Tuple[int, int, int, int]:
|
|
53
|
+
x1, y1, z1, t1 = a
|
|
54
|
+
x2, y2, z2, t2 = b
|
|
55
|
+
aa = (y1 - x1) * (y2 - x2) % _ED_P
|
|
56
|
+
bb = (y1 + x1) * (y2 + x2) % _ED_P
|
|
57
|
+
cc = 2 * t1 * t2 * _ED_D % _ED_P
|
|
58
|
+
dd = 2 * z1 * z2 % _ED_P
|
|
59
|
+
e, f, g, h = bb - aa, dd - cc, dd + cc, bb + aa
|
|
60
|
+
return (e * f % _ED_P, g * h % _ED_P, f * g % _ED_P, e * h % _ED_P)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _ed_mul(scalar: int, point: Tuple[int, int, int, int]) -> Tuple[int, int, int, int]:
|
|
64
|
+
result = (0, 1, 1, 0)
|
|
65
|
+
while scalar:
|
|
66
|
+
if scalar & 1:
|
|
67
|
+
result = _ed_add(result, point)
|
|
68
|
+
point = _ed_add(point, point)
|
|
69
|
+
scalar >>= 1
|
|
70
|
+
return result
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _ed_cswap(a: Tuple[int, int, int, int], b: Tuple[int, int, int, int], bit: int) -> Tuple[Tuple[int, int, int, int], Tuple[int, int, int, int]]:
|
|
74
|
+
"""``(b, a)`` when ``bit`` is 1, ``(a, b)`` when it is 0, by arithmetic rather than a branch on ``bit``."""
|
|
75
|
+
deltas = [(x - y) * bit for x, y in zip(a, b)]
|
|
76
|
+
return (tuple(x - d for x, d in zip(a, deltas)), tuple(y + d for y, d in zip(b, deltas))) # type: ignore[return-value]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def ed_mul_secret(scalar: int, point: Tuple[int, int, int, int]) -> Tuple[int, int, int, int]:
|
|
80
|
+
"""``[scalar]point`` for a secret scalar below 2**256: a Montgomery ladder over all 256 bits with a conditional
|
|
81
|
+
swap, so every scalar takes one addition and one doubling per bit and no step depends on a bit (finding 45). The
|
|
82
|
+
addition law is complete on edwards25519, so the ladder needs no special case for the identity or a doubling."""
|
|
83
|
+
r0, r1 = (0, 1, 1, 0), point
|
|
84
|
+
for i in range(255, -1, -1):
|
|
85
|
+
bit = (scalar >> i) & 1
|
|
86
|
+
r0, r1 = _ed_cswap(r0, r1, bit)
|
|
87
|
+
r1 = _ed_add(r0, r1)
|
|
88
|
+
r0 = _ed_add(r0, r0)
|
|
89
|
+
r0, r1 = _ed_cswap(r0, r1, bit)
|
|
90
|
+
return r0
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _secret_scalar(seed: bytes) -> Tuple[int, bytes]:
|
|
94
|
+
h = hashlib.sha512(seed).digest()
|
|
95
|
+
a = int.from_bytes(h[:32], "little")
|
|
96
|
+
a &= (1 << 254) - 8
|
|
97
|
+
a |= 1 << 254
|
|
98
|
+
return a, h[32:]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def ed25519_seed_public_key(seed: bytes) -> bytes:
|
|
102
|
+
"""The 32-byte public key of a 32-byte Ed25519 seed (RFC 8032 §5.1.5)."""
|
|
103
|
+
return _ed_encode_point(ed_mul_secret(_secret_scalar(seed)[0], _ED_G))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def ed25519_sign(seed: bytes, message: bytes) -> bytes:
|
|
107
|
+
"""A deterministic Ed25519 signature (RFC 8032 §5.1.6); the same seed and message give the bytes Node's crypto.sign
|
|
108
|
+
gives. Both secret multiplications (the key's scalar, the nonce) go through ``ed_mul_secret``."""
|
|
109
|
+
a, prefix = _secret_scalar(seed)
|
|
110
|
+
public = _ed_encode_point(ed_mul_secret(a, _ED_G))
|
|
111
|
+
r = int.from_bytes(hashlib.sha512(prefix + message).digest(), "little") % _ED_L
|
|
112
|
+
big_r = _ed_encode_point(ed_mul_secret(r, _ED_G))
|
|
113
|
+
k = int.from_bytes(hashlib.sha512(big_r + public + message).digest(), "little") % _ED_L
|
|
114
|
+
return big_r + ((r + k * a) % _ED_L).to_bytes(32, "little")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _ed_decode_point(data: bytes) -> Optional[Tuple[int, int, int, int]]:
|
|
118
|
+
if len(data) != 32:
|
|
119
|
+
return None
|
|
120
|
+
y = int.from_bytes(data, "little")
|
|
121
|
+
sign = y >> 255
|
|
122
|
+
y &= (1 << 255) - 1
|
|
123
|
+
if y >= _ED_P:
|
|
124
|
+
return None
|
|
125
|
+
x2 = (y * y - 1) * pow(_ED_D * y * y + 1, _ED_P - 2, _ED_P) % _ED_P
|
|
126
|
+
if x2 == 0:
|
|
127
|
+
if sign:
|
|
128
|
+
return None
|
|
129
|
+
return (0, y, 1, 0)
|
|
130
|
+
x = pow(x2, (_ED_P + 3) // 8, _ED_P)
|
|
131
|
+
if (x * x - x2) % _ED_P:
|
|
132
|
+
x = x * _ED_SQRT_M1 % _ED_P
|
|
133
|
+
if (x * x - x2) % _ED_P:
|
|
134
|
+
return None
|
|
135
|
+
if x & 1 != sign:
|
|
136
|
+
x = _ED_P - x
|
|
137
|
+
return (x, y, 1, x * y % _ED_P)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _ed_encode_point(point: Tuple[int, int, int, int]) -> bytes:
|
|
141
|
+
x, y, z, _ = point
|
|
142
|
+
zi = pow(z, _ED_P - 2, _ED_P)
|
|
143
|
+
x, y = x * zi % _ED_P, y * zi % _ED_P
|
|
144
|
+
return (y | ((x & 1) << 255)).to_bytes(32, "little")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
_ED_G = _ed_decode_point(bytes.fromhex("5866666666666666666666666666666666666666666666666666666666666666"))
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
_ED_IDENTITY = (1).to_bytes(32, "little")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def ed25519_verify(public_key: bytes, message: bytes, signature: bytes) -> bool:
|
|
154
|
+
"""RFC 8032 §5.1.7, cofactorless: [S]B == R + [k]A, compared by R's encoding. False, never an exception."""
|
|
155
|
+
if len(public_key) != 32 or len(signature) != 64:
|
|
156
|
+
return False
|
|
157
|
+
s = int.from_bytes(signature[32:], "little")
|
|
158
|
+
if s >= _ED_L:
|
|
159
|
+
return False
|
|
160
|
+
a = _ed_decode_point(public_key)
|
|
161
|
+
if a is None or _ed_encode_point(_ed_mul(8, a)) == _ED_IDENTITY:
|
|
162
|
+
return False # a small-order key would accept R = [S]B for a quarter or more of all messages; OpenSSL refuses it
|
|
163
|
+
k = int.from_bytes(hashlib.sha512(signature[:32] + public_key + message).digest(), "little") % _ED_L
|
|
164
|
+
neg_a = ((-a[0]) % _ED_P, a[1], a[2], (-a[3]) % _ED_P)
|
|
165
|
+
return secrets.compare_digest(_ed_encode_point(_ed_add(_ed_mul(s, _ED_G), _ed_mul(k, neg_a))), signature[:32])
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def b64url_bytes(text: str) -> bytes:
|
|
169
|
+
"""Buffer.from(text, "base64url") for text of the base64url alphabet: a dangling sixth bit group is ignored."""
|
|
170
|
+
return b64url_decode(text)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def b64url(data: bytes) -> str:
|
|
174
|
+
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def node_base64(text: str) -> bytes:
|
|
178
|
+
"""Buffer.from(text, "base64"): either alphabet, other characters skipped, stops at the first "=", a dangling
|
|
179
|
+
sixth bit group ignored."""
|
|
180
|
+
kept = []
|
|
181
|
+
for ch in text:
|
|
182
|
+
if ch == "=":
|
|
183
|
+
break
|
|
184
|
+
if ch.isascii() and (ch.isalnum() or ch in "+/-_"):
|
|
185
|
+
kept.append({"-": "+", "_": "/"}.get(ch, ch))
|
|
186
|
+
body = "".join(kept)
|
|
187
|
+
if len(body) % 4 == 1:
|
|
188
|
+
body = body[:-1]
|
|
189
|
+
return base64.b64decode(body + "=" * (-len(body) % 4))
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def sha256(data: bytes) -> bytes:
|
|
193
|
+
return hashlib.sha256(data).digest()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
_P256_P = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
_P256_A = _P256_P - 3
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
_P256_B = 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
_P256_N = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
_P256_GX = 0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
_P256_GY = 0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def is_valid_p256_key(raw: bytes) -> bool:
|
|
215
|
+
"""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."""
|
|
216
|
+
if len(raw) != 65 or raw[0] != 0x04:
|
|
217
|
+
return False
|
|
218
|
+
x = int.from_bytes(raw[1:33], "big")
|
|
219
|
+
y = int.from_bytes(raw[33:], "big")
|
|
220
|
+
if x >= _P256_P or y >= _P256_P:
|
|
221
|
+
return False
|
|
222
|
+
return (y * y - (x * x * x + _P256_A * x + _P256_B)) % _P256_P == 0
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _jacobian_double(p: Tuple[int, int, int]) -> Tuple[int, int, int]:
|
|
226
|
+
x, y, z = p
|
|
227
|
+
if y == 0 or z == 0:
|
|
228
|
+
return (0, 1, 0)
|
|
229
|
+
ysq = y * y % _P256_P
|
|
230
|
+
s = 4 * x * ysq % _P256_P
|
|
231
|
+
zsq = z * z % _P256_P
|
|
232
|
+
m = 3 * (x - zsq) * (x + zsq) % _P256_P # a = −3
|
|
233
|
+
nx = (m * m - 2 * s) % _P256_P
|
|
234
|
+
ny = (m * (s - nx) - 8 * ysq * ysq) % _P256_P
|
|
235
|
+
nz = 2 * y * z % _P256_P
|
|
236
|
+
return (nx, ny, nz)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _jacobian_add(p: Tuple[int, int, int], q: Tuple[int, int, int]) -> Tuple[int, int, int]:
|
|
240
|
+
if p[2] == 0:
|
|
241
|
+
return q
|
|
242
|
+
if q[2] == 0:
|
|
243
|
+
return p
|
|
244
|
+
z1z1 = p[2] * p[2] % _P256_P
|
|
245
|
+
z2z2 = q[2] * q[2] % _P256_P
|
|
246
|
+
u1 = p[0] * z2z2 % _P256_P
|
|
247
|
+
u2 = q[0] * z1z1 % _P256_P
|
|
248
|
+
s1 = p[1] * q[2] * z2z2 % _P256_P
|
|
249
|
+
s2 = q[1] * p[2] * z1z1 % _P256_P
|
|
250
|
+
if u1 == u2:
|
|
251
|
+
return _jacobian_double(p) if s1 == s2 else (0, 1, 0)
|
|
252
|
+
h = (u2 - u1) % _P256_P
|
|
253
|
+
r = (s2 - s1) % _P256_P
|
|
254
|
+
hh = h * h % _P256_P
|
|
255
|
+
hhh = h * hh % _P256_P
|
|
256
|
+
v = u1 * hh % _P256_P
|
|
257
|
+
nx = (r * r - hhh - 2 * v) % _P256_P
|
|
258
|
+
ny = (r * (v - nx) - s1 * hhh) % _P256_P
|
|
259
|
+
nz = h * p[2] * q[2] % _P256_P
|
|
260
|
+
return (nx, ny, nz)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _shamir(u1: int, u2: int, q: Tuple[int, int]) -> Optional[Tuple[int, int]]:
|
|
264
|
+
"""u1·G + u2·Q in affine form, or None at infinity."""
|
|
265
|
+
g = (_P256_GX, _P256_GY, 1)
|
|
266
|
+
qj = (q[0], q[1], 1)
|
|
267
|
+
both = _jacobian_add(g, qj)
|
|
268
|
+
out = (0, 1, 0)
|
|
269
|
+
for bit in range(max(u1.bit_length(), u2.bit_length()) - 1, -1, -1):
|
|
270
|
+
out = _jacobian_double(out)
|
|
271
|
+
a, b = (u1 >> bit) & 1, (u2 >> bit) & 1
|
|
272
|
+
if a and b:
|
|
273
|
+
out = _jacobian_add(out, both)
|
|
274
|
+
elif a:
|
|
275
|
+
out = _jacobian_add(out, g)
|
|
276
|
+
elif b:
|
|
277
|
+
out = _jacobian_add(out, qj)
|
|
278
|
+
if out[2] == 0:
|
|
279
|
+
return None
|
|
280
|
+
zinv = pow(out[2], -1, _P256_P)
|
|
281
|
+
return (out[0] * zinv * zinv % _P256_P, out[1] * zinv * zinv * zinv % _P256_P)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _der_integer(data: bytes, at: int) -> Tuple[Optional[int], int]:
|
|
285
|
+
if at + 2 > len(data) or data[at] != 0x02:
|
|
286
|
+
return None, at
|
|
287
|
+
length = data[at + 1]
|
|
288
|
+
start = at + 2
|
|
289
|
+
if length == 0 or length > 33 or start + length > len(data):
|
|
290
|
+
return None, at
|
|
291
|
+
body = data[start:start + length]
|
|
292
|
+
if body[0] & 0x80:
|
|
293
|
+
return None, at # negative
|
|
294
|
+
if length > 1 and body[0] == 0 and not body[1] & 0x80:
|
|
295
|
+
return None, at # not minimal
|
|
296
|
+
return int.from_bytes(body, "big"), start + length
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _der_signature(signature: bytes) -> Optional[Tuple[int, int]]:
|
|
300
|
+
"""An ASN.1 DER Ecdsa-Sig-Value (RFC 3279 §2.2.3), strictly: one SEQUENCE of two minimal positive INTEGERs."""
|
|
301
|
+
if len(signature) < 8 or signature[0] != 0x30:
|
|
302
|
+
return None
|
|
303
|
+
if signature[1] & 0x80:
|
|
304
|
+
return None # a P-256 signature fits a short-form length
|
|
305
|
+
if signature[1] != len(signature) - 2:
|
|
306
|
+
return None
|
|
307
|
+
r, at = _der_integer(signature, 2)
|
|
308
|
+
if r is None:
|
|
309
|
+
return None
|
|
310
|
+
s, at = _der_integer(signature, at)
|
|
311
|
+
if s is None or at != len(signature):
|
|
312
|
+
return None
|
|
313
|
+
return r, s
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def es256_verify(x: bytes, y: bytes, message: bytes, signature: bytes) -> bool:
|
|
317
|
+
"""ECDSA P-256 with SHA-256 over `message`, the signature in DER (FIPS 186-5 §6.4.2). False, never an exception."""
|
|
318
|
+
if not is_valid_p256_key(b"\x04" + x + y):
|
|
319
|
+
return False
|
|
320
|
+
parsed = _der_signature(signature)
|
|
321
|
+
if parsed is None:
|
|
322
|
+
return False
|
|
323
|
+
r, s = parsed
|
|
324
|
+
if not (1 <= r < _P256_N and 1 <= s < _P256_N):
|
|
325
|
+
return False
|
|
326
|
+
e = int.from_bytes(sha256(message), "big")
|
|
327
|
+
w = pow(s, -1, _P256_N)
|
|
328
|
+
point = _shamir(e * w % _P256_N, r * w % _P256_N, (int.from_bytes(x, "big"), int.from_bytes(y, "big")))
|
|
329
|
+
return point is not None and point[0] % _P256_N == r
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def b64url_decode(segment: str) -> bytes:
|
|
333
|
+
"""Base64url without padding, as Node reads it: a dangling sixth bit group is ignored."""
|
|
334
|
+
if len(segment) % 4 == 1:
|
|
335
|
+
segment = segment[:-1]
|
|
336
|
+
return base64.urlsafe_b64decode(segment + "=" * (-len(segment) % 4))
|