Zum Inhalt springen

Passsignatur offline prüfen

Prüfen Sie allein mit dem öffentlichen JSON und den veröffentlichten Schlüsseln, dass die öffentlichen Daten eines Passes genau so signiert wurden.

Jede veröffentlichte Passversion trägt eine Ed25519-Signatur über ihr öffentliches Dokument. Sie können sie ohne API-Schlüssel prüfen und müssen BatteryPass nicht fragen, ob die Daten echt sind: Das öffentliche JSON und das Schlüsselset unter /.well-known/batterypass/keys.json genügen. Den Hintergrund erklärt Signaturen.

Was Sie brauchen

  • Den Pass: seine ID (die UUID aus urn:dpp:{uuid}) oder den GS1 Digital Link aus seinem QR-Code.
  • Die BatteryPass-Adresse, die ihn veröffentlicht hat, als BATTERYPASS_URL. Öffentliche Abfragen brauchen keinen Schlüssel.
  • Eine Implementierung von RFC 8785 (JCS) und Ed25519 für Ihre Sprache:
SpracheJSON-Kanonisierung (RFC 8785)Ed25519
TypeScript, Node 20+canonicalizenode:crypto
Python 3rfc8785cryptography
Gogithub.com/gowebpki/jcscrypto/ed25519
flowchart LR
  J["Öffentliches JSON"] --> D["document"]
  J --> V["verification"]
  D -->|"RFC 8785"| C["Kanonische Bytes"]
  C -->|"SHA-256"| H{"Gleich verification.hash?"}
  K["keys.json"] -->|"kid = verification.key_id"| P["Öffentlicher Ed25519-Schlüssel"]
  C --> S{"verification.signature gültig?"}
  P --> S

Schritt für Schritt

Öffentliches JSON abrufen

Lesen Sie den Pass mit getPublicPassport oder folgen Sie dem Digital Link mit ?linkType=describedby: Der Resolver antwortet mit 307 und dem öffentlichen JSON als Ziel. getPublicPassportByProductId liefert dasselbe Dokument über GTIN und Seriennummer.

cURL
curl -sS "$BATTERYPASS_URL/api/v1/public/passports/0192f0c4-7d1e-7a3b-9c55-2f1e8d4a6b10"

# oder ausgehend vom QR-Code (-L folgt der Weiterleitung)
curl -sSL "https://batterypass.example.com/01/04012345000023/21/SV2027-000184?linkType=describedby"
200
{
  "passport_id": "urn:dpp:0192f0c4-7d1e-7a3b-9c55-2f1e8d4a6b10",
  "status": "published",
  "document": {
    "passport_id": "urn:dpp:0192f0c4-7d1e-7a3b-9c55-2f1e8d4a6b10",
    "gtin": "04012345000023",
    "serial_number": "SV2027-000184",
    "gs1_digital_link": "https://batterypass.example.com/01/04012345000023/21/SV2027-000184",
    "model_name": "SolarVolt Home 10",
    "tier": "public",
    "version": { "no": 3, "published_at": "2026-10-20T09:12:44Z", "registry_version": "0.5.0" },
    "category": {
      "key": "stationary",
      "label": { "de": "Stationärer Energiespeicher (> 2 kWh)", "en": "Stationary storage battery (> 2 kWh)" }
    },
    "sections": [
      {
        "key": "performance",
        "label": { "de": "Leistung & Haltbarkeit", "en": "Performance & durability" },
        "fields": [
          {
            "key": "performance.nominal_voltage",
            "label": { "de": "Nennspannung", "en": "Nominal voltage" },
            "tier": "public",
            "level": "model",
            "status": "provided",
            "value": 51.2,
            "unit": "V"
          }
        ]
      }
    ],
    "restricted_fields_present": ["condition.state_of_charge"]
  },
  "verification": {
    "algorithm": "Ed25519",
    "canonicalization": "RFC8785",
    "hash": "sha256:9c1f0e5b7d4a3c2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e",
    "signature": "q2v0Yb3m8yJ1kQe7Zt5nC4xL9pW2sR6uH0aF3dG8jK1oM5iT7vX4zB2cN6eQ9rS1wY3uA5gD8hJ0lP4fK7mV2Q",
    "key_id": "2026-10-k1",
    "signed_at": "2026-10-20T09:12:44Z",
    "keys_url": "https://batterypass.example.com/.well-known/batterypass/keys.json"
  },
  "links": {
    "page": "https://batterypass.example.com/pass/0192f0c4-7d1e-7a3b-9c55-2f1e8d4a6b10",
    "json": "https://batterypass.example.com/api/v1/public/passports/0192f0c4-7d1e-7a3b-9c55-2f1e8d4a6b10",
    "keys": "https://batterypass.example.com/.well-known/batterypass/keys.json",
    "gs1_digital_link": "https://batterypass.example.com/01/04012345000023/21/SV2027-000184"
  }
}

