sdata 1.5.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.
Files changed (98) hide show
  1. sdata/__init__.py +140 -0
  2. sdata/base.py +684 -0
  3. sdata/contrib/__init__.py +0 -0
  4. sdata/contrib/piexif/__init__.py +12 -0
  5. sdata/contrib/piexif/_common.py +94 -0
  6. sdata/contrib/piexif/_dump.py +346 -0
  7. sdata/contrib/piexif/_exceptions.py +2 -0
  8. sdata/contrib/piexif/_exif.py +638 -0
  9. sdata/contrib/piexif/_insert.py +60 -0
  10. sdata/contrib/piexif/_load.py +270 -0
  11. sdata/contrib/piexif/_remove.py +56 -0
  12. sdata/contrib/piexif/_transplant.py +45 -0
  13. sdata/contrib/piexif/_webp.py +240 -0
  14. sdata/contrib/piexif/helper.py +66 -0
  15. sdata/contrib/ranger/__init__.py +8 -0
  16. sdata/contrib/ranger/collections/__init__.py +0 -0
  17. sdata/contrib/ranger/collections/rangebucketmap.py +346 -0
  18. sdata/contrib/ranger/collections/rangemap.py +396 -0
  19. sdata/contrib/ranger/collections/rangeset.py +483 -0
  20. sdata/contrib/ranger/range/__init__.py +0 -0
  21. sdata/contrib/ranger/range/cut.py +285 -0
  22. sdata/contrib/ranger/range/range.py +644 -0
  23. sdata/contrib/sobol_seq.py +394 -0
  24. sdata/contrib/sortedcontainers/LICENSE +13 -0
  25. sdata/contrib/sortedcontainers/__init__.py +74 -0
  26. sdata/contrib/sortedcontainers/sorteddict.py +789 -0
  27. sdata/contrib/sortedcontainers/sortedlist.py +2646 -0
  28. sdata/contrib/sortedcontainers/sortedset.py +733 -0
  29. sdata/contrib/sqlitedict.py +598 -0
  30. sdata/contrib/timeflake/__init__.py +40 -0
  31. sdata/contrib/timeflake/flake.py +69 -0
  32. sdata/contrib/timeflake/utils.py +47 -0
  33. sdata/deprecated/__init__.py +10 -0
  34. sdata/deprecated/data.py +1503 -0
  35. sdata/did/__init__.py +129 -0
  36. sdata/did/_http.py +67 -0
  37. sdata/did/base58btc.py +49 -0
  38. sdata/did/did_github.py +152 -0
  39. sdata/did/did_peer4.py +155 -0
  40. sdata/did/did_web.py +222 -0
  41. sdata/did/diddoc.py +75 -0
  42. sdata/did/ed25519.py +225 -0
  43. sdata/did/eddsa.py +107 -0
  44. sdata/did/errors.py +40 -0
  45. sdata/did/issue_vc.py +193 -0
  46. sdata/did/jose.py +126 -0
  47. sdata/did/keys.py +104 -0
  48. sdata/did/utils_didkey.py +161 -0
  49. sdata/did/verify_vp.py +158 -0
  50. sdata/doe.py +101 -0
  51. sdata/dtypes.py +510 -0
  52. sdata/experiments/__init__.py +331 -0
  53. sdata/experiments/blob0.py +131 -0
  54. sdata/experiments/file.py +42 -0
  55. sdata/experiments/filegroup.py +144 -0
  56. sdata/experiments/image_old.py +313 -0
  57. sdata/experiments/ks2.py +15 -0
  58. sdata/experiments/lapshear.py +8 -0
  59. sdata/experiments/material.py +7 -0
  60. sdata/experiments/parameters_deprecated.py +394 -0
  61. sdata/experiments/workbook.py +226 -0
  62. sdata/format.py +125 -0
  63. sdata/imagemeta.py +533 -0
  64. sdata/interactive.py +118 -0
  65. sdata/iolib/__init__.py +51 -0
  66. sdata/iolib/hashtree.py +264 -0
  67. sdata/iolib/json1sqlitestore.py +554 -0
  68. sdata/iolib/jsonsqlitestore.py +704 -0
  69. sdata/iolib/pud.py +242 -0
  70. sdata/iolib/reader.py +262 -0
  71. sdata/iolib/writer.py +419 -0
  72. sdata/metadata.py +882 -0
  73. sdata/parameter.py +414 -0
  74. sdata/schema.py +273 -0
  75. sdata/sclass/__init__.py +111 -0
  76. sdata/sclass/bfo_classes.py +362 -0
  77. sdata/sclass/blob.py +362 -0
  78. sdata/sclass/content.py +87 -0
  79. sdata/sclass/dataframe.py +1251 -0
  80. sdata/sclass/dataframegroup.py +187 -0
  81. sdata/sclass/dependency_graph.py +85 -0
  82. sdata/sclass/filereference.py +124 -0
  83. sdata/sclass/image.py +218 -0
  84. sdata/sclass/lazy_namespace.py +155 -0
  85. sdata/sclass/process.py +383 -0
  86. sdata/sclass/process_graph.py +221 -0
  87. sdata/sclass/zipfile.py +135 -0
  88. sdata/semantic.py +346 -0
  89. sdata/suuid.py +136 -0
  90. sdata/timestamp.py +336 -0
  91. sdata/tools.py +14 -0
  92. sdata/units.py +602 -0
  93. sdata/vocab.py +149 -0
  94. sdata-1.5.0.dist-info/METADATA +264 -0
  95. sdata-1.5.0.dist-info/RECORD +98 -0
  96. sdata-1.5.0.dist-info/WHEEL +5 -0
  97. sdata-1.5.0.dist-info/licenses/LICENSE-MIT +21 -0
  98. sdata-1.5.0.dist-info/top_level.txt +1 -0
sdata/did/did_web.py ADDED
@@ -0,0 +1,222 @@
1
+ # -*- coding: utf-8 -*-
2
+ """``did:web``: DID-Dokumente erzeugen und über HTTPS auflösen.
3
+
4
+ Zwei Schlüsseldarstellungen werden unterstützt:
5
+
6
+ * **JWK** (empfohlen für JWS/JWT): ``JsonWebKey2020`` + ``publicKeyJwk`` (jws-2020),
7
+ * **Multibase**: ``Ed25519VerificationKey2020`` + ``publicKeyMultibase`` (ed25519-2020).
8
+
9
+ ``did:web:example.com`` wird zu ``https://example.com/.well-known/did.json``
10
+ aufgelöst; mit Pfadanteilen (``did:web:example.com:user:alice``) entsprechend zu
11
+ ``https://example.com/user/alice/did.json``.
12
+
13
+ CLI::
14
+
15
+ python -m sdata.did.did_web build --domain example.com \\
16
+ --pub issuer_pub.json --out did.json --format jwk
17
+ python -m sdata.did.did_web resolve --kid did:web:example.com#key-1
18
+
19
+ Referenzen:
20
+ * `did:web Method <https://w3c-ccg.github.io/did-method-web/>`_
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ import logging
26
+ import argparse
27
+ from urllib.parse import quote
28
+ from pathlib import Path
29
+ from typing import Any, Dict, Optional, Tuple
30
+
31
+ from ._http import http_get, HttpError
32
+ from .base58btc import b58encode
33
+ from .errors import EncodingError, ResolutionError
34
+ from .utils_didkey import pub_jwk_from_did_key, b64url_decode
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+ __all__ = ["build_did_document", "did_web_to_url", "resolve_public_jwk_for_kid"]
39
+
40
+ JWS2020_CONTEXT = "https://w3id.org/security/suites/jws-2020/v1"
41
+ ED25519_2020_CONTEXT = "https://w3id.org/security/suites/ed25519-2020/v1"
42
+ DID_CORE_CONTEXT = "https://www.w3.org/ns/did/v1"
43
+
44
+ #: HTTP-Timeout (Sekunden) für die Auflösung.
45
+ HTTP_TIMEOUT = 15
46
+
47
+
48
+ def did_web(domain: str) -> str:
49
+ """``'example.com'`` → ``'did:web:example.com'``."""
50
+ return "did:web:{}".format(domain)
51
+
52
+
53
+ def _load_pub_input(path: str) -> Tuple[str, Optional[Dict[str, Any]]]:
54
+ """Lade die ``--pub``-Eingabe.
55
+
56
+ Returns:
57
+ ``("jwk", jwk_dict)`` oder ``("multibase", {"mb": "z…"})``.
58
+
59
+ Akzeptiert ein JWK-JSON (``kty=OKP``/``crv=Ed25519``/``x``), ein
60
+ ``{"multibase": "z…"}``-JSON oder eine Textdatei mit einem ``z…``-String.
61
+
62
+ Raises:
63
+ EncodingError: wenn die Eingabe weder JWK noch gültige Multibase ist.
64
+ """
65
+ text = Path(path).read_text().strip()
66
+ try:
67
+ obj = json.loads(text)
68
+ except json.JSONDecodeError:
69
+ obj = None
70
+
71
+ if isinstance(obj, dict):
72
+ if obj.get("kty") == "OKP" and obj.get("crv") == "Ed25519" and "x" in obj:
73
+ return "jwk", obj
74
+ if "multibase" in obj and str(obj["multibase"]).startswith("z"):
75
+ return "multibase", {"mb": str(obj["multibase"])}
76
+
77
+ if text.startswith("z"):
78
+ return "multibase", {"mb": text}
79
+
80
+ raise EncodingError(
81
+ "--pub '{}' ist weder JWK-JSON noch eine gültige Multibase (z…)".format(path))
82
+
83
+
84
+ def _did_context(add_security: str, variant: str):
85
+ """Stelle die ``@context``-Liste zusammen (``add_security``: 'auto'|'on'|'off')."""
86
+ ctx = [DID_CORE_CONTEXT]
87
+ if add_security == "off":
88
+ return ctx
89
+ security = JWS2020_CONTEXT if variant == "jwk" else ED25519_2020_CONTEXT
90
+ ctx.append(security)
91
+ return ctx
92
+
93
+
94
+ def build_did_document(domain: str, public_input_path: str,
95
+ key_id_suffix: str = "key-1", variant: str = "jwk",
96
+ add_security_context: str = "auto") -> Dict[str, Any]:
97
+ """Erzeuge ein DID-Dokument (``did.json``) für ``did:web:<domain>``.
98
+
99
+ Args:
100
+ domain: Domain des Issuers.
101
+ public_input_path: Pfad zu Public-JWK-JSON **oder** Multibase-Datei.
102
+ key_id_suffix: Fragment der ``verificationMethod``-ID (Default ``key-1``).
103
+ variant: ``'jwk'`` (JsonWebKey2020) oder ``'multibase'`` (Ed25519VerificationKey2020).
104
+ add_security_context: ``'auto'`` | ``'on'`` | ``'off'``.
105
+
106
+ Raises:
107
+ EncodingError: bei ungültiger ``variant`` oder Eingabe.
108
+ """
109
+ if variant not in ("jwk", "multibase"):
110
+ raise EncodingError("variant muss 'jwk' oder 'multibase' sein")
111
+
112
+ kind, data = _load_pub_input(public_input_path)
113
+ did = did_web(domain)
114
+ vm_id = "{}#{}".format(did, key_id_suffix)
115
+ ctx = _did_context(add_security_context, variant)
116
+
117
+ if variant == "jwk":
118
+ jwk_pub = pub_jwk_from_did_key(data["mb"]) if kind == "multibase" else data
119
+ vm = {"id": vm_id, "type": "JsonWebKey2020", "controller": did,
120
+ "publicKeyJwk": jwk_pub}
121
+ else:
122
+ if kind == "jwk":
123
+ raw = b64url_decode(data["x"])
124
+ mb = "z" + b58encode(bytes([0xED, 0x01]) + raw)
125
+ else:
126
+ mb = data["mb"]
127
+ vm = {"id": vm_id, "type": "Ed25519VerificationKey2020", "controller": did,
128
+ "publicKeyMultibase": mb}
129
+
130
+ return {
131
+ "@context": ctx,
132
+ "id": did,
133
+ "verificationMethod": [vm],
134
+ "assertionMethod": [vm_id],
135
+ "authentication": [vm_id],
136
+ }
137
+
138
+
139
+ def did_web_to_url(did: str) -> str:
140
+ """Bilde die HTTPS-URL des ``did.json`` für eine ``did:web``-DID.
141
+
142
+ Raises:
143
+ EncodingError: wenn ``did`` keine ``did:web``-DID ist.
144
+ """
145
+ if not did.startswith("did:web:"):
146
+ raise EncodingError("keine did:web-DID: {}".format(did))
147
+ segments = ":".join(did.split(":")[2:]).split(":")
148
+ host = segments[0]
149
+ path = "/".join(map(quote, segments[1:]))
150
+ if path:
151
+ return "https://{}/{}/did.json".format(host, path)
152
+ return "https://{}/.well-known/did.json".format(host)
153
+
154
+
155
+ def resolve_public_jwk_for_kid(kid: str) -> Dict[str, str]:
156
+ """Lade das ``did.json`` und liefere immer einen Public-**JWK** zur ``kid``.
157
+
158
+ Unterstützt sowohl ``publicKeyJwk`` als auch ``publicKeyMultibase``
159
+ (Letzteres wird zu JWK konvertiert).
160
+
161
+ Raises:
162
+ ResolutionError: bei Netzwerkfehlern oder wenn die ``kid`` nicht gefunden
163
+ bzw. kein Schlüsselmaterial vorhanden ist.
164
+ """
165
+ did = kid.split("#")[0]
166
+ url = did_web_to_url(did)
167
+ try:
168
+ status, body = http_get(url, timeout=HTTP_TIMEOUT)
169
+ except HttpError as exc:
170
+ raise ResolutionError("did:web-Auflösung fehlgeschlagen ({}): {}".format(url, exc))
171
+ if not (200 <= status < 300):
172
+ raise ResolutionError(
173
+ "did:web-Auflösung fehlgeschlagen ({}): HTTP {}".format(url, status))
174
+ doc = json.loads(body)
175
+
176
+ by_id = {vm.get("id"): vm for vm in doc.get("verificationMethod", [])}
177
+ vm = by_id.get(kid)
178
+ if not vm:
179
+ raise ResolutionError("Schlüssel {} nicht im did.json gefunden".format(kid))
180
+
181
+ if "publicKeyJwk" in vm:
182
+ return vm["publicKeyJwk"]
183
+ mb = vm.get("publicKeyMultibase")
184
+ if isinstance(mb, str) and mb.startswith("z"):
185
+ return pub_jwk_from_did_key(mb)
186
+ raise ResolutionError("weder publicKeyJwk noch publicKeyMultibase gefunden")
187
+
188
+
189
+ # --------------------------------------------------------------------------
190
+ # CLI
191
+ # --------------------------------------------------------------------------
192
+ def main(argv=None) -> None:
193
+ logging.basicConfig(level=logging.INFO, format="%(message)s")
194
+ ap = argparse.ArgumentParser(description="did:web-Dokumente erzeugen/auflösen")
195
+ sub = ap.add_subparsers(required=True)
196
+
197
+ p = sub.add_parser("build", help="did.json erzeugen")
198
+ p.add_argument("--domain", required=True)
199
+ p.add_argument("--pub", required=True, help="Public-JWK-JSON ODER Datei mit Multibase (z…)")
200
+ p.add_argument("--out", required=True)
201
+ p.add_argument("--key-id", default="key-1")
202
+ p.add_argument("--format", choices=["jwk", "multibase"], default="jwk")
203
+ p.add_argument("--context", choices=["auto", "on", "off"], default="auto")
204
+ p.set_defaults(cmd="build")
205
+
206
+ p = sub.add_parser("resolve", help="Public-JWK per kid auflösen")
207
+ p.add_argument("--kid", required=True)
208
+ p.set_defaults(cmd="resolve")
209
+
210
+ args = ap.parse_args(argv)
211
+ if args.cmd == "build":
212
+ doc = build_did_document(args.domain, args.pub, key_id_suffix=args.key_id,
213
+ variant=args.format, add_security_context=args.context)
214
+ with open(args.out, "w") as fh:
215
+ json.dump(doc, fh, indent=2)
216
+ logger.info("✔ did.json erzeugt: %s", args.out)
217
+ else:
218
+ logger.info("%s", json.dumps(resolve_public_jwk_for_kid(args.kid), indent=2))
219
+
220
+
221
+ if __name__ == "__main__":
222
+ main()
sdata/did/diddoc.py ADDED
@@ -0,0 +1,75 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Lern-/Demo-Modul: Signieren einer DIDComm-Nachricht als Flattened JWS.
3
+
4
+ .. warning::
5
+ Nur zu Demonstrationszwecken. Es nutzt :mod:`sdata.did.ed25519`, eine
6
+ **nicht** für den Produktiveinsatz geeignete (nicht constant-time)
7
+ Referenzimplementierung. Für echte Anwendungen :mod:`sdata.did.jose`
8
+ (python-ecdsa) bzw. ein libsodium-Backend verwenden.
9
+
10
+ Ausführen::
11
+
12
+ python -m sdata.did.diddoc
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import base64
17
+ import json
18
+ import logging
19
+
20
+ from .ed25519 import sign, publickey
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ __all__ = ["base64url_encode", "base64url_decode", "build_didcomm_jws"]
25
+
26
+
27
+ def base64url_encode(data: bytes) -> bytes:
28
+ return base64.urlsafe_b64encode(data).rstrip(b"=")
29
+
30
+
31
+ def base64url_decode(data: bytes) -> bytes:
32
+ return base64.urlsafe_b64decode(data + b"=" * (4 - len(data) % 4))
33
+
34
+
35
+ def build_didcomm_jws(message: dict, seed: bytes, kid: str = "did:example:alice#key-1") -> dict:
36
+ """Signiere eine DIDComm-Nachricht und gib die Flattened-JWS-Struktur zurück.
37
+
38
+ Args:
39
+ message: Die DIDComm-Nachricht (wird als JSON serialisiert).
40
+ seed: 32-Byte-Ed25519-Seed (privater Schlüssel; nur Demo!).
41
+ kid: Schlüssel-ID für den JWS-Header.
42
+ """
43
+ pk = publickey(seed)
44
+ header = {"typ": "JWM", "alg": "EdDSA", "kid": kid}
45
+ protected = base64url_encode(json.dumps(header, separators=(",", ":")).encode())
46
+ payload = base64url_encode(json.dumps(message, separators=(",", ":")).encode())
47
+ sig = sign(protected + b"." + payload, seed, pk)
48
+ return {
49
+ "payload": payload.decode("utf-8"),
50
+ "signatures": [{
51
+ "protected": protected.decode("utf-8"),
52
+ "signature": base64url_encode(sig).decode("utf-8"),
53
+ "header": {"kid": kid},
54
+ }],
55
+ }
56
+
57
+
58
+ def _demo() -> dict:
59
+ import os
60
+ message = {
61
+ "id": "1234567890",
62
+ "type": "http://example.com/protocols/lets_do_lunch/1.0/proposal",
63
+ "from": "did:example:alice",
64
+ "to": ["did:example:bob"],
65
+ "created_time": 1516269022,
66
+ "expires_time": 1516385931,
67
+ "body": {"messagespecificattribute": "and its value"},
68
+ }
69
+ return build_didcomm_jws(message, seed=os.urandom(32))
70
+
71
+
72
+ if __name__ == "__main__":
73
+ logging.basicConfig(level=logging.INFO, format="%(message)s")
74
+ logger.info("Signierte DIDComm-Nachricht (JWS):")
75
+ logger.info("%s", json.dumps(_demo(), indent=2))
sdata/did/ed25519.py ADDED
@@ -0,0 +1,225 @@
1
+ # ed25519_typed.py
2
+ """
3
+ Lern-/Test-Implementierung von Ed25519 (pure Python).
4
+
5
+ ================================ SICHERHEITSHINWEIS ================================
6
+ NICHT FÜR DEN PRODUKTIVEINSATZ GEEIGNET.
7
+
8
+ Dieser Code dient ausschließlich Lehr- und Testzwecken (z. B. zum Nachvollziehen
9
+ des RFC-8032-Testvektors). Er ist:
10
+ - NICHT constant-time → anfällig für Timing-/Seitenkanalangriffe, die den
11
+ privaten Schlüssel preisgeben können,
12
+ - nicht gehärtet gegen Fault-Injection und andere Angriffe,
13
+ - nicht unabhängig auditiert.
14
+
15
+ Für echte Signaturen/Verifikation eine geprüfte Bibliothek verwenden, z. B.
16
+ PyNaCl (libsodium), `cryptography` oder python-ecdsa (wie im übrigen did-Modul).
17
+ ===================================================================================
18
+
19
+ Verwendet:
20
+ - Point: typing.NamedTuple (x, y)
21
+ - KeyPair: @dataclass (sk, pk)
22
+ """
23
+
24
+ from __future__ import annotations
25
+ import hashlib
26
+ from dataclasses import dataclass
27
+ from typing import NamedTuple
28
+
29
+ # --- Konstanten ---
30
+ b = 256
31
+ q = 2**255 - 19
32
+ l = 2**252 + 27742317777372353535851937790883648493 # Gruppenordnung
33
+
34
+ class Point(NamedTuple):
35
+ x: int
36
+ y: int
37
+
38
+ @dataclass(frozen=True)
39
+ class KeyPair:
40
+ sk: bytes # 32-Byte-Seed
41
+ pk: bytes # 32-Byte Public Key (compressed)
42
+
43
+ # --- Basisfunktionen ---
44
+ def H(m: bytes) -> bytes:
45
+ """SHA-512-Hash m -> 64 Bytes."""
46
+ return hashlib.sha512(m).digest()
47
+
48
+ def inv(x: int) -> int:
49
+ """Multiplikatives Inverses in F_q (q prim)."""
50
+ return pow(x, q - 2, q)
51
+
52
+ # Edwards-d und sqrt(-1)
53
+ d = (-121665 * inv(121666)) % q
54
+ I = pow(2, (q - 1) // 4, q)
55
+
56
+ def xrecover(y: int) -> int:
57
+ """Rekonstruiere x aus y gemäß Edwards-Kurvengleichung; even x."""
58
+ xx = (y*y - 1) * inv(d*y*y + 1) % q
59
+ x = pow(xx, (q + 3)//8, q)
60
+ if (x*x - xx) % q != 0:
61
+ x = (x * I) % q
62
+ if x % 2 != 0:
63
+ x = q - x
64
+ return x
65
+
66
+ By = (4 * inv(5)) % q
67
+ Bx = xrecover(By)
68
+ B = Point(Bx, By)
69
+ O = Point(0, 1) # neutrales Element
70
+
71
+ def edwards(P: Point, Q: Point) -> Point:
72
+ """Edwards-Punktaddition."""
73
+ x1, y1 = P
74
+ x2, y2 = Q
75
+ den_x = inv(1 + (d * x1 * x2 * y1 * y2) % q)
76
+ x3 = (x1*y2 + x2*y1) * den_x % q
77
+ den_y = inv(1 - (d * x1 * x2 * y1 * y2) % q)
78
+ y3 = (y1*y2 + x1*x2) * den_y % q
79
+ return Point(x3, y3)
80
+
81
+ def scalarmult(P: Point, e: int) -> Point:
82
+ """Double-and-Add, iterativ (nicht constant-time)."""
83
+ Q = O
84
+ N = P
85
+ k = e
86
+ while k > 0:
87
+ if k & 1:
88
+ Q = edwards(Q, N)
89
+ N = edwards(N, N)
90
+ k >>= 1
91
+ return Q
92
+
93
+ # --- Kodierung ---
94
+ def encodeint(y: int) -> bytes:
95
+ """Little-Endian-Encoding von 256 Bit."""
96
+ bits = [(y >> i) & 1 for i in range(b)]
97
+ return bytes(sum(bits[8*i + j] << j for j in range(8)) for i in range(b//8))
98
+
99
+ def bit(h: bytes, i: int) -> int:
100
+ """Lese Bit i aus Bytes (LE pro Byte)."""
101
+ return (h[i//8] >> (i % 8)) & 1
102
+
103
+ def encodepoint(P: Point) -> bytes:
104
+ """Komprimierte Punktdarstellung: y (255 Bit) || sign(x) (1 Bit)."""
105
+ x, y = P
106
+ bits = [(y >> i) & 1 for i in range(b-1)] + [x & 1]
107
+ return bytes(sum(bits[8*i + j] << j for j in range(8)) for i in range(b//8))
108
+
109
+ def isoncurve(P: Point) -> bool:
110
+ """Prüfe Kurvengleichung -x^2 + y^2 = 1 + d x^2 y^2 (mod q)."""
111
+ x, y = P
112
+ return (-x*x + y*y - 1 - d*x*x*y*y) % q == 0
113
+
114
+ def decodeint(s: bytes) -> int:
115
+ """Little-Endian-Integer aus 32 Bytes (256 Bit)."""
116
+ if len(s) != 32:
117
+ raise ValueError("Integer-Kodierung muss 32 Bytes haben")
118
+ return sum(2**i * bit(s, i) for i in range(b))
119
+
120
+ def decodepoint(s: bytes) -> Point:
121
+ """Dekodiere komprimierten Punkt und prüfe Kurvenzugehörigkeit."""
122
+ if len(s) != 32:
123
+ raise ValueError("Punktkodierung muss 32 Bytes haben")
124
+ y = sum(2**i * bit(s, i) for i in range(b-1))
125
+ x = xrecover(y)
126
+ if (x & 1) != bit(s, b-1):
127
+ x = q - x
128
+ P = Point(x, y)
129
+ if not isoncurve(P): # pragma: no cover - Off-Curve-Punkt schwer konstruierbar
130
+ raise ValueError("Punkt liegt nicht auf der Kurve")
131
+ return P
132
+
133
+ # --- Ed25519 API ---
134
+ def publickey(sk: bytes) -> bytes:
135
+ """
136
+ Ableitung des Public Keys aus 32-Byte-Seed.
137
+ Clamping: Bits 0..2 = 0, Bit 254 = 1, Bit 255 = 0.
138
+ """
139
+ if len(sk) != 32:
140
+ raise ValueError("sk muss 32 Bytes (Seed) sein")
141
+ h = H(sk)
142
+ a = 2**(b-2) + sum(2**i * bit(h, i) for i in range(3, b-2))
143
+ A = scalarmult(B, a)
144
+ return encodepoint(A)
145
+
146
+ def Hint(m: bytes) -> int:
147
+ """SHA-512(m) als 512-Bit little-endian Integer."""
148
+ h = H(m)
149
+ return sum(2**i * bit(h, i) for i in range(2*b))
150
+
151
+ def sign(m: bytes, sk: bytes, pk: bytes) -> bytes:
152
+ """
153
+ Signiere Nachricht m mit Seed sk und zugehörigem pk.
154
+ Ergebnis: 64 Byte = R(32) || S(32).
155
+ """
156
+ if len(sk) != 32 or len(pk) != 32:
157
+ raise ValueError("sk/pk müssen 32 Bytes sein")
158
+ h = H(sk)
159
+ a = 2**(b-2) + sum(2**i * bit(h, i) for i in range(3, b-2))
160
+ inter = bytes(h[b//8 : b//4]) # h[32:64]
161
+ r = Hint(inter + m) % l
162
+ R = scalarmult(B, r)
163
+ Rcomp = encodepoint(R)
164
+ hram = Hint(Rcomp + pk + m) % l
165
+ S = (r + hram * a) % l
166
+ return Rcomp + encodeint(S)
167
+
168
+ def verify(sig: bytes, m: bytes, pk: bytes) -> bool:
169
+ """
170
+ Verifiziere Signatur (R||S) über Nachricht m mit Public Key pk.
171
+ Zusätzliche Checks: Längen, S < l.
172
+ """
173
+ if len(sig) != 64 or len(pk) != 32:
174
+ return False
175
+ try:
176
+ R = decodepoint(sig[:32])
177
+ A = decodepoint(pk)
178
+ S = decodeint(sig[32:])
179
+ except Exception: # pragma: no cover - nur bei Off-Curve-/Decode-Fehlern
180
+ return False
181
+ if S >= l:
182
+ return False
183
+ hram = Hint(encodepoint(R) + pk + m) % l
184
+ # Prüfe B*S == R + A*hram
185
+ left = scalarmult(B, S)
186
+ right = edwards(R, scalarmult(A, hram))
187
+ return left == right
188
+
189
+ def keypair_from_seed(seed32: bytes) -> KeyPair:
190
+ """Erzeuge KeyPair aus 32-Byte-Seed."""
191
+ pk = publickey(seed32)
192
+ return KeyPair(sk=seed32, pk=pk)
193
+
194
+ # --- Demo & Testvektor ---
195
+ if __name__ == "__main__":
196
+ import os
197
+ import logging
198
+
199
+ logging.basicConfig(level=logging.INFO, format="%(message)s")
200
+ logger = logging.getLogger(__name__)
201
+
202
+ # Demo
203
+ kp = keypair_from_seed(os.urandom(32))
204
+ msg = b"Hallo, DIDComm!"
205
+ sig = sign(msg, kp.sk, kp.pk)
206
+ logger.info("Sig OK?: %s", verify(sig, msg, kp.pk))
207
+
208
+ # RFC 8032 Testvektor (leere Nachricht)
209
+ sk_tv = bytes.fromhex(
210
+ "9d61b19deffd5a60ba844af492ec2cc4"
211
+ "4449c5697b326919703bac031cae7f60"
212
+ )
213
+ pk_tv = bytes.fromhex(
214
+ "d75a980182b10ab7d54bfed3c964073a"
215
+ "0ee172f3daa62325af021a68f707511a"
216
+ )
217
+ sig_tv = bytes.fromhex(
218
+ "e5564300c360ac729086e2cc806e828a"
219
+ "84877f1eb8e5d974d873e06522490155"
220
+ "5fb8821590a33bacc61e39701cf9b46b"
221
+ "d25bf5f0595bbe24655141438e7a100b"
222
+ )
223
+ kp_tv = keypair_from_seed(sk_tv)
224
+ logger.info("RFC PK OK: %s", kp_tv.pk == pk_tv)
225
+ logger.info("RFC SIG OK: %s", verify(sig_tv, b"", kp_tv.pk))
sdata/did/eddsa.py ADDED
@@ -0,0 +1,107 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Ed25519-Schlüsselobjekte – drop-in-Teilmenge der ``python-ecdsa``-API.
3
+
4
+ Dieses Modul stellt genau die ``SigningKey``/``VerifyingKey``-Operationen
5
+ bereit, die ``jose`` und ``keys`` zuvor aus dem externen Paket ``ecdsa``
6
+ bezogen haben – jetzt jedoch ohne externe Abhängigkeit, gestützt auf die
7
+ RFC-8032-Primitive in :mod:`sdata.did.ed25519`.
8
+
9
+ .. warning::
10
+ Die zugrunde liegende Ed25519-Implementierung ist **pure Python und nicht
11
+ constant-time** (siehe Sicherheitshinweis in :mod:`sdata.did.ed25519`).
12
+ Sie verhält sich darin wie ``python-ecdsa`` (ebenfalls pure Python, nicht
13
+ seitenkanalresistent). Für Umgebungen mit Seitenkanal-Anforderungen ein
14
+ libsodium-basiertes Backend (PyNaCl) oder ``cryptography`` verwenden.
15
+
16
+ Korrektheit wird gegen die offiziellen RFC-8032-Testvektoren abgesichert
17
+ (siehe ``tests/test_eddsa.py``).
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import os
22
+ from typing import Optional
23
+
24
+ from . import ed25519 as _ed
25
+
26
+ __all__ = ["Ed25519", "SigningKey", "VerifyingKey", "BadSignatureError"]
27
+
28
+
29
+ class BadSignatureError(Exception):
30
+ """Signatur konnte nicht verifiziert werden (API-kompatibel zu ``ecdsa``)."""
31
+
32
+
33
+ class _Ed25519Curve:
34
+ """Sentinel für den Kurvenparameter ``curve=Ed25519`` (API-Kompatibilität)."""
35
+
36
+ name = "Ed25519"
37
+
38
+ def __repr__(self) -> str: # pragma: no cover - rein kosmetisch
39
+ return "Ed25519"
40
+
41
+
42
+ #: Kurven-Sentinel, analog zu ``ecdsa.Ed25519``.
43
+ Ed25519 = _Ed25519Curve()
44
+
45
+
46
+ def _check_len(raw: bytes, expected: int, what: str) -> bytes:
47
+ if not isinstance(raw, (bytes, bytearray)) or len(raw) != expected:
48
+ raise ValueError("{} muss {} Bytes sein".format(what, expected))
49
+ return bytes(raw)
50
+
51
+
52
+ class VerifyingKey:
53
+ """Öffentlicher Ed25519-Schlüssel (32-Byte, komprimiert)."""
54
+
55
+ def __init__(self, pub: bytes):
56
+ self._pub = pub
57
+
58
+ @classmethod
59
+ def from_string(cls, raw: bytes, curve=Ed25519) -> "VerifyingKey":
60
+ """Erzeuge einen ``VerifyingKey`` aus 32 rohen Public-Key-Bytes."""
61
+ return cls(_check_len(raw, 32, "Public Key"))
62
+
63
+ def to_string(self) -> bytes:
64
+ """Liefere den rohen 32-Byte Public Key."""
65
+ return self._pub
66
+
67
+ def verify(self, signature: bytes, data: bytes) -> bool:
68
+ """Verifiziere ``signature`` über ``data``.
69
+
70
+ Returns:
71
+ ``True`` bei gültiger Signatur.
72
+ Raises:
73
+ BadSignatureError: wenn die Signatur ungültig ist.
74
+ """
75
+ if not _ed.verify(bytes(signature), bytes(data), self._pub):
76
+ raise BadSignatureError("Signatur ungültig")
77
+ return True
78
+
79
+
80
+ class SigningKey:
81
+ """Privater Ed25519-Schlüssel (32-Byte-Seed)."""
82
+
83
+ def __init__(self, seed: bytes):
84
+ self._seed = seed
85
+ self._pub = _ed.publickey(seed)
86
+
87
+ @classmethod
88
+ def generate(cls, curve=Ed25519) -> "SigningKey":
89
+ """Erzeuge einen frischen Schlüssel aus 32 zufälligen Bytes (``os.urandom``)."""
90
+ return cls(os.urandom(32))
91
+
92
+ @classmethod
93
+ def from_string(cls, raw: bytes, curve=Ed25519) -> "SigningKey":
94
+ """Erzeuge einen ``SigningKey`` aus einem 32-Byte-Seed."""
95
+ return cls(_check_len(raw, 32, "Seed"))
96
+
97
+ def to_string(self) -> bytes:
98
+ """Liefere den rohen 32-Byte-Seed (geheim halten!)."""
99
+ return self._seed
100
+
101
+ def get_verifying_key(self) -> VerifyingKey:
102
+ """Leite den zugehörigen öffentlichen Schlüssel ab."""
103
+ return VerifyingKey(self._pub)
104
+
105
+ def sign(self, data: bytes) -> bytes:
106
+ """Signiere ``data`` und gib die 64-Byte-Signatur zurück."""
107
+ return _ed.sign(bytes(data), self._seed, self._pub)
sdata/did/errors.py ADDED
@@ -0,0 +1,40 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Ausnahmehierarchie für das :mod:`sdata.did`-Subpackage.
3
+
4
+ Bibliotheksfunktionen werfen ausschließlich diese Ausnahmen (nie ``SystemExit``),
5
+ damit Aufrufer sie gezielt behandeln können::
6
+
7
+ from sdata.did import verify_presentation, VerificationError
8
+ try:
9
+ verify_presentation(vp_jws)
10
+ except VerificationError as exc:
11
+ ...
12
+
13
+ Hierarchie::
14
+
15
+ DidError # Basisklasse für alles in sdata.did
16
+ ├── EncodingError # ungültiges Format (JWK, Multibase, JWS-Struktur)
17
+ ├── ResolutionError # DID/Schlüssel konnte nicht aufgelöst werden
18
+ └── VerificationError # Signatur, Hash oder Claims ungültig
19
+
20
+ ``EncodingError`` erbt zusätzlich von :class:`ValueError`, damit bestehender Code,
21
+ der ``ValueError`` abfängt, weiterhin funktioniert.
22
+ """
23
+
24
+ __all__ = ["DidError", "EncodingError", "ResolutionError", "VerificationError"]
25
+
26
+
27
+ class DidError(Exception):
28
+ """Basisklasse aller Fehler des ``sdata.did``-Subpackages."""
29
+
30
+
31
+ class EncodingError(DidError, ValueError):
32
+ """Ein Wert hat ein ungültiges/unerwartetes Format (JWK, Multibase, JWS …)."""
33
+
34
+
35
+ class ResolutionError(DidError):
36
+ """Eine DID oder ein Schlüssel konnte nicht aufgelöst werden."""
37
+
38
+
39
+ class VerificationError(DidError):
40
+ """Eine Signatur, ein Hash oder eine Claim-Prüfung ist fehlgeschlagen."""