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/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."""
|