Signiert ist nur document. passport_id, status, valid_until, verification und links ändern sich im Leben eines Passes und fallen nicht unter die Signatur. Ein Pass, der gerade nicht öffentlich ist (qr_locked, suspended, withdrawn, deleted_recycled), antwortet nur mit seinem Status, ohne document und verification: Dann gibt es nichts zu prüfen.

Dokument kanonisieren

Lesen Sie die Antwort mit einem gewöhnlichen JSON-Parser und serialisieren Sie das Objekt document mit JCS (RFC 8785). JCS sortiert die Felder, entfernt Leerraum und schreibt Zahlen und Zeichenketten in einer festen Form. Dieselben Daten ergeben so immer dieselben Bytes, egal in welcher Reihenfolge und mit welchen Leerzeichen die Antwort kam. Kanonisieren Sie das geparste Objekt; hashen Sie weder die Bytes der Antwort noch die Ausgabe Ihres eigenen JSON-Writers.

Hash vergleichen

Berechnen Sie SHA-256 über die kanonischen Bytes und schreiben Sie das Ergebnis als sha256: gefolgt vom Hex-Digest in Kleinbuchstaben. Es muss verification.hash entsprechen. Weicht es ab, ist Ihr Dokument nicht das signierte.

Schlüssel wählen

Laden Sie das JWK-Set von /.well-known/batterypass/keys.json der BatteryPass-Adresse, von der der Pass stammen soll, und nehmen Sie den Schlüssel, dessen kid gleich verification.key_id ist:

keys.json
{
  "keys": [
    { "kty": "OKP", "crv": "Ed25519", "x": "WuoaQRoQuh2kPs5yYB6HxNz3Y58ndE8fRHnzG10NxoU", "kid": "2026-10-k1", "use": "sig", "alg": "EdDSA" }
  ]
}

Das Set braucht keinen Schlüssel, darf eine Stunde zwischengespeichert werden und enthält auch abgelöste Schlüssel. Signaturen alter Versionen bleiben so nach einem Schlüsselwechsel prüfbar.

Signatur prüfen

Dekodieren Sie x (32 Bytes) und verification.signature (64 Bytes) aus base64url ohne Padding und prüfen Sie die Ed25519-Signatur über die kanonischen Bytes, nicht über den Hash.

Woher die Schlüssel kommen

verification.keys_url und links.keys sind nicht signiert. Laden Sie die Schlüssel von der Adresse, der Sie vertrauen (der BatteryPass-Adresse des Herstellers oder einem früher gespeicherten Schlüsselset), und werten Sie eine keys_url, die woanders hinzeigt, als fehlgeschlagene Prüfung.

Vollständiger Code

Jedes Programm erwartet die UUID des Passes als Argument und gibt valid aus oder bricht mit dem Grund ab.

// verify.mts: npm install canonicalize, then: npx tsx verify.mts <passport-uuid>
import { createHash, createPublicKey, verify } from 'node:crypto';
import canonicalize from 'canonicalize';

const base = process.env.BATTERYPASS_URL; // the BatteryPass address that published the passport
const passportId = process.argv[2]; // the UUID of urn:dpp:{uuid}

async function getJSON(url: string) {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`GET ${url}: ${res.status}`);
  return res.json();
}

const passport = await getJSON(`${base}/api/v1/public/passports/${passportId}`);
const { document, verification } = passport;
if (!document || !verification) throw new Error(`status ${passport.status}: nothing to verify`);

// 1. Canonicalise the signed document (RFC 8785) and compare its SHA-256 with verification.hash.
const canonical = Buffer.from(canonicalize(document) ?? '', 'utf8');
const hash = `sha256:${createHash('sha256').update(canonical).digest('hex')}`;
if (hash !== verification.hash) throw new Error(`hash mismatch: ${hash}`);

// 2. Take the key named by verification.key_id from the published JWK set.
const { keys } = await getJSON(`${base}/.well-known/batterypass/keys.json`);
const jwk = keys.find((k: { kid: string }) => k.kid === verification.key_id);
if (!jwk) throw new Error(`unknown key ${verification.key_id}`);

// 3. Check the Ed25519 signature (base64url, no padding) over the canonical bytes.
const publicKey = createPublicKey({ key: jwk, format: 'jwk' });
const valid = verify(null, canonical, publicKey, Buffer.from(verification.signature, 'base64url'));
console.log(valid ? 'valid' : 'INVALID');
process.exitCode = valid ? 0 : 1;

Viele Pässe auf einmal prüft das Rezept Signaturen mehrerer Pässe prüfen.

Wenn die Prüfung fehlschlägt

ErgebnisBedeutungWas zu tun ist
Kein document oder verificationDer Pass ist gerade nicht öffentlich; status nennt den GrundStatus anzeigen; es gibt nichts zu prüfen
Hash weicht abDas Dokument ist nicht das signierte oder wurde nicht richtig kanonisiertDas geparste document mit einer RFC-8785-Bibliothek kanonisieren; weicht der Hash dann noch ab, gelten die Daten als nicht geprüft
Unbekannte key_idDas geladene Schlüsselset kennt diesen Schlüssel nichtSchlüsselset neu laden (Ihres ist vielleicht älter als die Signatur); fehlt er weiterhin, ist die Prüfung fehlgeschlagen
Signatur ungültigDas Dokument wurde nicht mit diesem Schlüssel signiertDie Daten gelten als nicht geprüft

Ältere Versionen

getPublicPassportVersion liefert das öffentliche JSON der Version, die zu einem Zeitpunkt galt (?at=2026-09-01T00:00:00Z); sobald eine spätere Version sie abgelöst hat, ist valid_until gesetzt. Sie prüfen sie genauso: Versionen ändern sich nach der Veröffentlichung nie, und abgelöste Schlüssel bleiben im Schlüsselset.

Prüfung auf dem Server

verifyPassport führt dieselbe Prüfung auf dem Server aus und prüft zusätzlich den vollständigen Snapshot der Version, den ein Scanner nie erhält. Das eignet sich für einen schnellen Blick oder ein Monitoring. Unabhängig vom Wort des Servers ist nur die Offline-Prüfung oben.

cURL
curl -sS "$BATTERYPASS_URL/api/v1/public/passports/0192f0c4-7d1e-7a3b-9c55-2f1e8d4a6b10/verify?version_no=3"
200
{
  "valid": true,
  "version_no": 3,
  "key_id": "2026-10-k1",
  "public_document_valid": true,
  "snapshot_valid": true,
  "signed_at": "2026-10-20T09:12:44Z",
  "checked_at": "2026-10-24T08:00:00Z"
}

Ohne version_no prüft der Server die aktuelle Version. valid ist nur dann true, wenn beide Signaturen gültig sind und beide Hashes stimmen.

Auf dieser Seite