qredential

Dokumentation

Nachweise, die sich ohne Netz prüfen lassen

Ein QR-Code trägt seinen eigenen Beweis. qredential prüft Signatur, Ablauf, Sperrstatus und Claims, ohne dass eine einzige Anfrage das Gerät verlässt.

Erste Schritte

npm i qredential

Node 20 oder neuer, jeder aktuelle Browser und React Native. Die Bibliothek nutzt WebCrypto und nichts Node-Spezifisches, denn das prüfende Gerät ist meistens ein Telefon. Es gibt keine Laufzeitabhängigkeiten.

Die vollständige Testsuite läuft bei jedem Commit auf Node 20, 22 und 24 unter Linux, macOS und Windows sowie in Chromium, Firefox und WebKit. Ed25519 wird in allen vieren ausgeübt.

Ein Vorbehalt zu React Native. Dort gibt es kein CompressionStream, und Ausstellen, Vorzeigen und Prüfen funktionieren auch ohne: der Umschlag bleibt schlicht unkomprimiert, was Größe kostet und sonst nichts. Sperrprüfung offline funktioniert nicht, denn eine Statusliste zu lesen heißt, eine Bitfolge zu entpacken. Ein Prüfer, der die zwischengespeicherte Liste nicht entpacken kann, lehnt mit status_unavailable ab, statt eine unlesbare Liste als saubere zu behandeln. Wer Sperrprüfung auf React Native braucht, ergänzt ein Polyfill für DecompressionStream. Ein Test führt den gesamten Ablauf mit beiden gelöschten Globals aus, diese Beschreibung ist also geprüft und nicht vermutet.

Etwas prüfen

import { verify } from 2

const result = await verify(scannedText, { trust })

if (result.ok) {
  console.log(result.claims.given_name)   0
} else {
  console.log(result.reason)              1
}

Der ganze Umlauf

Drei Beteiligte, drei Aufrufe. Der Aussteller signiert einmal, der Inhaber verengt, was mitreist, der Prüfer entscheidet.

import { issue, present, verify } from 9

0
const { credential, qr } = await issue({
  issuer: 'https:1
  kid: 10,
  key: issuerPrivateJwk,
  claims: {
    given_name: 11,
    family_name: 12,
    birth_date: 13,
    over_18: true,
  },
  disclose: [14, 15],
  holderKey: holderPublicJwk,
  expiresIn: 16,
})

