@passioncode-ai/fabric-agent-adapter 0.22.0 → 0.24.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.
Files changed (23) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  5. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +3 -3
  6. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +2 -2
  7. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +7 -4
  8. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +7 -4
  9. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +10 -7
  10. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +32 -15
  11. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +64 -14
  12. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +9 -5
  13. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +24 -4
  14. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +35 -20
  15. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +77 -9
  16. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +317 -45
  17. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +53 -19
  18. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +107 -16
  19. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +472 -62
  20. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +10 -3
  21. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +3 -3
  22. package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +1 -1
  23. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +1 -1
@@ -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
- def _estate_problem(estate: Any, jwks: Dict[str, Any], kid: str, now: float) -> Optional[Tuple[str, str]]:
436
- """DL-14 with DEC-0055: a verifier that reads the estate record trusts a kid only while a current revision certifies
437
- it for a machine and does not revoke it. `estate` is a revision the caller verified (fabric_estate.
438
- verify_estate_record) or a callable returning the current one; None skips the rule. (refusal kind, detail) or None."""
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
- except Exception: # an unreadable record certifies nothing; it is never a pass
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
- jwk = next((k for k in jwks["keys"] if isinstance(k, dict) and k.get("kid") == kid), {})
447
- if not fabric_estate.hub_key_certified(record, kid, jwk.get("x"), now):
448
- return ("token", "kid-not-certified")
449
- return None
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) -> Dict[str, Any]:
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, jwks, checked["kid"], now)
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) -> Dict[str, Any]:
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, jwks, checked["kid"], now)
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) -> Callable[..., Dict[str, Any]]:
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
@@ -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
- if now < issued_at:
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
- def hub_key_certified(record: Any, kid: str, x: str, at: Any) -> bool:
584
- """DL-14 with DEC-0055: a delegation verifier that reads the estate record trusts a token's `kid` only while a
585
- current revision certifies it for a machine and does not revoke it. ``record`` is a revision the caller verified
586
- (verify_estate_record); ``x`` is the key the published key set gives for ``kid``; ``at`` is the clock."""
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 not isinstance(record, dict) or not math.isfinite(now):
589
- return False
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
- if not (math.isfinite(issued_at) and math.isfinite(valid_until) and issued_at <= now <= valid_until):
592
- return False
593
- revoked = {_revocation_key(e) for e in _list(_get(record, "revocations"))}
594
- for machine in _list(_get(record, "machines")):
595
- hub = _get(machine, "hubKey")
596
- if (isinstance(hub, dict) and _same(_get(hub, "kid"), kid) and _same(_get(hub, "x"), x)
597
- and "hub-key %s" % kid not in revoked and "machine %s" % _s(machine, "id") not in revoked):
598
- return True
599
- return False
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