eID-smartkort, signatur och validering i JavaScript

Jag skapar ett formulär för signaturer med eID-smartkort och signaturvalidering i denna handledning. En kvalificerad digital e-signatur med smartkort är giltig i många länder idag. Ett elektroniskt smartkort (eID) är ett identitetskort med ett chip som innehåller ett elektroniskt certifikat. Ett eID-smartkort kan användas för att skapa digitala signaturer.

Elektroniska signaturer med ett eID-smartkort i en webbläsare kräver en smartkortläsare, mjukvara för smartkortläsaren och ett tillägg i webbläsaren. Du kan ladda ner tillägget Token Signing från chrome web store, från windows store och från firefox browser add-ons för att kunna implementera lösningen i den här handledningen.

Elektroniska signaturer är säkrare än vanliga signaturer och digitala signaturer gör det snabbare och enklare att administrera signaturer avseende avtal eller kontrakt. Det är viktigt att elektroniska signaturer kan valideras, denna handledning innehåller kod för att skapa signaturer och kod för att validera skapade signaturer. Denna typ av tjänst kan användas för att samla in digitala signaturer från alla parter som berörs av ett avtal.

Denna kod har testats och fungerar med Google Chrome (75.0.3770.100) och Mozilla Firefox (75.0), detta utan några pollyfills. Koden fungerar (SHA-256 och SHA-384) i Internet Explorer (11.829.17134.0) med polyfills för 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 och transpilering. Om du vill stödja äldre webbläsare kan du läsa vårt inlägg om transpilering och komplettering av JavaScript. Den här koden har beroenden till annytab.effects, Font Awesome, annytab.notifier, hwcrypto och js-spark-md5.

Modeller

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 och JavaScript

Detta formulär har en filöverföringskontroll som startar signeringsprocessen, dagens datum och filens md5-hash är den data som blir signerad. En användare har alternativ när det gäller att välja algoritm och utfyllnad (endast ett alternativ för tillfället). En signatur kan valideras med ett anrop till en servermetod.

<!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>

Lämna ett svar

Din e-postadress kommer inte publiceras. Obligatoriska fält är märkta *