2
3
const presentation = await present(credential, {
  disclose: [17],
  keyBinding: { key: holderPrivateJwk, audience: 'https:4
})

5
const result = await verify(presentation, {
  trust,
  nonce: challenge,
  audience: 'https:6
})
result.claims       7
result.claims.birth_date  8

Selektive Offenlegung

Zwei verschiedene Beteiligte treffen zwei verschiedene Entscheidungen, und sie zu vermischen ist der übliche Entwurfsfehler. Der Aussteller legt beim Signieren mit disclose fest, welche Claims zurückgehalten werden dürfen. Der Inhaber entscheidet danach beim Scannen, ebenfalls mit disclose, welche davon er tatsächlich zeigt. Einen Claim, den der Aussteller nie markiert hat, kann niemand zurückhalten.

Der Mechanismus ist gewöhnliches Hashen. Jeder offenlegbare Claim wird mit einem zufälligen 128-Bit-Salt serialisiert, und nur der Digest dieser Zeichenkette wird in den Nachweis signiert. Einen Claim zu zeigen heißt, die ursprüngliche Zeichenkette mitzuschicken, damit der Prüfer sie hashen und den Digest finden kann. Ihn zurückzuhalten heißt, den Prüfer mit einem Digest sitzen zu lassen, den er nie öffnen kann.

Verschachtelte, Array- und rekursive Offenlegung

RFC 9901 erlaubt alle drei, und das europäische Wallet-Ökosystem nutzt alle drei. Die Prüfung löst sie hier in beliebiger Tiefe auf, gemäß dem Verarbeitungsmodell aus Abschnitt 7.1:

  • Ein _sd-Array in einem verschachtelten Objekt verbirgt Eigenschaften dieses Objekts.
  • Ein Array-Element der Form {"...": digest} verbirgt das Element selbst. Ein zurückgehaltenes Element wird aus dem Array entfernt statt als Platzhalter stehen zu bleiben, sodass Sie einen sauberen, darstellbaren Wert erhalten.
  • Ein offengelegter Wert kann selbst wieder eine der beiden Formen enthalten, das Auflösen des einen bringt also weitere zum Vorschein.

Nichts davon sickert nach claims durch: kein _sd-Schlüssel und kein "..."-Element erreicht den Aufrufer. Offengelegte Namen kommen als Pfade zurück, etwa address.locality, und das Ganze wird in beide Richtungen gegen eine unabhängige Implementierung derselben RFC geprüft.

Warum das bei Altersprüfungen zählt. Die übliche Umsetzung lässt Kundschaft ein Foto des Ausweises bei Dritten hochladen, was ein Datenleck erzeugt, das nur noch auf seinen Tag wartet. Hier erfährt die Bar einen Wahrheitswert und kann das Geburtsdatum nicht erfahren, selbst wenn sie wollte. Diese Eigenschaft ist kryptografisch, kein Versprechen in einer Datenschutzerklärung.

Nach einem Claim zu fragen, den der Aussteller nicht offenlegbar gemacht hat, wirft eine Ausnahme, statt stillschweigend weniger zurückzugeben als verlangt.

Den Inhaber beweisen

Selektive Offenlegung beweist, dass der Aussteller diese Claims signiert hat. Für sich genommen beweist sie nicht, dass die vorzeigende Person auch die betroffene ist, und die Lücke ist nicht theoretisch: das Foto vom Code einer anderen Person trägt dieselbe Signatur und prüft sich genauso erfolgreich.

Key Binding schließt sie. Der Aussteller schreibt den öffentlichen Schlüssel des Inhabers in den Nachweis, und beim Scannen signiert dessen Wallet eine frische Aufforderung mit dem passenden privaten Schlüssel. Ein Foto kann das nicht.

Die drei Schritte

0
const { credential } = await issue({
  ...,
  holderKey: holderPublicJwk,
})

1
const presentation = await present(credential, {
  disclose: [6],
  keyBinding: {
    key: holderPrivateJwk,
    audience: 'https:2
    nonce: challengeFromTheVerifier,
  },
})

3
const result = await verify(scanned, {
  trust,
  nonce: challengeIIssued,
  audience: 'https:4
})
result.holderVerified   5

Der Beweis legt sich auf vier Dinge fest, und jedes schließt einen bestimmten Angriff: die Nonce verhindert, dass eine aufgezeichnete Vorlage erneut abgespielt wird, die Audience verhindert, dass ein für einen Prüfer erstellter Beweis bei einem anderen benutzt wird, die Signatur bindet ihn an den vom Aussteller hinterlegten Schlüssel, und sd_hash deckt genau die vorgelegte Menge an Offenlegungen ab, sodass eine Zwischenstation nach der Unterschrift des Inhabers keine hinzufügen oder entfernen kann. Er verfällt außerdem, standardmäßig nach fünf Minuten, einstellbar über maxKeyBindingAge.

Statische Nachweise, und warum die Entscheidung laut ausgesprochen Ihre ist

Ein auf eine Karte gedruckter Code kann nichts davon. Es gibt kein Gerät, das beim Scannen signiert, also ist ein statischer Nachweis von Natur aus kopierbar. Das ist eine reale und häufige Lage, kein Fehler, aber es ist eine Entscheidung, die ein Prüfer wissentlich treffen sollte:

const result = await verify(scanned, { trust, acceptWithoutHolderProof: true })
result.holderVerified   0

Ohne diese Option wird eine Vorlage ohne Beweis mit holder_proof_missing abgelehnt. Der Standard ist absichtlich der strenge: gefährlich ist der Fall, in dem jemand einen Türscanner baut, nie von Key Binding gehört hat und etwas ausliefert, das ein Bildschirmfoto aushebelt. Ein lautes Scheitern, das das Problem benennt, ist mehr wert als ein stiller Standard, der es verbirgt.

Bewusst zu akzeptieren winkt niemals einen kaputten Beweis durch. acceptWithoutHolderProof deckt den Fall ab, dass gar kein Beweis vorgelegt wurde. Liegt einer vor und scheitert, wird der Nachweis unabhängig von der Option abgelehnt.

Was es kostet

Binden ist in QR-Maßstäben nicht umsonst. Der öffentliche Schlüssel des Inhabers lebt im Nachweis, und der Beweis reist mit der Vorlage mit:

VorlageZeichenQR-Version
statisch, alles sichtbar~74018, scannt gut
nur Volljährigkeit, gebunden aber unbewiesen~132024, dicht
nur Volljährigkeit, mit Inhaberbeweis~160527, zu dicht

Der Beweis selbst umfasst etwa 285 Zeichen. Das zählt weniger, als es aussieht, denn ein Nachweis, der Key Binding beherrscht, wird per Definition auf einem Bildschirm gezeigt, wo der Code groß und hell sein kann. Die Dichtegrenze ist ein Problem für abgenutzte gedruckte Karten, und eine gedruckte Karte hätte Key Binding ohnehin nie geschafft.

Das Größenbudget

Ein QR-Code fasst in seiner größten Fassung rund 4300 alphanumerische Zeichen, aber ein so großer Code ist auf einer zerkratzten Karte oder einem gesprungenen Display unlesbar. Die praktische Obergrenze liegt bei etwa Version 20.

So viel kostet ein realistischer Führerschein. Acht Claims, fünf Jahre Gültigkeit, ein Zeiger auf eine Statusliste, gemessen mit examples/sizes.mjs im Repository:

NachweisZeichenQR-Version
alles sichtbar~74018, scannt gut
alle acht Claims offenlegbar~159027, zu dicht
nur over_18 vorgelegt~111522, immer noch dicht

Die mittlere Zeile ist die unangenehme, und man lernt sie besser hier als nach dem Druck der Karten. Selektive Offenlegung verdoppelt den Nachweis beinahe, denn jeder offenlegbare Claim kostet ein Salt plus einen signierten Digest, und die Digests bleiben in der Nutzlast, ob der Inhaber den Claim zeigt oder nicht. Das ist Absicht: eine Digest-Anzahl, die mit dem Gezeigten schrumpft, würde verraten, was Sie zurückgehalten haben. Praktisch heißt das, die Ersparnis beim Vorzeigen ist kleiner, als die Intuition verspricht. Dreißig Prozent hier, nicht achtzig.

Machen Sie also nur die Claims offenlegbar, die ein Prüfer wirklich einzeln sehen könnte. Zwei oder drei, nicht alle. fits() sagt Ihnen vorher, wo Sie stehen.

Zu base45. Das Küchenwissen sagt, es werde wegen der Kompaktheit gewählt. Wird es nicht. base45 im alphanumerischen QR-Modus kostet etwa 8,25 Bit je Ursprungsbyte, gegenüber 10,67 für base64 im Byte-Modus und glatt 8 für rohes Binär. Es schlägt base64 deutlich und verliert knapp gegen rohe Bytes. Rohe Bytes werden absichtlich aufgegeben, weil der Byte-Modus Zeichensatz-Mehrdeutigkeit mitschleppt und viele Lesegeräte eine verstümmelte Zeichenkette zurückgeben. Ein Nachweis, der Kopieren, Einfügen und Protokollieren übersteht, ist drei Prozent Aufschlag wert.

Sperrprüfung offline

Die Sperrprüfung ist der Teil, den Implementierungen auslassen, und dann funktioniert ein gestohlener Nachweis für immer.

Eine Statusliste ist eine komprimierte Bitfolge mit einem Bit je Nachweis. Eine Liste für eine Million Nachweise sind 125 KB fast nur aus Nullen, die auf wenige Kilobyte schrumpfen. Holen Sie sie, wenn Sie Empfang haben, prüfen Sie sie, wenn nicht.

0
const statusList = await createStatusList({
  issuer: 'https:1
  kid: 4,
  key: issuerPrivateJwk,
  uri: 'https:2
  size: 1_000_000,
  revoked: [48219],
  expiresIn: 5,
})

3
const result = await verify(scanned, {
  trust,
  status: cachedStatusList,
  maxStatusAge: 6,
})

Die Ablehnungen lohnen das Lesen, denn jede ist eine Stelle, an der eine stillere Bibliothek ein falsches Ja zurückgäbe:

  • Sie haben keine Liste übergeben, die Sperrung wurde also nie geprüft: status_unavailable
  • Ihre zwischengespeicherte Liste ist älter als maxStatusAge, eine Sperrung lässt sich also nicht ausschließen: status_list_stale
  • Die Liste ist mit einem Schlüssel signiert, der nicht auf Ihrer Vertrauensliste steht, und genau so würde ein Angreifer einen gesperrten Nachweis reinwaschen: bad_signature
  • Die Liste stammt von einem anderen Aussteller als der Nachweis, oder gilt für eine andere uri. Ein und derselbe Index bedeutet in jeder Liste etwas anderes, eine ungebundene Liste ist also keine Antwort: status_unavailable
  • Der Index des Nachweises liegt außerhalb der Liste, die Sie zwischengespeichert haben, es wurde also tatsächlich nichts gelesen: status_unavailable
Ein sauberes Ergebnis schweigt darüber nie. Wurde die Sperrung tatsächlich geprüft, sagt das Ergebnis revocationChecked: true. Kann die Bibliothek keine echte Antwort erreichen, lehnt sie ab, statt den Nachweis mit gesetzter Marke durchzulassen, denn eine falsche Zusicherung ist schlimmer als keine Antwort.

Was zu tun ist, wenn Sie sich nicht sicher sein können, ist eine Richtlinienfrage Ihres Einsatzes, nicht der Bibliothek, also weigert sich qredential, sie stillschweigend für Sie zu entscheiden. Wurde die Sperrung geprüft, sagt das erfolgreiche Ergebnis das mit revocationChecked: true.

Vertrauenslisten

Das Einzige, was auf anderem Weg auf das Gerät gelangen muss, ist die Menge der öffentlichen Ausstellerschlüssel, denen Sie zu glauben bereit sind. Sie ändert sich selten, sie mit der App auszuliefern und wöchentlich aufzufrischen ist also eine völlig vernünftige Verteilstrategie.

const trust = {
  issuers: {
    'https:0
      name: 1,
      keys: [{ kid: 2, alg: 3, jwk: publicJwk }],
    },
  },
}

