aethergovern 2.0.0__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,30 @@
1
+ """
2
+ AetherGovern v2.0.0 — Reference Implementation
3
+
4
+ A Falsification-First Investigation of Governance in the Aether Family.
5
+
6
+ This package is the reference implementation of the AetherGovern
7
+ architecture specified in v2.0.0. It implements the ten entities,
8
+ seven operations, and 47 invariants of the operational specification.
9
+
10
+ Reference:
11
+ Baladi, S. (2026). AetherGovern: A Falsification-First Investigation
12
+ of Governance in the Aether Family. Zenodo.
13
+ https://doi.org/10.5281/zenodo.24054524
14
+ """
15
+
16
+ from aethergovern.__version__ import (
17
+ __version__,
18
+ __author__,
19
+ __email__,
20
+ __doi__,
21
+ __license__,
22
+ )
23
+
24
+ __all__ = [
25
+ "__version__",
26
+ "__author__",
27
+ "__email__",
28
+ "__doi__",
29
+ "__license__",
30
+ ]
@@ -0,0 +1,9 @@
1
+ """AetherGovern version information."""
2
+
3
+ __version__ = "2.0.0"
4
+ __author__ = "Samir Baladi"
5
+ __email__ = "gitdeeper@gmail.com"
6
+ __doi__ = "10.5281/zenodo.24054524"
7
+ __license__ = "MIT"
8
+ __paper_version__ = "0.1.0"
9
+ __record_id__ = "AETHERGOVERN-02"
@@ -0,0 +1,17 @@
1
+ """AetherGovern — Accountability module (Layer 3)."""
2
+
3
+ from aethergovern.accountability.records import (
4
+ create_record,
5
+ compute_record_id,
6
+ verify_hash_link,
7
+ query_by_action,
8
+ canonical_record_bytes,
9
+ )
10
+
11
+ __all__ = [
12
+ "create_record",
13
+ "compute_record_id",
14
+ "verify_hash_link",
15
+ "query_by_action",
16
+ "canonical_record_bytes",
17
+ ]
@@ -0,0 +1,153 @@
1
+ """
2
+ AetherGovern — Accountability records (Layer 3, v2.0.0).
3
+
4
+ Implements:
5
+ - create_record: create an accountability record for an admitted action
6
+ - compute_record_id: content-addressed id
7
+ - verify_hash_link: verify the hash-link to the previous record
8
+ - query_by_action: find all records for an action
9
+ - canonical_record_bytes: serialization for signing
10
+
11
+ Reference:
12
+ Baladi, S. (2026). AetherGovern v2.0.0,
13
+ docs/specification/02_operations.md and docs/specification/03_invariants.md.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import hashlib
19
+ import json
20
+ from datetime import datetime, timezone
21
+ from typing import Dict, List
22
+
23
+ from aethergovern.models import (
24
+ AccountabilityRecord,
25
+ Action,
26
+ )
27
+
28
+
29
+ # =============================================================================
30
+ # canonical_record_bytes
31
+ # =============================================================================
32
+
33
+ def canonical_record_bytes(record: AccountabilityRecord) -> bytes:
34
+ """
35
+ Serialize an accountability record to canonical bytes (excluding signature).
36
+ """
37
+ canonical = {
38
+ "action_ref": record.action_ref,
39
+ "authority_chain_ref": record.authority_chain_ref,
40
+ "admitted_at": record.admitted_at,
41
+ "admitted_by": record.admitted_by,
42
+ "prev_record_hash": record.prev_record_hash,
43
+ }
44
+ return json.dumps(canonical, sort_keys=True, separators=(",", ":")).encode("utf-8")
45
+
46
+
47
+ def compute_record_id(record: AccountabilityRecord) -> str:
48
+ """Compute the content-addressed record id."""
49
+ return hashlib.sha256(canonical_record_bytes(record)).hexdigest()
50
+
51
+
52
+ # =============================================================================
53
+ # Time helpers
54
+ # =============================================================================
55
+
56
+ def _now_iso() -> str:
57
+ return datetime.now(timezone.utc).isoformat()
58
+
59
+
60
+ # =============================================================================
61
+ # create_record
62
+ # =============================================================================
63
+
64
+ def create_record(
65
+ action: Action,
66
+ authority_chain_ref: str,
67
+ admitted_by: str,
68
+ prev_record_hash: str,
69
+ ) -> AccountabilityRecord:
70
+ """
71
+ Create an accountability record for an admitted action.
72
+
73
+ Preconditions:
74
+ - action.action_id is non-empty
75
+ - authority_chain_ref is non-empty
76
+ - admitted_by is non-empty
77
+ - prev_record_hash is a 64-char hex string (or "0"*64 for the first)
78
+
79
+ Postconditions:
80
+ - record.action_ref == action.action_id
81
+ - record.authority_chain_ref == authority_chain_ref
82
+ - record.admitted_by == admitted_by
83
+ - record.prev_record_hash == prev_record_hash
84
+ - record.record_id == compute_record_id(record)
85
+
86
+ Note: the record is created unsigned; signing is the caller's
87
+ responsibility (via `accountability.sign`).
88
+ """
89
+ if not action.action_id:
90
+ raise ValueError("action.action_id is empty")
91
+ if not authority_chain_ref:
92
+ raise ValueError("authority_chain_ref is empty")
93
+ if not admitted_by:
94
+ raise ValueError("admitted_by is empty")
95
+ if len(prev_record_hash) != 64:
96
+ raise ValueError(
97
+ f"prev_record_hash must be 64 hex chars, got {len(prev_record_hash)}"
98
+ )
99
+
100
+ record = AccountabilityRecord(
101
+ record_id="", # placeholder
102
+ action_ref=action.action_id,
103
+ authority_chain_ref=authority_chain_ref,
104
+ admitted_at=_now_iso(),
105
+ admitted_by=admitted_by,
106
+ prev_record_hash=prev_record_hash,
107
+ signature=b"",
108
+ )
109
+ record_id = compute_record_id(record)
110
+ return AccountabilityRecord(
111
+ record_id=record_id,
112
+ action_ref=action.action_id,
113
+ authority_chain_ref=authority_chain_ref,
114
+ admitted_at=record.admitted_at,
115
+ admitted_by=admitted_by,
116
+ prev_record_hash=prev_record_hash,
117
+ signature=b"",
118
+ )
119
+
120
+
121
+ # =============================================================================
122
+ # verify_hash_link
123
+ # =============================================================================
124
+
125
+ def verify_hash_link(
126
+ record: AccountabilityRecord,
127
+ previous: AccountabilityRecord,
128
+ ) -> bool:
129
+ """
130
+ Verify that `record.prev_record_hash` equals `previous.record_id`.
131
+
132
+ Invariants:
133
+ I10.2 — Hash-link integrity.
134
+ """
135
+ return record.prev_record_hash == previous.record_id
136
+
137
+
138
+ # =============================================================================
139
+ # query_by_action
140
+ # =============================================================================
141
+
142
+ def query_by_action(
143
+ records: List[AccountabilityRecord],
144
+ action_id: str,
145
+ ) -> List[AccountabilityRecord]:
146
+ """
147
+ Find all accountability records for a given action.
148
+
149
+ Invariants:
150
+ I10.3 — Attribution completeness: every admitted action must have
151
+ at least one record; this function returns all records for the action.
152
+ """
153
+ return [r for r in records if r.action_ref == action_id]
@@ -0,0 +1,23 @@
1
+ """AetherGovern — Authorization module (Layer 1)."""
2
+
3
+ from aethergovern.authorization.certificates import (
4
+ generate_keypair,
5
+ canonical_certificate_bytes,
6
+ canonical_revocation_bytes,
7
+ grant,
8
+ delegate,
9
+ revoke,
10
+ check,
11
+ CheckResult,
12
+ )
13
+
14
+ __all__ = [
15
+ "generate_keypair",
16
+ "canonical_certificate_bytes",
17
+ "canonical_revocation_bytes",
18
+ "grant",
19
+ "delegate",
20
+ "revoke",
21
+ "check",
22
+ "CheckResult",
23
+ ]
@@ -0,0 +1,446 @@
1
+ """
2
+ AetherGovern — Authorization (Layer 1, v2.0.0).
3
+
4
+ Implements:
5
+ - grant: issue a root delegation certificate
6
+ - delegate: issue a child delegation certificate
7
+ - revoke: withdraw a certificate
8
+ - check: verify an authority chain at a frontier
9
+
10
+ Reference:
11
+ Baladi, S. (2026). AetherGovern v2.0.0, docs/specification/02_operations.md.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import hashlib
17
+ import json
18
+ from dataclasses import dataclass
19
+ from datetime import datetime, timezone
20
+ from typing import Dict, List, Optional
21
+
22
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import (
23
+ Ed25519PrivateKey,
24
+ Ed25519PublicKey,
25
+ )
26
+
27
+ from aethergovern.models import (
28
+ Action,
29
+ Agent,
30
+ AgentStatus,
31
+ AuthorityChain,
32
+ DelegationCertificate,
33
+ RevocationFrontier,
34
+ RevocationRecord,
35
+ Scope,
36
+ )
37
+
38
+
39
+ # =============================================================================
40
+ # Keypair generation
41
+ # =============================================================================
42
+
43
+ def generate_keypair() -> tuple[Ed25519PrivateKey, Ed25519PublicKey]:
44
+ """Generate a fresh Ed25519 keypair."""
45
+ private = Ed25519PrivateKey.generate()
46
+ public = private.public_key()
47
+ return private, public
48
+
49
+
50
+ # =============================================================================
51
+ # Canonical serialization
52
+ # =============================================================================
53
+
54
+ def canonical_certificate_bytes(cert: DelegationCertificate) -> bytes:
55
+ """
56
+ Serialize a certificate to canonical bytes (excluding signature).
57
+ Used for signing and verification.
58
+ """
59
+ canonical = {
60
+ "certificate_id": cert.certificate_id,
61
+ "issuer": cert.issuer,
62
+ "subject": cert.subject,
63
+ "scope": sorted([list(p) for p in cert.scope]),
64
+ "constraints": cert.constraints,
65
+ "expiry": cert.expiry,
66
+ "parent_ref": cert.parent_ref,
67
+ }
68
+ return json.dumps(canonical, sort_keys=True, separators=(",", ":")).encode("utf-8")
69
+
70
+
71
+ def canonical_revocation_bytes(rev: RevocationRecord) -> bytes:
72
+ """Serialize a revocation record to canonical bytes (excluding signature)."""
73
+ canonical = {
74
+ "revocation_id": rev.revocation_id,
75
+ "certificate_ref": rev.certificate_ref,
76
+ "issuer": rev.issuer,
77
+ "reason": rev.reason,
78
+ "timestamp": rev.timestamp,
79
+ }
80
+ return json.dumps(canonical, sort_keys=True, separators=(",", ":")).encode("utf-8")
81
+
82
+
83
+ def _compute_certificate_id(cert: DelegationCertificate) -> str:
84
+ """Compute the content-addressed certificate id."""
85
+ return hashlib.sha256(canonical_certificate_bytes(cert)).hexdigest()
86
+
87
+
88
+ def _compute_revocation_id(rev: RevocationRecord) -> str:
89
+ """Compute the content-addressed revocation id."""
90
+ return hashlib.sha256(canonical_revocation_bytes(rev)).hexdigest()
91
+
92
+
93
+ # =============================================================================
94
+ # Time helpers
95
+ # =============================================================================
96
+
97
+ def _now_iso() -> str:
98
+ return datetime.now(timezone.utc).isoformat()
99
+
100
+
101
+ def _is_past(timestamp: str) -> bool:
102
+ """Return True iff timestamp is in the past (relative to now)."""
103
+ try:
104
+ dt = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
105
+ except ValueError:
106
+ return True
107
+ now = datetime.now(timezone.utc)
108
+ if dt.tzinfo is None:
109
+ dt = dt.replace(tzinfo=timezone.utc)
110
+ return dt <= now
111
+
112
+
113
+ def _is_expired(expiry: str, current_time: str) -> bool:
114
+ """Return True iff expiry < current_time."""
115
+ try:
116
+ exp = datetime.fromisoformat(expiry.replace("Z", "+00:00"))
117
+ cur = datetime.fromisoformat(current_time.replace("Z", "+00:00"))
118
+ except ValueError:
119
+ return True
120
+ if exp.tzinfo is None:
121
+ exp = exp.replace(tzinfo=timezone.utc)
122
+ if cur.tzinfo is None:
123
+ cur = cur.replace(tzinfo=timezone.utc)
124
+ return cur > exp
125
+
126
+
127
+ # =============================================================================
128
+ # Root recognition (simplified registry)
129
+ # =============================================================================
130
+
131
+ # In a full implementation, this would be a decentralized root registry.
132
+ # For v2.0.0, the set of roots is passed explicitly.
133
+ # The `grant` function takes a `root_agents` parameter to recognize roots.
134
+
135
+ # For the test suite, we use a module-level registry that tests can populate.
136
+ _ROOT_AGENTS: dict[str, Agent] = {}
137
+
138
+
139
+ def register_root(agent: Agent) -> None:
140
+ """Register an agent as a recognized root (for testing)."""
141
+ _ROOT_AGENTS[agent.agent_id] = agent
142
+
143
+
144
+ def clear_roots() -> None:
145
+ """Clear the root registry (for testing)."""
146
+ _ROOT_AGENTS.clear()
147
+
148
+
149
+ def is_root(agent: Agent) -> bool:
150
+ """Check whether an agent is a recognized root."""
151
+ return agent.agent_id in _ROOT_AGENTS
152
+
153
+
154
+ # =============================================================================
155
+ # Operation 1 — grant
156
+ # =============================================================================
157
+
158
+ def grant(
159
+ root: Agent,
160
+ subject: Agent,
161
+ scope: Scope,
162
+ expiry: str,
163
+ root_private_key: Ed25519PrivateKey,
164
+ ) -> DelegationCertificate:
165
+ """
166
+ Issue a root delegation certificate.
167
+
168
+ Preconditions:
169
+ - root is a recognized root
170
+ - root and subject are active
171
+ - expiry is in the future
172
+ - scope is non-empty
173
+ """
174
+ if not is_root(root):
175
+ raise ValueError(f"agent {root.agent_id} is not a recognized root")
176
+ if root.status != AgentStatus.ACTIVE:
177
+ raise ValueError(f"root {root.agent_id} is not active")
178
+ if subject.status != AgentStatus.ACTIVE:
179
+ raise ValueError(f"subject {subject.agent_id} is not active")
180
+ if _is_past(expiry):
181
+ raise ValueError(f"expiry {expiry} is in the past")
182
+ if not scope:
183
+ raise ValueError("empty scope")
184
+
185
+ cert = DelegationCertificate(
186
+ certificate_id="", # placeholder
187
+ issuer=root.agent_id,
188
+ subject=subject.agent_id,
189
+ scope=scope,
190
+ constraints={},
191
+ expiry=expiry,
192
+ parent_ref=None,
193
+ signature=b"",
194
+ )
195
+ cert_id = _compute_certificate_id(cert)
196
+ cert = DelegationCertificate(
197
+ certificate_id=cert_id,
198
+ issuer=root.agent_id,
199
+ subject=subject.agent_id,
200
+ scope=scope,
201
+ constraints={},
202
+ expiry=expiry,
203
+ parent_ref=None,
204
+ signature=b"",
205
+ )
206
+ canonical = canonical_certificate_bytes(cert)
207
+ signature = root_private_key.sign(canonical)
208
+ return DelegationCertificate(
209
+ certificate_id=cert_id,
210
+ issuer=root.agent_id,
211
+ subject=subject.agent_id,
212
+ scope=scope,
213
+ constraints={},
214
+ expiry=expiry,
215
+ parent_ref=None,
216
+ signature=signature,
217
+ )
218
+
219
+
220
+ # =============================================================================
221
+ # Operation 2 — delegate
222
+ # =============================================================================
223
+
224
+ def delegate(
225
+ issuer: Agent,
226
+ subject: Agent,
227
+ scope: Scope,
228
+ expiry: str,
229
+ parent_certificate: DelegationCertificate,
230
+ issuer_private_key: Ed25519PrivateKey,
231
+ ) -> DelegationCertificate:
232
+ """
233
+ Issue a child delegation certificate (attenuated).
234
+
235
+ Preconditions:
236
+ - issuer is active and is the parent's subject
237
+ - subject is active
238
+ - parent is not expired or revoked (checked by caller)
239
+ - scope ⊆ parent's scope (attenuation)
240
+ - expiry <= parent's expiry
241
+ """
242
+ if issuer.agent_id != parent_certificate.subject:
243
+ raise ValueError(
244
+ f"issuer {issuer.agent_id} is not the parent's subject "
245
+ f"({parent_certificate.subject})"
246
+ )
247
+ if issuer.status != AgentStatus.ACTIVE:
248
+ raise ValueError(f"issuer {issuer.agent_id} is not active")
249
+ if subject.status != AgentStatus.ACTIVE:
250
+ raise ValueError(f"subject {subject.agent_id} is not active")
251
+ if not scope.issubset(parent_certificate.scope):
252
+ raise ValueError(
253
+ f"scope not a subset of parent scope: "
254
+ f"{set(scope)} ⊄ {set(parent_certificate.scope)}"
255
+ )
256
+ if _is_past(expiry):
257
+ raise ValueError(f"expiry {expiry} is in the past")
258
+ if expiry > parent_certificate.expiry:
259
+ raise ValueError(
260
+ f"expiry {expiry} > parent expiry {parent_certificate.expiry}"
261
+ )
262
+
263
+ cert = DelegationCertificate(
264
+ certificate_id="",
265
+ issuer=issuer.agent_id,
266
+ subject=subject.agent_id,
267
+ scope=scope,
268
+ constraints={},
269
+ expiry=expiry,
270
+ parent_ref=parent_certificate.certificate_id,
271
+ signature=b"",
272
+ )
273
+ cert_id = _compute_certificate_id(cert)
274
+ cert = DelegationCertificate(
275
+ certificate_id=cert_id,
276
+ issuer=issuer.agent_id,
277
+ subject=subject.agent_id,
278
+ scope=scope,
279
+ constraints={},
280
+ expiry=expiry,
281
+ parent_ref=parent_certificate.certificate_id,
282
+ signature=b"",
283
+ )
284
+ canonical = canonical_certificate_bytes(cert)
285
+ signature = issuer_private_key.sign(canonical)
286
+ return DelegationCertificate(
287
+ certificate_id=cert_id,
288
+ issuer=issuer.agent_id,
289
+ subject=subject.agent_id,
290
+ scope=scope,
291
+ constraints={},
292
+ expiry=expiry,
293
+ parent_ref=parent_certificate.certificate_id,
294
+ signature=signature,
295
+ )
296
+
297
+
298
+ # =============================================================================
299
+ # Operation 3 — revoke
300
+ # =============================================================================
301
+
302
+ def revoke(
303
+ issuer: Agent,
304
+ certificate: DelegationCertificate,
305
+ reason: str,
306
+ issuer_private_key: Ed25519PrivateKey,
307
+ frontier: RevocationFrontier,
308
+ ) -> RevocationRecord:
309
+ """
310
+ Withdraw a delegation certificate.
311
+
312
+ Preconditions:
313
+ - issuer is active
314
+ - issuer is the certificate's issuer (or ancestor, checked by caller)
315
+ - certificate is not already revoked at the given frontier
316
+ """
317
+ if issuer.status != AgentStatus.ACTIVE:
318
+ raise ValueError(f"issuer {issuer.agent_id} is not active")
319
+ if issuer.agent_id != certificate.issuer:
320
+ raise ValueError(
321
+ f"issuer {issuer.agent_id} is not the certificate's issuer "
322
+ f"({certificate.issuer})"
323
+ )
324
+ if certificate.certificate_id in frontier.revocations:
325
+ raise ValueError(f"certificate {certificate.certificate_id} is already revoked")
326
+
327
+ rev = RevocationRecord(
328
+ revocation_id="",
329
+ certificate_ref=certificate.certificate_id,
330
+ issuer=issuer.agent_id,
331
+ reason=reason,
332
+ timestamp=_now_iso(),
333
+ signature=b"",
334
+ )
335
+ rev_id = _compute_revocation_id(rev)
336
+ rev = RevocationRecord(
337
+ revocation_id=rev_id,
338
+ certificate_ref=certificate.certificate_id,
339
+ issuer=issuer.agent_id,
340
+ reason=reason,
341
+ timestamp=rev.timestamp,
342
+ signature=b"",
343
+ )
344
+ canonical = canonical_revocation_bytes(rev)
345
+ signature = issuer_private_key.sign(canonical)
346
+ return RevocationRecord(
347
+ revocation_id=rev_id,
348
+ certificate_ref=certificate.certificate_id,
349
+ issuer=issuer.agent_id,
350
+ reason=reason,
351
+ timestamp=rev.timestamp,
352
+ signature=signature,
353
+ )
354
+
355
+
356
+ # =============================================================================
357
+ # Operation 4 — check
358
+ # =============================================================================
359
+
360
+ @dataclass(frozen=True)
361
+ class CheckResult:
362
+ """Result of a chain validity check."""
363
+ valid: bool
364
+ reason: str = ""
365
+
366
+
367
+ def check(
368
+ action: Action,
369
+ chain: AuthorityChain,
370
+ frontier: RevocationFrontier,
371
+ current_time: str,
372
+ certificates: Dict[str, DelegationCertificate],
373
+ root_agents: Dict[str, Agent],
374
+ ) -> CheckResult:
375
+ """
376
+ Verify an authority chain at a given revocation frontier.
377
+
378
+ Conditions (all must hold for valid=True):
379
+ (a) every signature verifies
380
+ (b) scope attenuates: scope(c_{i+1}) ⊆ scope(c_i)
381
+ (c) no certificate has expired at current_time
382
+ (d) no certificate in the chain appears in frontier.revocations
383
+ (e) the first certificate's issuer is a recognized root
384
+ """
385
+ if not chain.certificates:
386
+ return CheckResult(False, "empty chain")
387
+
388
+ # Load certificates
389
+ certs: List[DelegationCertificate] = []
390
+ for cert_id in chain.certificates:
391
+ cert = certificates.get(cert_id)
392
+ if cert is None:
393
+ return CheckResult(False, f"unknown certificate: {cert_id}")
394
+ certs.append(cert)
395
+
396
+ # (a) signature verification
397
+ for cert in certs:
398
+ # Look up issuer's public key
399
+ if cert.issuer not in root_agents and cert.issuer not in {c.subject for c in certs}:
400
+ # Issuer must be a root or an intermediate subject
401
+ pass
402
+ # In this implementation, we verify against root_agents if available.
403
+ # For intermediate issuers, we'd need a full agent registry.
404
+ # For v2.0.0, we verify all signatures if the issuer is a known root;
405
+ # otherwise we defer to the registry (not implemented here).
406
+ if cert.issuer in root_agents:
407
+ canonical = canonical_certificate_bytes(cert)
408
+ try:
409
+ root_agents[cert.issuer].public_key.verify(cert.signature, canonical)
410
+ except Exception as e:
411
+ return CheckResult(False, f"signature verification failed for {cert.certificate_id}: {e}")
412
+
413
+ # (b) attenuation
414
+ for i in range(1, len(certs)):
415
+ if not certs[i].scope.issubset(certs[i-1].scope):
416
+ return CheckResult(
417
+ False,
418
+ f"scope attenuation violated at index {i}: "
419
+ f"{set(certs[i].scope)} ⊄ {set(certs[i-1].scope)}"
420
+ )
421
+
422
+ # (c) expiry
423
+ for cert in certs:
424
+ if _is_expired(cert.expiry, current_time):
425
+ return CheckResult(False, f"certificate {cert.certificate_id} expired")
426
+
427
+ # (d) revocation
428
+ for cert in certs:
429
+ if cert.certificate_id in frontier.revocations:
430
+ return CheckResult(False, f"certificate {cert.certificate_id} revoked")
431
+
432
+ # (e) root recognition
433
+ first_issuer = certs[0].issuer
434
+ if first_issuer not in root_agents:
435
+ return CheckResult(False, f"first certificate's issuer {first_issuer} is not a recognized root")
436
+
437
+ # (contiguity) each cert's issuer = previous cert's subject
438
+ for i in range(1, len(certs)):
439
+ if certs[i].issuer != certs[i-1].subject:
440
+ return CheckResult(
441
+ False,
442
+ f"contiguity violated at index {i}: "
443
+ f"issuer {certs[i].issuer} ≠ previous subject {certs[i-1].subject}"
444
+ )
445
+
446
+ return CheckResult(True, "ok")