Signatur
Beschreibung
AuthenticatorAssertionResponse ist Teil der Web Authentication API (WebAuthn). Bei einer Anmeldung mit Passkey oder Sicherheitsschlüssel ruft man navigator.credentials.get({ publicKey }) auf. Das Promise wird mit einem PublicKeyCredential erfüllt. Dessen Eigenschaft response ist eine Instanz dieser Schnittstelle. Sie enthält den Beweis, dass der Nutzer den privaten Schlüssel besitzt: Der Authenticator hat die Challenge des Servers zusammen mit weiteren Daten signiert.
Die Schnittstelle erbt von AuthenticatorResponse und stellt diese Eigenschaften bereit (alle schreibgeschützt):
clientDataJSON(ArrayBuffer, geerbt): UTF-8-kodiertes JSON mittype("webauthn.get"),challengeundorigin.authenticatorData(ArrayBuffer): Hash der RP-ID, Flags (z. B. User Present und User Verified) sowie der Signaturzähler.signature(ArrayBuffer): Signatur überauthenticatorData || SHA-256(clientDataJSON), erstellt mit dem privaten Schlüssel des Credentials.userHandle(ArrayBuffer | null): Die bei der Registrierung vergebene Benutzer-ID (user.id). Bei auffindbaren Credentials (Passkeys) ist sie in der Regel gesetzt.attestationObject(ArrayBuffer | null): Nur vorhanden, wenn bei der Assertion eine Attestierung angefordert wurde (neuere Spezifikation, selten unterstützt).
Instanzen erzeugt ausschließlich der Browser. Es gibt keinen öffentlichen Konstruktor. Im Client werden die Binärdaten nur kodiert (meist Base64URL) und an den Server geschickt. Die eigentliche Prüfung der Signatur mit dem gespeicherten öffentlichen Schlüssel, der Challenge, des Origins, der RP-ID und des Zählers muss immer serverseitig stattfinden.
Rückgabewert
PublicKeyCredential.response erreichbar, nachdem navigator.credentials.get() erfolgreich war.Beispiele
Anmeldung mit Passkey und Zugriff auf die Assertion
// Challenge kommt vom Server (hier nur zur Demonstration zufällig erzeugt)
const challenge = crypto.getRandomValues(new Uint8Array(32));
const credential = await navigator.credentials.get({
publicKey: {
challenge,
rpId: location.hostname,
userVerification: 'preferred',
timeout: 60000
}
});
const response = credential.response; // AuthenticatorAssertionResponse
console.log(response instanceof AuthenticatorAssertionResponse);
console.log(response.signature.byteLength > 0);
console.log(response.userHandle !== null);
clientDataJSON und authenticatorData clientseitig lesen (Debugging)
const inspectAssertion = (response) => {
const clientData = JSON.parse(new TextDecoder().decode(response.clientDataJSON));
const authData = new Uint8Array(response.authenticatorData);
const flags = authData[32];
const userPresent = Boolean(flags & 0x01);
const userVerified = Boolean(flags & 0x04);
const signCount = new DataView(response.authenticatorData).getUint32(33, false);
return { type: clientData.type, origin: clientData.origin, userPresent, userVerified, signCount };
};
const cred = await navigator.credentials.get({
publicKey: { challenge: crypto.getRandomValues(new Uint8Array(32)) }
});
console.log(inspectAssertion(cred.response));
Assertion Base64URL-kodiert an den Server senden
const toBase64Url = (buffer) =>
btoa(String.fromCharCode(...new Uint8Array(buffer)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const login = async () => {
const { challenge } = await (await fetch('/webauthn/login/options')).json();
const cred = await navigator.credentials.get({
publicKey: {
challenge: Uint8Array.from(atob(challenge.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0))
}
});
const { response } = cred;
const payload = {
id: cred.id,
rawId: toBase64Url(cred.rawId),
type: cred.type,
response: {
clientDataJSON: toBase64Url(response.clientDataJSON),
authenticatorData: toBase64Url(response.authenticatorData),
signature: toBase64Url(response.signature),
userHandle: response.userHandle ? toBase64Url(response.userHandle) : null
}
};
const res = await fetch('/webauthn/login/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
console.log(res.ok ? 'Angemeldet' : 'Prüfung fehlgeschlagen');
};
login();
Kurzform mit toJSON()
// Neuere Browser serialisieren das Credential direkt (Base64URL)
const cred = await navigator.credentials.get({
publicKey: { challenge: crypto.getRandomValues(new Uint8Array(32)) }
});
const json = cred.toJSON();
console.log(Object.keys(json.response));
// Wichtig · Fallstricke
- Sicherheit: Verlass dich nie auf eine Prüfung im Client. Der Server muss prüfen, dass die Challenge stimmt und nur einmal verwendet wird, dass der
originund der RP-ID-Hash passen und dass die Flags (UP/UV) gesetzt sind. Außerdem muss er die Signatur mit dem gespeicherten öffentlichen Schlüssel prüfen. Die Challenge sollte vom Server kommen, kryptografisch zufällig sein und nur kurz gelten. - Signaturformat: Bei ES256 ist die Signatur ASN.1/DER-kodiert und nicht roh (r||s). Wer mit
crypto.subtle.verify()prüft, muss sie vorher umwandeln. - Signaturzähler: Viele Passkey-Anbieter (z. B. synchronisierte Passkeys von Apple oder Google) liefern immer
0. Ein Zähler von 0 ist also kein Hinweis auf ein geklontes Credential. userHandlekann bei nicht auffindbaren Credentials (mitallowCredentials)nullsein.- Sicherer Kontext: WebAuthn funktioniert nur über HTTPS (oder
localhost). In Cross-Origin-iframes ist es nur mit der Permissions-Policypublickey-credentials-getverfügbar. - Kompatibilität: Wird in allen modernen Browsern unterstützt.
attestationObjectundPublicKeyCredential.prototype.toJSON()sind neuer und nicht überall vorhanden. Prüfe vorher auf ihre Verfügbarkeit.