Charles Engelke's Blog

August 26, 2014

Digital Signatures in the Browser

Filed under: Uncategorized — Charles Engelke @ 5:31 pm
Tags: ,

Digital signatures are kind of a mirror image of public-key encryption. They use essentially the same algorithms and kinds of key pairs, but for authentication instead of secrecy. Alice can send Bob a document that Bob can be certain was created by Alice, and not changed by anyone else, by signing the document with her privateKey. Bob can verify the signature using her publicKey. It’s the reverse of how you use keys for secrecy.

Because digital signatures are closely related to public-key encryption, the Web Cryptography API offers methods to create and use them. This post will build an example web page and related JavaScript to digitally sign a selected file, and then verify (or reject verification of) a signed file. The structure of the page and code are very similar to the encryption sample build in the last post on using the Web Cryptography API. The sample code is available on Github, and a live demonstration is also available.

About Digital Signatures

People have been signing documents for as long as there has been writing. And people having been forging signatures probably nearly as long. Forging an ink signature on paper requires skill, and there are various ways to analyze the signature to indicate whether it was forged or not. But when we start dealing with digital documents, the problem gets harder. It may be legal to “sign” a digital document, perhaps by appending your name to it, but how do we deal with avoiding forgeries? Or even with people repudiating completely valid documents by claiming they are forgeries? After all, anybody can change the bits in a file and there’s no way to know whether that happened.

Digital signatures apply public-key cryptography to make forgeries, or repudiation by claiming forgery, essentially impossible. Recall that anything signed one of a key pair’s keys can be decrypted only by the other member of the same key pair. Encryption used this for secret messages: Alice can send Bob a message that only Bob can read by encrypting it with Bob’s public key, which everybody knows. But only Bob has the matching private key, so only Bob can read the message. But what if Alice encrypted the message with her private key instead? Then anybody could read the message, because everybody knows her public key. But only Alice could have created the encrypted file, because only Alice has the matching private key. If Alice provides a message in both original and encrypted forms, where the encryption used her private key, she cannot credibly repudiate it because nobody but Alice could have created the encrypted version. You could even say that the encrypted version is a signature of the original message.

Digital signatures work almost like this, but with an optimization. Instead of encrypting the entire message to provide a signature, a message is digitally signed by creating a message digest and then encrypting it with the creator’s private key. A message digest is just a hashed value of the original message, so that any change to the message would change the hashed value, and there’s no effective way to create a second message with the same hashed value. Recipients decrypt the encrypted message digest with the purported signer’s public key, and also compute their own message digest on the original message, and compare the two. If they match, the signature is verified. If not, it is considered invalid.

The Web Page

The demonstration web page is almost exactly the same as the one for public-key encryption and decryption, except that it references a different JavaScript file and labels the buttons sign and verify instead of encrypt and decrypt:

<!DOCTYPE html>
<html>
<head>
    <title>Digital Signature</title>
    <script src="digitalsign.js"></script>
</head>
<body>
    <h1>Digital Signature</h1>
    <section id="sign-and-verify">
        <input type="file" id="source-file"/>
        <button id="sign">Sign File</button>
        <button id="verify">Verify Signature</button>
    </section>
    <section id="results">
        Download results:
        <ul id="download-links">
        </ul>
    </section>
</body>
</html>

The basic structure of the JavaScript file is also quite similar to the encryption and decryption example:

document.addEventListener("DOMContentLoaded", function() {
    "use strict";

    if (!window.crypto || !window.crypto.subtle) {
        alert("Your current browser does not support the Web Cryptography API! This page will not work.");
        return;
    }

    var keyPair;
    createAndSaveAKeyPair().
    then(function() {
        document.getElementById("sign").addEventListener("click", signTheFile);
        document.getElementById("verify").addEventListener("click", verifyTheFile);
    }).
    catch(function(err) {
        alert("Could not create a keyPair or enable buttons: " + err.message + "\n" + err.stack);
    });
});

The only difference in this high-level JavaScript is the IDs of the buttons and the names of the click handlers being attached.

Creating a Key Pair

There are only three differences between creating a key pair for encryption and creating one for signing:

  1. The name of the algorithm to use must be one that supports signing and verifying.
  2. Keys used for digital signing must have an additional parameter stating which hash function to use for message digests.
  3. The usages array must include sign and verify instead of encrypt and decrypt.

The public-key encryption sample application used the RSAES-PKCS1-v1_5 algorithm, but the table of registered algorithms shows that can only be used for encryption and decryption. The closely related algorithm RSASSA-PKCS1-v1_5 supports signing and verifying, and is already implemented in Chrome and Firefox, so that’s the one we use. As for the hash function to use, any of the SHA-2 family should be fine, such as SHA-256.

With those minor changes, the code is:

function createAndSaveAKeyPair() {
    return window.crypto.subtle.generateKey(
        {
            name: "RSASSA-PKCS1-v1_5",
            modulusLength: 2048,
            publicExponent: new Uint8Array([1, 0, 1]),  // 24 bit representation of 65537
            hash: {name: "SHA-256"}
        },
        true,   // can extract it later if we want
        ["sign", "verify"]).
    then(function (key) {
        keyPair = key;
        return key;
    });
}

Signing

Once again, the high level structure is the same as before, except for performing a sign operation instead of an encrypt one:

