eID Smart Card Signature and Validation in JavaScript

I am creating a form for eID smart card signatures and signature validation in this tutorial. A qualified digital e-signature with a smart card is valid in many countries today. A electronic smart card (eID) is an identity card with a chip that contains an electronic certificate. An eID smart card can be used to create digital signatures.

Electronic signatures with an eID smart card in a web browser requires a smart card reader, software for the smart card reader and an extension in the web browser. You can download a Token Signing extension from chrome web store, from windows store and from firefox add-ons to be able to implement the solution in this tutorial.

Electronic signatures is more secure than ordinary signatures and digital signatures makes it faster and easier to administer signatures on contracts. It is important that electronic signatures can be validated, this tutorial includes code to create signatures and code to validate created signatures. This type of service can be used to collect digital signatures from all parties concerned by an agreement.

This code have been tested and is working with Google Chrome (75.0.3770.100) and Mozilla Firefox (75.0) without any polyfill. It works (SHA-256 and SHA-384) in Internet Explorer (11.829.17134.0) with polyfills for Array.from, Promise, String.prototype.padStart, TextEncoder, WebCrypto, XMLHttpRequest, Array.prototype.includes, CustomEvent, Array.prototype.closest, Array.prototype.remove, String.prototype.endsWith and String.prototype.includes and code transpilation. If you want to support older browsers, check out our post on transpilation and polyfilling of JavaScript. This code depends on annytab.effects, Font Awesome, annytab.notifier, hwcrypto and js-spark-md5.

Models

using System.Security.Cryptography.X509Certificates;

namespace Annytab.Scripts.Models
{
    public class ResponseData
    {
        #region variables

        public bool success { get; set; }
        public string id { get; set; }
        public string message { get; set; }
        public string url { get; set; }

        #endregion

        #region Constructors

        public ResponseData()
        {
            // Set values for instance variables
            this.success = false;
            this.id = "";
            this.message = "";
            this.url = "";

        } // End of the constructor

        public ResponseData(bool success, string id, string message, string url = "")
        {
            // Set values for instance variables
            this.success = success;
            this.id = id;
            this.message = message;
            this.url = url;

        } // End of the constructor

        #endregion

    } // End of the class

    public class Signature
    {
        #region Variables

        public string validation_type { get; set; }
        public string algorithm { get; set; }
        public string padding { get; set; }
        public string data { get; set; }
        public string value { get; set; }
        public string certificate { get; set; }

        #endregion

        #region Constructors

        public Signature()
        {
            // Set values for instance variables
            this.validation_type = null;
            this.algorithm = null;
            this.padding = null;
            this.data = null;
            this.value = null;
            this.certificate = null;

        } // End of the constructor

        #endregion

    } // End of the class

    public class SignatureValidationResult
    {
        #region Variables

        public bool valid { get; set; }
        public string signature_data { get; set; }
        public string signatory { get; set; }
        public X509Certificate2 certificate { get; set; }

        #endregion

        #region Constructors

        public SignatureValidationResult()
        {
            // Set values for instance variables
            this.valid = false;
            this.signature_data = null;
            this.signatory = null;
            this.certificate = null;

        } // End of the constructor

        #endregion

    } // End of the class

} // End of the namespace

Controller

using System;
using System.Text;
using System.Security.Cryptography;
using System.Collections.Generic;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Annytab.Scripts.Models;

namespace Annytab.Scripts.Controllers
{
    public class eidsmartcardController : Controller
    {
        #region Variables

        private readonly ILogger logger;

        #endregion

        #region Constructors

        public eidsmartcardController(ILogger<eidsmartcardController> logger)
        {
            // Set values for instance variables
            this.logger = logger;

        } // End of the constructor

        #endregion

        #region Post methods

        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult validate(IFormCollection collection)
        {
            // Create a signature
            Annytab.Scripts.Models.Signature signature = new Annytab.Scripts.Models.Signature();
            signature.validation_type = "eID Smart Card";
            signature.algorithm = collection["selectSignatureAlgorithm"];
            signature.padding = collection["selectSignaturePadding"];
            signature.data = collection["txtSignatureData"];
            signature.value = collection["txtSignatureValue"];
            signature.certificate = collection["txtSignatureCertificate"];

            // Validate the signature
            SignatureValidationResult result = ValidateSignature(signature);

            // Set a title and a message
            string title = result.valid == false ? "Invalid Signature" : "Valid Signature";
            string message = "<b>" + title + "</b><br />" + signature.data + "<br />";
            message += result.certificate != null ? result.certificate.GetNameInfo(X509NameType.SimpleName, false) + ", " + result.certificate.GetNameInfo(X509NameType.SimpleName, true)
                + ", " + result.certificate.NotBefore.ToString("yyyy-MM-dd") + " to "
                + result.certificate.NotAfter.ToString("yyyy-MM-dd") : "";

            // Return a response
            return Json(data: new ResponseData(result.valid, title, message));

        } // End of the validate method