Der vertrauenswürdige Schlüssel entscheidet, welcher Algorithmus verwendet wird, niemals der im Header des Nachweises genannte. Das ist die gesamte Abwehr gegen Algorithmus-Unterschiebung, und deshalb steht alg am Schlüssel in Ihrer Vertrauensliste, statt hergeleitet zu werden.

Wie die Liste auf das Gerät kommt, wie Schlüssel rotieren und wo der private Schlüssel liegt, ist außerhalb des Rahmens. Die Bibliothek nimmt das als Eingabe entgegen.

API-Referenz

Vier Funktionen decken den Lebenszyklus des Nachweises ab, dazu eine für Aussteller, die Sperrlisten veröffentlichen.

issue(options)

issue(options: IssueOptions): Promise<IssueResult>
OptionTypBedeutung
issuerstringKennung, die zu einem Schlüssel in der Vertrauensliste des Prüfers passen muss.
keyJwkPrivater Schlüssel. Verlässt den Aufruf nie.
kidstringSchlüsselkennung, in den Header geschrieben, damit Prüfer bei einer Rotation den richtigen Schlüssel wählen.
alg'ES256' | 'EdDSA'Standard ES256, das jede Plattform unterstützt.
claimsobjectWas der Nachweis behauptet.
disclosestring[]Namen der Claims, die der Inhaber zurückhalten darf. Alles andere ist immer sichtbar. Ein nicht vorhandener Name wirft.
vctstringNachweistyp, das vct von SD-JWT VC.
subjectstringOptionale Kennung der betroffenen Person.
expiresInnumber | stringSekunden, oder eine Dauer wie '1825d'.
notBeforenumber | stringDieselben Formen, für Nachweise, die später beginnen.
status{ idx, uri }Der Platz dieses Nachweises in einer Statusliste.
holderKeyJwkDer öffentliche Schlüssel des Inhabers, in cnf geschrieben. Nur bei statischen Nachweisen weglassen. Ein Schlüssel mit privatem Anteil wird abgelehnt.

Liefert credential (die kombinierte SD-JWT-Form, diese gehört in die Wallet), qr (den scanbaren Umschlag), bytes (Zeichen in qr) und disclosable (die Claim-Namen, die der Inhaber zurückhalten kann).

present(credential, options)

present(credential: string, options: { disclose: string[]; keyBinding?: KeyBindingRequest }): Promise<string>

Verengt einen Nachweis auf die aufgeführten Claims und liefert einen scanbaren Umschlag. Das signierte JWT wird nie angefasst, die Signatur des Ausstellers gilt also weiterhin für das, was übrig bleibt. Nimmt sowohl die kombinierte Form als auch einen Umschlag entgegen. Nach einem Claim zu fragen, den der Aussteller nicht offenlegbar gemacht hat, wirft.

disclose nimmt Pfade entgegen, dieselben, die verify() in disclosed zurückgibt:

await present(credential, {
  disclose: [0, 1, 2],
})

Ein bloßer Name ist ein Pfad aus einem Segment, 'over_18' bedeutet also, was es immer bedeutet hat. Array-Indizes sind Positionen im Nachweis, wie er ausgestellt wurde, nicht in der Vorlage, sodass ein Auswähler weiterhin sagt, was er gesagt hat, was der Inhaber sonst auch zurückhält. Und eine verschachtelte Offenlegung darf nicht ohne die sie enthaltende reisen, address.locality schickt also auch address mit, für Sie aufgelöst statt dem Aufrufer überlassen.

verify(input, options)

verify(input: string, options: VerifyOptions): Promise<VerifyResult>
OptionTypBedeutung
trustTrustListPflicht. Aussteller und Schlüssel, denen Sie zu glauben bereit sind.
statusstringEin zwischengespeichertes Statuslisten-Token. Ohne es lässt sich ein Nachweis, der auf eine verweist, nicht freigeben.
maxStatusAgenumber | stringDie Antwort verweigern, wenn die Liste älter ist als dies.
clockSkewnumberToleranz in Sekunden für Uhrendrift zwischen Aussteller und Prüfer. Standard 60.
noncestringDie Aufforderung, die dieser Prüfer für diesen Scan ausgegeben hat. Pflicht, um einen Inhaberbeweis anzunehmen.
audiencestringDie Kennung dieses Prüfers, gegen das aud des Beweises geprüft.
acceptWithoutHolderProofbooleanEine Vorlage ohne Beweis annehmen. Nötig für statische Nachweise, und winkt nie einen vorhandenen, kaputten Beweis durch.
maxKeyBindingAgenumber | stringWie alt ein Inhaberbeweis sein darf. Standard 5 Minuten.
nownumberDie aktuelle Zeit überschreiben. Für Tests und Replay-Analyse.

Wirft bei feindseliger Eingabe nie. Liefert eine unterschiedene Union: bei Erfolg { ok: true, claims, issuer, subject, issuedAt, expiresAt, disclosed, withheld, revocationChecked }, bei Misserfolg { ok: false, reason, message } mit reason aus der Tabelle weiter unten.

withheld zählt die offenlegbaren Claims, die nicht mitgereist sind. Das hilft bei Richtlinien, und konstruktionsbedingt kann es Ihnen nicht sagen, welche es waren.

Alles in claims beschreibt die betroffene Person. Registrierte Claims, die das Token beschreiben, etwa iss, iat, exp und status, erscheinen stattdessen als typisierte Felder, und eine Offenlegung, die eines davon setzen oder einen von der Nutzlast bereits festgelegten Claim überschreiben will, lässt den ganzen Nachweis scheitern. Über claims zu iterieren ist daher sicher.

fits(payload, errorCorrection)

fits(payload: string, errorCorrection?: 'L' | 'M' | 'Q' | 'H'): FitResult

Synchron. Liefert { chars, version, capacity, comfortable, errorCorrection, advice }. Die Fehlerkorrektur ist standardmäßig M, weil L auf dem Papier großzügig wirkt und dann auf einer abgenutzten gedruckten Karte versagt. version ist null, wenn nichts die Nutzlast fasst, und advice ist ein Satz, den Sie direkt in ein Build-Log schreiben können.

createStatusList(options)

createStatusList(options): Promise<string>

Für Aussteller. Nimmt issuer, key, kid, alg, uri, size, revoked, suspended, expiresIn und issuedAt entgegen und liefert ein signiertes Statuslisten-Token. Ein Index außerhalb der Liste wirft, statt das Bit eines benachbarten Nachweises zu beschädigen.

Ebenfalls exportiert

pack, unpack und isEnvelope für den QR-Umschlag sowie encodeBase45 und decodeBase45 für den Codec allein. Nützlich für Werkzeuge, im gewöhnlichen Gebrauch nicht nötig.

Fehlerbehandlung

Es gibt genau zwei Zusagen, und die Trennung ist absichtlich, nicht zufällig. Merken Sie sich diese zwei Sätze, und Sie können einen einzigen catch-Block schreiben und wissen, was darin landen kann.

1. verify() wirft nie. Bei jeder Eingabe. 2. Alles andere wirft ausschließlich QredentialError.

Der Grund für den Unterschied: verify() ist dafür da, auf feindselige Eingabe gerichtet zu werden, und ein Prüfer, der wirft, ist einer, den jemand in ein try/catch wickelt, das die Leute durchwinkt. Also liefert er ein Ergebnis, das Sie ansehen müssen. Alles andere scheitert an Bedingungen, die der Aufrufer im Code behebt, und dort ist Werfen die richtige Form.

Beide Regeln werden von eigenschaftsbasierten Tests gehalten, die zufällige Zeichenketten, fehlerhafte Schlüssel, beschädigte Umschläge und feindselige Optionen erzeugen und behaupten, dass nichts sonst entkommt. Kein SyntaxError aus einem JSON-Parse, kein DOMException von WebCrypto, kein RangeError aus einer Speicheranforderung.

Ein Prüfergebnis behandeln

Das Ergebnis ist eine unterschiedene Union, TypeScript engt es also für Sie ein:

const result = await verify(scanned, { trust })

if (result.ok) {
  result.claims          0
  result.withheld        1
} else {
  switch (result.reason) {
    case 2:            return askForARenewal()
    case 3:            return refuseAndLog()
    case 4:  return retryWhenOnline()
    default:                   return refuse(result.reason)
  }
}

Ist Ihr Code um try/catch herum gebaut, schicken Sie dasselbe Ergebnis stattdessen durch assertVerified(). Es geht nichts verloren: der geworfene Fehler trägt den ursprünglichen Grund.

import { verify, assertVerified, isQredentialError } from 1

try {
  const credential = assertVerified(await verify(scanned, { trust }))
  admit(credential.claims)
} catch (error) {
  if (isQredentialError(error) && error.code === 2) {
    refuse(error.reason)   0
  } else {
    throw error
  }
}

Ablehnungsgründe

Von verify() in result.reason geliefert. Der Playground feuert jeden davon auf den Prüfer ab, damit Sie ihn ankommen sehen.

GrundWas passiert ist
malformedÜberhaupt kein Nachweis, ein beschädigter Umschlag, oder eine kombinierte Form ohne abschließendes Trennzeichen.
unknown_issuerDer iss-Claim steht nicht auf Ihrer Vertrauensliste.
unknown_keyDer Aussteller ist vertrauenswürdig, hat aber keinen Schlüssel mit diesem kid.
unsupported_algDer Header verlangt einen Algorithmus, den der vertrauenswürdige Schlüssel nicht verwendet.
bad_signatureDie Signatur geht nicht auf. Deckt auch eine gefälschte Statusliste ab.
expiredNach exp, jenseits der Uhrentoleranz.
not_yet_validVor nbf.
digest_mismatchEine Offenlegung, die der Aussteller nie signiert hat, eine doppelt gesendete, eine mit dem Namen eines registrierten Claims, oder eine, die mit einem bereits in der Nutzlast vorhandenen Claim kollidiert.
revokedDer Aussteller hat das Bit dieses Nachweises gesetzt. Deckt auch Aussetzung ab.
status_unavailableDie Sperrung ließ sich nicht bestimmen: keine Liste übergeben, unlesbar, an einen anderen Aussteller oder eine andere uri gebunden, oder der Index liegt außerhalb.
status_list_staleIhre zwischengespeicherte Liste ist älter als maxStatusAge.
holder_proof_missingEs wurde kein Inhaberbeweis vorgelegt und der Aufrufer hat acceptWithoutHolderProof nicht übergeben.
holder_proof_invalidEin Beweis lag vor und scheiterte: falscher Schlüssel, falsche Nonce, falsche Audience, andere Menge an Offenlegungen, veraltet, oder an einen Nachweis ohne gebundenen Schlüssel geheftet.