function signTheFile() {
    var sourceFile = document.getElementById("source-file").files[0];
    var reader = new FileReader();
    reader.onload = processTheFile;
    reader.readAsArrayBuffer(sourceFile);

    function processTheFile() {
        var reader = this;              // Was invoked by the reader object
        var plaintext = reader.result;

        sign(plaintext, keyPair.privateKey).
        then(function(blob) {
            var url = URL.createObjectURL(blob);
            document.getElementById("download-links").insertAdjacentHTML(
                'beforeEnd',
                '<li><a href="' + url + '">Signed file</a></li>');
        }).
        catch(function(err) {
            alert("Something went wrong signing: " + err.message + "\n" + err.stack);
        });
    }
}

The sign function, though, is much simpler than the encrypt one was. That’s because the API’s sign function handles creating the message digest for us, instead of our having to perform several additional steps as was needed to create a session key. There’s only one API method needed: window.crypto.subtle.sign. That yields the digital signature to the returned Promise’s then method, which needs to put the signature and original plaintext together into one file. The simple packaging used here is just:

    2-byte-integer-signature-length:signature-length-byte-signature:plaintext

The needed code is:

function sign(plaintext, privateKey) {
    return window.crypto.subtle.sign(
        {name: "RSASSA-PKCS1-v1_5"},
        privateKey,
        plaintext).
    then(packageResults);

    function packageResults(signature) {
        var length = new Uint16Array([signature.byteLength]);
        return new Blob(
            [
                length,     // Always a 2 byte unsigned integer
                signature,  // "length" bytes long
                plaintext   // Remainder is the original plaintext
            ],
            {type: "application/octet-stream"}
        );
    }
}

Verifying

To verify a signature we need to extract the signature and plaintext from the combined package, then check that the signature is valid for that plaintext and public key. The new piece here is informing the user of whether the signature is valid or not, done with the alert function. A download link will only be provided if the signature checks out:

function verifyTheFile() {
    var sourceFile = document.getElementById("source-file").files[0];
    var reader = new FileReader();
    reader.onload = processTheFile;
    reader.readAsArrayBuffer(sourceFile);

    function processTheFile() {
        var reader = this;              // Invoked by the reader object
        var data = reader.result;
        var signatureLength = new Uint16Array(data, 0, 2)[0];   // First 16 bit integer
        var signature       = new Uint8Array( data, 2, signatureLength);
        var plaintext       = new Uint8Array( data, 2 + signatureLength);

        verify(plaintext, signature, keyPair.publicKey).
        then(function(blob) {
            if (blob === null) {
                alert("Invalid signature!");
            } else {
                alert("Signature is valid.");
                var url = URL.createObjectURL(blob);
                document.getElementById("download-links").insertAdjacentHTML(
                    'beforeEnd',
                    '<li><a href="' + url + '">Verified file</a></li>');
            }
        }).
        catch(function(err) {
            alert("Something went wrong verifying: " + err.message + "\n" + err.stack);
        });
    }
}

The verify function yields a Blob containing the plaintext if the signature is verified, and null otherwise. Again, only one API function is needed, because the API handles the message digest operation automatically:

function verify(plaintext, signature, publicKey) {
    return window.crypto.subtle.verify(
        {name: "RSASSA-PKCS1-v1_5"},
        publicKey,
        signature,
        plaintext
    ).
    then(handleVerification);

    function handleVerification(successful) {
        if (successful) {
            return new Blob([plaintext], {type: "application/octet-stream"});
        } else {
            return null;
        }
    }
}

Summing Up

This post is quite a bit shorter than the example of public-key encryption and decryption. That’s partly because there was less exposition needed now, and partly because signing and verifying with the API are quite a bit simpler. As before, this illustrates how to use the API, but has no practical use because there is no key persistence, export, or import performed. I hope to address those things in a future post.

Advertisement

7 Comments

  1. […] posts here have used the Web Cryptography API to encrypt and decrypt files, and to sign and verify them. But those examples have no practical use because the keys being used are stored in JavaScript […]

    Pingback by Saving Cryptographic Keys in the Browser | Charles Engelke's Blog — September 19, 2014 @ 2:39 pm

  2. Amazing article guys should read this blog it will help you to make secure in your business.

    Comment by Digital Signature — January 7, 2015 @ 7:11 am

  3. great post, but how you can use certificates from user keystore?I think you can not do yet

    Comment by pedro — May 7, 2015 @ 4:35 pm

    • No, you can’t get to any system keystore from the Web Cryptography API. At least, not yet. There are some requests for that in the mailing list, but so far no agreement to offer it.

      Comment by Charles Engelke — May 7, 2015 @ 4:42 pm

      • I guess the webcrypto itself can be considered a user keystore, with import and export services. I do not know if you can import certificates issued by a qualified certification service provider

        Comment by pedro — May 8, 2015 @ 1:42 pm

  4. Yes, you can keep your own keystore in an IndexedDB database inside the browser. That keystore is tied to a specific origin, just as other browser resources (like cookies) are. You can import certificates and keys into this store, certainly if they are in standard X.509 and PKCS formats. If the keys are encrypted, though, you might require a decryption algorithm that isn’t supported via web crypto. For example, Microsoft Windows 7 “certificate” export (which also exports the key) uses PFX/PKCS12 format with triple-DES to derive the encryption key.

    Comment by Charles Engelke — May 8, 2015 @ 1:46 pm

  5. Great! This was helpful.

    Comment by Tung Luu — June 5, 2015 @ 6:21 am


RSS feed for comments on this post.

Blog at WordPress.com.

%d bloggers like this: