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/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)
|