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.
- sdata/__init__.py +140 -0
- sdata/base.py +684 -0
- sdata/contrib/__init__.py +0 -0
- sdata/contrib/piexif/__init__.py +12 -0
- sdata/contrib/piexif/_common.py +94 -0
- sdata/contrib/piexif/_dump.py +346 -0
- sdata/contrib/piexif/_exceptions.py +2 -0
- sdata/contrib/piexif/_exif.py +638 -0
- sdata/contrib/piexif/_insert.py +60 -0
- sdata/contrib/piexif/_load.py +270 -0
- sdata/contrib/piexif/_remove.py +56 -0
- sdata/contrib/piexif/_transplant.py +45 -0
- sdata/contrib/piexif/_webp.py +240 -0
- sdata/contrib/piexif/helper.py +66 -0
- sdata/contrib/ranger/__init__.py +8 -0
- sdata/contrib/ranger/collections/__init__.py +0 -0
- sdata/contrib/ranger/collections/rangebucketmap.py +346 -0
- sdata/contrib/ranger/collections/rangemap.py +396 -0
- sdata/contrib/ranger/collections/rangeset.py +483 -0
- sdata/contrib/ranger/range/__init__.py +0 -0
- sdata/contrib/ranger/range/cut.py +285 -0
- sdata/contrib/ranger/range/range.py +644 -0
- sdata/contrib/sobol_seq.py +394 -0
- sdata/contrib/sortedcontainers/LICENSE +13 -0
- sdata/contrib/sortedcontainers/__init__.py +74 -0
- sdata/contrib/sortedcontainers/sorteddict.py +789 -0
- sdata/contrib/sortedcontainers/sortedlist.py +2646 -0
- sdata/contrib/sortedcontainers/sortedset.py +733 -0
- sdata/contrib/sqlitedict.py +598 -0
- sdata/contrib/timeflake/__init__.py +40 -0
- sdata/contrib/timeflake/flake.py +69 -0
- sdata/contrib/timeflake/utils.py +47 -0
- sdata/deprecated/__init__.py +10 -0
- sdata/deprecated/data.py +1503 -0
- sdata/did/__init__.py +129 -0
- sdata/did/_http.py +67 -0
- sdata/did/base58btc.py +49 -0
- sdata/did/did_github.py +152 -0
- sdata/did/did_peer4.py +155 -0
- sdata/did/did_web.py +222 -0
- sdata/did/diddoc.py +75 -0
- sdata/did/ed25519.py +225 -0
- sdata/did/eddsa.py +107 -0
- sdata/did/errors.py +40 -0
- sdata/did/issue_vc.py +193 -0
- sdata/did/jose.py +126 -0
- sdata/did/keys.py +104 -0
- sdata/did/utils_didkey.py +161 -0
- sdata/did/verify_vp.py +158 -0
- sdata/doe.py +101 -0
- sdata/dtypes.py +510 -0
- sdata/experiments/__init__.py +331 -0
- sdata/experiments/blob0.py +131 -0
- sdata/experiments/file.py +42 -0
- sdata/experiments/filegroup.py +144 -0
- sdata/experiments/image_old.py +313 -0
- sdata/experiments/ks2.py +15 -0
- sdata/experiments/lapshear.py +8 -0
- sdata/experiments/material.py +7 -0
- sdata/experiments/parameters_deprecated.py +394 -0
- sdata/experiments/workbook.py +226 -0
- sdata/format.py +125 -0
- sdata/imagemeta.py +533 -0
- sdata/interactive.py +118 -0
- sdata/iolib/__init__.py +51 -0
- sdata/iolib/hashtree.py +264 -0
- sdata/iolib/json1sqlitestore.py +554 -0
- sdata/iolib/jsonsqlitestore.py +704 -0
- sdata/iolib/pud.py +242 -0
- sdata/iolib/reader.py +262 -0
- sdata/iolib/writer.py +419 -0
- sdata/metadata.py +882 -0
- sdata/parameter.py +414 -0
- sdata/schema.py +273 -0
- sdata/sclass/__init__.py +111 -0
- sdata/sclass/bfo_classes.py +362 -0
- sdata/sclass/blob.py +362 -0
- sdata/sclass/content.py +87 -0
- sdata/sclass/dataframe.py +1251 -0
- sdata/sclass/dataframegroup.py +187 -0
- sdata/sclass/dependency_graph.py +85 -0
- sdata/sclass/filereference.py +124 -0
- sdata/sclass/image.py +218 -0
- sdata/sclass/lazy_namespace.py +155 -0
- sdata/sclass/process.py +383 -0
- sdata/sclass/process_graph.py +221 -0
- sdata/sclass/zipfile.py +135 -0
- sdata/semantic.py +346 -0
- sdata/suuid.py +136 -0
- sdata/timestamp.py +336 -0
- sdata/tools.py +14 -0
- sdata/units.py +602 -0
- sdata/vocab.py +149 -0
- sdata-1.5.0.dist-info/METADATA +264 -0
- sdata-1.5.0.dist-info/RECORD +98 -0
- sdata-1.5.0.dist-info/WHEEL +5 -0
- sdata-1.5.0.dist-info/licenses/LICENSE-MIT +21 -0
- sdata-1.5.0.dist-info/top_level.txt +1 -0
sdata/did/__init__.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""``sdata.did`` – Decentralized Identifiers (DID) & Verifiable Credentials (VC).
|
|
3
|
+
|
|
4
|
+
Dieses Subpackage bündelt eine schlanke, **pure-Python** Umsetzung gängiger
|
|
5
|
+
SSI-Bausteine (Self-Sovereign Identity) rund um Ed25519:
|
|
6
|
+
|
|
7
|
+
============================ ====================================================
|
|
8
|
+
Bereich Modul / Funktionen
|
|
9
|
+
============================ ====================================================
|
|
10
|
+
Schlüssel (Ed25519/JWK) :mod:`~sdata.did.keys` – :func:`gen_ed25519_jwk`, :func:`pub_from_priv_jwk`
|
|
11
|
+
JWK / did:key / Thumbprint :mod:`~sdata.did.utils_didkey`
|
|
12
|
+
JOSE / Compact-JWS (EdDSA) :mod:`~sdata.did.jose` – :func:`sign_compact`, :func:`verify_compact`
|
|
13
|
+
VC / VP ausstellen :mod:`~sdata.did.issue_vc` – :func:`issue_vc`, :func:`make_vp`
|
|
14
|
+
VC / VP verifizieren :mod:`~sdata.did.verify_vp` – :func:`verify_presentation`
|
|
15
|
+
did:web :mod:`~sdata.did.did_web` – :func:`build_did_document`, :func:`resolve_did_web`
|
|
16
|
+
did:key :mod:`~sdata.did.utils_didkey` – :func:`did_key_from_jwk_ed25519`, :func:`pub_jwk_from_did_key`
|
|
17
|
+
did:peer:4 :mod:`~sdata.did.did_peer4` – :func:`did_peer4_from_payload`, :func:`resolve_long_form`
|
|
18
|
+
did:github :mod:`~sdata.did.did_github` – :func:`resolve_did_github`
|
|
19
|
+
============================ ====================================================
|
|
20
|
+
|
|
21
|
+
Unterstützte Standards: W3C DID Core, did:web, did:key, did:peer:4,
|
|
22
|
+
W3C Verifiable Credentials (JWT-VC), JOSE/JWS (RFC 7515), EdDSA (RFC 8037),
|
|
23
|
+
JWK-Thumbprint (RFC 7638).
|
|
24
|
+
|
|
25
|
+
Installation
|
|
26
|
+
------------
|
|
27
|
+
Krypto (Ed25519) und base58btc sind **abhängigkeitsfrei** (reine
|
|
28
|
+
Standardbibliothek) – siehe :mod:`~sdata.did.eddsa` und
|
|
29
|
+
:mod:`~sdata.did.base58btc`. Auch die HTTP-Auflösung von ``did:web`` /
|
|
30
|
+
``did:github`` läuft ohne externe Abhängigkeit: :mod:`~sdata.did._http` nutzt
|
|
31
|
+
``requests`` falls installiert, sonst ``urllib`` (Standardbibliothek)::
|
|
32
|
+
|
|
33
|
+
pip install "sdata" # voll funktionsfähig (urllib-HTTP-Backend)
|
|
34
|
+
pip install "sdata[http]" # optional: 'requests' als HTTP-Backend
|
|
35
|
+
|
|
36
|
+
.. warning::
|
|
37
|
+
**Sicherheit / SOTA-Hinweis.** Der produktive Krypto-Pfad nutzt die
|
|
38
|
+
pure-Python-Ed25519-Implementierung in :mod:`sdata.did.ed25519` (über den
|
|
39
|
+
Adapter :mod:`sdata.did.eddsa`). Sie ist abhängigkeitsfrei, aber **nicht
|
|
40
|
+
garantiert constant-time** und damit potenziell anfällig für
|
|
41
|
+
Timing-Seitenkanäle – genau wie das zuvor genutzte ``python-ecdsa``
|
|
42
|
+
(ebenfalls pure Python). Für hochsensible Umgebungen ein libsodium-Backend
|
|
43
|
+
(PyNaCl) oder ``cryptography`` (OpenSSL) erwägen. Korrektheit ist gegen die
|
|
44
|
+
offiziellen RFC-8032-Testvektoren abgesichert.
|
|
45
|
+
|
|
46
|
+
Beispiel
|
|
47
|
+
--------
|
|
48
|
+
::
|
|
49
|
+
|
|
50
|
+
from sdata.did import (gen_ed25519_jwk, pub_from_priv_jwk,
|
|
51
|
+
did_key_from_jwk_ed25519, sign_compact, verify_compact)
|
|
52
|
+
|
|
53
|
+
jwk = gen_ed25519_jwk()
|
|
54
|
+
did = did_key_from_jwk_ed25519(pub_from_priv_jwk(jwk))
|
|
55
|
+
kid = f"{did}#{did.split(':', 2)[2]}"
|
|
56
|
+
jws = sign_compact({"hello": "world"}, jwk, kid=kid, typ="JWT")
|
|
57
|
+
assert verify_compact(jws, pub_from_priv_jwk(jwk))["hello"] == "world"
|
|
58
|
+
"""
|
|
59
|
+
from __future__ import annotations
|
|
60
|
+
|
|
61
|
+
import importlib
|
|
62
|
+
|
|
63
|
+
# Lazy-Loading (PEP 562): Submodule werden erst beim Zugriff importiert. Vorteile:
|
|
64
|
+
# * ``python -m sdata.did.<modul>`` löst keine runpy-Doppelimport-Warnung aus,
|
|
65
|
+
# * ``import sdata.did`` ist leichtgewichtig – schwerere Submodule (z. B.
|
|
66
|
+
# ``did_web`` mit dem HTTP-Backend) werden erst geladen, wenn eine sie
|
|
67
|
+
# nutzende Funktion tatsächlich angefasst wird.
|
|
68
|
+
# Mapping: exportierter Name -> "submodul" oder ("submodul", "originalname").
|
|
69
|
+
_EXPORTS = {
|
|
70
|
+
# Fehler
|
|
71
|
+
"DidError": "errors", "EncodingError": "errors",
|
|
72
|
+
"ResolutionError": "errors", "VerificationError": "errors",
|
|
73
|
+
# Schlüssel / JWK / did:key
|
|
74
|
+
"gen_ed25519_jwk": "keys", "pub_from_priv_jwk": "keys",
|
|
75
|
+
"b64url": "utils_didkey", "b64url_decode": "utils_didkey",
|
|
76
|
+
"jwk_thumbprint_rfc7638": "utils_didkey",
|
|
77
|
+
"did_key_from_jwk_ed25519": "utils_didkey",
|
|
78
|
+
"did_key_fragment_from_jwk": "utils_didkey",
|
|
79
|
+
"pub_jwk_from_did_key": "utils_didkey",
|
|
80
|
+
"kid_for_did_web": "utils_didkey", "kid_for_did_key": "utils_didkey",
|
|
81
|
+
# JOSE / JWS
|
|
82
|
+
"sign_compact": "jose", "verify_compact": "jose", "decode_header": "jose",
|
|
83
|
+
# VC / VP
|
|
84
|
+
"issue_vc": "issue_vc", "make_vp": "issue_vc", "sign_compact_jws": "issue_vc",
|
|
85
|
+
"verify_presentation": "verify_vp", "jws_header": "verify_vp",
|
|
86
|
+
"jws_verify": "verify_vp", "resolve_public_jwk_for_kid": "verify_vp",
|
|
87
|
+
# DID-Methoden
|
|
88
|
+
"build_did_document": "did_web", "did_web_to_url": "did_web",
|
|
89
|
+
"resolve_did_web": ("did_web", "resolve_public_jwk_for_kid"),
|
|
90
|
+
"did_peer4_from_payload": "did_peer4", "resolve_long_form": "did_peer4",
|
|
91
|
+
"resolve_did_github": "did_github", "parse_did_github": "did_github",
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def __getattr__(name):
|
|
96
|
+
"""Importiere das zugehörige Submodul beim ersten Zugriff (PEP 562)."""
|
|
97
|
+
target = _EXPORTS.get(name)
|
|
98
|
+
if target is None:
|
|
99
|
+
raise AttributeError(
|
|
100
|
+
"module {!r} has no attribute {!r}".format(__name__, name))
|
|
101
|
+
modname, attrname = target if isinstance(target, tuple) else (target, name)
|
|
102
|
+
mod = importlib.import_module("." + modname, __name__)
|
|
103
|
+
value = getattr(mod, attrname)
|
|
104
|
+
globals()[name] = value # cachen, damit __getattr__ nur einmal feuert
|
|
105
|
+
return value
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def __dir__():
|
|
109
|
+
return sorted(__all__)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
__all__ = [
|
|
113
|
+
# Fehler
|
|
114
|
+
"DidError", "EncodingError", "ResolutionError", "VerificationError",
|
|
115
|
+
# Schlüssel / JWK / did:key
|
|
116
|
+
"gen_ed25519_jwk", "pub_from_priv_jwk",
|
|
117
|
+
"b64url", "b64url_decode", "jwk_thumbprint_rfc7638",
|
|
118
|
+
"did_key_from_jwk_ed25519", "did_key_fragment_from_jwk", "pub_jwk_from_did_key",
|
|
119
|
+
"kid_for_did_web", "kid_for_did_key",
|
|
120
|
+
# JOSE / JWS
|
|
121
|
+
"sign_compact", "verify_compact", "decode_header",
|
|
122
|
+
# VC / VP
|
|
123
|
+
"issue_vc", "make_vp", "sign_compact_jws",
|
|
124
|
+
"verify_presentation", "resolve_public_jwk_for_kid", "jws_header", "jws_verify",
|
|
125
|
+
# DID-Methoden
|
|
126
|
+
"build_did_document", "did_web_to_url", "resolve_did_web",
|
|
127
|
+
"did_peer4_from_payload", "resolve_long_form",
|
|
128
|
+
"resolve_did_github", "parse_did_github",
|
|
129
|
+
]
|
sdata/did/_http.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""HTTP-GET mit optionalem ``requests``-Backend und ``urllib``-Fallback.
|
|
3
|
+
|
|
4
|
+
Wenn ``requests`` installiert ist, wird es genutzt (gewohntes Verhalten,
|
|
5
|
+
Connection-Pooling, certifi-CA-Bundle); andernfalls greift ein Fallback auf
|
|
6
|
+
``urllib.request`` aus der Standardbibliothek. **Die TLS-Zertifikatsprüfung
|
|
7
|
+
bleibt in beiden Fällen aktiv** (kein ``verify=False``).
|
|
8
|
+
|
|
9
|
+
Damit ist ``requests`` für ``sdata.did`` rein optional – das Subpackage bleibt
|
|
10
|
+
auch ohne die Abhängigkeit voll funktionsfähig.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import ssl
|
|
15
|
+
import urllib.request
|
|
16
|
+
import urllib.error
|
|
17
|
+
from typing import Dict, Optional, Tuple
|
|
18
|
+
|
|
19
|
+
try: # bevorzugtes Backend, falls installiert
|
|
20
|
+
import requests as _requests
|
|
21
|
+
except ImportError: # pragma: no cover - abhängig vom Environment
|
|
22
|
+
_requests = None
|
|
23
|
+
|
|
24
|
+
__all__ = ["http_get", "HttpError"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class HttpError(Exception):
|
|
28
|
+
"""Transportfehler (DNS/Timeout/Verbindung) beim HTTP-GET – backend-neutral.
|
|
29
|
+
|
|
30
|
+
HTTP-Statuscodes (auch 4xx/5xx) sind *keine* ``HttpError`` – sie werden von
|
|
31
|
+
:func:`http_get` als ``status_code`` zurückgegeben.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def http_get(url: str, headers: Optional[Dict[str, str]] = None,
|
|
36
|
+
timeout: float = 10.0) -> Tuple[int, str]:
|
|
37
|
+
"""GET ``url`` und liefere ``(status_code, body_text)``.
|
|
38
|
+
|
|
39
|
+
Nutzt ``requests`` falls verfügbar, sonst ``urllib.request``.
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
HttpError: bei Transportfehlern (DNS, Timeout, Verbindungsabbruch).
|
|
43
|
+
"""
|
|
44
|
+
if _requests is not None:
|
|
45
|
+
return _get_requests(url, headers, timeout)
|
|
46
|
+
return _get_urllib(url, headers, timeout)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _get_requests(url, headers, timeout):
|
|
50
|
+
try:
|
|
51
|
+
resp = _requests.get(url, headers=headers, timeout=timeout)
|
|
52
|
+
except _requests.RequestException as exc:
|
|
53
|
+
raise HttpError(str(exc)) from exc
|
|
54
|
+
return resp.status_code, resp.text
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _get_urllib(url, headers, timeout):
|
|
58
|
+
req = urllib.request.Request(url, headers=headers or {})
|
|
59
|
+
ctx = ssl.create_default_context() # Zertifikats-/Hostname-Prüfung aktiv
|
|
60
|
+
try:
|
|
61
|
+
with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
|
|
62
|
+
charset = resp.headers.get_content_charset() or "utf-8"
|
|
63
|
+
return resp.status, resp.read().decode(charset, "replace")
|
|
64
|
+
except urllib.error.HTTPError as exc: # 4xx/5xx: Status, kein Transportfehler
|
|
65
|
+
return exc.code, exc.read().decode("utf-8", "replace")
|
|
66
|
+
except urllib.error.URLError as exc: # DNS/Timeout/Verbindung
|
|
67
|
+
raise HttpError(str(exc)) from exc
|
sdata/did/base58btc.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""base58btc-Kodierung (Bitcoin-Alphabet) – reine Standardbibliothek.
|
|
3
|
+
|
|
4
|
+
Ersetzt die externe ``base58``-Abhängigkeit im ``did``-Subpackage. Die
|
|
5
|
+
Funktionen sind bewusst klein und an die multibase-/``did:key``-Nutzung
|
|
6
|
+
angepasst (führende Nullbytes bleiben als ``1`` erhalten).
|
|
7
|
+
|
|
8
|
+
Referenz: https://datatracker.ietf.org/doc/html/draft-msporny-base58
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
__all__ = ["b58encode", "b58decode"]
|
|
13
|
+
|
|
14
|
+
# Bitcoin-/IPFS-Alphabet (ohne 0, O, I, l)
|
|
15
|
+
_B58_ALPHABET = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
|
16
|
+
_B58_INDEX = {ch: i for i, ch in enumerate(_B58_ALPHABET)}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def b58encode(b: bytes) -> str:
|
|
20
|
+
"""Kodiere Bytes als base58btc (führende Nullbytes bleiben als ``1`` erhalten)."""
|
|
21
|
+
n = int.from_bytes(b, "big", signed=False)
|
|
22
|
+
out = bytearray()
|
|
23
|
+
while n > 0:
|
|
24
|
+
n, r = divmod(n, 58)
|
|
25
|
+
out.append(_B58_ALPHABET[r])
|
|
26
|
+
pad = 0
|
|
27
|
+
for byte in b:
|
|
28
|
+
if byte == 0:
|
|
29
|
+
pad += 1
|
|
30
|
+
else:
|
|
31
|
+
break
|
|
32
|
+
return ("1" * pad) + out[::-1].decode()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def b58decode(s: str) -> bytes:
|
|
36
|
+
"""Dekodiere einen base58btc-String zurück zu Bytes.
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
ValueError: bei Zeichen ausserhalb des base58-Alphabets.
|
|
40
|
+
"""
|
|
41
|
+
n = 0
|
|
42
|
+
for ch in s.encode():
|
|
43
|
+
try:
|
|
44
|
+
n = n * 58 + _B58_INDEX[ch]
|
|
45
|
+
except KeyError:
|
|
46
|
+
raise ValueError("ungültiges base58-Zeichen: {!r}".format(chr(ch)))
|
|
47
|
+
pad = len(s) - len(s.lstrip("1"))
|
|
48
|
+
full = n.to_bytes((n.bit_length() + 7) // 8, "big") if n else b""
|
|
49
|
+
return b"\x00" * pad + full
|
sdata/did/did_github.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""``did:github``: DID-Dokumente aus GitHub-Repositories auflösen.
|
|
3
|
+
|
|
4
|
+
Eine ``did:github``-DID verweist auf ein ``did.json`` in einem GitHub-Repo und
|
|
5
|
+
wird über ``raw.githubusercontent.com`` geladen. Formen::
|
|
6
|
+
|
|
7
|
+
did:github:<user>:<repo>
|
|
8
|
+
did:github:<user>:<repo>:<ref>
|
|
9
|
+
did:github:<user>:<repo>:<ref>:<subpath>
|
|
10
|
+
|
|
11
|
+
Im ``subpath`` darf ``__`` als ``/``-Ersatz verwendet werden (CLI-freundlich).
|
|
12
|
+
Gesucht werden ``<subpath>/.well-known/did.json``, ``<subpath>/did.json`` sowie
|
|
13
|
+
die Repo-Wurzel – in dieser Reihenfolge, für die Refs ``<ref>``, ``main``, ``master``.
|
|
14
|
+
|
|
15
|
+
Ein optionales ``GITHUB_TOKEN`` (Umgebungsvariable) erhöht das Rate-Limit und
|
|
16
|
+
erlaubt Zugriff auf private Repos.
|
|
17
|
+
|
|
18
|
+
.. note::
|
|
19
|
+
``did:github`` ist **keine** registrierte W3C-DID-Methode, sondern eine
|
|
20
|
+
pragmatische, projektspezifische Auflösung über GitHub-Rohinhalte.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
import re
|
|
26
|
+
import json
|
|
27
|
+
import logging
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
from functools import lru_cache
|
|
30
|
+
from typing import List, Optional, Tuple
|
|
31
|
+
|
|
32
|
+
from ._http import http_get
|
|
33
|
+
from .errors import EncodingError, ResolutionError
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
__all__ = ["DidGithub", "parse_did_github", "resolve_did_github"]
|
|
38
|
+
|
|
39
|
+
DID_GITHUB_METHOD = "did:github"
|
|
40
|
+
RAW_BASE = "https://raw.githubusercontent.com"
|
|
41
|
+
HTTP_TIMEOUT = 10
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class DidGithub:
|
|
46
|
+
"""Geparste ``did:github``-Identität."""
|
|
47
|
+
user: str
|
|
48
|
+
repo: str
|
|
49
|
+
ref: Optional[str] = None # z. B. "main", "master", Tag oder Commit
|
|
50
|
+
subpath: Optional[str] = None # optionaler Unterordner im Repo
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def did(self) -> str:
|
|
54
|
+
parts = [DID_GITHUB_METHOD, self.user, self.repo]
|
|
55
|
+
if self.ref:
|
|
56
|
+
parts.append(self.ref)
|
|
57
|
+
if self.subpath:
|
|
58
|
+
parts.append(self.subpath)
|
|
59
|
+
return ":".join(parts)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def make_subpath(raw: Optional[str]) -> Optional[str]:
|
|
63
|
+
"""Mappe ``__`` auf ``/`` (echte Slashes bleiben erhalten)."""
|
|
64
|
+
if not raw:
|
|
65
|
+
return None
|
|
66
|
+
return raw.replace("__", "/")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def parse_did_github(did: str) -> DidGithub:
|
|
70
|
+
"""Parse eine ``did:github``-DID.
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
EncodingError: wenn ``did`` keine gültige ``did:github``-DID ist.
|
|
74
|
+
"""
|
|
75
|
+
if not did.startswith(DID_GITHUB_METHOD + ":"):
|
|
76
|
+
raise EncodingError("kein did:github-Identifier")
|
|
77
|
+
parts = did.split(":")
|
|
78
|
+
if len(parts) < 4:
|
|
79
|
+
raise EncodingError("Form: did:github:<user>:<repo>[:<ref>[:<subpath>]]")
|
|
80
|
+
_, _, user, repo, *rest = parts
|
|
81
|
+
ref = rest[0] if rest else None
|
|
82
|
+
subpath = make_subpath(":".join(rest[1:]) if len(rest) > 1 else None)
|
|
83
|
+
return DidGithub(user=user, repo=repo, ref=ref, subpath=subpath)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def candidate_paths(dg: DidGithub) -> List[Tuple[str, str]]:
|
|
87
|
+
"""Geordnete ``(ref, path)``-Kandidaten für die Auflösung."""
|
|
88
|
+
well_known, root = ".well-known/did.json", "did.json"
|
|
89
|
+
sub_candidates = []
|
|
90
|
+
if dg.subpath:
|
|
91
|
+
base = dg.subpath.rstrip("/")
|
|
92
|
+
sub_candidates = ["{}/{}".format(base, well_known), "{}/{}".format(base, root)]
|
|
93
|
+
paths = sub_candidates + [well_known, root]
|
|
94
|
+
refs = [r for r in [dg.ref, "main", "master"] if r]
|
|
95
|
+
return [(r, p) for r in refs for p in paths]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _raw_url(user: str, repo: str, ref: str, path: str) -> str:
|
|
99
|
+
return "{}/{}/{}/{}/{}".format(RAW_BASE, user, repo, ref, path)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _headers() -> dict:
|
|
103
|
+
headers = {"Accept": "application/json"}
|
|
104
|
+
token = os.getenv("GITHUB_TOKEN")
|
|
105
|
+
if token:
|
|
106
|
+
headers["Authorization"] = "Bearer {}".format(token)
|
|
107
|
+
return headers
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _strip_json_comments(text: str) -> str:
|
|
111
|
+
"""Entferne ``/* … */`` und ``// …`` (rudimentär, für einfache Fälle)."""
|
|
112
|
+
text = re.sub(r"/\*.*?\*/", "", text, flags=re.S)
|
|
113
|
+
text = re.sub(r"//.*?$", "", text, flags=re.M)
|
|
114
|
+
return text
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@lru_cache(maxsize=256)
|
|
118
|
+
def fetch_json(url: str) -> Optional[dict]:
|
|
119
|
+
"""Lade JSON von ``url`` (mit kleinem Cache); ``None`` bei Status != 200."""
|
|
120
|
+
status, text = http_get(url, headers=_headers(), timeout=HTTP_TIMEOUT)
|
|
121
|
+
if status != 200:
|
|
122
|
+
return None
|
|
123
|
+
try:
|
|
124
|
+
return json.loads(text)
|
|
125
|
+
except json.JSONDecodeError:
|
|
126
|
+
return json.loads(_strip_json_comments(text))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def resolve_did_github(did: str) -> dict:
|
|
130
|
+
"""Löse eine ``did:github``-DID zum DID-Dokument auf.
|
|
131
|
+
|
|
132
|
+
Raises:
|
|
133
|
+
EncodingError: bei ungültiger DID.
|
|
134
|
+
ResolutionError: wenn kein gültiges ``did.json`` gefunden wird.
|
|
135
|
+
EncodingError: wenn ein gefundenes Dokument ``@context``/``id`` vermisst.
|
|
136
|
+
"""
|
|
137
|
+
dg = parse_did_github(did)
|
|
138
|
+
tried = []
|
|
139
|
+
for ref, path in candidate_paths(dg):
|
|
140
|
+
url = _raw_url(dg.user, dg.repo, ref, path)
|
|
141
|
+
tried.append(url)
|
|
142
|
+
doc = fetch_json(url)
|
|
143
|
+
if doc is None:
|
|
144
|
+
continue
|
|
145
|
+
if "@context" not in doc:
|
|
146
|
+
raise EncodingError("DID-Dokument ohne @context ({})".format(url))
|
|
147
|
+
if "id" not in doc:
|
|
148
|
+
raise EncodingError("DID-Dokument ohne id ({})".format(url))
|
|
149
|
+
logger.debug("did:github aufgelöst via %s", url)
|
|
150
|
+
return doc
|
|
151
|
+
raise ResolutionError(
|
|
152
|
+
"did:github konnte nicht aufgelöst werden. Versucht:\n- " + "\n- ".join(tried))
|
sdata/did/did_peer4.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""``did:peer:4``: statische, self-zertifizierende Peer-DIDs (Long-/Short-Form).
|
|
3
|
+
|
|
4
|
+
Diese Implementierung ist bewusst **abhängigkeitsfrei** (nur Standardbibliothek)
|
|
5
|
+
und bringt eine eigene base58btc- und varint-Kodierung mit – passend zum
|
|
6
|
+
pure-Python-Designziel des Subpackages.
|
|
7
|
+
|
|
8
|
+
Aufbau einer Long-Form::
|
|
9
|
+
|
|
10
|
+
did:peer:4{hash}:{encoded_document}
|
|
11
|
+
|
|
12
|
+
* ``encoded_document`` = multibase(base58btc, multicodec(JSON) + kanonisches JSON),
|
|
13
|
+
* ``hash`` = multibase(base58btc, multihash(sha2-256, encoded_document)).
|
|
14
|
+
|
|
15
|
+
Die Short-Form ``did:peer:4{hash}`` ist self-zertifizierend: beim Auflösen der
|
|
16
|
+
Long-Form wird der Hash gegen das eingebettete Dokument validiert.
|
|
17
|
+
|
|
18
|
+
Referenzen:
|
|
19
|
+
* `did:peer Method, Variante 4 <https://identity.foundation/peer-did-method-spec/>`_
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import hashlib
|
|
25
|
+
from typing import Dict, Tuple
|
|
26
|
+
|
|
27
|
+
from .errors import EncodingError, VerificationError
|
|
28
|
+
# base58btc liegt jetzt zentral im eigenen Modul (frühere Inline-Variante);
|
|
29
|
+
# Re-Export aus Kompatibilitätsgründen.
|
|
30
|
+
from .base58btc import b58encode, b58decode
|
|
31
|
+
|
|
32
|
+
__all__ = ["did_peer4_from_payload", "resolve_long_form", "b58encode", "b58decode"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# --- Varint (LEB128, wie in multicodec) -----------------------------------
|
|
36
|
+
def varint_encode(n: int) -> bytes:
|
|
37
|
+
"""Kodiere ``n`` als unsigned LEB128-varint."""
|
|
38
|
+
out = bytearray()
|
|
39
|
+
while True:
|
|
40
|
+
to_write = n & 0x7F
|
|
41
|
+
n >>= 7
|
|
42
|
+
if n:
|
|
43
|
+
out.append(to_write | 0x80)
|
|
44
|
+
else:
|
|
45
|
+
out.append(to_write)
|
|
46
|
+
break
|
|
47
|
+
return bytes(out)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def varint_decode(b: bytes) -> Tuple[int, int]:
|
|
51
|
+
"""Dekodiere einen unsigned LEB128-varint; gibt ``(wert, gelesene_bytes)`` zurück."""
|
|
52
|
+
shift = 0
|
|
53
|
+
value = 0
|
|
54
|
+
for i, byte in enumerate(b):
|
|
55
|
+
value |= (byte & 0x7F) << shift
|
|
56
|
+
if not (byte & 0x80):
|
|
57
|
+
return value, i + 1
|
|
58
|
+
shift += 7
|
|
59
|
+
raise EncodingError("ungültiger varint")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# --- Multicodec- & Multihash-Konstanten -----------------------------------
|
|
63
|
+
MULTICODEC_JSON = 0x0200 # multicodec-Code für JSON
|
|
64
|
+
MH_SHA256 = 0x12 # multihash: sha2-256
|
|
65
|
+
MH_SHA256_LEN = 32
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _canonical_json_bytes(payload: Dict) -> bytes:
|
|
69
|
+
"""Deterministische JSON-Serialisierung (Keys sortiert, ohne Whitespace)."""
|
|
70
|
+
s = json.dumps(payload, separators=(",", ":"), sort_keys=True, ensure_ascii=False)
|
|
71
|
+
return s.encode("utf-8")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def encode_document(payload: Dict) -> bytes:
|
|
75
|
+
"""Multicodec-Framing: ``varint(JSON-Code) + kanonisches JSON``."""
|
|
76
|
+
return varint_encode(MULTICODEC_JSON) + _canonical_json_bytes(payload)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def multibase_b58btc(data: bytes) -> str:
|
|
80
|
+
"""Präfix ``z`` (base58btc) gemäß Multibase."""
|
|
81
|
+
return "z" + b58encode(data)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def multihash_sha256(data: bytes) -> bytes:
|
|
85
|
+
"""``multihash(sha2-256, data)`` = ``0x12 0x20 || sha256(data)``."""
|
|
86
|
+
return bytes([MH_SHA256, MH_SHA256_LEN]) + hashlib.sha256(data).digest()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# --- did:peer:4 Erzeugung & Auflösung -------------------------------------
|
|
90
|
+
def did_peer4_from_payload(payload: Dict) -> Tuple[str, str, Dict]:
|
|
91
|
+
"""Erzeuge Long-Form, Short-Form und kontextualisiertes DID-Dokument.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
payload: DID-Dokument **ohne** ``id`` (relative Fragmente wie ``#key-0``
|
|
95
|
+
sind erlaubt).
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
``(long_form, short_form, did_document)``.
|
|
99
|
+
"""
|
|
100
|
+
encoded_doc_bytes = encode_document(payload)
|
|
101
|
+
encoded_doc_mb = multibase_b58btc(encoded_doc_bytes)
|
|
102
|
+
mh_mb = multibase_b58btc(multihash_sha256(encoded_doc_bytes))
|
|
103
|
+
|
|
104
|
+
long_form = "did:peer:4{}:{}".format(mh_mb, encoded_doc_mb)
|
|
105
|
+
short_form = "did:peer:4{}".format(mh_mb)
|
|
106
|
+
did_doc = _contextualize_did_doc(payload, did_long=long_form, did_short=short_form)
|
|
107
|
+
return long_form, short_form, did_doc
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def resolve_long_form(did_long: str) -> Dict:
|
|
111
|
+
"""Löse eine Long-Form ``did:peer:4`` auf und validiere den Hash.
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
EncodingError: bei strukturell ungültiger DID oder falschem multicodec/multihash.
|
|
115
|
+
VerificationError: wenn der eingebettete Hash nicht zum Dokument passt.
|
|
116
|
+
"""
|
|
117
|
+
if not did_long.startswith("did:peer:4"):
|
|
118
|
+
raise EncodingError("keine did:peer:4")
|
|
119
|
+
try:
|
|
120
|
+
_, rest = did_long.split("did:peer:4", 1)
|
|
121
|
+
hash_mb, doc_mb = rest.split(":", 1)
|
|
122
|
+
except ValueError:
|
|
123
|
+
raise EncodingError("erwarte Long-Form: did:peer:4{hash}:{doc}")
|
|
124
|
+
|
|
125
|
+
if not (hash_mb.startswith("z") and doc_mb.startswith("z")):
|
|
126
|
+
raise EncodingError("erwarte multibase base58btc ('z'-Präfix)")
|
|
127
|
+
|
|
128
|
+
mh = b58decode(hash_mb[1:])
|
|
129
|
+
encoded_doc_bytes = b58decode(doc_mb[1:])
|
|
130
|
+
|
|
131
|
+
code, offset = varint_decode(encoded_doc_bytes)
|
|
132
|
+
if code != MULTICODEC_JSON:
|
|
133
|
+
raise EncodingError("multicodec-Code ist nicht JSON (0x0200)")
|
|
134
|
+
json_bytes = encoded_doc_bytes[offset:]
|
|
135
|
+
|
|
136
|
+
if len(mh) < 2 or mh[0] != MH_SHA256 or mh[1] != MH_SHA256_LEN:
|
|
137
|
+
raise EncodingError("multihash ist nicht sha2-256/32")
|
|
138
|
+
if mh[2:] != hashlib.sha256(encoded_doc_bytes).digest():
|
|
139
|
+
raise VerificationError("Hash-Validierung fehlgeschlagen")
|
|
140
|
+
|
|
141
|
+
payload = json.loads(json_bytes.decode("utf-8"))
|
|
142
|
+
did_short = "did:peer:4{}".format(multibase_b58btc(mh))
|
|
143
|
+
return _contextualize_did_doc(payload, did_long=did_long, did_short=did_short)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _contextualize_did_doc(payload: Dict, *, did_long: str, did_short: str) -> Dict:
|
|
147
|
+
"""Setze ``id``/``alsoKnownAs`` und ergänze fehlende ``controller``-Felder."""
|
|
148
|
+
doc = json.loads(json.dumps(payload)) # tiefe Kopie
|
|
149
|
+
doc["id"] = did_long
|
|
150
|
+
doc.setdefault("alsoKnownAs", [])
|
|
151
|
+
if did_short not in doc["alsoKnownAs"]:
|
|
152
|
+
doc["alsoKnownAs"].append(did_short)
|
|
153
|
+
for vm in doc.get("verificationMethod", []):
|
|
154
|
+
vm.setdefault("controller", did_long)
|
|
155
|
+
return doc
|