        #endregion

        #region Helper methods

        public static IDictionary<string, string> GetHashDictionary(byte[] data)
        {
            // Create the dictionary to return
            IDictionary<string, string> hashes = new Dictionary<string, string>(4);

            using (SHA1 sha = SHA1.Create())
            {
                hashes.Add("SHA-1", GetHexString(sha.ComputeHash(data)));
            }

            using (SHA256 sha = SHA256.Create())
            {
                hashes.Add("SHA-256", GetHexString(sha.ComputeHash(data)));
            }

            using (SHA384 sha = SHA384.Create())
            {
                hashes.Add("SHA-384", GetHexString(sha.ComputeHash(data)));
            }

            using (SHA512 sha = SHA512.Create())
            {
                hashes.Add("SHA-512", GetHexString(sha.ComputeHash(data)));
            }

            // Return the dictionary
            return hashes;

        } // End of the GetHashDictionary method

        public static string GetHexString(byte[] data)
        {
            // Create a new Stringbuilder to collect the bytes and create a string.
            StringBuilder sBuilder = new StringBuilder();

            // Loop through each byte of the hashed data and format each one as a hexadecimal string.
            for (int i = 0; i < data.Length; i++)
            {
                sBuilder.Append(data[i].ToString("x2"));
            }

            // Return the hexadecimal string.
            return sBuilder.ToString();

        } // End of the GetHexString method

        public static HashAlgorithmName GetHashAlgorithmName(string signature_algorithm)
        {
            if (signature_algorithm == "SHA-256")
            {
                return HashAlgorithmName.SHA256;
            }
            else if (signature_algorithm == "SHA-384")
            {
                return HashAlgorithmName.SHA384;
            }
            else if (signature_algorithm == "SHA-512")
            {
                return HashAlgorithmName.SHA512;
            }
            else
            {
                return HashAlgorithmName.SHA1;
            }

        } // End of the GetHashAlgorithmName method

        public static RSASignaturePadding GetRSASignaturePadding(string signature_padding)
        {
            if (signature_padding == "Pss")
            {
                return RSASignaturePadding.Pss;
            }
            else
            {
                return RSASignaturePadding.Pkcs1;
            }

        } // End of the GetRSASignaturePadding method

        public static SignatureValidationResult ValidateSignature(Annytab.Scripts.Models.Signature signature)
        {
            // Create the result to return
            SignatureValidationResult result = new SignatureValidationResult();
            result.signature_data = signature.data;

            try
            {
                // Get the certificate
                result.certificate = new X509Certificate2(Convert.FromBase64String(signature.certificate));

                // Get the public key
                using (RSA rsa = result.certificate.GetRSAPublicKey())
                {
                    // Convert the signature value to a byte array
                    byte[] digest = Convert.FromBase64String(signature.value);

                    // Check if the signature is valid
                    result.valid = rsa.VerifyData(Encoding.UTF8.GetBytes(signature.data), digest, GetHashAlgorithmName(signature.algorithm), GetRSASignaturePadding(signature.padding));
                }
            }
            catch (Exception ex)
            {
                string exMessage = ex.Message;
                result.certificate = null;
            }

            // Return the validation result
            return result;

        } // End of the ValidateSignature method

        #endregion

    } // End of the class

} // End of the namespace

HTML and JavaScript

This form has a file upload control that starts the signing process, todays date and the md5-hash of the file is the data that is signed. A user has an option to select algorithm and padding (only one option at the moment). A signature can be validated with a request to a server method.

<!DOCTYPE html>
<html>
<head>
    <title>eId Smart Card</title>
    <style>
        .annytab-textarea{width:300px;height:100px;}
        .annytab-textbox {width:300px;}
    </style>
