ID MANDAT

Vor dem Vorgang nachfragen: Gehört dieser Agent zum Unternehmen, und darf er das?

Für Shops und Agenten · 17. Oktober 2026

Das Problem

Ein Agent legt eine Anfrage oder eine Freigabe vor. Verfahren wie signierte Anfragen oder Freigaben der Person zeigen, welche Software anklopft oder dass jemand zugestimmt hat. Offen bleibt oft: Gehört der Agent zu dem Unternehmen, das er nennt, und was ist ihm dauerhaft erlaubt oder verboten? Anlass sind die Meldungen dieser Woche: Wer ist der Agent? Zwei Antworten in einer Woche, und eine Lücke.

Die Lösung

Vor der Annahme fragt der Prüfer bei ID MANDAT nach. Eine Abfrage, ohne Konto, Antwort in Sekunden:

  1. Ist die Domain registriert, und ist der Agent in ihrer Liste?
  2. Ist die Antwort echt (Ed25519-Signatur) und frisch (15 Minuten)?
  3. Gilt der Eintrag noch, oder ist er widerrufen, ausgesetzt oder abgelaufen?
  4. Steht die geplante Handlung unter „darf“ (may) oder „darf nicht“ (mayNot)?

Im Zweifel gilt: nicht ausführen.

Die Abfrage: https://idmandat.com/api/v1/verify?domain=beispiel.de&agent=Name. Die öffentlichen Schlüssel stehen unter https://idmandat.com/api/v1/keys. Wer erst ausprobieren will, nimmt den Spielplatz mit erfundenen Firmen.

Beispielcode

Zwei kleine Beispiele, die genau diese vier Prüfungen machen. Sie sind Beispiele: Bitte vor dem Einsatz selbst testen und an Ihre Umgebung anpassen.

Python (3.9+, pip install cryptography)

"""ID MANDAT: Vor dem Vorgang nachfragen (Beispiel, Python 3.9+).

Braucht:  pip install cryptography
Prüft:    ist der Agent bei der Domain eingetragen, ist die Antwort echt signiert
          und frisch, ist der Eintrag nicht widerrufen/ausgesetzt/abgelaufen,
          und steht die geplante Handlung in "may" bzw. "mayNot".

Wichtig: "may"/"mayNot" sind Angaben des Unternehmens (declared_by_operator).
ID MANDAT bestätigt Identität und Befugnis, nicht Bonität, Vertrauenswürdigkeit
oder Qualität. Im Zweifel gilt: NICHT ausführen (fail closed).
"""
import base64, json, urllib.parse, urllib.request
from datetime import datetime, timezone
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey

API = "https://idmandat.com/api/v1"


def _b64u(s):
    return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))


def _canon(o):
    if isinstance(o, dict):
        return {k: _canon(o[k]) for k in sorted(o)}
    if isinstance(o, list):
        return [_canon(x) for x in o]
    return o


def _get(url):
    with urllib.request.urlopen(url, timeout=10) as r:
        return json.load(r)


def signature_ok(answer):
    """Echtheit prüfen: Ed25519 über die kanonische JSON-Antwort ohne 'signature'."""
    body = dict(answer)
    sig = body.pop("signature", None)
    if not answer.get("signed") or not sig or sig.get("alg") != "Ed25519":
        return False
    keys = {k["key_id"]: k for k in _get(API + "/keys")["keys"]}
    key = keys.get(answer.get("key_id"))
    if not key or key.get("status") != "active":
        return False
    msg = json.dumps(_canon(body), separators=(",", ":"), ensure_ascii=False).encode()
    try:
        Ed25519PublicKey.from_public_bytes(_b64u(key["public_key"])).verify(_b64u(sig["value"]), msg)
        return True
    except Exception:
        return False


def darf_ich(domain, agent_name, handlung):
    """Gibt (erlaubt, grund) zurück. erlaubt ist nur True, wenn alles klar ist."""
    q = urllib.parse.urlencode({"domain": domain, "agent": agent_name})
    try:
        a = _get(API + "/verify?" + q)
    except Exception as e:
        return False, "Abfrage fehlgeschlagen: %s" % e
    if not signature_ok(a):
        return False, "Antwort nicht (gültig) signiert"
    if datetime.fromisoformat(a["expires_at"].replace("Z", "+00:00")) < datetime.now(timezone.utc):
        return False, "Antwort abgelaufen, neu abfragen"
    if not a.get("registered"):
        return False, "nicht registriert (%s)" % a.get("entry_state", "unbekannt")
    if a.get("entry_state") or a.get("revoked"):
        return False, "Eintrag gilt nicht: %s" % a.get("entry_state", "widerrufen")
    treffer = [x for x in a.get("agents", []) if x.get("name") == agent_name]
    if not treffer:
        return False, "Agent steht nicht in der Liste dieser Domain"
    ag = treffer[0]
    if handlung in ag.get("mayNot", []):
        return False, "Handlung ist ausdrücklich ausgeschlossen (mayNot)"
    if handlung not in ag.get("may", []):
        return False, "Handlung ist nicht ausdrücklich erlaubt (may)"
    return True, "erlaubt laut Erklärung des Unternehmens (declared_by_operator)"


if __name__ == "__main__":
    print(darf_ich("idmandat.com", "Grok (xAI)", "draft_social_posts"))
    print(darf_ich("idmandat.com", "Grok (xAI)", "place_order"))

Node (18+, keine Pakete)

// ID MANDAT: Vor dem Vorgang nachfragen (Beispiel, Node 18+, keine Pakete nötig).
// may/mayNot sind Angaben des Unternehmens. Im Zweifel: NICHT ausführen.
import { createPublicKey, verify } from "node:crypto";

const API = "https://idmandat.com/api/v1";
const get = async (u) => { const r = await fetch(u); if (!r.ok) throw new Error(r.status); return r.json(); };
const canon = (o) => Array.isArray(o) ? o.map(canon)
  : (o && typeof o === "object") ? Object.fromEntries(Object.keys(o).sort().map(k => [k, canon(o[k])])) : o;
const b64u = (s) => Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/"), "base64");

async function signaturOk(a) {
  const { signature, ...body } = a;
  if (!a.signed || !signature || signature.alg !== "Ed25519") return false;
  const key = (await get(API + "/keys")).keys.find(k => k.key_id === a.key_id && k.status === "active");
  if (!key) return false;
  const pub = createPublicKey({ key: { kty: "OKP", crv: "Ed25519", x: key.public_key }, format: "jwk" });
  return verify(null, Buffer.from(JSON.stringify(canon(body))), pub, b64u(signature.value));
}

export async function darfIch(domain, agent, handlung) {
  let a;
  try { a = await get(`${API}/verify?domain=${encodeURIComponent(domain)}&agent=${encodeURIComponent(agent)}`); }
  catch (e) { return [false, "Abfrage fehlgeschlagen: " + e.message]; }
  if (!(await signaturOk(a))) return [false, "Antwort nicht (gültig) signiert"];
  if (new Date(a.expires_at) < new Date()) return [false, "Antwort abgelaufen, neu abfragen"];
  if (!a.registered) return [false, "nicht registriert (" + (a.entry_state ?? "unbekannt") + ")"];
  if (a.entry_state || a.revoked) return [false, "Eintrag gilt nicht: " + (a.entry_state ?? "widerrufen")];
  const ag = (a.agents ?? []).find(x => x.name === agent);
  if (!ag) return [false, "Agent steht nicht in der Liste dieser Domain"];
  if ((ag.mayNot ?? []).includes(handlung)) return [false, "Handlung ausdrücklich ausgeschlossen (mayNot)"];
  if (!(ag.may ?? []).includes(handlung)) return [false, "Handlung nicht ausdrücklich erlaubt (may)"];
  return [true, "erlaubt laut Erklärung des Unternehmens (declared_by_operator)"];
}

if (import.meta.url === `file://${process.argv[1]}`) {
  console.log(await darfIch("idmandat.com", "Grok (xAI)", "draft_social_posts"));
  console.log(await darfIch("idmandat.com", "Grok (xAI)", "place_order"));
}

Die Antwort ist 15 Minuten gültig (expires_at); nicht länger zwischenspeichern.

Was das nicht leistet

Für Shops · Domain prüfen · Technischer Standard · Unternehmen eintragen

Stand: 17. Oktober 2026.