Geworfene Fehler

Alles außer verify() wirft QredentialError, der einen code trägt. Nutzen Sie isQredentialError() statt instanceof: es prüft die Gestalt und funktioniert daher weiter, wenn zwei Kopien des Pakets im selben Abhängigkeitsbaum landen, was der übliche Grund dafür ist, dass instanceof still aufhört zu greifen.

CodeGeworfen vonWas passiert ist
invalid_optionissue, createStatusListEin Argument, mit dem die API nichts anfangen kann: ein unbekannter Claim-Name, eine ungültige Dauer, ein Statusindex außerhalb der Liste.
not_disclosablepresentSie wollten einen Claim zeigen, den der Aussteller nie offenlegbar gemacht hat.
malformed_credentialpresentDie kombinierte SD-JWT-Form ist fehlerhaft.
malformed_envelopeunpack, presentDer QR-Umschlag ist fehlerhaft. Sehen Sie in cause nach dem darunterliegenden Codec-Fehler.
malformed_status_listLesen der StatuslisteDas Token ist keine lesbare Statusliste.
invalid_encodingdecodeBase45Text, der base45 oder base64url sein sollte, ist es nicht, oder ein Segment ist kein JSON.
unsupported_algissueEin Algorithmus, den diese Fassung nicht umsetzt.
unsupported_runtimeunpackDer Plattform fehlt etwas Nötiges, etwa DecompressionStream auf älterem React Native.
crypto_failureissue, createStatusListWebCrypto hat einen Schlüssel oder eine Operation abgelehnt. Die ursprüngliche DOMException steht in cause.
verification_failedassertVerifiedNur aus diesem Helfer. Trägt den ursprünglichen reason.

Lesen, was darunter liegt

Wo diese Bibliothek fremdes Scheitern einwickelt, behält sie das Original in der Standardeigenschaft cause, sodass ein genauer Code Sie nie das Detail kostet:

try {
  await unpack(scanned)
} catch (error) {
  if (isQredentialError(error)) {
    error.code           0
    error.cause          1
  }
}
Codes sind API, Meldungen nicht. Jeder Wert von code und reason fällt unter semantische Versionierung: ein Wert wird nie umgewidmet, und neue kommen nur in einer Minor-Version dazu. Der Meldungstext darf sich in einem Patch ändern, verzweigen Sie also über den Code und geben Sie die Meldung aus. Ein Test im Repository fixiert die vollständige Menge der Codes, sodass einen hinzuzufügen oder zu entfernen ein bewusster Akt sein muss und kein Nebeneffekt.

Was dies nicht ist

  • Keine Wallet. Keine Oberfläche, kein Speicher.
  • Keine Schlüsselverwaltung. Sie bringen Ihre Schlüssel und Ihre eigene Verteilung der Vertrauensliste mit.
  • Noch kein ISO 18013-5 mDL. Das ist CBOR und COSE statt JWT. Es steht auf der Liste, und die Hälfte eines Konformitätsstandards zu behaupten ist schlimmer, als ihn gar nicht zu behaupten.
  • Nicht auditiert. Die Bibliothek setzt veröffentlichte Standards um und wird gegen die Angriffe im Playground getestet, aber Tests belegen das Vorhandensein von Abwehr, nie deren Vollständigkeit.

Der Quellcode umfasst etwa 1.300 Zeilen ohne jede Laufzeitabhängigkeit, genau damit es realistisch ist, ihn zu lesen, bevor Sie ihm vertrauen. Schwachstellen laufen über den Security-Reiter auf GitHub, und SECURITY.md legt Umfang und Antwortzeiten fest.

Diese Übersetzung wurde noch von niemandem mit Deutsch als Muttersprache gegengelesen. Sie entstand mit Hilfe einer KI aus dem englischen Text, der maßgeblich bleibt. Wenn ein Satz falsch ist, holprig klingt oder einen Begriff verwendet, den niemand benutzt, ist das ein Fehler, und eine Meldung ist willkommen. Diese Seite korrigieren