Start · Sprachen · JavaScript · Referenz · AuthenticatorAssertionResponse.signature

AuthenticatorAssertionResponse.signature

Eigenschaft

Schreibgeschützter <code>ArrayBuffer</code> mit der Signatur des Authenticators über <code>authenticatorData</code> und den SHA-256-Hash von <code>clientDataJSON</code>.

seit JavaScript Baseline: alle modernen Browser (Chrome 67+, Firef Kategorie: webapi

Signatur

AuthenticatorAssertionResponse.signature

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

Typ
ArrayBuffer
Beschreibung
Die rohen Signatur-Bytes des Authenticators. Das Format hängt vom Algorithmus ab (z. B. DER-kodiertes ECDSA bei ES256). Der Wert ist schreibgeschützt.

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);
true 71 // typische Länge einer DER-kodierten ES256-Signatur (70–72 Byte)

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!
true // bei gültiger Signatur

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
  );
};
true