attestation.py python
662 lines 26.8 KB
Raw
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 32 days ago
1 """Generic attestation provider framework — Phase 7.1.
2
3 A small extension point that lets domain plugins anchor commit provenance
4 in *external trust roots* — HMAC AIR endpoints, blockchain canisters, TSA
5 timestamping authorities, etc. — without coupling the Muse engine to any
6 specific provider implementation.
7
8 The first consumer is the Knowtation domain (see
9 :mod:`muse.plugins.knowtation.attestation`), which implements an HMAC AIR
10 provider that is functionally equivalent to the JS ``knowtation/lib/air.mjs``
11 module. The second is the ICP push hook (Phase 7.4) that anchors the
12 attestation record on the live Knowtation attestation canister
13 (``dejku-syaaa-aaaaa-qgy3q-cai``).
14
15 Design contract — security-critical
16 -----------------------------------
17
18 1. **Misuse-resistance: partial verifications are hard errors.**
19 :meth:`MuseAttestationProvider.verify` MUST raise
20 :class:`AttestationVerifyError` whenever it cannot complete the verification
21 chain (e.g. ICP unreachable, signature parse failure, missing key material).
22 Returning ``VerifyResult(valid=False)`` is reserved for the case of a
23 *known-bad* record where the verifier was able to complete the check.
24 Silent ``True`` returns are *never* permitted.
25
26 2. **Determinism.** :meth:`MuseAttestationProvider.compute_attestation` MUST
27 be deterministic for the same ``(snapshot_id, commit_meta)`` pair — no
28 random salts in the interface, no wall-clock timestamps in the ID. This
29 enables idempotent re-attestation: replaying a commit on a fresh node
30 produces the same attestation ID, so the canister will reject the second
31 write with ``"already exists"`` and the system stays in a consistent state.
32
33 3. **Concurrency safety.** :meth:`MuseAttestationProvider.anchor` and
34 :meth:`MuseAttestationProvider.verify` MUST be safe to call from
35 concurrent threads without sharing mutable state. The registry itself is
36 serialised by an internal :class:`threading.Lock` so concurrent
37 registrations cannot lose entries.
38
39 4. **Write-once receipts.** :class:`AnchorReceipt.tx_id` is write-once after
40 :meth:`MuseAttestationProvider.anchor` returns. This matches the live
41 ICP canister design — the attestation ledger is immutable and append-only;
42 re-anchoring the same record raises
43 :class:`ImmutableReceiptError` rather than silently mutating state.
44
45 5. **No silent exception swallowing.** Errors at the framework layer are
46 propagated or wrapped in typed errors (:class:`AttestationError` and
47 subclasses). Provider implementations are free to convert their
48 transport-level errors into these typed errors but MUST NOT log-and-ignore.
49
50 Threat model documented inline
51 ------------------------------
52
53 * **Replay across branches** — :func:`muse.core.provenance.provenance_payload`
54 binds the commit_id, so the same payload cannot be re-used for a different
55 commit. Different commits have different commit_ids by construction.
56 * **Anchor forgery** — the canister's ``storeAttestation`` requires an
57 authorised principal; unauthorised calls are rejected at the canister
58 boundary. The :meth:`MuseAttestationProvider.verify` method MUST cross-check
59 the receipt against the canister's HTTP query interface to rule out
60 spoofed receipts.
61 * **Identity-chain depth manipulation** — :class:`VerifyResult.depth` records
62 how many identity-record hops were validated during verification (e.g. for
63 org commits with quorum signing — Phase 7.7). A low depth on a high-stakes
64 commit is an audit signal but not a verification failure on its own.
65 """
66
67 from __future__ import annotations
68
69 import threading
70 from typing import Protocol, TypedDict, runtime_checkable
71
72
73 # ---------------------------------------------------------------------------
74 # Typed records — flat TypedDicts are JSON-serialisable for storage in
75 # CommitRecord.metadata and for transport over the MCP/CLI boundary.
76 # ---------------------------------------------------------------------------
77
78
79 class AttestationRecord(TypedDict, total=False):
80 """A single attestation record produced by a provider.
81
82 Stored under ``commit.metadata["attestation"]`` after a successful
83 ``muse commit --attest``. All fields are strings to keep the record
84 JSON-clean and forward-compatible across provider implementations.
85
86 Attributes:
87 id: Globally-unique attestation identifier — typically
88 ``SHA-256(snapshot_id + "\\x00" + path)`` so the ID is deterministic
89 and content-addressed. Must be stable across provider invocations
90 with the same inputs.
91 action: The action being attested (``"write"``, ``"export"``,
92 ``"merge"``, …). Provider-defined; the Knowtation HMAC provider
93 uses the same vocabulary as ``knowtation/lib/air.mjs``.
94 path: The vault-relative or repo-relative path being attested.
95 Empty string when the attestation is repo-scoped rather than
96 file-scoped.
97 timestamp: ISO-8601 UTC timestamp of attestation creation. Bound
98 into the signature so back-dating cannot alter verification.
99 content_hash: SHA-256 hex of the content being attested. Lower-case,
100 64 hex characters.
101 sig: Provider-specific signature/MAC over the canonical record bytes.
102 Format and verification semantics are owned by the provider.
103 algorithm: Stable identifier for the signature algorithm
104 (e.g. ``"hmac-sha256"``, ``"ed25519"``). Required so verifiers
105 can reject records using a deprecated algorithm.
106 provider_id: The domain string the provider is registered under
107 (e.g. ``"knowtation"``). Required so verifiers can dispatch
108 to the correct provider on disconnected systems.
109 """
110
111 id: str
112 action: str
113 path: str
114 timestamp: str
115 content_hash: str
116 sig: str
117 algorithm: str
118 provider_id: str
119
120
121 class AnchorReceipt(TypedDict, total=False):
122 """Receipt returned by :meth:`MuseAttestationProvider.anchor`.
123
124 Embeds the off-chain transaction identifier and a public anchor URL that
125 third parties can use to independently verify the record without contacting
126 the original signer.
127
128 Attributes:
129 tx_id: Transaction identifier from the anchor (e.g. ICP canister seq
130 number, blockchain tx hash). Write-once: re-anchoring the same
131 record MUST raise :class:`ImmutableReceiptError`, not produce a
132 second receipt.
133 anchor_url: HTTPS URL where the record can be independently fetched
134 (e.g. ``https://<canister_id>.ic0.app/attest/<id>``). MUST be
135 HTTPS — verifiers MUST reject non-HTTPS anchor URLs.
136 anchored_at: ISO-8601 UTC timestamp of when the anchor was written.
137 For audit trails; not bound into the signature.
138 provider_id: Same domain string as the corresponding
139 :class:`AttestationRecord.provider_id`. Required because a
140 single repo may hold receipts from multiple providers.
141 """
142
143 tx_id: str
144 anchor_url: str
145 anchored_at: str
146 provider_id: str
147
148
149 class VerifyResult(TypedDict, total=False):
150 """Result of a complete verification check by a provider.
151
152 A complete verification check is one where the provider was able to
153 validate every link in the chain (signature, anchor receipt, identity
154 record). When the provider cannot complete the chain — for any reason
155 that is not "the record is known-bad" — it MUST raise
156 :class:`AttestationVerifyError` instead of returning this dict.
157
158 Attributes:
159 valid: ``True`` iff every link was validated and matched. Any value
160 of ``False`` means the provider proved the record bad — it does
161 *not* mean "could not check". ``False``-results MUST be
162 accompanied by a non-empty :attr:`reason`.
163 reason: Human-readable diagnostic. Empty when ``valid=True``. When
164 ``valid=False``, must explain *which* link broke (e.g.
165 ``"hmac signature mismatch"``, ``"anchor URL resolves to
166 different content_hash"``). Logged in audit trails.
167 depth: Identity-chain depth validated. ``0`` for a single-signer
168 commit, ``N`` for an N-deep org-quorum commit. This is an audit
169 signal, not a flow-control flag.
170 provider_id: Same domain string used to register the provider.
171 """
172
173 valid: bool
174 reason: str
175 depth: int
176 provider_id: str
177
178
179 # ---------------------------------------------------------------------------
180 # Typed error hierarchy — verify() must raise these, never silent True.
181 # ---------------------------------------------------------------------------
182
183
184 class AttestationError(Exception):
185 """Base exception for the attestation framework.
186
187 All framework- and provider-level errors derive from this class so callers
188 can catch the entire family with one ``except AttestationError`` handler.
189 """
190
191
192 class AttestationRequiredError(AttestationError):
193 """Raised when ``air.required=True`` and attestation could not be obtained.
194
195 Functionally equivalent to the JS ``AttestationRequiredError`` defined in
196 ``knowtation/lib/air.mjs``. Callers (typically the ``muse commit --attest``
197 code path) MUST surface this as a hard write rejection — the commit must
198 not be persisted to the ref.
199
200 Attributes:
201 code: Stable error code string ``"ATTESTATION_REQUIRED"``. Used by
202 CLI/MCP layers to map back to a structured error result without
203 string-matching the message.
204 """
205
206 code: str = "ATTESTATION_REQUIRED"
207
208
209 class AttestationVerifyError(AttestationError):
210 """Raised when :meth:`MuseAttestationProvider.verify` cannot complete.
211
212 Distinct from a returned ``VerifyResult(valid=False)``: this exception
213 means the verifier was unable to *prove* the record valid or invalid —
214 e.g. the canister was unreachable, the signature could not be parsed,
215 a required key was missing. Callers MUST treat this as
216 "verification status unknown", never as "valid".
217
218 Attributes:
219 code: Stable error code string for programmatic handling. Defaults
220 to ``"VERIFY_INCOMPLETE"`` but providers may set a more specific
221 code (e.g. ``"ICP_UNREACHABLE"``, ``"KEY_MISSING"``).
222 """
223
224 code: str = "VERIFY_INCOMPLETE"
225
226 def __init__(self, message: str, *, code: str | None = None) -> None:
227 """Construct a verify error with an optional override code.
228
229 Args:
230 message: Human-readable description of the incomplete verification.
231 code: Optional override for the default ``"VERIFY_INCOMPLETE"``
232 code. Should be a short uppercase token (e.g.
233 ``"ICP_UNREACHABLE"``).
234 """
235 super().__init__(message)
236 if code is not None:
237 self.code = code
238
239
240 class ImmutableReceiptError(AttestationError):
241 """Raised when re-anchoring a record that already has a receipt.
242
243 Receipts are write-once by contract — see the canister's
244 ``storeAttestation`` which rejects duplicate IDs with
245 ``#err("Attestation X already exists (records are immutable)")``.
246 The Python framework mirrors that semantics so application code sees the
247 same error regardless of where it was raised.
248
249 Attributes:
250 code: Stable error code string ``"IMMUTABLE_RECEIPT"``.
251 """
252
253 code: str = "IMMUTABLE_RECEIPT"
254
255
256 # ---------------------------------------------------------------------------
257 # Provider protocol — runtime_checkable so isinstance() works for tests.
258 # ---------------------------------------------------------------------------
259
260
261 @runtime_checkable
262 class MuseAttestationProvider(Protocol):
263 """Structural type for an attestation provider.
264
265 A provider is any object that exposes the three methods below. Domain
266 plugins typically expose a class implementing this protocol in
267 ``muse/plugins/<domain>/attestation.py`` and register a singleton via
268 :func:`register_attestation_provider`.
269
270 Implementations MUST:
271
272 * Be safe to call from any thread (no shared mutable state between
273 :meth:`anchor` and :meth:`verify`).
274 * Make :meth:`compute_attestation` deterministic for the same input.
275 * Raise :class:`AttestationVerifyError` from :meth:`verify` whenever
276 verification cannot be completed; never return ``valid=True`` on
277 partial-verification.
278 * Raise :class:`ImmutableReceiptError` when :meth:`anchor` is called
279 with a record whose ID is already anchored.
280 """
281
282 def compute_attestation(
283 self,
284 snapshot_id: str,
285 commit_meta: dict[str, object],
286 ) -> AttestationRecord:
287 """Compute a deterministic attestation record for a commit.
288
289 Called from ``muse commit --attest`` after the commit object has been
290 constructed but before it is written to ``.muse/refs/heads/<branch>``.
291
292 Args:
293 snapshot_id: SHA-256 hex of the snapshot being attested. Bound
294 into the deterministic record ID so the same snapshot always
295 produces the same attestation.
296 commit_meta: A free-form dict of commit metadata. Provider
297 implementations should treat it as opaque except for keys
298 they explicitly document (e.g. the Knowtation HMAC provider
299 reads ``"path"`` and ``"action"``).
300
301 Returns:
302 A :class:`AttestationRecord` with all required fields set. The
303 record is *not* yet anchored — call :meth:`anchor` to write it
304 to the external trust root.
305
306 Raises:
307 AttestationRequiredError: If a required configuration field is
308 missing and the provider's policy requires attestation.
309 """
310 ...
311
312 def anchor(self, record: AttestationRecord) -> AnchorReceipt:
313 """Anchor *record* in the provider's external trust root.
314
315 Typically calls a network endpoint (HTTP POST to AIR daemon, ICP
316 canister update call, blockchain transaction). May spawn a background
317 thread for long-running anchors, in which case the returned receipt's
318 ``tx_id`` may be a placeholder until the background work completes —
319 see :class:`AnchorReceipt` semantics in each provider's docstring.
320
321 Args:
322 record: A :class:`AttestationRecord` from
323 :meth:`compute_attestation`. Must include all required
324 fields; providers MAY validate this and raise
325 :class:`AttestationError` on malformed input.
326
327 Returns:
328 An :class:`AnchorReceipt` containing the off-chain transaction
329 ID and a public anchor URL.
330
331 Raises:
332 ImmutableReceiptError: If a receipt already exists for
333 ``record["id"]`` (write-once semantics).
334 AttestationError: For any other anchoring failure.
335 """
336 ...
337
338 def verify(
339 self,
340 record: AttestationRecord,
341 receipt: AnchorReceipt,
342 ) -> VerifyResult:
343 """Verify that *record* and *receipt* are valid and consistent.
344
345 Performs the full verification chain:
346
347 1. Re-compute the canonical bytes of *record* and check the signature
348 against the provider's known signing key.
349 2. Fetch the anchored copy from ``receipt["anchor_url"]`` and check
350 that it matches *record* (same ID, same content_hash, same path).
351 3. (Org commits — Phase 7.7) Walk the identity chain and validate
352 each member's signature; record the depth in the result.
353
354 Args:
355 record: The :class:`AttestationRecord` to verify.
356 receipt: The :class:`AnchorReceipt` returned by :meth:`anchor`.
357
358 Returns:
359 A :class:`VerifyResult` with ``valid=True`` iff every link in
360 the chain validated. ``valid=False`` iff the verifier proved
361 the record bad — never returned for "could not check".
362
363 Raises:
364 AttestationVerifyError: If the verifier cannot complete the
365 check (network unreachable, key missing, malformed record,
366 etc.). Distinct from a returned ``valid=False``, which
367 means the verifier *proved* the record bad.
368 """
369 ...
370
371
372 # ---------------------------------------------------------------------------
373 # Registry — same threading pattern as muse/core/merge_hooks.py
374 # ---------------------------------------------------------------------------
375
376
377 _REGISTRY_LOCK: threading.Lock = threading.Lock()
378 _PROVIDERS: dict[str, MuseAttestationProvider] = {}
379
380
381 def register_attestation_provider(
382 domain: str,
383 provider: MuseAttestationProvider,
384 ) -> None:
385 """Register *provider* under *domain* in the process-wide registry.
386
387 Idempotent — calling this function more than once with the same domain
388 replaces the previous registration. Safe to call eagerly at import time
389 (e.g. from ``muse/plugins/<domain>/__init__.py``) as well as lazily on
390 first need.
391
392 Concurrency:
393 Serialised by a module-level :class:`threading.Lock` so concurrent
394 registrations from different threads cannot lose entries.
395
396 Domain isolation:
397 Domains are isolated — :func:`get_attestation_provider` for domain A
398 will never return the provider registered for domain B, even if
399 domain A has no registered provider. This guarantees that a
400 misconfigured commit cannot accidentally invoke a provider for an
401 unrelated domain.
402
403 Args:
404 domain: Non-empty domain name string (e.g. ``"knowtation"``).
405 Case-sensitive.
406 provider: An object implementing the :class:`MuseAttestationProvider`
407 protocol. Validated structurally via :func:`isinstance` against
408 the runtime-checkable protocol — duck-typing accepted because
409 only the three public methods are ever called.
410
411 Raises:
412 ValueError: If *domain* is empty or not a string.
413 TypeError: If *provider* does not conform to
414 :class:`MuseAttestationProvider`.
415 """
416 if not isinstance(domain, str) or not domain:
417 raise ValueError("domain must be a non-empty string")
418 if not isinstance(provider, MuseAttestationProvider):
419 raise TypeError(
420 f"provider must implement MuseAttestationProvider "
421 f"(got {type(provider).__name__})"
422 )
423 with _REGISTRY_LOCK:
424 _PROVIDERS[domain] = provider
425
426
427 def get_attestation_provider(domain: str) -> MuseAttestationProvider | None:
428 """Return the registered provider for *domain* or ``None``.
429
430 Never raises. When *domain* has no registered provider, returns ``None``
431 so the caller can decide whether to treat absence as a soft skip
432 (``muse commit --attest`` when no provider is registered) or a hard
433 failure (``muse verify-commit --attest --required``).
434
435 Args:
436 domain: Domain name string.
437
438 Returns:
439 The :class:`MuseAttestationProvider` registered for *domain*, or
440 ``None`` when no provider has been registered. Domain isolation is
441 enforced — this function never falls back to a provider registered
442 under a different domain.
443 """
444 with _REGISTRY_LOCK:
445 return _PROVIDERS.get(domain)
446
447
448 def unregister_attestation_provider(domain: str) -> bool:
449 """Remove the provider registered under *domain*, if any.
450
451 Useful in tests that need to restore a clean registry state between
452 cases. Production code should not need to call this.
453
454 Args:
455 domain: Domain name string.
456
457 Returns:
458 ``True`` if a provider was removed, ``False`` when no provider was
459 registered for *domain*.
460 """
461 with _REGISTRY_LOCK:
462 return _PROVIDERS.pop(domain, None) is not None
463
464
465 def _registered_domains() -> list[str]:
466 """Return a snapshot of registered domains, sorted for determinism.
467
468 Internal helper for tests and diagnostic CLIs. Not part of the public
469 API — production callers should use :func:`get_attestation_provider`
470 to look up by name.
471 """
472 with _REGISTRY_LOCK:
473 return sorted(_PROVIDERS.keys())
474
475
476 # ---------------------------------------------------------------------------
477 # Phase 7.7 — Identity / quorum types and provider Protocol
478 # ---------------------------------------------------------------------------
479
480
481 class IdentityRecord(TypedDict, total=False):
482 """A single identity record from the identity domain.
483
484 Identity records describe agents, humans, and orgs. Org records carry a
485 ``quorum_threshold`` and a list of member ``handles`` so that downstream
486 quorum verification can fan out and check member signatures.
487
488 Attributes:
489 handle: Stable identity handle. Org handles MUST start with ``@``;
490 single-actor handles MUST NOT. Case-sensitive.
491 kind: One of ``"human"``, ``"agent"``, ``"org"``. Verifiers MUST
492 reject quorum signing for any handle whose ``kind != "org"``.
493 public_key: Base64url-encoded raw Ed25519 public key (32 bytes →
494 43 chars no padding). Empty for org records (orgs have no
495 standalone signing key — they're verified through their members).
496 members: For org records — list of member handles. Each member
497 MUST itself resolve to an :class:`IdentityRecord` (recursive
498 lookup, depth-bounded by the verifier).
499 quorum_threshold: For org records — minimum number of valid member
500 signatures required to consider the quorum satisfied. MUST be
501 an integer in ``[1, len(members)]``.
502 spawned_by: Optional handle of the identity that spawned this one
503 (for audit trails of the SPAWNS edge in the identity graph).
504 revoked_at: Optional ISO-8601 timestamp when the identity was
505 revoked. Verifiers MUST treat any non-empty ``revoked_at`` as
506 "identity invalid" — signatures by revoked identities do not
507 count toward quorum.
508 """
509
510 handle: str
511 kind: str
512 public_key: str
513 members: list[str]
514 quorum_threshold: int
515 spawned_by: str
516 revoked_at: str
517
518
519 class IdentityChainResult(TypedDict, total=False):
520 """Result of walking an identity chain to validate a handle.
521
522 Returned by :func:`muse.core.verify.verify_identity_chain` and surfaced
523 in :class:`QuorumVerifyResult.depth`.
524
525 Attributes:
526 valid: ``True`` iff every node in the walked chain resolved and was
527 non-revoked. ``False`` for any verifiable failure.
528 depth: Number of identity-graph hops walked (1 for a direct lookup,
529 N for an N-deep org-of-orgs chain). Zero means the root handle
530 could not even be resolved.
531 reason: Human-readable diagnostic. Empty when ``valid=True``.
532 cycle_detected: ``True`` iff a cycle was detected during the walk
533 (member A → member B → member A). Cycles are a security
534 failure — the identity domain rejects them at write time, but
535 the verifier re-checks defensively in case a malicious actor
536 was able to inject a cycle into a stale local cache.
537 """
538
539 valid: bool
540 depth: int
541 reason: str
542 cycle_detected: bool
543
544
545 @runtime_checkable
546 class MuseIdentityProvider(Protocol):
547 """Protocol for resolving identity handles into :class:`IdentityRecord`s.
548
549 Distinct from :class:`MuseAttestationProvider` because identity lookups
550 have different concurrency, caching, and trust-root semantics than
551 attestation anchoring. A domain MAY register both providers (knowtation
552 does — see :mod:`muse.plugins.knowtation.quorum`).
553
554 Implementations MUST:
555
556 * Be safe to call from any thread.
557 * Be deterministic given a fixed backing store (the same handle always
558 resolves to the same record between cache invalidations).
559 * Return ``None`` for unknown handles — never raise for "not found".
560 * Raise :class:`AttestationVerifyError` for transport/IO errors that
561 prevent the resolver from determining whether the handle exists.
562 """
563
564 def get_identity(self, handle: str) -> "IdentityRecord | None":
565 """Resolve *handle* to its :class:`IdentityRecord`, or ``None``.
566
567 Args:
568 handle: Identity handle string. Case-sensitive.
569
570 Returns:
571 The :class:`IdentityRecord` for *handle*, or ``None`` if no such
572 identity exists in the provider's backing store.
573
574 Raises:
575 AttestationVerifyError: If the provider cannot determine
576 whether the handle exists (e.g. backing store unreachable).
577 Callers MUST surface this as ``partial=True`` in the
578 outer verification result, never silently treat as
579 ``None``.
580 """
581 ...
582
583
584 _IDENTITY_REGISTRY_LOCK: threading.Lock = threading.Lock()
585 _IDENTITY_PROVIDERS: dict[str, MuseIdentityProvider] = {}
586
587
588 def register_identity_provider(domain: str, provider: MuseIdentityProvider) -> None:
589 """Register *provider* under *domain* in the identity provider registry.
590
591 Mirrors :func:`register_attestation_provider` exactly — same locking,
592 same domain-isolation guarantees. Kept as a separate registry so that
593 a domain can implement attestation without identity (or vice versa).
594
595 Args:
596 domain: Non-empty domain name.
597 provider: Object implementing :class:`MuseIdentityProvider`.
598
599 Raises:
600 ValueError: If *domain* is empty.
601 TypeError: If *provider* does not implement
602 :class:`MuseIdentityProvider`.
603 """
604 if not isinstance(domain, str) or not domain:
605 raise ValueError("domain must be a non-empty string")
606 if not isinstance(provider, MuseIdentityProvider):
607 raise TypeError(
608 f"provider must implement MuseIdentityProvider "
609 f"(got {type(provider).__name__})"
610 )
611 with _IDENTITY_REGISTRY_LOCK:
612 _IDENTITY_PROVIDERS[domain] = provider
613
614
615 def get_identity_provider(domain: str) -> "MuseIdentityProvider | None":
616 """Return the identity provider for *domain*, or ``None``.
617
618 Domain isolation is enforced — never falls back across domains.
619
620 Args:
621 domain: Domain name string.
622
623 Returns:
624 :class:`MuseIdentityProvider` or ``None``.
625 """
626 with _IDENTITY_REGISTRY_LOCK:
627 return _IDENTITY_PROVIDERS.get(domain)
628
629
630 def unregister_identity_provider(domain: str) -> bool:
631 """Remove the identity provider registered under *domain*, if any.
632
633 Args:
634 domain: Domain name string.
635
636 Returns:
637 ``True`` when a provider was removed; ``False`` when none was
638 registered.
639 """
640 with _IDENTITY_REGISTRY_LOCK:
641 return _IDENTITY_PROVIDERS.pop(domain, None) is not None
642
643
644 __all__ = [
645 "AnchorReceipt",
646 "AttestationError",
647 "AttestationRecord",
648 "AttestationRequiredError",
649 "AttestationVerifyError",
650 "IdentityChainResult",
651 "IdentityRecord",
652 "ImmutableReceiptError",
653 "MuseAttestationProvider",
654 "MuseIdentityProvider",
655 "VerifyResult",
656 "get_attestation_provider",
657 "get_identity_provider",
658 "register_attestation_provider",
659 "register_identity_provider",
660 "unregister_attestation_provider",
661 "unregister_identity_provider",
662 ]
File History 2 commits
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 32 days ago
sha256:d8316ffae901be06347e16ab55be11868eb519dd16ade3e8aa16a99e662f7e62 baseline: rc14 re-baseline after rc3 store corruption recovery Human patch 32 days ago