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?

  • Wondering about the same. Were you able to find the answer to this? Aug 16 '19 at 0:32

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: "jdoe@example.com",
            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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.