Signatur
Beschreibung
Die schreibgeschützte Eigenschaft signature eines AuthenticatorAssertionResponse-Objekts enthält die Signatur, die der Authenticator (z. B. Sicherheitsschlüssel, Touch ID, Windows Hello oder ein Passkey) bei einer WebAuthn-Anmeldung mit navigator.credentials.get() erzeugt. Signiert wird mit dem privaten Schlüssel des Credentials, der den Authenticator nie verlässt.
Die signierten Daten sind die Aneinanderreihung von authenticatorData und dem SHA-256-Hash von clientDataJSON, also authenticatorData || SHA-256(clientDataJSON). Weil clientDataJSON die vom Server erzeugte challenge und den origin enthält, belegt eine gültige Signatur, dass der Nutzer den privaten Schlüssel besitzt und die Anmeldung frisch für genau diese Website erfolgt ist.
Die Signatur wird im Normalfall auf dem Server geprüft (Relying Party). Dort verwendet man den öffentlichen Schlüssel, der bei der Registrierung (navigator.credentials.create()) gespeichert wurde. Im Browser selbst wird der Wert meist nur base64url-kodiert und zusammen mit authenticatorData, clientDataJSON und der Credential-ID an das Backend geschickt.
Wie die Bytes aufgebaut sind, hängt vom Algorithmus des Credentials ab: Bei ES256 (COSE -7) ist es eine ASN.1/DER-kodierte ECDSA-Signatur, bei RS256 (COSE -257) eine PKCS#1-v1.5-Signatur und bei EdDSA (COSE -8) eine rohe 64-Byte-Ed25519-Signatur.
Rückgabewert
Beispiele
Signatur bei der Anmeldung auslesen
// challenge und credentialId kommen vom Server (hier vereinfacht)
const challenge = crypto.getRandomValues(new Uint8Array(32));
const credential = await navigator.credentials.get({
publicKey: {
challenge,
rpId: location.hostname,
userVerification: 'preferred',
timeout: 60000
}
});
const { signature } = credential.response; // AuthenticatorAssertionResponse
console.log(signature instanceof ArrayBuffer, signature.byteLength);
Assertion base64url-kodiert an den Server senden
const toBase64Url = (buffer) =>
btoa(String.fromCharCode(...new Uint8Array(buffer)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
const sendAssertion = async (credential) => {
const { response } = credential;
const payload = {
id: credential.id,
rawId: toBase64Url(credential.rawId),
type: credential.type,
response: {
authenticatorData: toBase64Url(response.authenticatorData),
clientDataJSON: toBase64Url(response.clientDataJSON),
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)
});
return res.ok;
};
Signatur serverseitig in Node.js prüfen
import { createHash, verify } from 'node:crypto';
// Werte aus dem Request (base64url) und gespeicherter Public Key (PEM)
const verifyAssertion = ({ authenticatorData, clientDataJSON, signature }, publicKeyPem) => {
const authData = Buffer.from(authenticatorData, 'base64url');
const clientData = Buffer.from(clientDataJSON, 'base64url');
const sig = Buffer.from(signature, 'base64url');
// Signierte Daten: authenticatorData || SHA-256(clientDataJSON)
const clientDataHash = createHash('sha256').update(clientData).digest();
const signedData = Buffer.concat([authData, clientDataHash]);
// ES256: Node erwartet standardmäßig DER-kodierte ECDSA-Signaturen
return verify('sha256', signedData, publicKeyPem, sig);
};
// Vorher zusätzlich prüfen: challenge, origin, type === 'webauthn.get',
// rpIdHash und signCount in authenticatorData!
ES256-Signatur für die Web Crypto API umwandeln (DER → r||s)
// Web Crypto erwartet bei ECDSA das rohe Format r||s (64 Byte), nicht DER
const derToRaw = (der) => {
const bytes = new Uint8Array(der);
let offset = 2; // 0x30 + Gesamtlänge
const readInt = () => {
const len = bytes[offset + 1];
let int = bytes.slice(offset + 2, offset + 2 + len);
offset += 2 + len;
while (int.length > 32 && int[0] === 0) int = int.slice(1); // führende Null entfernen
const padded = new Uint8Array(32);
padded.set(int, 32 - int.length);
return padded;
};
const r = readInt();
const s = readInt();
return new Uint8Array([...r, ...s]);
};
const verifyInBrowser = async (publicKey, response) => {
const hash = await crypto.subtle.digest('SHA-256', response.clientDataJSON);
const data = new Uint8Array([
...new Uint8Array(response.authenticatorData),
...new Uint8Array(hash)
]);
return crypto.subtle.verify(
{ name: 'ECDSA', hash: 'SHA-256' },
publicKey, // CryptoKey (ECDSA P-256)
derToRaw(response.signature),
data
);
};