@passioncode-ai/fabric-agent-adapter 0.21.0 → 0.23.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 +112 -0
- package/README.md +38 -6
- package/package.json +1 -1
- package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +2 -2
- package/plugins/fabric-agent-adapter/hooks/hooks.json +43 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +3 -3
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +10 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +32 -15
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +46 -14
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +5 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +35 -20
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +77 -9
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +167 -36
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +53 -19
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +107 -16
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +270 -49
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +8 -2
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +3 -3
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +217 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +99 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +111 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +1265 -0
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py
CHANGED
|
@@ -309,7 +309,7 @@ def _key_for(jwks: Dict[str, Any], kid: str) -> Dict[str, Any]:
|
|
|
309
309
|
return {"problem": "unusable-key"}
|
|
310
310
|
if is_small_order_key(raw):
|
|
311
311
|
return {"problem": "small-order-key"} # refused before any verification (DEC-0045, CT-29)
|
|
312
|
-
return {"key": raw}
|
|
312
|
+
return {"key": raw, "x": jwk["x"]}
|
|
313
313
|
|
|
314
314
|
|
|
315
315
|
def _has_keys(jwks: Any) -> bool:
|
|
@@ -331,7 +331,7 @@ def _authentic(token: Any, jwks: Dict[str, Any], typ: str) -> Dict[str, Any]:
|
|
|
331
331
|
return {"problem": found["problem"], "issuer": found["problem"] != "unknown-key"}
|
|
332
332
|
if len(compact["signature"]) != 64 or not ed25519_verify(found["key"], compact["signing_input"], compact["signature"]):
|
|
333
333
|
return {"problem": "bad-signature"}
|
|
334
|
-
return {"compact": compact, "kid": kid}
|
|
334
|
+
return {"compact": compact, "kid": kid, "x": found["x"]}
|
|
335
335
|
|
|
336
336
|
|
|
337
337
|
def chain_depth(act: Any) -> float:
|
|
@@ -432,30 +432,58 @@ def _approval_shape(payload: Dict[str, Any]) -> bool:
|
|
|
432
432
|
return payload.get("method") in ("prompt", "operator-channel") # DEC-0045: `device` is removed
|
|
433
433
|
|
|
434
434
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
435
|
+
#: ECMAScript's time range in seconds (±8.64e15 ms): a NumericDate outside it is no instant a Date can hold.
|
|
436
|
+
MAX_CLOCK_SECONDS = 8.64e12
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _readable_clock(now: Any) -> bool:
|
|
440
|
+
"""A clock the time rows can compare: a finite number of seconds within a Date's range (DEC-0060). NaN or text
|
|
441
|
+
would make every time row false and let an expired token through."""
|
|
442
|
+
if not isinstance(now, (int, float)) or isinstance(now, bool):
|
|
443
|
+
return False
|
|
444
|
+
try:
|
|
445
|
+
return math.isfinite(now) and abs(now) <= MAX_CLOCK_SECONDS
|
|
446
|
+
except OverflowError:
|
|
447
|
+
return False
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _estate_problem(estate: Any, kid: str, x: str, now: float, applied: Any = None,
|
|
451
|
+
revocations: Any = None) -> Optional[Tuple[str, str]]:
|
|
452
|
+
"""DL-14 row 4a (DEC-0055, DEC-0060), right after the signature: when the callee reads the estate record, the
|
|
453
|
+
token's kid and the published key must be certified by that revision. `estate` is a revision the caller verified
|
|
454
|
+
(fabric_estate.verify_estate_record) and its machine applied, or a callable returning it; None skips the rule (a
|
|
455
|
+
callable returning None is an unreadable source). `applied` is the highest revision the machine applied
|
|
456
|
+
(`{"revision", "digest"?}`); `revocations` the revocation delta's entries applied since (a list or a callable). A
|
|
457
|
+
source that raises is unreadable, never a pass. Returns ("token" | "issuer", detail) or None."""
|
|
439
458
|
if estate is None:
|
|
440
459
|
return None
|
|
441
460
|
try:
|
|
442
461
|
record = estate() if callable(estate) else estate
|
|
443
|
-
|
|
462
|
+
extra = revocations() if callable(revocations) else revocations
|
|
463
|
+
except Exception:
|
|
444
464
|
return ("issuer", "estate-unreadable")
|
|
445
465
|
import fabric_estate # beside this file; imported here, since fabric_estate imports this module
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
466
|
+
verdict = fabric_estate.hub_key_certified(record, kid, x, now, applied,
|
|
467
|
+
fabric_estate._MISSING if extra is None else extra)
|
|
468
|
+
if verdict["certified"]:
|
|
469
|
+
return None
|
|
470
|
+
# The clock was refused before row 1, so a verdict here is the token's or the trust source's.
|
|
471
|
+
return ("token" if verdict["fault"] == "token" else "issuer", verdict["detail"])
|
|
450
472
|
|
|
451
473
|
|
|
452
474
|
def verify_delegation(token: Any, *, audience: str, jwks: Any, now: float, capability: str, effect: str,
|
|
453
|
-
issuer: Optional[str], read_only: bool = False, estate: Any = None
|
|
475
|
+
issuer: Optional[str], read_only: bool = False, estate: Any = None,
|
|
476
|
+
estate_applied: Any = None, estate_revocations: Any = None) -> Dict[str, Any]:
|
|
454
477
|
"""DL-13, in its fixed order: nothing about a token is believed before its signature is.
|
|
455
478
|
`now` is seconds since the epoch. Returns {ok: True, claims, principal, caller, chain, kid} or a refusal
|
|
456
479
|
{ok: False, code, detail[, data]} whose detail is a short machine word, never the token. `issuer` is required.
|
|
457
480
|
With `estate` (a verified estate revision, or a callable returning the current one), the token's kid must be one
|
|
458
|
-
the revision certifies for a machine (DL-14, DEC-0055)
|
|
481
|
+
the revision certifies for a machine (DL-14, DEC-0055, DEC-0060); `estate_applied` is the highest revision the
|
|
482
|
+
machine applied and `estate_revocations` the revocation delta's entries since. A clock that is not a number of
|
|
483
|
+
seconds is refused `unknown-state` `no-clock` before row 1."""
|
|
484
|
+
# Before row 1: every time row compares against the clock, and a comparison with NaN is always false.
|
|
485
|
+
if not _readable_clock(now):
|
|
486
|
+
return _refuse("unknown-state", "no-clock")
|
|
459
487
|
if not _is_string(issuer):
|
|
460
488
|
return _refuse("issuer-unavailable", "no-trusted-issuer")
|
|
461
489
|
if not _has_keys(jwks):
|
|
@@ -463,7 +491,7 @@ def verify_delegation(token: Any, *, audience: str, jwks: Any, now: float, capab
|
|
|
463
491
|
checked = _authentic(token, jwks, DELEGATION_TYP)
|
|
464
492
|
if "problem" in checked:
|
|
465
493
|
return _refuse("issuer-unavailable" if checked.get("issuer") else "unauthenticated", checked["problem"])
|
|
466
|
-
uncertified = _estate_problem(estate,
|
|
494
|
+
uncertified = _estate_problem(estate, checked["kid"], checked["x"], now, estate_applied, estate_revocations)
|
|
467
495
|
if uncertified:
|
|
468
496
|
return _refuse("issuer-unavailable" if uncertified[0] == "issuer" else "unauthenticated", uncertified[1])
|
|
469
497
|
claims = checked["compact"]["payload"]
|
|
@@ -565,13 +593,16 @@ UNSAFE_NO_JTI_STORE = _NoReplayProtection()
|
|
|
565
593
|
|
|
566
594
|
|
|
567
595
|
def verify_approval(approval: Optional[str], *, audience: str, jwks: Any, now: float, action_digest: str,
|
|
568
|
-
jti_store: Any, issuer: Optional[str], estate: Any = None
|
|
596
|
+
jti_store: Any, issuer: Optional[str], estate: Any = None, estate_applied: Any = None,
|
|
597
|
+
estate_revocations: Any = None) -> Dict[str, Any]:
|
|
569
598
|
"""DL-18: a missing approval is approval-required carrying the action digest; any fault of the document is
|
|
570
599
|
approval-invalid; a second use is approval-reused. The jti is consumed last."""
|
|
571
600
|
if approval is None or approval == "":
|
|
572
601
|
return _refuse("approval-required", "no-approval", {"actionDigest": action_digest})
|
|
573
602
|
if jti_store is None or not callable(getattr(jti_store, "consume", None)):
|
|
574
603
|
return _refuse("unknown-state", "no-jti-store")
|
|
604
|
+
if not _readable_clock(now):
|
|
605
|
+
return _refuse("unknown-state", "no-clock")
|
|
575
606
|
if not _is_string(issuer):
|
|
576
607
|
return _refuse("issuer-unavailable", "no-trusted-issuer")
|
|
577
608
|
if not _has_keys(jwks):
|
|
@@ -579,7 +610,7 @@ def verify_approval(approval: Optional[str], *, audience: str, jwks: Any, now: f
|
|
|
579
610
|
checked = _authentic(approval, jwks, APPROVAL_TYP)
|
|
580
611
|
if "problem" in checked:
|
|
581
612
|
return _refuse("issuer-unavailable" if checked.get("issuer") else "approval-invalid", checked["problem"])
|
|
582
|
-
uncertified = _estate_problem(estate,
|
|
613
|
+
uncertified = _estate_problem(estate, checked["kid"], checked["x"], now, estate_applied, estate_revocations)
|
|
583
614
|
if uncertified:
|
|
584
615
|
return _refuse("issuer-unavailable" if uncertified[0] == "issuer" else "approval-invalid", uncertified[1])
|
|
585
616
|
claims = checked["compact"]["payload"]
|
|
@@ -820,7 +851,8 @@ TIERS = ("T0", "T1", "T2")
|
|
|
820
851
|
|
|
821
852
|
|
|
822
853
|
def delegation_gate(*, audience: str, jwks: Any, effects: Mapping[str, str], issuer: str,
|
|
823
|
-
read_only: bool = False, clock: Callable[[], float] = time.time, estate: Any = None
|
|
854
|
+
read_only: bool = False, clock: Callable[[], float] = time.time, estate: Any = None,
|
|
855
|
+
estate_applied: Any = None, estate_revocations: Any = None) -> Callable[..., Dict[str, Any]]:
|
|
824
856
|
"""A check for one tools/call: `gate(tool, headers, tier="T2")` answers the accepted delegation or a DL-19
|
|
825
857
|
refusal. `effects` maps each tool to its declared effect class (its manifest entry); a tool with none is
|
|
826
858
|
refused rather than guessed. `jwks` is a JwksCache or a key set; `issuer` (auth.delegation.issuer) is required.
|
|
@@ -830,7 +862,8 @@ def delegation_gate(*, audience: str, jwks: Any, effects: Mapping[str, str], iss
|
|
|
830
862
|
relayed from off the machine. A T0 or T1 caller is never required to present a token: with none, the call
|
|
831
863
|
passes with no delegation ({ok: True, tier, principal: None, ...}). A present token is verified at every tier
|
|
832
864
|
and refused when invalid. Unknown tiers count as T2. `estate` (a callable returning the current verified estate
|
|
833
|
-
revision) makes the gate trust only kids that revision certifies (DL-14, DEC-0055)
|
|
865
|
+
revision) makes the gate trust only kids that revision certifies (DL-14, DEC-0055, DEC-0060); `estate_applied` and
|
|
866
|
+
`estate_revocations` pass the applied mark and the revocation delta's entries through."""
|
|
834
867
|
if not _is_string(issuer):
|
|
835
868
|
raise ValueError("the gate needs the issuer it trusts (auth.delegation.issuer)")
|
|
836
869
|
|
|
@@ -845,7 +878,8 @@ def delegation_gate(*, audience: str, jwks: Any, effects: Mapping[str, str], iss
|
|
|
845
878
|
return _refuse("unauthenticated", "no-token")
|
|
846
879
|
keys = jwks.get(_kid_of(carried["token"])) if isinstance(jwks, JwksCache) else jwks
|
|
847
880
|
verdict = verify_delegation(carried["token"], audience=audience, jwks=keys, now=clock(), capability=tool,
|
|
848
|
-
effect=effect, issuer=issuer, read_only=read_only, estate=estate
|
|
881
|
+
effect=effect, issuer=issuer, read_only=read_only, estate=estate,
|
|
882
|
+
estate_applied=estate_applied, estate_revocations=estate_revocations)
|
|
849
883
|
if verdict["ok"]:
|
|
850
884
|
verdict["tier"] = tier if tier in TIERS else "T2"
|
|
851
885
|
return verdict
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py
CHANGED
|
@@ -36,6 +36,9 @@ import fabric_secrets as _secrets # noqa: E402 - beside this file: the contract
|
|
|
36
36
|
ESTATE_RECORD_SCHEMA = "estate-record/1"
|
|
37
37
|
ESTATE_CHALLENGE_TYPE = "estate-revision/1"
|
|
38
38
|
MAX_VALIDITY_SECONDS = 30 * 24 * 60 * 60
|
|
39
|
+
#: How far ahead of the verifier's clock a signed `issuedAt` may be before it is `not-yet-valid` (DEC-0066): the one
|
|
40
|
+
#: skew of the relay family's and the estate record's times (`CLOCK_SKEW_SECONDS` in the contract's `src/rfc3339.ts`).
|
|
41
|
+
CLOCK_SKEW_SECONDS = 60
|
|
39
42
|
ASSERTION_ALGORITHMS = ("ES256", "EdDSA")
|
|
40
43
|
FLAGS = {"UP": 0x01, "UV": 0x04, "BE": 0x08, "BS": 0x10, "AT": 0x40, "ED": 0x80}
|
|
41
44
|
_MISSING = object()
|
|
@@ -479,6 +482,37 @@ def _clock(at: Any) -> float:
|
|
|
479
482
|
return math.nan
|
|
480
483
|
|
|
481
484
|
|
|
485
|
+
_LABEL = "[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?"
|
|
486
|
+
#: `sign.` and at least two more labels, lower case: a signing RP ID is never a bare `sign.<tld>` (DEC-0063).
|
|
487
|
+
_SIGNING_RP = re.compile(r"sign(?:\.%s){2,}" % _LABEL, re.ASCII)
|
|
488
|
+
_HTTPS_HOST = re.compile(r"https://([a-z0-9.-]{1,253})", re.ASCII)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def is_signing_rp_id(rp_id: Any) -> bool:
|
|
492
|
+
"""Whether an RP ID is a dedicated signing RP (v4 §7, DEC-0063): first label `sign`, then the organization's or
|
|
493
|
+
PassionCode's domain, lower case. A credential under any other RP ID is a sign-in credential and never signs."""
|
|
494
|
+
return isinstance(rp_id, str) and len(rp_id) <= 253 and _SIGNING_RP.fullmatch(rp_id) is not None
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def origin_within(origin: Any, rp_id: str) -> bool:
|
|
498
|
+
"""Whether a pinned origin may run a signing ceremony for ``rp_id``: `https://`, no port, no path, its host the RP
|
|
499
|
+
ID or a subdomain of it, lower case. Related origins are never used for signing."""
|
|
500
|
+
match = _HTTPS_HOST.fullmatch(origin) if isinstance(origin, str) else None
|
|
501
|
+
return match is not None and (match.group(1) == rp_id or match.group(1).endswith("." + rp_id))
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _signing_credential(credential: Any, path: str, flag) -> None:
|
|
505
|
+
"""FAC-SEM-092 on one signing credential: a signing RP ID, and every pinned origin within it."""
|
|
506
|
+
value = credential if isinstance(credential, dict) else {}
|
|
507
|
+
if not is_signing_rp_id(_get(value, "rpId")):
|
|
508
|
+
flag("%s/rpId" % path, "a passkey signs only under a dedicated signing RP, sign.<domain>, never under the RP ID it signs in with")
|
|
509
|
+
return
|
|
510
|
+
origins = _get(value, "origins")
|
|
511
|
+
for index, origin in enumerate(origins if isinstance(origins, list) else []):
|
|
512
|
+
if not origin_within(origin, value["rpId"]):
|
|
513
|
+
flag("%s/origins/%d" % (path, index), "a signing ceremony runs only on %s or a host under it, over https without a port" % value["rpId"])
|
|
514
|
+
|
|
515
|
+
|
|
482
516
|
def _revocation_key(entry: Dict[str, Any]) -> str:
|
|
483
517
|
return "%s %s" % (_s(entry, "kind"), _s(entry, "ref"))
|
|
484
518
|
|
|
@@ -548,6 +582,11 @@ def verify_estate_record(record: Any, *, at: Any, parent: Any = _MISSING, author
|
|
|
548
582
|
credential = {"credentialId": _s(authority, "credentialId"), "alg": _s(authority, "alg"),
|
|
549
583
|
"publicKey": authority["publicKey"] if isinstance(_get(authority, "publicKey"), dict) else {},
|
|
550
584
|
"rpId": _s(authority, "rpId"), "origins": [o for o in origins if isinstance(o, str)] if isinstance(origins, list) else []}
|
|
585
|
+
# DEC-0063: the estate passkey is a signing credential. An RP ID that is not sign.<domain> is wrong-rp before the
|
|
586
|
+
# assertion is read; an origin outside the RP ID is not pinned, so a ceremony that ran there is wrong-origin.
|
|
587
|
+
if not is_signing_rp_id(_get(authority, "rpId")):
|
|
588
|
+
return {"ok": False, "problem": "wrong-rp"}
|
|
589
|
+
credential["origins"] = [o for o in credential["origins"] if origin_within(o, credential["rpId"])]
|
|
551
590
|
assertion = verify_assertion(signature, credential, estate_challenge(digest))
|
|
552
591
|
if not assertion["ok"]:
|
|
553
592
|
return {"ok": False, "problem": assertion["problem"]}
|
|
@@ -557,7 +596,8 @@ def verify_estate_record(record: Any, *, at: Any, parent: Any = _MISSING, author
|
|
|
557
596
|
if (not math.isfinite(issued_at) or not math.isfinite(valid_until) or valid_until <= issued_at
|
|
558
597
|
or valid_until - issued_at > MAX_VALIDITY_SECONDS * 1000):
|
|
559
598
|
return {"ok": False, "problem": "too-long-lived"}
|
|
560
|
-
|
|
599
|
+
# A revision issued a moment ahead of this clock is admitted within the skew every relay time allows (DEC-0066).
|
|
600
|
+
if now < issued_at - CLOCK_SKEW_SECONDS * 1000:
|
|
561
601
|
return {"ok": False, "problem": "not-yet-valid"}
|
|
562
602
|
if now > valid_until:
|
|
563
603
|
return {"ok": False, "problem": "expired"}
|
|
@@ -580,23 +620,72 @@ def placement_trust(record: Dict[str, Any], machine_id: str, agent_ref: str) ->
|
|
|
580
620
|
return {"publisher": [k for k in _list(_get(record, "publisherKeys")) if _same(_get(k, "kid"), _get(placement, "publisherKid"))]}
|
|
581
621
|
|
|
582
622
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
623
|
+
#: v4 §7, Freshness: an estate revision's window is at most 30 days (DEC-0055 item 2).
|
|
624
|
+
_MAX_WINDOW_MS = MAX_VALIDITY_SECONDS * 1000
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def _refused(fault: str, detail: str) -> Dict[str, Any]:
|
|
628
|
+
return {"certified": False, "fault": fault, "detail": detail}
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def _revision_number(value: Any) -> bool:
|
|
632
|
+
return _safe_integer(value) and value >= 1
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def hub_key_certified(record: Any, kid: Any, x: Any, at: Any, applied: Any = None,
|
|
636
|
+
extra_revocations: Any = _MISSING) -> Dict[str, Any]:
|
|
637
|
+
"""DL-14 (DEC-0055, DEC-0060): whether ``record`` certifies the delegation key ``kid`` with the published key's ``x``
|
|
638
|
+
at ``at`` (an aware datetime or epoch seconds), for a verifier whose machine applied ``applied``
|
|
639
|
+
(``{"revision", "digest"?}``). ``extra_revocations`` are the revocation delta's entries applied since the revision
|
|
640
|
+
(DEC-0054). The contract's ``hubKeyCertified``, in its order; never raises. Answers ``{"certified": True, "machine",
|
|
641
|
+
"revision"}`` or ``{"certified": False, "fault": "token"|"estate"|"clock", "detail"}`` with detail
|
|
642
|
+
``kid-not-certified`` (the token's fault), ``estate-unreadable``, ``estate-outside-window``, ``estate-rolled-back``,
|
|
643
|
+
``estate-forked`` (the trust source's) or ``no-clock``."""
|
|
587
644
|
now = _clock(at)
|
|
588
|
-
if
|
|
589
|
-
return
|
|
645
|
+
if math.isnan(now):
|
|
646
|
+
return _refused("clock", "no-clock")
|
|
647
|
+
if (not isinstance(record, dict) or not _revision_number(_get(record, "revision"))
|
|
648
|
+
or not isinstance(_get(record, "digest"), str) or not isinstance(_get(record, "machines"), list)):
|
|
649
|
+
return _refused("estate", "estate-unreadable")
|
|
650
|
+
revocations = _get(record, "revocations")
|
|
651
|
+
if revocations is not _MISSING and not isinstance(revocations, list):
|
|
652
|
+
return _refused("estate", "estate-unreadable")
|
|
653
|
+
if extra_revocations is not _MISSING and not isinstance(extra_revocations, list):
|
|
654
|
+
return _refused("estate", "estate-unreadable")
|
|
590
655
|
issued_at, valid_until = estate_time(_get(record, "issuedAt")), estate_time(_get(record, "validUntil"))
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
656
|
+
# A window that is not a time, not positive or longer than 30 days is no window: NaN fails every comparison below.
|
|
657
|
+
if math.isnan(issued_at) or math.isnan(valid_until) or valid_until <= issued_at or valid_until - issued_at > _MAX_WINDOW_MS:
|
|
658
|
+
return _refused("estate", "estate-unreadable")
|
|
659
|
+
if applied is not None:
|
|
660
|
+
if (not isinstance(applied, dict) or not _revision_number(_get(applied, "revision"))
|
|
661
|
+
or (_get(applied, "digest") is not _MISSING and not isinstance(_get(applied, "digest"), str))):
|
|
662
|
+
return _refused("estate", "estate-unreadable")
|
|
663
|
+
# An older revision may still be inside its window and list a key the applied one revokes (a rollback).
|
|
664
|
+
if record["revision"] < applied["revision"]:
|
|
665
|
+
return _refused("estate", "estate-rolled-back")
|
|
666
|
+
if (record["revision"] == applied["revision"] and _get(applied, "digest") is not _MISSING
|
|
667
|
+
and record["digest"] != applied["digest"]):
|
|
668
|
+
return _refused("estate", "estate-forked")
|
|
669
|
+
# The window verify_estate_record admits the revision in: from 60 s before issuedAt to validUntil (DEC-0066).
|
|
670
|
+
if now < issued_at - CLOCK_SKEW_SECONDS * 1000 or now > valid_until:
|
|
671
|
+
return _refused("estate", "estate-outside-window")
|
|
672
|
+
|
|
673
|
+
entries = list(revocations if isinstance(revocations, list) else []) + list(extra_revocations if isinstance(extra_revocations, list) else [])
|
|
674
|
+
|
|
675
|
+
def revokes(kind: str, ref: Any) -> bool: # a ref is compared exactly, never as a string conversion
|
|
676
|
+
return any(isinstance(e, dict) and _same(_get(e, "kind"), kind) and _same(_get(e, "ref"), ref) for e in entries)
|
|
677
|
+
|
|
678
|
+
if not isinstance(kid, str) or revokes("hub-key", kid):
|
|
679
|
+
return _refused("token", "kid-not-certified")
|
|
680
|
+
holders = [m for m in record["machines"] if isinstance(m, dict) and isinstance(_get(m, "hubKey"), dict) and _same(_get(m["hubKey"], "kid"), kid)]
|
|
681
|
+
if len(holders) != 1: # two machines under one kid break FAC-SEM-073; such a revision certifies neither
|
|
682
|
+
return _refused("token", "kid-not-certified")
|
|
683
|
+
machine = holders[0]
|
|
684
|
+
if not isinstance(x, str) or not _same(_get(machine["hubKey"], "x"), x): # a missing x never equals a missing x
|
|
685
|
+
return _refused("token", "kid-not-certified")
|
|
686
|
+
if revokes("machine", _get(machine, "id")):
|
|
687
|
+
return _refused("token", "kid-not-certified")
|
|
688
|
+
return {"certified": True, "machine": _s(machine, "id"), "revision": record["revision"]}
|
|
600
689
|
|
|
601
690
|
|
|
602
691
|
# --- FAC-SEM-072…076 ------------------------------------------------------------------------------------------------
|
|
@@ -773,6 +862,8 @@ def _record_findings(value: Dict[str, Any]) -> List[Dict[str, str]]:
|
|
|
773
862
|
_placements(value, _finding(findings, "FAC-SEM-074"))
|
|
774
863
|
_secret_references(value, _finding(findings, "FAC-SEM-075"))
|
|
775
864
|
_grants_and_revocations(value, _finding(findings, "FAC-SEM-076"))
|
|
865
|
+
for index, authority in enumerate(_list(_get(value, "authorities"))): # FAC-SEM-092 (DEC-0063)
|
|
866
|
+
_signing_credential(authority, "/authorities/%d" % index, _finding(findings, "FAC-SEM-092"))
|
|
776
867
|
return findings
|
|
777
868
|
|
|
778
869
|
|