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:
| Sprache | JSON-Kanonisierung (RFC 8785) | Ed25519 |
|---|---|---|
| TypeScript, Node 20+ | canonicalize | node:crypto |
| Python 3 | rfc8785 | cryptography |
| Go | github.com/gowebpki/jcs | crypto/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 -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"{
"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": [
{ "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
| Ergebnis | Bedeutung | Was zu tun ist |
|---|---|---|
Kein document oder verification | Der Pass ist gerade nicht öffentlich; status nennt den Grund | Status anzeigen; es gibt nichts zu prüfen |
| Hash weicht ab | Das Dokument ist nicht das signierte oder wurde nicht richtig kanonisiert | Das geparste document mit einer RFC-8785-Bibliothek kanonisieren; weicht der Hash dann noch ab, gelten die Daten als nicht geprüft |
Unbekannte key_id | Das geladene Schlüsselset kennt diesen Schlüssel nicht | Schlüsselset neu laden (Ihres ist vielleicht älter als die Signatur); fehlt er weiterhin, ist die Prüfung fehlgeschlagen |
| Signatur ungültig | Das Dokument wurde nicht mit diesem Schlüssel signiert | Die 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 -sS "$BATTERYPASS_URL/api/v1/public/passports/0192f0c4-7d1e-7a3b-9c55-2f1e8d4a6b10/verify?version_no=3"{
"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.
Überblick
Schritt-für-Schritt-Anleitungen für die häufigsten Aufgaben einer Integration, mit Anfragen, Antworten und den wichtigen Fehlerfällen.
Zustandsdaten senden
Melden Sie Messwerte wie Ladezustand oder Vollzyklen mit updateCondition und lesen Sie, was übernommen, übersprungen und neu veröffentlicht wurde.