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/issue_vc.py ADDED
@@ -0,0 +1,193 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Ausstellung von Verifiable Credentials (VC) und Verifiable Presentations (VP).
3
+
4
+ Signaturformat: **JWT-VC** als Compact-JWS (EdDSA/Ed25519) über
5
+ :mod:`sdata.did.jose`. Der Issuer wird als ``did:web`` modelliert, der Holder
6
+ wahlweise als ``did:key`` (Multibase) oder als beliebige DID.
7
+
8
+ Das hier ausgestellte Credential ist ein einfacher **Digital Product Passport**
9
+ (``ProductPassport``) im Sinne von GS1/EU-DPP: GTIN, Seriennummer, GS1 Digital
10
+ Link sowie CO₂-Fußabdruck und Rezyklatanteil.
11
+
12
+ CLI::
13
+
14
+ python -m sdata.did.issue_vc \\
15
+ --issuer-domain issuer.example --issuer-key issuer_key.json \\
16
+ --gtin 04012345678901 --serial SN-42 --co2 12.5 --recycled 80 \\
17
+ --out product.vc
18
+
19
+ Referenzen:
20
+ * `W3C VC Data Model <https://www.w3.org/TR/vc-data-model/>`_
21
+ * `GS1 Digital Link <https://www.gs1.org/standards/gs1-digital-link>`_
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import json
26
+ import time
27
+ import uuid
28
+ import logging
29
+ import argparse
30
+ from typing import Any, Dict
31
+
32
+ from . import jose
33
+ from .keys import pub_from_priv_jwk
34
+ from .utils_didkey import did_key_from_jwk_ed25519
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+ __all__ = ["issue_vc", "make_vp", "sign_compact_jws"]
39
+
40
+
41
+ def _now() -> int:
42
+ return int(time.time())
43
+
44
+
45
+ def _in_days(days: int) -> int:
46
+ return _now() + days * 24 * 3600
47
+
48
+
49
+ def _load_priv_jwk(path: str) -> Dict:
50
+ with open(path, "r") as fh:
51
+ return json.load(fh)
52
+
53
+
54
+ def sign_compact_jws(payload: Dict[str, Any], priv_jwk: Dict, kid: str, typ: str) -> str:
55
+ """Signiere ``payload`` als Compact-JWS.
56
+
57
+ Dünner Wrapper um :func:`sdata.did.jose.sign_compact`; aus Kompatibilität
58
+ mit dem bisherigen API-Namen erhalten geblieben.
59
+ """
60
+ return jose.sign_compact(payload, priv_jwk, kid=kid, typ=typ)
61
+
62
+
63
+ def issue_vc(issuer_domain: str, issuer_key_path: str, gtin: str, serial: str,
64
+ co2: float, recycled: float,
65
+ subject_did: str = "did:example:product-xyz",
66
+ validity_days: int = 365 * 3) -> str:
67
+ """Stelle ein ``ProductPassport``-VC aus und gib es als Compact-JWS zurück.
68
+
69
+ Args:
70
+ issuer_domain: Domain des Issuers; ergibt ``did:web:<domain>``.
71
+ issuer_key_path: Pfad zum **privaten** Issuer-JWK.
72
+ gtin: GS1 GTIN (Global Trade Item Number).
73
+ serial: Seriennummer des Produkts.
74
+ co2: CO₂-Fußabdruck in kg CO₂e.
75
+ recycled: Rezyklatanteil in Prozent.
76
+ subject_did: DID des Credential-Subjects.
77
+ validity_days: Gültigkeitsdauer ab jetzt (Default: 3 Jahre).
78
+
79
+ Returns:
80
+ Das signierte VC als Compact-JWS (``vc+ld+jwt``).
81
+ """
82
+ issuer_did = "did:web:{}".format(issuer_domain)
83
+ kid = "{}#key-1".format(issuer_did)
84
+ jwk_priv = _load_priv_jwk(issuer_key_path)
85
+ payload = {
86
+ "iss": issuer_did,
87
+ "sub": subject_did,
88
+ "nbf": _now(),
89
+ "iat": _now(),
90
+ "exp": _in_days(validity_days),
91
+ "jti": "urn:uuid:{}".format(uuid.uuid4()),
92
+ "vc": {
93
+ "@context": ["https://www.w3.org/2018/credentials/v1"],
94
+ "type": ["VerifiableCredential", "ProductPassport"],
95
+ "credentialSubject": {
96
+ "gtin": gtin,
97
+ "serialNumber": serial,
98
+ "gs1DigitalLink": "https://id.gs1.org/01/{}/21/{}".format(gtin, serial),
99
+ "co2Footprint": {"value": co2, "unitText": "kg CO2e"},
100
+ "recycledContent": {"value": recycled, "unitText": "%"},
101
+ },
102
+ "credentialStatus": {
103
+ "id": "https://{}/status/pp-2025#1".format(issuer_domain),
104
+ "type": "StatusList2021Entry",
105
+ },
106
+ },
107
+ }
108
+ return sign_compact_jws(payload, jwk_priv, kid=kid, typ="vc+ld+jwt")
109
+
110
+
111
+ def make_vp(holder_key_path: str, holder_did: str, vc_jws: str,
112
+ aud: str, nonce: str) -> str:
113
+ """Baue eine Verifiable Presentation um ein VC und signiere sie (Holder).
114
+
115
+ Für ``did:key``-Holder wird die konventionelle ``kid``
116
+ ``did:key:<mb>#<mb>`` gesetzt, sonst ``<holder_did>#key-1``.
117
+ """
118
+ holder_jwk = _load_priv_jwk(holder_key_path)
119
+ if holder_did.startswith("did:key:"):
120
+ frag = did_key_from_jwk_ed25519(pub_from_priv_jwk(holder_jwk)).split(":", 2)[2]
121
+ holder_kid = "{}#{}".format(holder_did, frag)
122
+ else:
123
+ holder_kid = "{}#key-1".format(holder_did)
124
+
125
+ payload = {
126
+ "holder": holder_did,
127
+ "vp": {"type": ["VerifiablePresentation"], "verifiableCredential": [vc_jws]},
128
+ "aud": aud,
129
+ "nonce": nonce,
130
+ }
131
+ return sign_compact_jws(payload, holder_jwk, kid=holder_kid, typ="vp+jwt")
132
+
133
+
134
+ # --------------------------------------------------------------------------
135
+ # CLI
136
+ # --------------------------------------------------------------------------
137
+ def _build_parser() -> argparse.ArgumentParser:
138
+ ap = argparse.ArgumentParser(description="Verifiable Credential / Presentation ausstellen")
139
+ ap.add_argument("--issuer-domain", required=True)
140
+ ap.add_argument("--issuer-key", required=True)
141
+ ap.add_argument("--gtin", required=True)
142
+ ap.add_argument("--serial", required=True)
143
+ ap.add_argument("--co2", type=float, required=True)
144
+ ap.add_argument("--recycled", type=float, required=True)
145
+ ap.add_argument("--subject", default="did:example:product-xyz")
146
+ ap.add_argument("--out", required=True)
147
+
148
+ ap.add_argument("--make-vp", action="store_true")
149
+ ap.add_argument("--holder-key")
150
+ ap.add_argument("--holder-did", default=None)
151
+ ap.add_argument("--aud", default="https://verifier.example.org")
152
+ ap.add_argument("--nonce", default=None)
153
+ return ap
154
+
155
+
156
+ def main(argv=None) -> None:
157
+ logging.basicConfig(level=logging.INFO, format="%(message)s")
158
+ args = _build_parser().parse_args(argv)
159
+
160
+ vc_jws = issue_vc(
161
+ issuer_domain=args.issuer_domain,
162
+ issuer_key_path=args.issuer_key,
163
+ gtin=args.gtin,
164
+ serial=args.serial,
165
+ co2=args.co2,
166
+ recycled=args.recycled,
167
+ subject_did=args.subject,
168
+ )
169
+
170
+ if not args.make_vp:
171
+ with open(args.out, "w") as fh:
172
+ fh.write(vc_jws)
173
+ logger.info("✔ VC geschrieben: %s", args.out)
174
+ return
175
+
176
+ if not args.holder_key:
177
+ raise SystemExit("--make-vp benötigt --holder-key")
178
+
179
+ if args.holder_did:
180
+ holder_did = args.holder_did
181
+ else:
182
+ holder_jwk = _load_priv_jwk(args.holder_key)
183
+ holder_did = did_key_from_jwk_ed25519(pub_from_priv_jwk(holder_jwk))
184
+
185
+ nonce = args.nonce or str(uuid.uuid4())
186
+ vp_jws = make_vp(args.holder_key, holder_did, vc_jws, aud=args.aud, nonce=nonce)
187
+ with open(args.out, "w") as fh:
188
+ fh.write(vp_jws)
189
+ logger.info("✔ VP geschrieben: %s (enthält eingebettetes VC)", args.out)
190
+
191
+
192
+ if __name__ == "__main__":
193
+ main()
sdata/did/jose.py ADDED
@@ -0,0 +1,126 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Kompakte JWS-Schicht (JOSE) für EdDSA/Ed25519 – gemeinsame Basis für VC & VP.
3
+
4
+ Diese Modul bündelt das Signieren/Verifizieren von **Compact-JWS**
5
+ (``header.payload.signature``, base64url), das zuvor doppelt in
6
+ ``issue_vc`` und ``verify_vp`` implementiert war.
7
+
8
+ Krypto-Backend: :mod:`sdata.did.eddsa` (reine Python-Ed25519-Implementierung,
9
+ abhängigkeitsfrei).
10
+
11
+ .. warning::
12
+ Das Ed25519-Backend ist nicht garantiert constant-time. Für hochsensible
13
+ Produktivumgebungen mit Seitenkanal-Anforderungen sollte ein
14
+ libsodium-basiertes Backend (PyNaCl) oder ``cryptography`` erwogen werden.
15
+ Siehe auch das Paket-Docstring von :mod:`sdata.did`.
16
+
17
+ Referenzen:
18
+ * `RFC 7515 <https://www.rfc-editor.org/rfc/rfc7515>`_ – JWS
19
+ * `RFC 8037 <https://www.rfc-editor.org/rfc/rfc8037>`_ – EdDSA in JOSE
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ from typing import Any, Dict
25
+
26
+ from .eddsa import SigningKey, VerifyingKey, Ed25519, BadSignatureError
27
+
28
+ from .errors import EncodingError, VerificationError
29
+ from .utils_didkey import b64url, b64url_decode
30
+
31
+ __all__ = [
32
+ "b64url_json",
33
+ "protected_header",
34
+ "signing_key_from_jwk",
35
+ "verifying_key_from_jwk",
36
+ "sign_compact",
37
+ "decode_header",
38
+ "verify_compact",
39
+ ]
40
+
41
+
42
+ def b64url_json(obj: Any) -> str:
43
+ """Serialisiere ``obj`` als kompaktes JSON und kodiere base64url (ohne Padding)."""
44
+ raw = json.dumps(obj, separators=(",", ":"), ensure_ascii=False).encode()
45
+ return b64url(raw)
46
+
47
+
48
+ def protected_header(kid: str, typ: str, alg: str = "EdDSA") -> Dict[str, str]:
49
+ """Baue einen geschützten JWS-Header ``{alg, kid, typ}``."""
50
+ return {"alg": alg, "kid": kid, "typ": typ}
51
+
52
+
53
+ def signing_key_from_jwk(jwk_priv: Dict) -> SigningKey:
54
+ """Erzeuge einen Ed25519-``SigningKey`` aus einem privaten JWK (Feld ``d``).
55
+
56
+ Raises:
57
+ EncodingError: wenn das private JWK ungültig ist oder ``d`` fehlt.
58
+ """
59
+ if not isinstance(jwk_priv, dict) or "d" not in jwk_priv:
60
+ raise EncodingError("privater JWK fehlt das Feld 'd'")
61
+ if jwk_priv.get("kty") != "OKP" or jwk_priv.get("crv") != "Ed25519":
62
+ raise EncodingError("nur OKP/Ed25519 wird unterstützt")
63
+ raw = b64url_decode(jwk_priv["d"]) # 32 Byte Seed
64
+ return SigningKey.from_string(raw, curve=Ed25519)
65
+
66
+
67
+ def verifying_key_from_jwk(jwk_pub: Dict) -> VerifyingKey:
68
+ """Erzeuge einen Ed25519-``VerifyingKey`` aus einem öffentlichen JWK (Feld ``x``).
69
+
70
+ Raises:
71
+ EncodingError: wenn das öffentliche JWK ungültig ist oder ``x`` fehlt.
72
+ """
73
+ if not isinstance(jwk_pub, dict) or "x" not in jwk_pub:
74
+ raise EncodingError("öffentlicher JWK fehlt das Feld 'x'")
75
+ if jwk_pub.get("kty") != "OKP" or jwk_pub.get("crv") != "Ed25519":
76
+ raise EncodingError("nur OKP/Ed25519 wird unterstützt")
77
+ raw = b64url_decode(jwk_pub["x"])
78
+ return VerifyingKey.from_string(raw, curve=Ed25519)
79
+
80
+
81
+ def sign_compact(payload: Dict[str, Any], priv_jwk: Dict, kid: str, typ: str,
82
+ alg: str = "EdDSA") -> str:
83
+ """Signiere ``payload`` als Compact-JWS und gib ``header.payload.signature`` zurück."""
84
+ header_b64 = b64url_json(protected_header(kid, typ, alg))
85
+ payload_b64 = b64url_json(payload)
86
+ signing_input = "{}.{}".format(header_b64, payload_b64).encode()
87
+ sig = signing_key_from_jwk(priv_jwk).sign(signing_input)
88
+ return "{}.{}.{}".format(header_b64, payload_b64, b64url(sig))
89
+
90
+
91
+ def decode_header(compact: str) -> Dict[str, Any]:
92
+ """Dekodiere (ohne Verifikation) den geschützten Header eines Compact-JWS.
93
+
94
+ Raises:
95
+ EncodingError: wenn das JWS strukturell ungültig ist.
96
+ """
97
+ parts = compact.split(".")
98
+ if len(parts) != 3:
99
+ raise EncodingError("kein gültiges Compact-JWS (erwarte 3 Segmente)")
100
+ try:
101
+ return json.loads(b64url_decode(parts[0]).decode())
102
+ except (ValueError, UnicodeDecodeError) as exc:
103
+ raise EncodingError("JWS-Header nicht dekodierbar: {}".format(exc))
104
+
105
+
106
+ def verify_compact(compact: str, pub_jwk: Dict) -> Dict[str, Any]:
107
+ """Verifiziere ein Compact-JWS gegen ``pub_jwk`` und gib das Payload zurück.
108
+
109
+ Raises:
110
+ EncodingError: bei strukturell ungültigem JWS.
111
+ VerificationError: wenn die Signatur ungültig ist.
112
+ """
113
+ parts = compact.split(".")
114
+ if len(parts) != 3:
115
+ raise EncodingError("kein gültiges Compact-JWS (erwarte 3 Segmente)")
116
+ header_b64, payload_b64, sig_b64 = parts
117
+ signing_input = "{}.{}".format(header_b64, payload_b64).encode()
118
+ vk = verifying_key_from_jwk(pub_jwk)
119
+ try:
120
+ vk.verify(b64url_decode(sig_b64), signing_input)
121
+ except BadSignatureError:
122
+ raise VerificationError("Signatur ungültig")
123
+ try:
124
+ return json.loads(b64url_decode(payload_b64).decode())
125
+ except (ValueError, UnicodeDecodeError) as exc:
126
+ raise EncodingError("JWS-Payload nicht dekodierbar: {}".format(exc))
sdata/did/keys.py ADDED
@@ -0,0 +1,104 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Erzeugung und Verwaltung von Ed25519-Schlüsseln im JWK-Format.
3
+
4
+ Schlüssel werden als JWK (RFC 8037, ``kty=OKP``, ``crv=Ed25519``) gehalten:
5
+
6
+ * öffentlich: Feld ``x`` (raw 32 Byte, base64url),
7
+ * privat: zusätzlich Feld ``d`` (raw 32 Byte Seed, base64url – **geheim halten!**).
8
+
9
+ CLI (das ``did``-Modul ist als Subpackage organisiert, daher via ``-m``)::
10
+
11
+ python -m sdata.did.keys gen --out issuer_key.json
12
+ python -m sdata.did.keys pub --in issuer_key.json --out issuer_pub.json
13
+ python -m sdata.did.keys thumb --in issuer_pub.json
14
+
15
+ .. note::
16
+ ``gen`` schreibt einen **privaten** Schlüssel. Solche Dateien gehören nicht
17
+ ins Versionskontrollsystem (siehe ``.gitignore``).
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import logging
23
+ import argparse
24
+ from typing import Dict
25
+
26
+ from .eddsa import SigningKey, Ed25519
27
+
28
+ from .utils_didkey import b64url, jwk_thumbprint_rfc7638, did_key_from_jwk_ed25519
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+ __all__ = ["gen_ed25519_jwk", "pub_from_priv_jwk"]
33
+
34
+
35
+ def gen_ed25519_jwk() -> Dict[str, str]:
36
+ """Erzeuge ein frisches privates Ed25519-JWK (mit Feldern ``x`` und ``d``)."""
37
+ sk = SigningKey.generate(curve=Ed25519)
38
+ vk = sk.get_verifying_key()
39
+ return {
40
+ "kty": "OKP",
41
+ "crv": "Ed25519",
42
+ "x": b64url(vk.to_string()), # Public Key (raw 32 Byte)
43
+ "d": b64url(sk.to_string()), # Private Key (raw 32 Byte – geheim halten!)
44
+ }
45
+
46
+
47
+ def pub_from_priv_jwk(jwk_priv: Dict) -> Dict[str, str]:
48
+ """Leite den öffentlichen JWK (ohne ``d``) aus einem privaten JWK ab."""
49
+ return {"kty": jwk_priv["kty"], "crv": jwk_priv["crv"], "x": jwk_priv["x"]}
50
+
51
+
52
+ # --------------------------------------------------------------------------
53
+ # CLI
54
+ # --------------------------------------------------------------------------
55
+ def _cmd_gen(args: argparse.Namespace) -> None:
56
+ with open(args.out, "w") as fh:
57
+ json.dump(gen_ed25519_jwk(), fh, indent=2)
58
+ logger.info("✔ Privates JWK gespeichert: %s", args.out)
59
+
60
+
61
+ def _cmd_pub(args: argparse.Namespace) -> None:
62
+ with open(args.in_, "r") as fh:
63
+ jwk = json.load(fh)
64
+ with open(args.out, "w") as fh:
65
+ json.dump(pub_from_priv_jwk(jwk), fh, indent=2)
66
+ logger.info("✔ Öffentliches JWK gespeichert: %s", args.out)
67
+
68
+
69
+ def _cmd_thumb(args: argparse.Namespace) -> None:
70
+ with open(args.in_, "r") as fh:
71
+ jwk_pub = json.load(fh)
72
+ logger.info("%s", json.dumps({
73
+ "thumbprint_rfc7638": jwk_thumbprint_rfc7638(jwk_pub),
74
+ "did_key": did_key_from_jwk_ed25519(jwk_pub),
75
+ }, indent=2))
76
+
77
+
78
+ def _build_parser() -> argparse.ArgumentParser:
79
+ ap = argparse.ArgumentParser(description="Ed25519-JWK-Schlüsselverwaltung")
80
+ sub = ap.add_subparsers(required=True)
81
+
82
+ p = sub.add_parser("gen", help="Ed25519-Key erzeugen")
83
+ p.add_argument("--out", required=True)
84
+ p.set_defaults(func=_cmd_gen)
85
+
86
+ p = sub.add_parser("pub", help="Public-Part aus privatem JWK ableiten")
87
+ p.add_argument("--in", dest="in_", required=True)
88
+ p.add_argument("--out", required=True)
89
+ p.set_defaults(func=_cmd_pub)
90
+
91
+ p = sub.add_parser("thumb", help="RFC-7638-Thumbprint + did:key aus Public-JWK")
92
+ p.add_argument("--in", dest="in_", required=True)
93
+ p.set_defaults(func=_cmd_thumb)
94
+ return ap
95
+
96
+
97
+ def main(argv=None) -> None:
98
+ logging.basicConfig(level=logging.INFO, format="%(message)s")
99
+ args = _build_parser().parse_args(argv)
100
+ args.func(args)
101
+
102
+
103
+ if __name__ == "__main__":
104
+ main()
@@ -0,0 +1,161 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Hilfsfunktionen rund um ``did:key``, JWK und RFC-7638-Thumbprints (Ed25519).
3
+
4
+ Funktionsumfang:
5
+
6
+ * Base64url-Kodierung ohne Padding (RFC 7515 §2),
7
+ * RFC-7638-Thumbprint: kanonisches JSON ``{crv,kty,x}`` → SHA-256 → base64url,
8
+ * ``did:key`` für Ed25519 erzeugen (multicodec ``0xED 0x01`` + multibase ``z``-base58btc),
9
+ * ``did:key`` (bzw. nackte Multibase) zurück zu einem Public-JWK auflösen,
10
+ * konventionelle ``kid``-Bildung für ``did:web`` und ``did:key``.
11
+
12
+ Es werden ausschließlich **öffentliche** Schlüssel verarbeitet; private Schlüssel
13
+ tauchen hier nicht auf. Abhängigkeitsfrei (nur Standardbibliothek + die eigene
14
+ base58btc-Kodierung in :mod:`sdata.did.base58btc`).
15
+
16
+ Referenzen:
17
+ * `RFC 7638 <https://www.rfc-editor.org/rfc/rfc7638>`_ – JWK Thumbprint
18
+ * `did:key Method <https://w3c-ccg.github.io/did-method-key/>`_
19
+ * `Multicodec <https://github.com/multiformats/multicodec>`_ (ed25519-pub = 0xED)
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import base64
25
+ import hashlib
26
+ from typing import Dict
27
+
28
+ from .base58btc import b58encode, b58decode
29
+ from .errors import EncodingError
30
+
31
+ __all__ = [
32
+ "b64url",
33
+ "b64url_decode",
34
+ "jwk_thumbprint_rfc7638",
35
+ "did_key_from_jwk_ed25519",
36
+ "did_key_fragment_from_jwk",
37
+ "pub_jwk_from_did_key",
38
+ "kid_for_did_web",
39
+ "kid_for_did_key",
40
+ ]
41
+
42
+ # multicodec-Präfix (unsigned varint) für einen öffentlichen Ed25519-Schlüssel.
43
+ _ED25519_PUB_PREFIX = bytes([0xED, 0x01])
44
+
45
+
46
+ # --------------------------------------------------------------------------
47
+ # Base64url-Helfer (ohne Padding, RFC 7515)
48
+ # --------------------------------------------------------------------------
49
+ def b64url(data: bytes) -> str:
50
+ """Kodiere ``data`` als base64url ohne ``=``-Padding."""
51
+ return base64.urlsafe_b64encode(data).decode().rstrip("=")
52
+
53
+
54
+ def b64url_decode(s: str) -> bytes:
55
+ """Dekodiere einen base64url-String; fehlendes Padding wird ergänzt."""
56
+ pad = "=" * (-len(s) % 4)
57
+ return base64.urlsafe_b64decode(s + pad)
58
+
59
+
60
+ # --------------------------------------------------------------------------
61
+ # Validierung
62
+ # --------------------------------------------------------------------------
63
+ def _assert_ed25519_jwk_pub(jwk_pub: Dict) -> None:
64
+ """Stelle sicher, dass ``jwk_pub`` ein gültiger öffentlicher Ed25519-JWK ist.
65
+
66
+ Raises:
67
+ EncodingError: bei falschem Typ, ``kty``/``crv`` oder Schlüssellänge.
68
+ """
69
+ if not isinstance(jwk_pub, dict):
70
+ raise EncodingError("jwk_pub muss ein dict sein")
71
+ if jwk_pub.get("kty") != "OKP" or jwk_pub.get("crv") != "Ed25519":
72
+ raise EncodingError("nur OKP/Ed25519 wird unterstützt")
73
+ if "x" not in jwk_pub:
74
+ raise EncodingError("JWK fehlt das Feld 'x' (Public Key)")
75
+ raw = b64url_decode(jwk_pub["x"])
76
+ if len(raw) != 32:
77
+ raise EncodingError(
78
+ "Ed25519 Public Key muss 32 Byte sein (war {})".format(len(raw)))
79
+
80
+
81
+ def _strip_didkey_multibase(did_or_mb: str) -> str:
82
+ """Normalisiere ``did:key:z…``, ``#z…`` oder ``z…`` auf die nackte Multibase ``z…``."""
83
+ s = did_or_mb
84
+ if s.startswith("did:key:"):
85
+ s = s.split(":", 2)[2]
86
+ if s.startswith("#"):
87
+ s = s[1:]
88
+ return s
89
+
90
+
91
+ # --------------------------------------------------------------------------
92
+ # RFC-7638-Thumbprint
93
+ # --------------------------------------------------------------------------
94
+ def jwk_thumbprint_rfc7638(jwk_pub: Dict) -> str:
95
+ """Berechne den RFC-7638-Thumbprint eines öffentlichen Ed25519-JWK.
96
+
97
+ Kanonisiert wird über die Pflichtfelder ``{crv, kty, x}`` (lexikografisch
98
+ sortiert, ohne Whitespace); davon der SHA-256, base64url-kodiert.
99
+
100
+ Returns:
101
+ Der Thumbprint als base64url-String (ohne Padding).
102
+ """
103
+ _assert_ed25519_jwk_pub(jwk_pub)
104
+ data = {"crv": jwk_pub["crv"], "kty": jwk_pub["kty"], "x": jwk_pub["x"]}
105
+ canon = json.dumps(data, separators=(",", ":"), sort_keys=True).encode()
106
+ return b64url(hashlib.sha256(canon).digest())
107
+
108
+
109
+ # --------------------------------------------------------------------------
110
+ # did:key <-> JWK
111
+ # --------------------------------------------------------------------------
112
+ def did_key_from_jwk_ed25519(jwk_pub: Dict) -> str:
113
+ """Erzeuge eine ``did:key``-DID aus einem öffentlichen Ed25519-JWK."""
114
+ _assert_ed25519_jwk_pub(jwk_pub)
115
+ raw = b64url_decode(jwk_pub["x"]) # 32 Byte
116
+ mb = "z" + b58encode(_ED25519_PUB_PREFIX + raw)
117
+ return "did:key:{}".format(mb)
118
+
119
+
120
+ def did_key_fragment_from_jwk(jwk_pub: Dict) -> str:
121
+ """Liefere den Multibase-Teil (``z…``) – praktisch als ``kid``-Fragment."""
122
+ _assert_ed25519_jwk_pub(jwk_pub)
123
+ raw = b64url_decode(jwk_pub["x"])
124
+ return "z" + b58encode(_ED25519_PUB_PREFIX + raw)
125
+
126
+
127
+ def pub_jwk_from_did_key(did_key_or_mb: str) -> Dict[str, str]:
128
+ """Gewinne den öffentlichen JWK aus einer ``did:key``-DID oder Multibase zurück.
129
+
130
+ Akzeptiert ``did:key:z…``, ``#z…`` oder die nackte Multibase ``z…``.
131
+
132
+ Raises:
133
+ EncodingError: bei fehlendem ``z``-Präfix oder falschem multicodec.
134
+ """
135
+ mb = _strip_didkey_multibase(did_key_or_mb)
136
+ if not mb.startswith("z"):
137
+ raise EncodingError("did:key erwartet base58btc (z-Codec)")
138
+ raw = b58decode(mb[1:]) # 'z' entfernen
139
+ if not (len(raw) >= 34 and raw[0] == 0xED and raw[1] == 0x01):
140
+ raise EncodingError("unerwartetes multicodec-Präfix für ed25519-pub")
141
+ pubkey = raw[2:34]
142
+ return {"kty": "OKP", "crv": "Ed25519", "x": b64url(pubkey)}
143
+
144
+
145
+ # --------------------------------------------------------------------------
146
+ # kid-Helfer (konventionell)
147
+ # --------------------------------------------------------------------------
148
+ def kid_for_did_web(did_web: str, key_id: str = "key-1") -> str:
149
+ """``kid_for_did_web('did:web:example.com')`` → ``'did:web:example.com#key-1'``."""
150
+ return "{}#{}".format(did_web, key_id)
151
+
152
+
153
+ def kid_for_did_key(did_key: str, jwk_pub: Dict = None) -> str:
154
+ """Bilde die konventionelle ``kid`` ``did:key:<mb>#<mb>``.
155
+
156
+ Wird ``jwk_pub`` übergeben, wird das Fragment aus dem JWK abgeleitet,
157
+ andernfalls die Multibase aus ``did_key`` wiederverwendet.
158
+ """
159
+ mb = _strip_didkey_multibase(did_key)
160
+ frag = mb if not jwk_pub else did_key_fragment_from_jwk(jwk_pub)
161
+ return "did:key:{}#{}".format(mb, frag)