</head>
<body style="width:100%;font-family:Arial, Helvetica, sans-serif;">

    <!-- Container -->
    <div style="display:block;padding:10px;">

        <h1>eId Smart Card</h1>
        <div>
            You can sign a file with an eID smart card and a smart card reader. To be able to sign files with an eID-card you need a browser extension for smart cards
            and software that comes with your smart card reader. Download <b>Token Signing</b> extension from <a href="https://chrome.google.com/webstore/detail/ckjefchnfjhjfedoccjbhjpbncimppeg">chrome web store</a> or from
            <a href="https://microsoftedge.microsoft.com/addons/detail/fofaekogmodbjplbmlbmjiglndceaajh">windows store</a> or from <a href="https://addons.mozilla.org/sv-SE/firefox/addon/token-signing2/">firefox add-ons.</a>.<br /><br />
        </div><br />

        <!-- Input form -->
        <form id="inputForm">

            <!-- Hidden data -->
            @Html.AntiForgeryToken()

            <div>Select file to sign <span id="loading"></span></div>
            <input id="fuFile" name="fuFile" type="file" onchange="calculateMd5();" class="annytab-textbox" /><br /><br />

            <div>Select algorithm</div>
            <select id="selectSignatureAlgorithm" name="selectSignatureAlgorithm" class="annytab-textbox">
                <option value="SHA-1" selected>SHA-1</option>
                <option value="SHA-256">SHA-256</option>
                <option value="SHA-384">SHA-384</option>
                <option value="SHA-512">SHA-512</option>
            </select><br /><br />

            <div>Select padding</div>
            <select name="selectSignaturePadding" class="annytab-textbox">
                <option value="Pkcs1" selected>Pkcs1</option>
            </select><br /><br />

            <div>Signature data</div>
            <textarea id="txtSignatureData" name="txtSignatureData" class="annytab-textarea"></textarea><br /><br />

            <div>Certificate</div>
            <textarea id="txtSignatureCertificate" name="txtSignatureCertificate" class="annytab-textarea"></textarea><br /><br />

            <div>Signature value</div>
            <textarea id="txtSignatureValue" name="txtSignatureValue" class="annytab-textarea"></textarea><br /><br />

            <input type="button" value="Sign file" class="btn-disablable" onclick="createSignature()" disabled />
            <input type="button" value="Validate signature" class="btn-disablable" onclick="validateSignature()" disabled />

        </form>

    </div>

    <!-- Style and scripts -->
    <link href="/css/annytab.notifier.css" rel="stylesheet" />
    <script src="/js/font-awesome/all.min.js"></script>
    <script src="/js/annytab.effects.js"></script>
    <script src="/js/annytab.notifier.js"></script>
    <script src="/js/crypto/spark-md5.js"></script>
    <script src="/js/crypto/hwcrypto.js"></script>
    <script src="/js/crypto/hex2base.js"></script>
    <script>

            // Set default focus
            document.querySelector('#fuFile').focus();

            // Create a signature
            async function createSignature() {
                // Make sure that the request is secure (SSL)
                if (location.protocol !== 'https:') {
                    annytab.notifier.show('error', 'You need a secure connection (SSL)!');
                    return;
                }

                // Disable buttons
                disableButtons();

                // Get input data
                var data = document.querySelector('#txtSignatureData').value;
                var algorithm = document.querySelector('#selectSignatureAlgorithm').value;
                var hash = await getHash(data, algorithm);

                // Log selected algorithm and hash
                console.log('Algorithm: ' + algorithm);
                console.log('Hash: ' + hash);

                // Get the certificate
                window.hwcrypto.getCertificate({ lang: 'en' }).then(function (response) {

                    // Get certificate
                    certificate = hexToBase64(response.hex);
                    document.querySelector('#txtSignatureCertificate').value = certificate;
                    console.log('Using certificate:\n' + certificate);

                    // Sign the hash
                    window.hwcrypto.sign(response, { type: algorithm, hex: hash }, { lang: 'en' }).then(function (response) {

                        // Get the signature value
                        signature_value = hexToBase64(response.hex);
                        document.querySelector('#txtSignatureValue').value = signature_value;
                        annytab.notifier.show('success', 'Signature was successfully created!');

                        // Enable buttons
                        enableButtons();

                        // Post the form

                    }, function (err) {

                        // Enable buttons
                        enableButtons();

                        if (err.message === 'no_implementation') {
                            annytab.notifier.show('error', 'You need to install an extension for smart cards in your browser!');
                        }
                        else if (err.message === 'pin_blocked') {
                            annytab.notifier.show('error', 'Your ID-card is blocked!');
                        }
                        else if (err.message === 'no_certificates') {
                            annytab.notifier.show('error', 'We could not find any certificates, check your smart card reader.');
                        }
                        else if (err.message === 'technical_error') {
                            annytab.notifier.show('error', 'The file could not be signed, your ID-card might not support {0}.'.replace('{0}', algorithm));
                        }
                    });

                }, function (err) {

                    // Enable buttons
                    enableButtons();

                    if (err.message === 'no_implementation') {
                        annytab.notifier.show('error', 'You need to install an extension for smart cards in your browser!');
                    }
                    else if (err.message === 'pin_blocked') {
                        annytab.notifier.show('error', 'Your ID-card is blocked!');
                    }
                    else if (err.message === 'no_certificates') {
                        annytab.notifier.show('error', 'We could not find any certificates, check your smart card reader.');
                    }
                    else if (err.message === 'technical_error') {
                        annytab.notifier.show('error', 'The file could not be signed, your ID-card might not support {0}.'.replace('{0}', algorithm));
                    }
                });

            } // End of the createSignature method

            // Validate signature
            function validateSignature() {

                // Disable buttons
                disableButtons();

                // Create form data
                var fd = new FormData(document.querySelector('#inputForm'));

                // Post form data
                postFormData('/eidsmartcard/validate', fd, function (data) {
                    if (data.success === true) {
                        annytab.notifier.show('success', data.message);
                    }
                    else {
                        annytab.notifier.show('error', data.message);
                    }

                    // Enable buttons
                    enableButtons();

                }, function (data) {
                    annytab.notifier.show('error', data.message);

                    // Enable buttons
                    enableButtons();
                });

            } // End of the validateSignature method

            // Get a hash of a message
            async function getHash(data, algorithm) {
                // Hash data
                var hashBuffer = await crypto.subtle.digest(algorithm, new TextEncoder().encode(data));

                // Convert buffer to byte array
                var hashArray = Array.from(new Uint8Array(hashBuffer));

                // Convert bytes to hex string
                var hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');

                // Return hash as hex string
                return hashHex;

            } // End of the getHash method

            // #region MD5

            // Convert Md5 to C# version
            function convertMd5(str) {
                return btoa(String.fromCharCode.apply(null,
                    str.replace(/\r|\n/g, "").replace(/([\da-fA-F]{2}) ?/g, "0x$1 ").replace(/ +$/, "").split(" "))
                );

            } // End of the convertMd5 method

            // Calculate a MD5 value of a file
            async function calculateMd5() {

                // Get the controls
                var data = document.querySelector("#txtSignatureData");
                var loading = document.querySelector("#loading");

                // Get the file
                var file = document.querySelector("#fuFile").files[0];

                // Make sure that a file is selected
                if (typeof file === 'undefined' || file === null) {
                    return;
                }

                // Add a loading animation
                loading.innerHTML = '- 0 %';

                // Variables
                var block_size = 4 * 1024 * 1024; // 4 MiB
                var offset = 0;

                // Create a spark object
                var spark = new SparkMD5.ArrayBuffer();
                var reader = new FileReader();

                // Create blocks
                while (offset < file.size) {
                    // Get the start and end indexes
                    var start = offset;
                    var end = Math.min(offset + block_size, file.size);

                    await loadToMd5(spark, reader, file.slice(start, end));
                    loading.innerHTML = '- ' + Math.round((offset / file.size) * 100) + ' %';

                    // Modify the offset and increment the index
                    offset = end;
                }

                // Get todays date
                var today = new Date();
                var dd = String(today.getDate()).padStart(2, '0');
                var mm = String(today.getMonth() + 1).padStart(2, '0');
                var yyyy = today.getFullYear();

                // Output signature data
                data.value = yyyy + '-' + mm + '-' + dd + ',' + convertMd5(spark.end());
                loading.innerHTML = '- 100 %';

                // Enable buttons
                enableButtons();

            } // End of the calculateMd5 method

            // Load to md5
            async function loadToMd5(spark, reader, chunk) {
                return new Promise((resolve, reject) => {
                    reader.readAsArrayBuffer(chunk);
                    reader.onload = function (e) {
                        resolve(spark.append(e.target.result));
                    };
                    reader.onerror = function () {
                        reject(reader.abort());
                    };
                });

            } // End of the loadToMd5 method

            // #endregion

            // #region Form methods

            // Post form data
            function postFormData(url, fd, successCallback, errorCallback) {

                var xhr = new XMLHttpRequest();
                xhr.open('POST', url, true);
                xhr.onload = function () {
                    if (xhr.status === 200) {
                        // Get response
                        var data = JSON.parse(xhr.response);

                        // Check success status
                        if (data.success === true) {
                            // Callback success
                            if (successCallback !== null) { successCallback(data); }
                        }
                        else {
                            // Callback error
                            if (errorCallback !== null) { errorCallback(data); }
                        }
                    }
                    else {
                        // Callback error information
                        data = { success: false, id: '', message: xhr.status + " - " + xhr.statusText };
                        if (errorCallback !== null) { errorCallback(data); }
                    }
                };
                xhr.onerror = function () {
                    // Callback error information
                    data = { success: false, id: '', message: xhr.status + " - " + xhr.statusText };
                    if (errorCallback !== null) { errorCallback(data); }
                };
                xhr.send(fd);

            } // End of the postFormData method

            // Disable buttons
            function disableButtons() {
                var buttons = document.getElementsByClassName('btn-disablable');
                for (var i = 0; i < buttons.length; i++) {
                    buttons[i].setAttribute('disabled', true);
                }

            } // End of the disableButtons method

            // Enable buttons
            function enableButtons() {
                var buttons = document.getElementsByClassName('btn-disablable');
                for (var i = 0; i < buttons.length; i++) {
                    setTimeout(function (button) { button.removeAttribute('disabled'); }, 1000, buttons[i]);
                }

            } // End of the enableButtons method

            // #endregion

        </script>

</body>
</html>

Leave a Reply

Your email address will not be published. Required fields are marked *