README.md
17.3 KB · 329 lines · markdown Raw
1 # PQC AI Governance
2
3 ![PQC Native](https://img.shields.io/badge/PQC-Native-blue)
4 ![ML-DSA-65](https://img.shields.io/badge/ML--DSA--65-FIPS%20204-green)
5 ![BFT](https://img.shields.io/badge/Byzantine%20Fault%20Tolerant-PBFT%20style-purple)
6 ![License](https://img.shields.io/badge/License-Apache%202.0-orange)
7 ![Version](https://img.shields.io/badge/version-0.1.0-lightgrey)
8
9 **A quantum-resistant DAO for enterprise AI governance.** Distributed governance nodes reach cryptographic agreement on which AI models may run, which agents may act, and which policies are in force - every proposal, every vote, and every consensus result carries an **ML-DSA** (FIPS 204) signature. When an agent tries to load a model, every node can independently verify the authorization is legitimate and quantum-unforgeable.
10
11 ## The Problem
12
13 Large enterprises run fleets of AI agents across business units. Today, whether an agent is allowed to load a particular model, call a particular tool, or delegate to a sub-agent is enforced by **classical PKI certificates** - RSA and ECDSA signatures that will be **retroactively forgeable** once a cryptographically relevant quantum computer exists. A forged authorization issued in 2027 still authorizes the agent in 2045, long after the underlying signature scheme has fallen. Governance decisions must outlive the crypto they were originally signed with, or they are not governance decisions at all.
14
15 Classical governance also typically relies on a single signing authority (a KMS, a root CA). Compromise that authority and the entire AI stack is silently re-authorized by the attacker.
16
17 ## The Solution
18
19 - **Byzantine-fault-tolerant consensus.** A `ConsensusRound` aggregates signed votes from an allow-listed set of `GovernanceNode`s and decides pass/reject under a configurable `QuorumPolicy` (default: PBFT-style 2/3 participation and 2/3 approval).
20 - **Every artifact is ML-DSA signed.** Proposals are signed by the proposer's node. Votes are signed by the voter's node. The finalised `ConsensusResult` is signed by the coordinating node. Auditors can verify any of the three forever.
21 - **Byzantine double-voting is caught at tally time.** `VoteTally` refuses to silently absorb conflicting votes from the same DID and raises `ByzantineDetectedError` with an unambiguous audit record.
22 - **Authorization chains.** `AuthorizationChain` walks the passed grants for a subject (a model DID, an agent DID) and applies `AUTHORIZE_* -> REVOKE_*` semantics to answer "is this subject currently authorized?".
23 - **No single point of trust.** No KMS, no root CA, no magic signing service. Authority lives in a quorum of node keys - quantum-safe from day one.
24
25 ## Installation
26
27 ```bash
28 pip install pqc-ai-governance
29 ```
30
31 Development:
32
33 ```bash
34 pip install -e ".[dev]"
35 pytest
36 ```
37
38 ## Quick Start
39
40 ```python
41 from quantumshield.identity.agent import AgentIdentity
42 from pqc_ai_governance import (
43 AuthorizationChain, AuthorizationGrant,
44 ConsensusRound, GovernanceNode, GovernanceProposal,
45 NodeRegistry, ProposalKind, QuorumPolicy, VoteDecision,
46 )
47
48 # 1. Stand up five governance nodes (each with a PQC identity).
49 alice = GovernanceNode(identity=AgentIdentity.create("alice"), name="alice", weight=1)
50 bob = GovernanceNode(identity=AgentIdentity.create("bob"), name="bob", weight=1)
51 carol = GovernanceNode(identity=AgentIdentity.create("carol"), name="carol", weight=1)
52 dave = GovernanceNode(identity=AgentIdentity.create("dave"), name="dave", weight=2)
53 eve = GovernanceNode(identity=AgentIdentity.create("eve"), name="eve", weight=1)
54
55 registry = NodeRegistry()
56 for n in (alice, bob, carol, dave, eve):
57 registry.register(n)
58
59 # 2. Alice proposes authorizing a medical-AI model.
60 proposal = GovernanceProposal.create(
61 kind=ProposalKind.AUTHORIZE_MODEL,
62 subject_id="did:pqaid:medical-ai-v2",
63 title="Authorize medical-ai-v2 for production",
64 proposer_did=alice.did,
65 parameters={"environment": "prod", "max_rate_qps": 50},
66 )
67 alice.sign_proposal(proposal)
68
69 # 3. Nodes vote.
70 rnd = ConsensusRound(proposal=proposal, registry=registry, policy=QuorumPolicy())
71 for voter in (alice, bob, carol, dave):
72 rnd.cast(voter.cast_vote(proposal, VoteDecision.APPROVE))
73 rnd.cast(eve.cast_vote(proposal, VoteDecision.ABSTAIN))
74
75 # 4. Finalize. The result is ML-DSA signed by the coordinator.
76 result = rnd.finalize(coordinator=alice)
77 assert result.decision == "passed"
78 assert ConsensusRound.verify_result(result)
79
80 # 5. Bind the passed result into an authorization chain.
81 chain = AuthorizationChain(subject_id=proposal.subject_id)
82 chain.add(AuthorizationGrant(
83 subject_id=proposal.subject_id,
84 kind=proposal.kind,
85 result=result,
86 ))
87 assert chain.is_authorized(ProposalKind.AUTHORIZE_MODEL)
88 ```
89
90 ## Architecture
91
92 ```
93 Proposer node Every other governance node
94 ------------- ---------------------------
95 | |
96 | GovernanceProposal.create(kind, |
97 | subject_id, parameters, ...) |
98 | |
99 | sign_proposal() |
100 | ML-DSA / SHA3-256 |
101 | |
102 +------------> broadcast --------->+
103 |
104 | verify_proposal()
105 | cast_vote(APPROVE |
106 | REJECT |
107 | ABSTAIN)
108 | ML-DSA / SHA3-256
109 v
110 +-------------------------------------+
111 | ConsensusRound |
112 | |
113 | VoteTally |
114 | - verify each signature |
115 | - reject non-member voters |
116 | - reject wrong proposal_hash |
117 | - Byzantine check: same DID |
118 | voting two different decisions |
119 | -> ByzantineDetectedError |
120 | |
121 | QuorumPolicy |
122 | - min_participation_fraction |
123 | (default 2/3 of total_weight) |
124 | - min_approval_fraction |
125 | (default 2/3 of non-abstain) |
126 | |
127 | finalize(coordinator) |
128 +------------------+------------------+
129 |
130 v
131 +-------------------------------------+
132 | ConsensusResult |
133 | proposal_id, proposal_hash, |
134 | decision (passed | rejected), |
135 | reason, approve/reject/abstain |
136 | weights, total_weight, |
137 | included_vote_ids[], decided_at |
138 | |
139 | + ML-DSA signature by coordinator|
140 +------------------+------------------+
141 |
142 v
143 AuthorizationChain(subject_id)
144 .add(AuthorizationGrant(result))
145 .is_authorized(AUTHORIZE_MODEL)
146 ```
147
148 ## ProposalKind Reference
149
150 | Kind | Meaning |
151 |---|---|
152 | `AUTHORIZE_MODEL` | Grant a specific AI model permission to run under the given scope. |
153 | `REVOKE_MODEL` | Withdraw a previously granted model authorization. |
154 | `AUTHORIZE_AGENT` | Grant a specific agent permission to act. |
155 | `REVOKE_AGENT` | Withdraw agent authorization. |
156 | `UPDATE_POLICY` | Change a runtime governance policy (rate limits, scopes, feature flags). |
157 | `ADD_NODE` | Admit a new node into the `NodeRegistry`. |
158 | `REMOVE_NODE` | Evict an existing governance node. |
159 | `EMERGENCY_FREEZE` | Halt all agent action immediately (break-glass). |
160 | `DELEGATION` | Allow agent X to delegate authority to agent Y. |
161
162 ## Cryptography
163
164 | Layer | Primitive | Notes |
165 |---|---|---|
166 | Signatures | **ML-DSA-65** (FIPS 204, a.k.a. CRYSTALS-Dilithium) | Default for every proposal, vote, and consensus result via `quantumshield`. Swap algorithm via `AgentIdentity.create(algorithm=...)`. |
167 | Canonical hashing | **SHA3-256** over RFC-8785-style canonical JSON | Used to bind signatures to a stable bytestring (alphabetised keys, no whitespace). |
168 | Identity | `did:pqaid:<SHA3-256(public_key)>` | From `quantumshield.identity.agent.AgentIdentity`. |
169
170 ## Quorum Policy
171
172 Default `QuorumPolicy` is PBFT-style:
173
174 | Setting | Default | Meaning |
175 |---|---|---|
176 | `min_participation_fraction` | `2/3` | `(approve+reject+abstain) / total_weight` must meet this. |
177 | `min_approval_fraction` | `2/3` | `approve / (approve+reject)` must meet this. Abstains are ignored for the ratio but count for participation. |
178
179 A 5-node cluster with weights `1/1/1/2/1` (total = 6) therefore needs at least 4 weight units cast and at least 2/3 approval among non-abstain voters to pass.
180
181 ## Threat Model
182
183 | Threat | Mitigation |
184 |---|---|
185 | **Proposal forgery** (attacker crafts a proposal claiming to be from node A) | Only A's private key produces a valid ML-DSA signature over A's canonical proposal bytes. `GovernanceNode.verify_proposal()` checks it. |
186 | **Proposal tampering in transit** (change `parameters` after signing) | `proposal_hash()` recomputes over canonical JSON; any edit invalidates the signature. |
187 | **Vote forgery** (attacker fabricates a vote from node B) | `GovernanceNode.verify_vote()` rejects anything whose ML-DSA signature does not verify against B's declared public key. |
188 | **Double-voting / Byzantine flip-flop** (node C votes APPROVE then later REJECT) | `VoteTally` raises `ByzantineDetectedError` on conflicting decisions. Identical repeat votes are idempotent. |
189 | **Cross-proposal replay** (replay a valid vote for proposal P against proposal Q) | Every vote binds both `proposal_id` *and* `proposal_hash`. Tally rejects hash mismatches. |
190 | **Non-member injection** (a rogue node signs a valid-looking vote) | Votes from DIDs absent from `NodeRegistry` are recorded as `"non-member voter"` and do not affect weights. |
191 | **Result forgery** (attacker hands auditor a fake "passed" result) | `ConsensusResult` is signed by the coordinator over canonical bytes covering every weight and every included vote id. `ConsensusRound.verify_result()` checks it. |
192 | **Harvest-now-decrypt-later** (adversary records today, breaks RSA/ECDSA in a future quantum computer) | Every signature is ML-DSA (FIPS 204); post-quantum secure against known attacks. |
193 | **Stale authorization** (old AUTHORIZE_MODEL grant lingers after policy change) | `AuthorizationChain.is_authorized()` honours subsequent passed `REVOKE_*` grants; operators issue a revocation proposal instead of mutating history. |
194
195 ## API Reference
196
197 ### `GovernanceNode`
198
199 Wraps a `quantumshield.identity.agent.AgentIdentity`. Produces signed proposals and signed votes.
200
201 | Member | Description |
202 |---|---|
203 | `identity`, `name`, `weight` | The underlying PQ-AID identity, display name, and voting weight. |
204 | `did` | Derived DID (read-only). |
205 | `sign_proposal(proposal)` | Populate signer/algorithm/signature/public_key on the proposal and return it. |
206 | `cast_vote(proposal, decision, rationale="")` | Create, sign, and return a `SignedVote`. |
207 | `GovernanceNode.verify_proposal(proposal)` | Static; ML-DSA check over canonical bytes. |
208 | `GovernanceNode.verify_vote(signed)` | Static; ML-DSA check over the vote's canonical bytes. |
209
210 ### `NodeRegistry`
211
212 Allow-list of governance nodes keyed by DID.
213
214 | Method | Description |
215 |---|---|
216 | `register(node)` / `remove(did)` | Add/remove membership. |
217 | `get(did)` | Look up; raises `UnknownNodeError` if absent. |
218 | `is_member(did)` / `total_weight()` / `list_dids()` / `len(...)` | Queries. |
219
220 ### `GovernanceProposal`
221
222 | Field | Description |
223 |---|---|
224 | `proposal_id`, `kind`, `subject_id`, `title`, `description`, `proposer_did`, `parameters`, `created_at`, `expires_at`, `status` | Proposal body. |
225 | `signer_did`, `algorithm`, `signature`, `public_key` | Signature envelope. |
226
227 | Method | Description |
228 |---|---|
229 | `GovernanceProposal.create(kind, subject_id, title, proposer_did, ...)` | Build with generated id and TTL. |
230 | `canonical_bytes()` / `proposal_hash()` | Deterministic canonical JSON / SHA3-256. |
231 | `is_expired()` | TTL check. |
232 | `to_dict()` / `from_dict()` | JSON-safe round-trip. |
233
234 ### `Vote` / `SignedVote`
235
236 `Vote` binds a `decision` to both `proposal_id` and `proposal_hash`. `SignedVote` is the envelope with ML-DSA signature.
237
238 ### `VoteTally`
239
240 Aggregates `SignedVote` objects. Rejects (and records) votes with: wrong proposal hash, wrong proposal id, invalid signature, non-member voter. Raises `ByzantineDetectedError` on conflicting repeat decisions from the same DID.
241
242 | Field | Description |
243 |---|---|
244 | `approve_weight` / `reject_weight` / `abstain_weight` | Running totals. |
245 | `valid_votes` / `invalid_votes` | Accepted and rejected entries (with reason). |
246 | `total_cast_weight()` / `to_dict()` | Views. |
247
248 ### `ConsensusRound`
249
250 | Method | Description |
251 |---|---|
252 | `ConsensusRound(proposal, registry, policy=QuorumPolicy())` | Construct. |
253 | `cast(signed_vote)` | Record a vote; raises `ProposalExpiredError` if TTL elapsed. |
254 | `finalize(coordinator)` | Decide, then ML-DSA sign the `ConsensusResult` with the coordinator's identity. |
255 | `ConsensusRound.verify_result(result)` | Static; verify the coordinator's signature. |
256
257 ### `QuorumPolicy`
258
259 Fractions of total voting weight: `min_participation_fraction` and `min_approval_fraction` (abstains do not count in the approval ratio).
260
261 ### `ConsensusResult`
262
263 Signed outcome. Fields: `proposal_id`, `proposal_hash`, `decision` (`"passed"|"rejected"`), `reason`, `approve_weight`, `reject_weight`, `abstain_weight`, `total_weight`, `included_vote_ids`, `decided_at`, plus signature envelope. Provides `canonical_bytes()`, `to_dict()`, `to_json()`.
264
265 ### `AuthorizationChain` / `AuthorizationGrant`
266
267 `AuthorizationChain` is an ordered list of `AuthorizationGrant`s for a single subject. `is_authorized(kind)` returns `True` iff the most recent passed grant for the relevant `AUTHORIZE_*` is not superseded by a later passed `REVOKE_*` (model and agent).
268
269 ### `GovernanceAuditLog`
270
271 In-memory append-only log. Typed helpers for `log_proposal_created`, `log_vote_cast`, `log_consensus_reached`, `log_byzantine_detected`, `log_node_added`, `log_node_removed`, `log_authorization_granted`, `log_authorization_revoked`. Filter via `entries(operation=..., proposal_id=..., actor_did=...)`. Exports to JSON.
272
273 ### Exceptions
274
275 | Exception | When |
276 |---|---|
277 | `GovernanceError` | Base class. |
278 | `InvalidProposalError` | Structurally invalid proposal or bad signature. |
279 | `InvalidVoteError` | Structurally invalid vote or bad signature. |
280 | `InsufficientQuorumError` | Participation threshold not met (when raised). |
281 | `ConsensusFailedError` | Round cannot reach a decision. |
282 | `UnknownNodeError` | DID absent from `NodeRegistry`. |
283 | `ByzantineDetectedError` | Conflicting repeat votes from a single DID. |
284 | `ProposalExpiredError` | Vote cast after the proposal's TTL. |
285
286 ## Why PQC for AI Governance?
287
288 Enterprise AI governance roots are trust anchors with a **multi-decade** shelf life. A model authorization issued today still authorizes the model in 2045. An agent delegation chain signed in 2027 still matters when that agent acts on a regulated dataset in 2040. The moment a classical signature scheme falls to a cryptographically relevant quantum computer, every governance decision ever signed with that scheme becomes retroactively forgeable - a "harvest-now, re-authorize-later" attack against your entire AI fleet.
289
290 Post-quantum signatures are the only way to make an AI governance audit trail that still means something after Q-day.
291
292 ## Examples
293
294 See the `examples/` directory:
295
296 - **`model_authorization.py`** - five nodes approve a medical AI model; signed result binds into an `AuthorizationChain`.
297 - **`byzantine_detection.py`** - one node submits conflicting votes; `VoteTally` raises `ByzantineDetectedError`; the audit log records the violation.
298 - **`emergency_freeze.py`** - unanimous EMERGENCY_FREEZE; signed `ConsensusResult` produced and verified.
299
300 Run them:
301
302 ```bash
303 python examples/model_authorization.py
304 python examples/byzantine_detection.py
305 python examples/emergency_freeze.py
306 ```
307
308 ## Development
309
310 ```bash
311 pip install -e ".[dev]"
312 pytest
313 ruff check src/ tests/ examples/
314 ```
315
316 ## Related
317
318 Part of the [QuantaMrkt](https://quantamrkt.com) post-quantum tooling registry. See also:
319
320 - **QuantumShield** - the PQC toolkit (`AgentIdentity`, `SignatureAlgorithm`, `sign/verify`).
321 - **PQC Federated Learning** - signed gradient updates and aggregation proofs.
322 - **PQC Audit Log FS** - rotating, Merkle-anchored on-disk audit segments.
323 - **PQC Reasoning Ledger** - ML-DSA signed agent reasoning traces.
324 - **PQC RAG Signing** - sign retrieval chunks with ML-DSA.
325
326 ## License
327
328 Apache License 2.0. See [LICENSE](LICENSE).
329