ca2a-runtime 0.1.0a1__py3-none-any.whl

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.
@@ -0,0 +1,176 @@
1
+ """Delegation credential model, canonicalization, and chain verification.
2
+
3
+ A delegation credential is a signed statement that ``issuer`` grants ``subject``
4
+ a set of capability strings (``scope``), optionally as a child of ``parent_id``.
5
+ A chain is a list of credentials ordered from root to leaf. Verification enforces
6
+ four invariants:
7
+
8
+ 1. Signature: each credential verifies against its issuer's Ed25519 public key.
9
+ 2. Continuity: each hop's issuer is the previous hop's subject.
10
+ 3. Attenuation: each hop's scope is a subset of its parent's scope.
11
+ 4. Anti-replay: parent_id links to the previous credential_id and every
12
+ credential_id in the chain is unique.
13
+
14
+ Canonicalization uses RFC 8785 (JSON Canonicalization Scheme), so the signed
15
+ byte string is identical across conforming implementations and cA2A signatures
16
+ are cross-verifiable with agent-manifest. See ca2a_runtime.canonical.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from dataclasses import dataclass
22
+ from typing import Any
23
+
24
+ from cryptography.exceptions import InvalidSignature
25
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import (
26
+ Ed25519PrivateKey,
27
+ Ed25519PublicKey,
28
+ )
29
+
30
+ from ca2a_runtime.canonical import canonicalize
31
+ from ca2a_runtime.errors import (
32
+ BrokenDelegationLink,
33
+ CredentialReplay,
34
+ DelegationDepthExceeded,
35
+ InvalidCredential,
36
+ ScopeEscalation,
37
+ )
38
+
39
+
40
+ def new_keypair() -> tuple[Ed25519PrivateKey, str]:
41
+ """Return a fresh Ed25519 private key and its public key as raw hex."""
42
+ priv = Ed25519PrivateKey.generate()
43
+ pub_hex = priv.public_key().public_bytes_raw().hex()
44
+ return priv, pub_hex
45
+
46
+
47
+ def canonical_bytes(payload: dict[str, Any]) -> bytes:
48
+ """RFC 8785 (JCS) canonical byte encoding of a credential body.
49
+
50
+ This is the stable byte string signed and verified; using JCS makes cA2A
51
+ signatures cross-verifiable with agent-manifest and any other conforming
52
+ implementation. See ca2a_runtime.canonical.
53
+ """
54
+ return canonicalize(payload)
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class DelegationCredential:
59
+ """A single signed delegation hop."""
60
+
61
+ credential_id: str
62
+ issuer: str # Ed25519 public key, raw hex
63
+ subject: str # Ed25519 public key, raw hex
64
+ scope: frozenset[str]
65
+ depth: int
66
+ parent_id: str | None = None
67
+ signature: str = "" # Ed25519 signature over canonical_bytes(body), hex
68
+
69
+ def body(self) -> dict[str, Any]:
70
+ """The signed portion of the credential (everything but the signature)."""
71
+ return {
72
+ "credential_id": self.credential_id,
73
+ "issuer": self.issuer,
74
+ "subject": self.subject,
75
+ "scope": sorted(self.scope),
76
+ "depth": self.depth,
77
+ "parent_id": self.parent_id,
78
+ }
79
+
80
+ def sign(self, private_key: Ed25519PrivateKey) -> DelegationCredential:
81
+ """Return a copy signed by ``private_key`` (must match ``issuer``)."""
82
+ expected = private_key.public_key().public_bytes_raw().hex()
83
+ if expected != self.issuer:
84
+ raise InvalidCredential(
85
+ "signing key does not match credential issuer",
86
+ detail=f"issuer={self.issuer} key={expected}",
87
+ )
88
+ sig = private_key.sign(canonical_bytes(self.body())).hex()
89
+ return DelegationCredential(
90
+ credential_id=self.credential_id,
91
+ issuer=self.issuer,
92
+ subject=self.subject,
93
+ scope=self.scope,
94
+ depth=self.depth,
95
+ parent_id=self.parent_id,
96
+ signature=sig,
97
+ )
98
+
99
+ def verify_signature(self) -> None:
100
+ """Raise InvalidCredential if the signature does not verify."""
101
+ if not self.signature:
102
+ raise InvalidCredential("credential is unsigned")
103
+ try:
104
+ pub = Ed25519PublicKey.from_public_bytes(bytes.fromhex(self.issuer))
105
+ pub.verify(bytes.fromhex(self.signature), canonical_bytes(self.body()))
106
+ except (InvalidSignature, ValueError) as exc:
107
+ raise InvalidCredential(
108
+ "credential signature failed to verify", detail=str(exc)
109
+ ) from exc
110
+
111
+ @classmethod
112
+ def from_dict(cls, data: dict[str, Any]) -> DelegationCredential:
113
+ try:
114
+ return cls(
115
+ credential_id=str(data["credential_id"]),
116
+ issuer=str(data["issuer"]),
117
+ subject=str(data["subject"]),
118
+ scope=frozenset(str(s) for s in data["scope"]),
119
+ depth=int(data["depth"]),
120
+ parent_id=(None if data.get("parent_id") is None else str(data["parent_id"])),
121
+ signature=str(data.get("signature", "")),
122
+ )
123
+ except (KeyError, TypeError, ValueError) as exc:
124
+ raise InvalidCredential("malformed credential", detail=str(exc)) from exc
125
+
126
+
127
+ def verify_chain(
128
+ chain: list[DelegationCredential], *, max_depth: int = 8
129
+ ) -> None:
130
+ """Verify a root-to-leaf delegation chain, raising on the first violation.
131
+
132
+ A well-formed chain of length N delegates from the root issuer down to the
133
+ leaf subject with monotonically narrowing scope. Raises the specific
134
+ CA2AError subtype for the invariant that failed.
135
+ """
136
+ if not chain:
137
+ raise BrokenDelegationLink("empty delegation chain")
138
+
139
+ seen_ids: set[str] = set()
140
+ prev: DelegationCredential | None = None
141
+
142
+ for i, cred in enumerate(chain):
143
+ cred.verify_signature()
144
+
145
+ if cred.credential_id in seen_ids:
146
+ raise CredentialReplay(f"duplicate credential_id at hop {i}: {cred.credential_id}")
147
+ seen_ids.add(cred.credential_id)
148
+
149
+ if cred.depth > max_depth:
150
+ raise DelegationDepthExceeded(
151
+ f"hop {i} depth {cred.depth} exceeds max {max_depth}"
152
+ )
153
+
154
+ if prev is None:
155
+ if cred.parent_id is not None:
156
+ raise BrokenDelegationLink("root credential must not name a parent")
157
+ if cred.depth != 0:
158
+ raise BrokenDelegationLink("root credential must have depth 0")
159
+ else:
160
+ if cred.parent_id != prev.credential_id:
161
+ raise BrokenDelegationLink(
162
+ f"hop {i} parent_id does not match previous credential_id"
163
+ )
164
+ if cred.issuer != prev.subject:
165
+ raise BrokenDelegationLink(
166
+ f"hop {i} issuer is not the previous hop's subject"
167
+ )
168
+ if cred.depth != prev.depth + 1:
169
+ raise BrokenDelegationLink(f"hop {i} depth is not previous + 1")
170
+ if not cred.scope.issubset(prev.scope):
171
+ escalated = sorted(cred.scope - prev.scope)
172
+ raise ScopeEscalation(
173
+ f"hop {i} scope exceeds parent grant", detail=f"added: {escalated}"
174
+ )
175
+
176
+ prev = cred
ca2a_runtime/errors.py ADDED
@@ -0,0 +1,83 @@
1
+ """Central error code registry - mirrors docs/spec/error-codes semantics."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class CA2AError(Exception):
7
+ """Base class for all ca2a-runtime errors."""
8
+
9
+ code: str = "CA2A_ERROR"
10
+ http_status: int = 500
11
+
12
+ def __init__(self, message: str, *, detail: str | None = None) -> None:
13
+ super().__init__(message)
14
+ self.detail = detail
15
+
16
+
17
+ class ConfigError(CA2AError):
18
+ code = "CONFIG_ERROR"
19
+ http_status = 500
20
+
21
+
22
+ class InvalidCredential(CA2AError):
23
+ """A delegation credential is malformed or its signature does not verify."""
24
+
25
+ code = "INVALID_CREDENTIAL"
26
+ http_status = 400
27
+
28
+
29
+ class ScopeEscalation(CA2AError):
30
+ """A child grant claims authority its parent did not hold."""
31
+
32
+ code = "SCOPE_ESCALATION"
33
+ http_status = 403
34
+
35
+
36
+ class BrokenDelegationLink(CA2AError):
37
+ """A hop does not chain to its stated parent, or continuity is broken."""
38
+
39
+ code = "BROKEN_DELEGATION_LINK"
40
+ http_status = 409
41
+
42
+
43
+ class DelegationDepthExceeded(CA2AError):
44
+ code = "DELEGATION_DEPTH_EXCEEDED"
45
+ http_status = 403
46
+
47
+
48
+ class CredentialReplay(CA2AError):
49
+ """A credential id appears more than once in a chain."""
50
+
51
+ code = "CREDENTIAL_REPLAY"
52
+ http_status = 409
53
+
54
+
55
+ class AttestationUnsupported(CA2AError):
56
+ code = "ATTESTATION_UNSUPPORTED"
57
+ http_status = 500
58
+
59
+
60
+ class AttestationFailed(CA2AError):
61
+ code = "ATTESTATION_FAILED"
62
+ http_status = 412
63
+
64
+
65
+ class SealedChannelError(CA2AError):
66
+ code = "SEALED_CHANNEL_ERROR"
67
+ http_status = 500
68
+
69
+
70
+ class ProvenanceLinkBroken(CA2AError):
71
+ """A delegation record does not chain to its stated parent record, or a
72
+ record has been tampered with so its hash no longer matches a child's link."""
73
+
74
+ code = "PROVENANCE_LINK_BROKEN"
75
+ http_status = 409
76
+
77
+
78
+ class ScopeNotPermitted(CA2AError):
79
+ """A requested capability is not in the effective scope (the delegated
80
+ scope intersected with the callee's local policy)."""
81
+
82
+ code = "SCOPE_NOT_PERMITTED"
83
+ http_status = 403
ca2a_runtime/peer.py ADDED
@@ -0,0 +1,142 @@
1
+ """Inbound peer-call enforcement: the decision the callee makes before it acts.
2
+
3
+ When a peer presents a delegation chain and requests a capability, the callee:
4
+
5
+ 1. verifies the chain (signature, continuity, attenuation, depth, replay);
6
+ 2. computes the effective scope as the leaf's delegated scope intersected with
7
+ the callee's local policy;
8
+ 3. enforces: the requested capability must be in the effective scope;
9
+ 4. emits a provenance record for the accepted hop, linked to its parent.
10
+
11
+ `enforce_peer_call` is the enforcement decision core. `handle_peer_request`
12
+ composes it into the full transport-agnostic inbound pipeline: verify, enforce,
13
+ open any sealed payload with the enclave key, and emit a provenance record. A
14
+ transport (an A2A server) parses its wire format into a `PeerRequest` and calls
15
+ this; cA2A does not define the transport itself, only what the peer does with a
16
+ parsed request.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from dataclasses import dataclass
22
+
23
+ from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
24
+
25
+ from ca2a_runtime.channel import open_sealed
26
+ from ca2a_runtime.delegation.credential import DelegationCredential, verify_chain
27
+ from ca2a_runtime.errors import ScopeNotPermitted, SealedChannelError
28
+ from ca2a_runtime.policy import Policy
29
+ from ca2a_runtime.provenance import DelegationRecord, record_for
30
+
31
+
32
+ def effective_scope(
33
+ chain: list[DelegationCredential], policy: Policy, *, max_depth: int = 8
34
+ ) -> frozenset[str]:
35
+ """Verify the chain and return the effective scope (delegated ∩ local policy).
36
+
37
+ Raises the relevant CA2AError if the chain does not verify.
38
+ """
39
+ verify_chain(chain, max_depth=max_depth)
40
+ return policy.intersect(chain[-1].scope)
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class PeerDecision:
45
+ """The result of an accepted peer call."""
46
+
47
+ effective_scope: frozenset[str]
48
+ granted_capability: str
49
+ record: DelegationRecord
50
+
51
+
52
+ def enforce_peer_call(
53
+ chain: list[DelegationCredential],
54
+ requested_capability: str,
55
+ *,
56
+ policy: Policy,
57
+ record_id: str,
58
+ parent_record_hash: str | None = None,
59
+ max_depth: int = 8,
60
+ ) -> PeerDecision:
61
+ """Verify, intersect with local policy, enforce, and emit a provenance record.
62
+
63
+ Raises ScopeNotPermitted if the requested capability is not in the effective
64
+ scope, and the underlying CA2AError if the chain does not verify. On accept,
65
+ returns a PeerDecision carrying the linked provenance record.
66
+ """
67
+ effective = effective_scope(chain, policy, max_depth=max_depth)
68
+ if requested_capability not in effective:
69
+ raise ScopeNotPermitted(
70
+ f"capability {requested_capability!r} is not in the effective scope",
71
+ detail=f"effective={sorted(effective)}",
72
+ )
73
+ record = record_for(chain[-1], record_id=record_id, parent_record_hash=parent_record_hash)
74
+ return PeerDecision(
75
+ effective_scope=effective,
76
+ granted_capability=requested_capability,
77
+ record=record,
78
+ )
79
+
80
+
81
+ @dataclass(frozen=True)
82
+ class PeerRequest:
83
+ """A transport-agnostic inbound peer request.
84
+
85
+ A transport (an A2A server) parses its wire format into this shape and hands
86
+ it to ``handle_peer_request``. cA2A does not define the transport; it defines
87
+ what a peer does with the request once parsed.
88
+ """
89
+
90
+ chain: list[DelegationCredential]
91
+ requested_capability: str
92
+ record_id: str
93
+ sealed_payload: bytes | None = None
94
+ parent_record_hash: str | None = None
95
+
96
+
97
+ @dataclass(frozen=True)
98
+ class PeerResult:
99
+ """The outcome of handling an accepted peer request."""
100
+
101
+ effective_scope: frozenset[str]
102
+ granted_capability: str
103
+ record: DelegationRecord
104
+ payload: bytes | None
105
+
106
+
107
+ def handle_peer_request(
108
+ request: PeerRequest,
109
+ *,
110
+ policy: Policy,
111
+ enclave_private_key: X25519PrivateKey | None = None,
112
+ max_depth: int = 8,
113
+ ) -> PeerResult:
114
+ """Run the full inbound pipeline for a parsed peer request.
115
+
116
+ Verifies the delegation chain, intersects the delegated scope with the local
117
+ policy and enforces the requested capability, opens any sealed payload with
118
+ the enclave-bound key, and emits a linked provenance record. Fails closed:
119
+ any verification or authorization failure raises the relevant CA2AError and
120
+ no payload is returned.
121
+ """
122
+ decision = enforce_peer_call(
123
+ request.chain,
124
+ request.requested_capability,
125
+ policy=policy,
126
+ record_id=request.record_id,
127
+ parent_record_hash=request.parent_record_hash,
128
+ max_depth=max_depth,
129
+ )
130
+
131
+ payload: bytes | None = None
132
+ if request.sealed_payload is not None:
133
+ if enclave_private_key is None:
134
+ raise SealedChannelError("a sealed payload was sent but no enclave key is available")
135
+ payload = open_sealed(request.sealed_payload, enclave_private_key)
136
+
137
+ return PeerResult(
138
+ effective_scope=decision.effective_scope,
139
+ granted_capability=decision.granted_capability,
140
+ record=decision.record,
141
+ payload=payload,
142
+ )
ca2a_runtime/policy.py ADDED
@@ -0,0 +1,48 @@
1
+ """Local peer policy for scope intersection.
2
+
3
+ A callee constrains what an inbound peer may do with a set of allowed
4
+ capabilities. The effective permission on a peer call is the delegated scope
5
+ intersected with this allow set, so a peer can never exercise more than both
6
+ its grant and the callee's local policy permit.
7
+
8
+ The intersection semantics here are policy-language-agnostic. Binding a full
9
+ Cedar policy engine (as cMCP does) is tracked separately; this allow-set model
10
+ is the enforcement primitive the peer path uses today.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from collections.abc import Iterable
16
+ from dataclasses import dataclass
17
+ from typing import Protocol, runtime_checkable
18
+
19
+
20
+ @runtime_checkable
21
+ class Policy(Protocol):
22
+ """A local policy the peer path intersects a delegated scope against.
23
+
24
+ Implemented by ``LocalPolicy`` (a capability allow set) and by
25
+ ``ca2a_runtime.cedar.CedarPolicy`` (a real Cedar policy engine).
26
+ """
27
+
28
+ def intersect(self, delegated: frozenset[str]) -> frozenset[str]:
29
+ """Return the effective scope: the delegated capabilities this policy allows."""
30
+ ...
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class LocalPolicy:
35
+ """A callee's local capability allow set."""
36
+
37
+ allow: frozenset[str]
38
+
39
+ @classmethod
40
+ def of(cls, caps: Iterable[str]) -> LocalPolicy:
41
+ return cls(allow=frozenset(caps))
42
+
43
+ def intersect(self, delegated: frozenset[str]) -> frozenset[str]:
44
+ """Return the effective scope: delegated capabilities this policy allows."""
45
+ return delegated & self.allow
46
+
47
+ def permits(self, capability: str) -> bool:
48
+ return capability in self.allow
@@ -0,0 +1,115 @@
1
+ """Delegation provenance: linked records forming a tamper-evident delegation DAG.
2
+
3
+ Each delegation hop emits a ``DelegationRecord`` that references its parent
4
+ record by hash and names the credential it acted under. A chain of records is
5
+ verifiable offline: the parent link is the hash of the parent record's canonical
6
+ body, so tampering with any record (its scope, subject, or its own parent link)
7
+ changes its hash and breaks the child's link. This is the runtime-evidence side
8
+ of the cA2A profile (see docs/spec/trace-a2a-profile.md); the full TRACE binding
9
+ lands with the Tier 2 provenance work.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import hashlib
15
+ from dataclasses import dataclass
16
+ from typing import Any
17
+
18
+ from ca2a_runtime.delegation.credential import DelegationCredential, canonical_bytes
19
+ from ca2a_runtime.errors import ProvenanceLinkBroken
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class DelegationRecord:
24
+ """A provenance record for a single delegation hop."""
25
+
26
+ record_id: str
27
+ credential_id: str
28
+ subject: str
29
+ scope: frozenset[str]
30
+ parent_record_hash: str | None = None
31
+
32
+ def body(self) -> dict[str, Any]:
33
+ """The hashed portion of the record."""
34
+ return {
35
+ "record_id": self.record_id,
36
+ "credential_id": self.credential_id,
37
+ "subject": self.subject,
38
+ "scope": sorted(self.scope),
39
+ "parent_record_hash": self.parent_record_hash,
40
+ }
41
+
42
+ def record_hash(self) -> str:
43
+ """SHA-256 over the canonical body. Any field change changes this value."""
44
+ return hashlib.sha256(canonical_bytes(self.body())).hexdigest()
45
+
46
+
47
+ def record_for(
48
+ credential: DelegationCredential,
49
+ record_id: str,
50
+ parent_record_hash: str | None,
51
+ ) -> DelegationRecord:
52
+ """Build the provenance record a hop emits for a delegation credential."""
53
+ return DelegationRecord(
54
+ record_id=record_id,
55
+ credential_id=credential.credential_id,
56
+ subject=credential.subject,
57
+ scope=credential.scope,
58
+ parent_record_hash=parent_record_hash,
59
+ )
60
+
61
+
62
+ def verify_dag(records: list[DelegationRecord]) -> list[DelegationRecord]:
63
+ """Verify a root-to-leaf provenance chain and return it in order.
64
+
65
+ Raises ProvenanceLinkBroken on the first violation:
66
+
67
+ - the first record must be a root (no parent link);
68
+ - every later record's parent_record_hash must equal the recomputed hash of
69
+ the immediately preceding record (so a tampered or reparented record is
70
+ detected because its hash no longer matches the stored link);
71
+ - no record_id may repeat.
72
+ """
73
+ if not records:
74
+ raise ProvenanceLinkBroken("empty provenance chain")
75
+
76
+ seen_ids: set[str] = set()
77
+ prev: DelegationRecord | None = None
78
+
79
+ for i, rec in enumerate(records):
80
+ if rec.record_id in seen_ids:
81
+ raise ProvenanceLinkBroken(f"duplicate record_id at position {i}: {rec.record_id}")
82
+ seen_ids.add(rec.record_id)
83
+
84
+ if prev is None:
85
+ if rec.parent_record_hash is not None:
86
+ raise ProvenanceLinkBroken("root record must not reference a parent")
87
+ else:
88
+ expected = prev.record_hash()
89
+ if rec.parent_record_hash != expected:
90
+ raise ProvenanceLinkBroken(
91
+ f"record {i} parent link does not match the previous record's hash",
92
+ detail="a tampered or reparented record was detected",
93
+ )
94
+ prev = rec
95
+
96
+ return records
97
+
98
+
99
+ def cross_check_chain(
100
+ records: list[DelegationRecord], chain: list[DelegationCredential]
101
+ ) -> None:
102
+ """Confirm a verified provenance chain lines up with a delegation chain.
103
+
104
+ Ties provenance to authority: record ``i`` must reference credential ``i``
105
+ and carry the same subject. Raises ProvenanceLinkBroken on any mismatch.
106
+ """
107
+ if len(records) != len(chain):
108
+ raise ProvenanceLinkBroken(
109
+ f"provenance length {len(records)} does not match chain length {len(chain)}"
110
+ )
111
+ for i, (rec, cred) in enumerate(zip(records, chain, strict=True)):
112
+ if rec.credential_id != cred.credential_id:
113
+ raise ProvenanceLinkBroken(f"record {i} credential_id does not match the chain")
114
+ if rec.subject != cred.subject:
115
+ raise ProvenanceLinkBroken(f"record {i} subject does not match the chain")
@@ -0,0 +1,22 @@
1
+ """TEE provider abstraction for peer attestation.
2
+
3
+ cA2A reuses the pluggable provider model from cmcp: a provider produces an
4
+ attestation report that binds a public key to a hardware measurement. Real
5
+ hardware backends are Tier 3 (see ROADMAP.md) and fail closed until implemented.
6
+ """
7
+
8
+ from ca2a_runtime.tee.base import AttestationReport, BaseProvider
9
+ from ca2a_runtime.tee.sev_snp import SevSnpProvider, SevSnpReport
10
+ from ca2a_runtime.tee.tdx import TdxProvider, TdxQuote
11
+ from ca2a_runtime.tee.tpm import TpmProvider, TpmQuote
12
+
13
+ __all__ = [
14
+ "AttestationReport",
15
+ "BaseProvider",
16
+ "SevSnpProvider",
17
+ "SevSnpReport",
18
+ "TdxProvider",
19
+ "TdxQuote",
20
+ "TpmProvider",
21
+ "TpmQuote",
22
+ ]
@@ -0,0 +1,36 @@
1
+ """Base TEE provider interface and attestation report model."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from dataclasses import dataclass
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class AttestationReport:
11
+ """An attestation report binding a public key to a hardware measurement."""
12
+
13
+ platform: str
14
+ measurement: str
15
+ public_key: str # raw hex of the key bound to this measurement
16
+ nonce: str
17
+
18
+
19
+ class BaseProvider(ABC):
20
+ """Normalized interface every TEE provider implements.
21
+
22
+ Real hardware providers (TPM, SEV-SNP, TDX, OPAQUE) are Tier 3 and are not
23
+ implemented in this release; ``detect`` returns False for them so they are
24
+ never selected automatically.
25
+ """
26
+
27
+ platform: str = "base"
28
+
29
+ @classmethod
30
+ @abstractmethod
31
+ def detect(cls) -> bool:
32
+ """Return True if this provider is available on the current host."""
33
+
34
+ @abstractmethod
35
+ def attest(self, public_key: str, nonce: str) -> AttestationReport:
36
+ """Produce an attestation report binding ``public_key`` under ``nonce``."""