WebAuthn is a relatively new API for authentication, and it uses public key cryptography instead of something like passwords.

I am wondering if it is possible to use the cryptographic part for a different purpose, specifically creating digital signatures of documents in the browser.

The idea is to find a way to have a user sign a document in the browser in a way that the server can't fake or manipulate. So the web application itself must never know the private key that is used for signing, which is the case for WebAuthn as far as I understand.

So if instead of the random challenge that WebAuthn expects I'd send the contents of a document I want the user to sign, I should get back a cryptographically signed hash of my document, if I understand the explanation on MDN correctly.

In the end I should have a digital signature that could be used to prove that this document was signed by a specific private key from a specific hardware token. And neither the web application inside the browser nor the server component ever saw the private key, and can't fake this signature (of course the application could compromise this by switching out the content before signing, but not later).

I couldn't find anything on someone using WebAuthn for this purpose, and anything I could find related to digital signatures in the browser was about stuff like Java applets that is simply not an option today anymore.

Is my idea sound in general? Or am I misunderstanding how WebAuthn works and it's simply not possible to use it in this way? Are there any weaknesses or flaws in this approach that I'm missing?


3 Answers 3


It is possible, however there are caveats that you need to keep in mind.

Webauthn basically works by the Authenticator signing the challenge provided by the web app (technical term: Relying Party). In the standard flow the challenge is randomly generated by the web app, but it is merely a bytestring, so, in theory, you can put anything there, e.g. the hash of the document.

Now, the caveats:

  1. The random challenge is there to prevent replay attacks. When you replace it with a hash, it becomes deterministic. It is, likely, not an problem for the document signing scenario, but it is important to understand the limitations.

  2. The Authenticator actually signs not the challenge, but rather a pair of client data and authenticator data (the former contains the challenge in it). So, when verifying the signature, it will not be enough to have just the document.

    • Client data is not hard to reconstruct – you will only need the signed document (to compute its hash and thus the challenge value) and the origin of your web app (e.g. https://docsign.example.com).
    • Authenticator data, on the other hand, is produced by the authenticator device, so there is no way to recreate it. Your only option is to store it alongside the signature of the document.

In practical terms, this means that when you call navigator.credentials.get(), it will produce a PublicKeyCredential, which, apart from other things, will contain three interesting fields in its response object: clientDataJSON, authenticatorData, and signature. You, of course, will want to keep the signature, but in order for it to be verifiable in the future you also need to keep the authenticator data and always carry it around with the signature (one can even say that your signature consists of both signature and authenticatorData). You can keep clientDataJSON if you want too as well, but you don’t have to, as it is easy to reconstruct (assuming the verifier knows the domain of the service used for signing).


Yes, this is possible

A security challenge for the Web Authentication API can by any byte array (of at least 16 bytes), and it will be signed by the clients secure private key. It can therefore also be used to sign documents or messages of all types by passing the document as a PublicKeyCredentialRequestOptions.challenge in the call to navigator.credentials.get().

The PublicKeyCredential returned by the call contains a response property, which contains a signature property which is the signature you are looking for.

The example code from developer.mozilla.com slightly modified to fit your use-case:

fetch(/* get file from server */).then(response => response.arrayBuffer()).then(fileFromServer => {
    const options = {
        challenge: fileFromServer,
        rp: {
            name: "Example CORP",
            id  : "login.example.com"
        user: {
            id: userId,
            name: "[email protected]",
            displayName: "John Doe"
        pubKeyCredParams: [
                type: "public-key",
                alg: -7
    navigator.credentials.get({  publickey: options })
        .then(function (pubKeyCredential) {
            const signature = pubKeyCredential.response.signature

            fetch(/* Send signature to server */)
    }).catch(function (err) {
      // Deal with any error

This is technically possible but doesn't necessarily provide you the guarantees you're looking for

If you're gonna call navigator.credentials.get() all the user will be prompted with is the browser WebAuthn dialog asking "Do you want to sign-in with <domain> with <username>?" (or something like that).

Even if the user clicks "yes" and provides the security key gesture (Face ID, touch, PIN, whatever), never was the actual transaction shown to the user in that native WebAuthn dialog.

So even though the user might be "signing" the transaction, because you made the transaction data somehow part of the challenge data that WebAuthn will sign, the user didn't actually explicitly consent to the transaction––the user only consented to signing in.

Evil JavaScript on the web application might have taken over and presented the user with transaction details that look proper, but under the hood was executing another, malicious, transaction.

I recommend to read this GitHub thread: https://github.com/w3c/webauthn/issues/1396

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .