Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions sdata/did/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# sdata.did — Decentralized Identifiers & Verifiable Credentials

Schlanke, **pure-Python** Bausteine für Self-Sovereign Identity (SSI) rund um
Ed25519: Schlüssel als JWK, Compact-JWS (JOSE/EdDSA), Verifiable Credentials &
Presentations sowie mehrere DID-Methoden.

## Installation

Die DID-Funktionen brauchen zwei optionale Abhängigkeiten:

```bash
pip install "sdata[did]" # zieht ecdsa + base58
```

## Unterstützte Standards

| Bereich | Standard |
|---|---|
| DID-Dokumente | [W3C DID Core](https://www.w3.org/TR/did-core/) |
| DID-Methoden | [did:web](https://w3c-ccg.github.io/did-method-web/), [did:key](https://w3c-ccg.github.io/did-method-key/), [did:peer:4](https://identity.foundation/peer-did-method-spec/), `did:github` (projektspezifisch) |
| Credentials | [W3C Verifiable Credentials](https://www.w3.org/TR/vc-data-model/) (JWT-VC) |
| Signatur | [JOSE/JWS RFC 7515](https://www.rfc-editor.org/rfc/rfc7515), [EdDSA RFC 8037](https://www.rfc-editor.org/rfc/rfc8037) |
| Thumbprint | [JWK Thumbprint RFC 7638](https://www.rfc-editor.org/rfc/rfc7638) |

## ⚠️ Sicherheitshinweis

Der produktive Krypto-Pfad nutzt `python-ecdsa` (reine Python-Implementierung).
Sie ist dependency-arm, aber **nicht garantiert constant-time** (Timing-
Seitenkanal-Risiko). Für hochsensible Umgebungen ein libsodium-Backend (PyNaCl)
oder `cryptography` (OpenSSL) erwägen.

Das Modul `ed25519.py` ist eine reine **Lern-Referenz** (RFC 8032) und wird vom
Funktionspfad **nicht** verwendet.

## Modulübersicht

| Modul | Inhalt |
|---|---|
| `errors.py` | Ausnahmehierarchie (`DidError`, `EncodingError`, `ResolutionError`, `VerificationError`) |
| `keys.py` | Ed25519-JWK erzeugen/ableiten (CLI) |
| `utils_didkey.py` | JWK ↔ `did:key`, RFC-7638-Thumbprint, `kid`-Helfer |
| `jose.py` | Compact-JWS signieren/verifizieren (EdDSA) |
| `issue_vc.py` | VC/VP ausstellen (ProductPassport, CLI) |
| `verify_vp.py` | VP+VC verifizieren (`verify_presentation`, CLI) |
| `did_web.py` | `did:web`-Dokumente bauen/auflösen (CLI) |
| `did_peer4.py` | `did:peer:4` (abhängigkeitsfrei) |
| `did_github.py` | `did:github`-Auflösung über GitHub-Rohinhalte |
| `ed25519.py` | Lern-Referenz Ed25519 (nicht für Produktion) |
| `diddoc.py` | Demo: DIDComm-JWS |

## Schnellstart (Bibliothek)

```python
from sdata.did import (
gen_ed25519_jwk, pub_from_priv_jwk, did_key_from_jwk_ed25519,
sign_compact, verify_compact,
)

jwk = gen_ed25519_jwk() # privates JWK (x + d)
pub = pub_from_priv_jwk(jwk) # nur x
did = did_key_from_jwk_ed25519(pub) # did:key:z…
kid = f"{did}#{did.split(':', 2)[2]}"

jws = sign_compact({"hello": "world"}, jwk, kid=kid, typ="JWT")
assert verify_compact(jws, pub)["hello"] == "world"
```

### VC ausstellen & Presentation verifizieren

```python
from sdata.did import verify_presentation, VerificationError

result = verify_presentation(vp_jws, expected_aud="https://verifier.example.org",
expected_nonce=nonce)
print(result["issuer"], result["credentialSubject"]["gtin"])
```

## CLI (als Modul, `-m`)

```bash
# Schlüssel
python -m sdata.did.keys gen --out issuer_key.json
python -m sdata.did.keys pub --in issuer_key.json --out issuer_pub.json
python -m sdata.did.keys thumb --in issuer_pub.json

# did:web-Dokument
python -m sdata.did.did_web build --domain example.com \
--pub issuer_pub.json --out did.json --format jwk
python -m sdata.did.did_web resolve --kid did:web:example.com#key-1

# VC ausstellen / VP verifizieren
python -m sdata.did.issue_vc --issuer-domain issuer.example \
--issuer-key issuer_key.json --gtin 04012345678901 --serial SN-42 \
--co2 12.5 --recycled 80 --out product.vc
python -m sdata.did.verify_vp --vp product.vp \
--expected-aud https://verifier.example.org
```

## Tests

```bash
pytest tests/test_did.py
```

Die Tests laufen ohne Netzwerk (HTTP wird gemockt) und überspringen sich
sauber (`skip`), falls optionale Abhängigkeiten fehlen.
123 changes: 123 additions & 0 deletions sdata/did/__init__.py
Original file line number Diff line number Diff line change
@@ -1,0 +1,123 @@
# -*- coding: utf-8 -*-
"""``sdata.did`` – Decentralized Identifiers (DID) & Verifiable Credentials (VC).

Check notice on line 2 in sdata/did/__init__.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

sdata/did/__init__.py#L2

Multi-line docstring summary should start at the second line (D213)

Dieses Subpackage bündelt eine schlanke, **pure-Python** Umsetzung gängiger
SSI-Bausteine (Self-Sovereign Identity) rund um Ed25519:

============================ ====================================================
Bereich Modul / Funktionen
============================ ====================================================
Schlüssel (Ed25519/JWK) :mod:`~sdata.did.keys` – :func:`gen_ed25519_jwk`, :func:`pub_from_priv_jwk`
JWK / did:key / Thumbprint :mod:`~sdata.did.utils_didkey`
JOSE / Compact-JWS (EdDSA) :mod:`~sdata.did.jose` – :func:`sign_compact`, :func:`verify_compact`
VC / VP ausstellen :mod:`~sdata.did.issue_vc` – :func:`issue_vc`, :func:`make_vp`
VC / VP verifizieren :mod:`~sdata.did.verify_vp` – :func:`verify_presentation`
did:web :mod:`~sdata.did.did_web` – :func:`build_did_document`, :func:`resolve_did_web`
did:key :mod:`~sdata.did.utils_didkey` – :func:`did_key_from_jwk_ed25519`, :func:`pub_jwk_from_did_key`
did:peer:4 :mod:`~sdata.did.did_peer4` – :func:`did_peer4_from_payload`, :func:`resolve_long_form`
did:github :mod:`~sdata.did.did_github` – :func:`resolve_did_github`
============================ ====================================================

Unterstützte Standards: W3C DID Core, did:web, did:key, did:peer:4,
W3C Verifiable Credentials (JWT-VC), JOSE/JWS (RFC 7515), EdDSA (RFC 8037),
JWK-Thumbprint (RFC 7638).

Installation
------------
Die DID-Funktionen benötigen die Extra-Abhängigkeiten ``ecdsa`` und ``base58``::

pip install "sdata[did]"

.. warning::
**Sicherheit / SOTA-Hinweis.** Der produktive Krypto-Pfad nutzt
``python-ecdsa`` (reine Python-Implementierung von Ed25519). Diese ist
bewusst dependency-arm, aber **nicht garantiert constant-time** und damit
potenziell anfällig für Timing-Seitenkanäle. Für hochsensible Umgebungen ein
libsodium-Backend (PyNaCl) oder ``cryptography`` (OpenSSL) erwägen.
Das Modul :mod:`sdata.did.ed25519` ist eine reine **Lern-Referenz** und
wird vom Funktionspfad nicht verwendet.

Beispiel
--------
::

from sdata.did import (gen_ed25519_jwk, pub_from_priv_jwk,
did_key_from_jwk_ed25519, sign_compact, verify_compact)

jwk = gen_ed25519_jwk()
did = did_key_from_jwk_ed25519(pub_from_priv_jwk(jwk))
kid = f"{did}#{did.split(':', 2)[2]}"
jws = sign_compact({"hello": "world"}, jwk, kid=kid, typ="JWT")
assert verify_compact(jws, pub_from_priv_jwk(jwk))["hello"] == "world"
"""
from __future__ import annotations

import importlib

# Lazy-Loading (PEP 562): Submodule werden erst beim Zugriff importiert. Vorteile:
# * ``python -m sdata.did.<modul>`` löst keine runpy-Doppelimport-Warnung aus,
# * ``import sdata.did`` ist leichtgewichtig – die optionalen Krypto-Deps
# (ecdsa/base58) werden erst geladen, wenn eine sie nutzende Funktion
# tatsächlich angefasst wird.
# Mapping: exportierter Name -> "submodul" oder ("submodul", "originalname").
_EXPORTS = {
# Fehler
"DidError": "errors", "EncodingError": "errors",
"ResolutionError": "errors", "VerificationError": "errors",
# Schlüssel / JWK / did:key
"gen_ed25519_jwk": "keys", "pub_from_priv_jwk": "keys",
"b64url": "utils_didkey", "b64url_decode": "utils_didkey",
"jwk_thumbprint_rfc7638": "utils_didkey",
"did_key_from_jwk_ed25519": "utils_didkey",
"did_key_fragment_from_jwk": "utils_didkey",
"pub_jwk_from_did_key": "utils_didkey",
"kid_for_did_web": "utils_didkey", "kid_for_did_key": "utils_didkey",
# JOSE / JWS
"sign_compact": "jose", "verify_compact": "jose", "decode_header": "jose",
# VC / VP
"issue_vc": "issue_vc", "make_vp": "issue_vc", "sign_compact_jws": "issue_vc",
"verify_presentation": "verify_vp", "jws_header": "verify_vp",
"jws_verify": "verify_vp", "resolve_public_jwk_for_kid": "verify_vp",
# DID-Methoden
"build_did_document": "did_web", "did_web_to_url": "did_web",
"resolve_did_web": ("did_web", "resolve_public_jwk_for_kid"),
"did_peer4_from_payload": "did_peer4", "resolve_long_form": "did_peer4",
"resolve_did_github": "did_github", "parse_did_github": "did_github",
}


def __getattr__(name):
"""Importiere das zugehörige Submodul beim ersten Zugriff (PEP 562)."""
target = _EXPORTS.get(name)
if target is None:
raise AttributeError(
"module {!r} has no attribute {!r}".format(__name__, name))
modname, attrname = target if isinstance(target, tuple) else (target, name)
mod = importlib.import_module("." + modname, __name__)
value = getattr(mod, attrname)
globals()[name] = value # cachen, damit __getattr__ nur einmal feuert
return value


def __dir__():
return sorted(__all__)


__all__ = [
# Fehler
"DidError", "EncodingError", "ResolutionError", "VerificationError",
# Schlüssel / JWK / did:key
"gen_ed25519_jwk", "pub_from_priv_jwk",
"b64url", "b64url_decode", "jwk_thumbprint_rfc7638",
"did_key_from_jwk_ed25519", "did_key_fragment_from_jwk", "pub_jwk_from_did_key",
"kid_for_did_web", "kid_for_did_key",
# JOSE / JWS
"sign_compact", "verify_compact", "decode_header",
# VC / VP
"issue_vc", "make_vp", "sign_compact_jws",
"verify_presentation", "resolve_public_jwk_for_kid", "jws_header", "jws_verify",
# DID-Methoden
"build_did_document", "did_web_to_url", "resolve_did_web",
"did_peer4_from_payload", "resolve_long_form",
"resolve_did_github", "parse_did_github",
]
Loading