Skip to content

Instantly share code, notes, and snippets.

@bseddon
Last active September 13, 2023 08:05
Show Gist options
  • Save bseddon/f848f1d532fd1488b9b4fd1f47fa6b37 to your computer and use it in GitHub Desktop.
Save bseddon/f848f1d532fd1488b9b4fd1f47fa6b37 to your computer and use it in GitHub Desktop.
Using crypto.subtle to sign text and verify a signature using an external asymmetric key pair

Subtle is the modern way to perform cryptographic task in JavaScript. However, examples provided in the Mozilla documentation or in their GitHub pages only show how to use the subtle functions to sign text and verify signatures using a key pair generated by subtle.

In my use case the key pair MUST be generated a recognised authority so how can the CryptoKey required by the subtle Sign and Verify functions be created from an external source. On this the documentation and examples are not very helpful so this gist provides a worked example showing how to do it.

Use case

In my case a signature is to be added to a document in the manner specified by XAdES which is defined by ETSI on behalf of the EU commission. The public key of the key pair used MUST be part of an X509 certificate created by a Trusted Service Provider (TSP) so the certificate and signatures created have legal standing. A TSP may be a member state government or an agent authorised by one of the member state governments. That is, the private key and certificate used cannot be generated on the fly.

It turns out that subtle does yet not have handy functions to work with the standard formats used to store keys and certificates such as the various PKCS formats like 8 (PEM), 12 (certificate store).

Example

Subtle includes the function importKey function that takes as a parameter a key in a DER encoding contained within an ArrayBuffer. A private key in a PEM format can be converted to its DER representation using only simple JavaScript functions. However the same is not true of the public key contained within an X509 certificate.

To facilitate loading and manipulating the certificate the example uses the Forge project which is also available as an NPM package. Specifically the pki and asn1 modules are used. Any other package with similar functionality can be used.

The pki module is used to load and access the private key and certificate. The asn1 module is used to convert keys to the DER encoding which the subtle importKey function requires.

<html>
    <head>
        <script src="https://cdn.jsdelivr.net/npm/node-forge@0.7.0/dist/forge.min.js"></script>
    </head>
    <body>
        <script>

            // Your private key in PEM format
            const pemPrivateKey = `-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwgg...

  YOUR PRIVATE KEY

...LYdSNb+6/p1wqZaY6mdVtTQ==
-----END PRIVATE KEY-----`;

            // Your signature in PEM format that contains a 
            // public key that corresponds to the private key
            const pemCertificate = `-----BEGIN CERTIFICATE-----
MIIDzDCCA3KgAwIBAgIUMaJrv...

    YOUR CERTIFICATE THAT CORRESPONDS TO THE PRIVATE KEY
          
...eu/CH98OmptagwG0FV0rSw==
-----END CERTIFICATE-----`;

            window.addEventListener("load", function()
            {
                // Set the key encryption parameters
                const signatureScheme = 'RSASSA-PKCS1-v1_5';
                const exponent = new Uint8Array([0x01, 0x00, 0x01]);
                const encryption = {name: signatureScheme, hash: {name: "SHA-256"}};

                // The payload to sign
                const message = "My Message";

                // Sign a string using a private key then verify the signature using a corresponding public key.  
                // In this case from subject public key information (SPKI) of a certificate.
                createKeyFromPrivateKeyPEM(pemPrivateKey, encryption)
                    .then(function(key)
                    {
                        const messageData = new TextEncoder().encode(message);
                        window.crypto.subtle.sign({name: signatureScheme}, key, messageData)
                            .then( signature =>
                            {
                                // Get the public key from a certificate signed by the private key used
                                createKeyFromCertificatePEM(pemCertificate,encryption)
                                    .then(key =>
                                    {
                                        // Use the public key to verify the signature
                                        window.crypto.subtle.verify(signatureScheme, key, signature, messageData )
                                            .then(success =>
                                            {
                                                console.log(success ? 'success' : 'failed');
                                            });
                                    });
                            });
                    })
                    .catch( reason => 
                    {
                        console.log(reason);
                    });
            });

            // from https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String
            /**
             * Convert a string to an ArrayBuffer.  In this example, the string will be
             * a DER encoding of a private or public key
             * @param string str A string representation of a set of ordinal octal values
             * @return ArrayBuffer
             */
            function str2ab(str) 
            {
                const buf = new ArrayBuffer(str.length);
                const bufView = new Uint8Array(buf);
                for (let i = 0, strLen = str.length; i < strLen; i++)
                {
                    bufView[i] = str.charCodeAt(i);
                }
                return buf;
            }

            /**
             * Generate a subtle.CryptoKey for the private key PEM supplied
             * @param pem A string in PEM format representing a private key
             * @param object encryption An object containing the parameters that are used to define the key encryption
             * @return CryptoKey
             */
            function createKeyFromPrivateKeyPEM(pem,encryption)
            {
                // Convert the PEM representation to its DER encoding
                var privateKey = forge.pki.privateKeyFromPem(pem);
                var asn1 = forge.pki.privateKeyToAsn1(privateKey);
                var rsa = forge.pki.wrapRsaPrivateKey(asn1);
                var der = forge.asn1.toDer(rsa);

                return createKeyFromPrivateKeyDER(der.data, encryption);
            }

            /**
             * Generate a subtle.CryptoKey for the private key DER string supplied
             * @param der A string in DER format representing a private key
             * @param object encryption An object containing the parameters that are used to define the key encryption
             * @return CryptoKey
             */
             function createKeyFromPrivateKeyDER(der,encryption)
            {
                // Get a der encoded object to use in the key subtle key creation
                return window.crypto.subtle.importKey(
                    "pkcs8",
                    str2ab(der), // Create an array from the der data string
                    encryption,
                    false,
                    ["sign"] // pkcs8 (private key) can only be used to sign documents
                )
            }

            /**
             * Generate a subtle.CryptoKey for the certificate PEM supplied
             * @param string pem A string in PEM format representing a certificate that contains the public key of interest
             * @param object encryption An object containing the parameters that are used to define the key encryption
             * @return CryptoKey
             */
            function createKeyFromCertificatePEM(pem, encryption)
            {
                // convert a PEM-formatted certificate to a forge certificate
                let cert = forge.pki.certificateFromPem(pem);

                // convert the (forge) public key to an ASN.1 format
                var asn1 = forge.pki.publicKeyToAsn1(cert.publicKey);

                // Get a der encoded object to use in the key subtle key creation
                var der = forge.asn1.toDer(asn1);

                return createKeyFromPublicKeyDER(der.data,encryption);
            }

            /** Generate a subtle.CryptoKey for the private key DER string supplied
             * @param der A string in DER format representing a private key
             * @param object encryption An object containing the parameters that are used to define the key encryption
             * @return CryptoKey
             */
            function createKeyFromPublicKeyDER(der,encryption)
            {
                return window.crypto.subtle.importKey(
                    "spki",
                    str2ab(der), // Create an array from the der data string
                    encryption,
                    false,
                    ["verify"] // spki (public key) can only be used to verify signatures
                )
            }

        </script>
    </body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment