aetheridentity 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,42 @@
1
+ """AetherIdentity v2.0.0 — Reference Implementation.
2
+
3
+ This package implements the specification in SPECIFICATION.md and the
4
+ invariants in INVARIANTS.md. It is a research artifact, not a production
5
+ system.
6
+
7
+ Public API:
8
+ records — Record types and canonical serialization
9
+ signer — Abstract Signer, FakeSigner, Ed25519Signer
10
+ simulator — Replicated state S as a flat list
11
+ identity — L1 Identity
12
+ continuity — L2 Continuity
13
+ reputation — L3 Reputation
14
+ sybil — L4 Sybil resistance
15
+ delegation — L5 Delegation
16
+ """
17
+
18
+ __version__ = "2.0.0"
19
+ __paper__ = "10.5281/zenodo.24102739"
20
+ __osf__ = "10.17605/OSF.IO/B6AEN"
21
+
22
+ from aetheridentity import (
23
+ continuity,
24
+ delegation,
25
+ identity,
26
+ records,
27
+ reputation,
28
+ signer,
29
+ simulator,
30
+ sybil,
31
+ )
32
+
33
+ __all__ = [
34
+ "continuity",
35
+ "delegation",
36
+ "identity",
37
+ "records",
38
+ "reputation",
39
+ "signer",
40
+ "simulator",
41
+ "sybil",
42
+ ]
@@ -0,0 +1,119 @@
1
+ """L2 — Continuity (§6.2 of SPECIFICATION.md).
2
+
3
+ Implements Definition 2 of the paper:
4
+ - Continuity chain with pre-committed next keys.
5
+ - Rotation requires the next key, kept offline.
6
+ - Fork detection: two valid rotations with the same predecessor.
7
+
8
+ Out of scope (v2.0.0):
9
+ - Fork resolution.
10
+ - Recovery after total key loss.
11
+ - Single-instance enforcement.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import hashlib
17
+ from typing import Iterable
18
+
19
+ from aetheridentity.records import (
20
+ InceptionRecord,
21
+ RotationRecord,
22
+ canonical_bytes,
23
+ )
24
+ from aetheridentity.signer import Signer
25
+
26
+
27
+ def hash_key(pk: bytes) -> bytes:
28
+ """Hash a public key for pre-commitment (H(pk))."""
29
+ return hashlib.sha256(pk).digest()
30
+
31
+
32
+ def create_inception(
33
+ signer: Signer,
34
+ label: str,
35
+ next_pk: bytes,
36
+ alg: str = "ed25519",
37
+ created_at: int = 0,
38
+ ) -> InceptionRecord:
39
+ """Create an inception record that commits to the next key.
40
+
41
+ The inception record itself has no `next_pk` field in Definition 1,
42
+ but the commitment is stored as the first rotation's `commitment`
43
+ field when the first rotation is created. To keep the record types
44
+ simple, we store the commitment implicitly: the caller must pass
45
+ `next_pk` when creating the first rotation.
46
+ """
47
+ from aetheridentity.identity import identifier
48
+
49
+ pk = signer.public_key()
50
+ signer_id = identifier(alg, pk, label)
51
+ record = InceptionRecord(
52
+ record_type="inception",
53
+ signer_id=signer_id,
54
+ created_at=created_at,
55
+ alg=alg,
56
+ pk=pk,
57
+ label=label,
58
+ t_c=created_at,
59
+ )
60
+ sig = signer.sign(canonical_bytes(record))
61
+ return InceptionRecord(**{**record.__dict__, "signature": sig})
62
+
63
+
64
+ def create_rotation(
65
+ signer: Signer,
66
+ ident: str,
67
+ epoch: int,
68
+ new_pk: bytes,
69
+ next_pk: bytes,
70
+ prev_hash: str,
71
+ created_at: int = 0,
72
+ ) -> RotationRecord:
73
+ """Create a rotation record that reveals `new_pk` and commits to `next_pk`."""
74
+ commitment = hash_key(next_pk)
75
+ record = RotationRecord(
76
+ record_type="rotation",
77
+ signer_id=ident,
78
+ created_at=created_at,
79
+ id=ident,
80
+ epoch=epoch,
81
+ new_pk=new_pk,
82
+ commitment=commitment,
83
+ prev_hash=prev_hash,
84
+ signing_pk=signer.public_key(),
85
+ )
86
+ sig = signer.sign(canonical_bytes(record))
87
+ return RotationRecord(**{**record.__dict__, "signature": sig})
88
+
89
+
90
+ def verify_rotation(
91
+ rotation: RotationRecord,
92
+ previous_commitment: bytes,
93
+ ) -> bool:
94
+ """Verify that a rotation reveals the pre-committed key."""
95
+ return hash_key(rotation.new_pk) == previous_commitment
96
+
97
+
98
+ def detect_fork(rotations: Iterable[RotationRecord]) -> bool:
99
+ """A fork is two valid rotations with the same predecessor and different content."""
100
+ seen: dict[str, bytes] = {}
101
+ for r in rotations:
102
+ if r.record_type != "rotation":
103
+ continue
104
+ content = canonical_bytes(r)
105
+ if r.prev_hash in seen:
106
+ if seen[r.prev_hash] != content:
107
+ return True
108
+ else:
109
+ seen[r.prev_hash] = content
110
+ return False
111
+
112
+
113
+ def continuity_chain(
114
+ records: Iterable,
115
+ ident: str,
116
+ ) -> list[RotationRecord]:
117
+ """Return the continuity chain for `ident`, ordered by epoch."""
118
+ chain = [r for r in records if isinstance(r, RotationRecord) and r.id == ident]
119
+ return sorted(chain, key=lambda r: r.epoch)
@@ -0,0 +1,251 @@
1
+ """L5 — Delegation (§6.5 of SPECIFICATION.md).
2
+
3
+ Implements Definition 4 of the paper:
4
+ - Delegation record: (i, j, S_d, e_d, p_d).
5
+ - Validity: S_d ⊆ S_p, e_d ≤ e_p, parent valid.
6
+ - Effective scope: intersection. Effective expiry: minimum.
7
+ - Revocation of any link invalidates downstream links.
8
+
9
+ Out of scope (v2.0.0):
10
+ - Expressive scopes (open question Q16).
11
+ - Retroactive vs. prospective revocation (open question Q17).
12
+ - Whether L5 is separable from AetherGovern (tested via governance_overlap).
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from dataclasses import dataclass
18
+ from typing import Iterable
19
+
20
+ from aetheridentity.records import DelegationRecord, canonical_bytes
21
+ from aetheridentity.signer import Signer
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class Scope:
26
+ """A scope is a set of permissions (strings)."""
27
+
28
+ permissions: frozenset[str]
29
+
30
+ def is_subset_of(self, other: "Scope") -> bool:
31
+ return self.permissions.issubset(other.permissions)
32
+
33
+ def intersect(self, other: "Scope") -> "Scope":
34
+ return Scope(self.permissions & other.permissions)
35
+
36
+ def __str__(self) -> str:
37
+ return "|".join(sorted(self.permissions))
38
+
39
+
40
+ def _parse_scope(s: str) -> Scope:
41
+ """Parse a scope string ('p1|p2|...') into a Scope."""
42
+ if not s:
43
+ return Scope(frozenset())
44
+ return Scope(frozenset(s.split("|")))
45
+
46
+
47
+ def delegate(
48
+ signer: Signer,
49
+ delegator: str,
50
+ delegatee: str,
51
+ scope: Scope,
52
+ expiry: int,
53
+ parent: str = "",
54
+ created_at: int = 0,
55
+ ) -> DelegationRecord:
56
+ """Create and sign a delegation record."""
57
+ record = DelegationRecord(
58
+ record_type="delegation",
59
+ signer_id=delegator,
60
+ created_at=created_at,
61
+ delegator=delegator,
62
+ delegatee=delegatee,
63
+ scope=str(scope),
64
+ expiry=expiry,
65
+ parent=parent,
66
+ )
67
+ sig = signer.sign(canonical_bytes(record))
68
+ return DelegationRecord(**{**record.__dict__, "signature": sig})
69
+
70
+
71
+ def _find_parent(
72
+ records: Iterable[DelegationRecord],
73
+ parent_id: str,
74
+ ) -> DelegationRecord | None:
75
+ """Find a parent delegation by its signature hash (acting as id)."""
76
+ if not parent_id:
77
+ return None
78
+ for r in records:
79
+ if r.record_type != "delegation":
80
+ continue
81
+ # Use the canonical hash as the delegation's identifier.
82
+ from aetheridentity.records import record_hash
83
+
84
+ if record_hash(r) == parent_id:
85
+ return r
86
+ return None
87
+
88
+
89
+ def validate_delegation(
90
+ records: Iterable[DelegationRecord],
91
+ child: DelegationRecord,
92
+ ) -> bool:
93
+ """A delegation is valid iff:
94
+ - its scope is a subset of the parent's scope,
95
+ - its expiry is ≤ the parent's expiry,
96
+ - the parent is valid (recursively).
97
+ A root delegation (no parent) is valid iff its scope is non-empty and
98
+ its expiry is positive.
99
+ """
100
+ child_scope = _parse_scope(child.scope)
101
+
102
+ if not child.parent:
103
+ return len(child_scope.permissions) > 0 and child.expiry > 0
104
+
105
+ parent = _find_parent(records, child.parent)
106
+ if parent is None:
107
+ return False
108
+
109
+ parent_scope = _parse_scope(parent.scope)
110
+ if not child_scope.is_subset_of(parent_scope):
111
+ return False
112
+ if child.expiry > parent.expiry:
113
+ return False
114
+ return validate_delegation(records, parent)
115
+
116
+
117
+ def effective_scope(
118
+ records: Iterable[DelegationRecord],
119
+ leaf: DelegationRecord,
120
+ ) -> Scope:
121
+ """The effective scope of a chain is the intersection of its scopes."""
122
+ records = list(records)
123
+ chain = _chain_of(records, leaf)
124
+ result = _parse_scope(chain[0].scope)
125
+ for r in chain[1:]:
126
+ result = result.intersect(_parse_scope(r.scope))
127
+ return result
128
+
129
+
130
+ def effective_expiry(
131
+ records: Iterable[DelegationRecord],
132
+ leaf: DelegationRecord,
133
+ ) -> int:
134
+ """The effective expiry of a chain is the minimum of its expiries."""
135
+ records = list(records)
136
+ chain = _chain_of(records, leaf)
137
+ return min(r.expiry for r in chain)
138
+
139
+
140
+ def _chain_of(
141
+ records: list[DelegationRecord],
142
+ leaf: DelegationRecord,
143
+ ) -> list[DelegationRecord]:
144
+ """Return the chain from root to leaf."""
145
+ chain = [leaf]
146
+ current = leaf
147
+ while current.parent:
148
+ parent = _find_parent(records, current.parent)
149
+ if parent is None:
150
+ break
151
+ chain.append(parent)
152
+ current = parent
153
+ return list(reversed(chain))
154
+
155
+
156
+ def revoke(
157
+ signer: Signer,
158
+ target_delegation: DelegationRecord,
159
+ created_at: int = 0,
160
+ ) -> DelegationRecord:
161
+ """Create and sign a revocation record for a delegation.
162
+
163
+ A revocation is represented as a delegation with an empty scope and
164
+ expiry 0, targeting the original delegation via `parent`.
165
+ """
166
+ from aetheridentity.records import record_hash
167
+
168
+ target_id = record_hash(target_delegation)
169
+ record = DelegationRecord(
170
+ record_type="delegation",
171
+ signer_id=target_delegation.delegator,
172
+ created_at=created_at,
173
+ delegator=target_delegation.delegator,
174
+ delegatee=target_delegation.delegatee,
175
+ scope="",
176
+ expiry=0,
177
+ parent=target_id,
178
+ )
179
+ sig = signer.sign(canonical_bytes(record))
180
+ return DelegationRecord(**{**record.__dict__, "signature": sig})
181
+
182
+
183
+ def is_revoked(
184
+ records: Iterable[DelegationRecord],
185
+ delegation: DelegationRecord,
186
+ ) -> bool:
187
+ """A delegation is revoked if any link in its chain has a revocation record."""
188
+ from aetheridentity.records import record_hash
189
+
190
+ records = list(records)
191
+ target_id = record_hash(delegation)
192
+ for r in records:
193
+ if r.record_type != "delegation":
194
+ continue
195
+ if r.scope == "" and r.expiry == 0 and r.parent == target_id:
196
+ return True
197
+ return False
198
+
199
+
200
+ def is_valid_for_action(
201
+ records: Iterable[DelegationRecord],
202
+ delegation: DelegationRecord,
203
+ action_time: int,
204
+ required_permission: str,
205
+ ) -> bool:
206
+ """Check that a delegation authorizes an action.
207
+
208
+ - The chain must be valid (validate_delegation).
209
+ - The action_time must be ≤ the effective expiry.
210
+ - The required permission must be in the effective scope.
211
+ - No link in the chain may be revoked.
212
+ """
213
+ records = list(records)
214
+ if not validate_delegation(records, delegation):
215
+ return False
216
+ if action_time > effective_expiry(records, delegation):
217
+ return False
218
+ if required_permission not in effective_scope(records, delegation).permissions:
219
+ return False
220
+ chain = _chain_of(records, delegation)
221
+ for link in chain:
222
+ if is_revoked(records, link):
223
+ return False
224
+ return True
225
+
226
+
227
+ def governance_overlap(
228
+ records: Iterable[DelegationRecord],
229
+ authority_chains: dict[str, frozenset[str]],
230
+ ) -> float:
231
+ """Measure the fraction of L5 decisions that can be re-expressed as
232
+ AetherGovern decisions (F3, tested in S6).
233
+
234
+ For each delegation in `records`, check whether its (delegator, delegatee)
235
+ pair already appears in the `authority_chains` mapping, where
236
+ authority_chains[delegator] is the set of principals authorized to act
237
+ on behalf of `delegator` under AetherGovern.
238
+
239
+ Returns:
240
+ Fraction in [0, 1]. 1.0 means every L5 decision is already an
241
+ AetherGovern decision (F3 falsified if ≥ the threshold 0.80).
242
+ """
243
+ records = [r for r in records if r.record_type == "delegation"]
244
+ if not records:
245
+ return 0.0
246
+ overlaps = 0
247
+ for r in records:
248
+ authorized = authority_chains.get(r.delegator, frozenset())
249
+ if r.delegatee in authorized:
250
+ overlaps += 1
251
+ return overlaps / len(records)
@@ -0,0 +1,53 @@
1
+ """L1 — Identity (§6.1 of SPECIFICATION.md)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ from typing import Iterable
7
+
8
+ from aetheridentity.records import InceptionRecord, canonical_bytes
9
+ from aetheridentity.signer import Signer
10
+
11
+ DOMAIN_TAG = b"AETHERIDENTITY/v2"
12
+
13
+
14
+ def identifier(alg: str, pk: bytes, label: str) -> str:
15
+ """Definition 1: id(t) = H(tag ‖ alg ‖ pk ‖ λ)."""
16
+ h = hashlib.sha256()
17
+ h.update(DOMAIN_TAG)
18
+ h.update(b"\x00")
19
+ h.update(alg.encode("utf-8"))
20
+ h.update(b"\x00")
21
+ h.update(pk)
22
+ h.update(b"\x00")
23
+ h.update(label.encode("utf-8"))
24
+ return h.hexdigest()
25
+
26
+
27
+ def create_identity(signer: Signer, label: str, alg: str = "ed25519", created_at: int = 0) -> InceptionRecord:
28
+ """Create an inception record and sign it."""
29
+ pk = signer.public_key()
30
+ signer_id = identifier(alg, pk, label)
31
+ record = InceptionRecord(
32
+ record_type="inception",
33
+ signer_id=signer_id,
34
+ created_at=created_at,
35
+ alg=alg,
36
+ pk=pk,
37
+ label=label,
38
+ t_c=created_at,
39
+ )
40
+ sig = signer.sign(canonical_bytes(record))
41
+ return InceptionRecord(
42
+ **{**record.__dict__, "signature": sig}
43
+ )
44
+
45
+
46
+ def verify_identity(records: Iterable[InceptionRecord], ident: str) -> bool:
47
+ """Check that the identifier recomputes from the record's fields."""
48
+ for r in records:
49
+ if r.record_type != "inception":
50
+ continue
51
+ if identifier(r.alg, r.pk, r.label) == ident:
52
+ return True
53
+ return False
@@ -0,0 +1,138 @@
1
+ """Record types for AetherIdentity.
2
+
3
+ Implements §3 of SPECIFICATION.md:
4
+ - Five record types (InceptionRecord, RotationRecord, ReputationRecord,
5
+ DelegationRecord, SybilClaim).
6
+ - Canonical JSON serialization (§3.3).
7
+ - Record hashing (SHA-256 over canonical JSON).
8
+
9
+ No signatures are verified here. Signature verification belongs to signer.py.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import hashlib
15
+ import json
16
+ from dataclasses import dataclass, field, asdict
17
+ from typing import Any, Literal
18
+
19
+ RecordType = Literal["inception", "rotation", "reputation", "delegation", "sybil_claim"]
20
+
21
+
22
+ def _canonical_json(obj: dict[str, Any]) -> bytes:
23
+ """Canonical JSON serialization (§3.3 of SPECIFICATION.md).
24
+
25
+ - Keys sorted lexicographically.
26
+ - No whitespace.
27
+ - UTF-8 encoding.
28
+ - Bytes encoded as lowercase hex.
29
+ """
30
+ return json.dumps(
31
+ obj,
32
+ sort_keys=True,
33
+ separators=(",", ":"),
34
+ ensure_ascii=False,
35
+ default=_json_default,
36
+ ).encode("utf-8")
37
+
38
+
39
+ def _json_default(obj: Any) -> Any:
40
+ if isinstance(obj, bytes):
41
+ return obj.hex()
42
+ raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")
43
+
44
+
45
+ def canonical_bytes(record: "BaseRecord") -> bytes:
46
+ """Canonical serialization of a record, without its signature field."""
47
+ data = asdict(record)
48
+ data.pop("signature", None)
49
+ return _canonical_json(data)
50
+
51
+
52
+ def record_hash(record: "BaseRecord") -> str:
53
+ """SHA-256 hash of the canonical serialization (without signature)."""
54
+ return hashlib.sha256(canonical_bytes(record)).hexdigest()
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class BaseRecord:
59
+ """Common fields for all record types (§3.2 of SPECIFICATION.md)."""
60
+
61
+ record_type: RecordType
62
+ signer_id: str
63
+ created_at: int
64
+ signature: bytes = field(default=b"", compare=False)
65
+
66
+
67
+ @dataclass(frozen=True)
68
+ class InceptionRecord(BaseRecord):
69
+ """Identity creation (Definition 1 of the paper)."""
70
+
71
+ alg: str = ""
72
+ pk: bytes = field(default=b"", compare=False)
73
+ label: str = ""
74
+ t_c: int = 0
75
+
76
+ def __post_init__(self) -> None:
77
+ object.__setattr__(self, "record_type", "inception")
78
+
79
+
80
+ @dataclass(frozen=True)
81
+ class RotationRecord(BaseRecord):
82
+ """Key rotation (Definition 2 of the paper)."""
83
+
84
+ id: str = ""
85
+ epoch: int = 0
86
+ new_pk: bytes = field(default=b"", compare=False)
87
+ commitment: bytes = field(default=b"", compare=False)
88
+ prev_hash: str = ""
89
+ signing_pk: bytes = field(default=b"", compare=False)
90
+
91
+ def __post_init__(self) -> None:
92
+ object.__setattr__(self, "record_type", "rotation")
93
+
94
+
95
+ @dataclass(frozen=True)
96
+ class ReputationRecord(BaseRecord):
97
+ """Signed observation (Definition 3 of the paper)."""
98
+
99
+ observer: str = ""
100
+ subject: str = ""
101
+ evidence: str = ""
102
+ outcome: int = 0
103
+ t: int = 0
104
+
105
+ def __post_init__(self) -> None:
106
+ object.__setattr__(self, "record_type", "reputation")
107
+ if self.outcome not in (-1, 0, 1):
108
+ raise ValueError("outcome must be -1, 0, or +1")
109
+
110
+
111
+ @dataclass(frozen=True)
112
+ class DelegationRecord(BaseRecord):
113
+ """Scoped authority (Definition 4 of the paper)."""
114
+
115
+ delegator: str = ""
116
+ delegatee: str = ""
117
+ scope: str = ""
118
+ expiry: int = 0
119
+ parent: str = ""
120
+
121
+ def __post_init__(self) -> None:
122
+ object.__setattr__(self, "record_type", "delegation")
123
+
124
+
125
+ @dataclass(frozen=True)
126
+ class SybilClaim(BaseRecord):
127
+ """Evidence-backed claim of shared control (§6.4 of SPECIFICATION.md)."""
128
+
129
+ attester: str = ""
130
+ target: str = ""
131
+ evidence: str = ""
132
+ t: int = 0
133
+
134
+ def __post_init__(self) -> None:
135
+ object.__setattr__(self, "record_type", "sybil_claim")
136
+
137
+
138
+ AnyRecord = InceptionRecord | RotationRecord | ReputationRecord | DelegationRecord | SybilClaim