Skip to content

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

  1. using System.Security.Cryptography.X509Certificates;
  2. namespace Annytab.Scripts.Models
  3. {
  4. public class ResponseData
  5. {
  6. #region variables
  7. public bool success { get; set; }
  8. public string id { get; set; }
  9. public string message { get; set; }
  10. public string url { get; set; }
  11. #endregion
  12. #region Constructors
  13. public ResponseData()
  14. {
  15. // Set values for instance variables
  16. this.success = false;
  17. this.id = "";
  18. this.message = "";
  19. this.url = "";
  20. } // End of the constructor
  21. public ResponseData(bool success, string id, string message, string url = "")
  22. {
  23. // Set values for instance variables
  24. this.success = success;
  25. this.id = id;
  26. this.message = message;
  27. this.url = url;
  28. } // End of the constructor
  29. #endregion
  30. } // End of the class
  31. public class Signature
  32. {
  33. #region Variables
  34. public string validation_type { get; set; }
  35. public string algorithm { get; set; }
  36. public string padding { get; set; }
  37. public string data { get; set; }
  38. public string value { get; set; }
  39. public string certificate { get; set; }
  40. #endregion
  41. #region Constructors
  42. public Signature()
  43. {
  44. // Set values for instance variables
  45. this.validation_type = null;
  46. this.algorithm = null;
  47. this.padding = null;
  48. this.data = null;
  49. this.value = null;
  50. this.certificate = null;
  51. } // End of the constructor
  52. #endregion
  53. } // End of the class
  54. public class SignatureValidationResult
  55. {
  56. #region Variables
  57. public bool valid { get; set; }
  58. public string signature_data { get; set; }
  59. public string signatory { get; set; }
  60. public X509Certificate2 certificate { get; set; }
  61. #endregion
  62. #region Constructors
  63. public SignatureValidationResult()
  64. {
  65. // Set values for instance variables
  66. this.valid = false;
  67. this.signature_data = null;
  68. this.signatory = null;
  69. this.certificate = null;
  70. } // End of the constructor
  71. #endregion
  72. } // End of the class
  73. } // End of the namespace

Controller

  1. using System;
  2. using System.Text;
  3. using System.Security.Cryptography;
  4. using System.Collections.Generic;
  5. using System.Security.Cryptography.X509Certificates;
  6. using Microsoft.AspNetCore.Http;
  7. using Microsoft.AspNetCore.Mvc;
  8. using Microsoft.Extensions.Logging;
  9. using Annytab.Scripts.Models;
  10. namespace Annytab.Scripts.Controllers
  11. {
  12. public class eidsmartcardController : Controller
  13. {
  14. #region Variables
  15. private readonly ILogger logger;
  16. #endregion
  17. #region Constructors
  18. public eidsmartcardController(ILogger<eidsmartcardController> logger)
  19. {
  20. // Set values for instance variables
  21. this.logger = logger;
  22. } // End of the constructor
  23. #endregion
  24. #region Post methods
  25. [HttpPost]
  26. [ValidateAntiForgeryToken]
  27. public IActionResult validate(IFormCollection collection)
  28. {
  29. // Create a signature
  30. Annytab.Scripts.Models.Signature signature = new Annytab.Scripts.Models.Signature();
  31. signature.validation_type = "eID Smart Card";
  32. signature.algorithm = collection["selectSignatureAlgorithm"];
  33. signature.padding = collection["selectSignaturePadding"];
  34. signature.data = collection["txtSignatureData"];
  35. signature.value = collection["txtSignatureValue"];
  36. signature.certificate = collection["txtSignatureCertificate"];
  37. // Validate the signature
  38. SignatureValidationResult result = ValidateSignature(signature);
  39. // Set a title and a message
  40. string title = result.valid == false ? "Invalid Signature" : "Valid Signature";
  41. string message = "<b>" + title + "</b><br />" + signature.data + "<br />";
  42. message += result.certificate != null ? result.certificate.GetNameInfo(X509NameType.SimpleName, false) + ", " + result.certificate.GetNameInfo(X509NameType.SimpleName, true)
  43. + ", " + result.certificate.NotBefore.ToString("yyyy-MM-dd") + " to "
  44. + result.certificate.NotAfter.ToString("yyyy-MM-dd") : "";
  45. // Return a response
  46. return Json(data: new ResponseData(result.valid, title, message));
  47. } // End of the validate method
  48. #endregion
  49. #region Helper methods
  50. public static IDictionary<string, string> GetHashDictionary(byte[] data)
  51. {
  52. // Create the dictionary to return
  53. IDictionary<string, string> hashes = new Dictionary<string, string>(4);
  54. using (SHA1 sha = SHA1.Create())
  55. {
  56. hashes.Add("SHA-1", GetHexString(sha.ComputeHash(data)));
  57. }
  58. using (SHA256 sha = SHA256.Create())
  59. {
  60. hashes.Add("SHA-256", GetHexString(sha.ComputeHash(data)));
  61. }
  62. using (SHA384 sha = SHA384.Create())
  63. {
  64. hashes.Add("SHA-384", GetHexString(sha.ComputeHash(data)));
  65. }
  66. using (SHA512 sha = SHA512.Create())
  67. {
  68. hashes.Add("SHA-512", GetHexString(sha.ComputeHash(data)));
  69. }
  70. // Return the dictionary
  71. return hashes;
  72. } // End of the GetHashDictionary method
  73. public static string GetHexString(byte[] data)
  74. {
  75. // Create a new Stringbuilder to collect the bytes and create a string.
  76. StringBuilder sBuilder = new StringBuilder();
  77. // Loop through each byte of the hashed data and format each one as a hexadecimal string.
  78. for (int i = 0; i < data.Length; i++)
  79. {
  80. sBuilder.Append(data[i].ToString("x2"));
  81. }
  82. // Return the hexadecimal string.
  83. return sBuilder.ToString();
  84. } // End of the GetHexString method
  85. public static HashAlgorithmName GetHashAlgorithmName(string signature_algorithm)
  86. {
  87. if (signature_algorithm == "SHA-256")
  88. {
  89. return HashAlgorithmName.SHA256;
  90. }
  91. else if (signature_algorithm == "SHA-384")
  92. {
  93. return HashAlgorithmName.SHA384;
  94. }
  95. else if (signature_algorithm == "SHA-512")
  96. {
  97. return HashAlgorithmName.SHA512;
  98. }
  99. else
  100. {
  101. return HashAlgorithmName.SHA1;
  102. }
  103. } // End of the GetHashAlgorithmName method
  104. public static RSASignaturePadding GetRSASignaturePadding(string signature_padding)
  105. {
  106. if (signature_padding == "Pss")
  107. {
  108. return RSASignaturePadding.Pss;
  109. }
  110. else
  111. {
  112. return RSASignaturePadding.Pkcs1;
  113. }
  114. } // End of the GetRSASignaturePadding method
  115. public static SignatureValidationResult ValidateSignature(Annytab.Scripts.Models.Signature signature)
  116. {
  117. // Create the result to return
  118. SignatureValidationResult result = new SignatureValidationResult();
  119. result.signature_data = signature.data;
  120. try
  121. {
  122. // Get the certificate
  123. result.certificate = new X509Certificate2(Convert.FromBase64String(signature.certificate));
  124. // Get the public key
  125. using (RSA rsa = result.certificate.GetRSAPublicKey())
  126. {
  127. // Convert the signature value to a byte array
  128. byte[] digest = Convert.FromBase64String(signature.value);
  129. // Check if the signature is valid
  130. result.valid = rsa.VerifyData(Encoding.UTF8.GetBytes(signature.data), digest, GetHashAlgorithmName(signature.algorithm), GetRSASignaturePadding(signature.padding));
  131. }
  132. }
  133. catch (Exception ex)
  134. {
  135. string exMessage = ex.Message;
  136. result.certificate = null;
  137. }
  138. // Return the validation result
  139. return result;
  140. } // End of the ValidateSignature method
  141. #endregion
  142. } // End of the class
  143. } // 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.

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>eId Smart Card</title>
  5. <style>
  6. .annytab-textarea{width:300px;height:100px;}
  7. .annytab-textbox {width:300px;}
  8. </style>
  9. </head>
  10. <body style="width:100%;font-family:Arial, Helvetica, sans-serif;">
  11. <!-- Container -->
  12. <div style="display:block;padding:10px;">
  13. <h1>eId Smart Card</h1>
  14. <div>
  15. 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
  16. 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
  17. <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 />
  18. </div><br />
  19. <!-- Input form -->
  20. <form id="inputForm">
  21. <!-- Hidden data -->
  22. @Html.AntiForgeryToken()
  23. <div>Select file to sign <span id="loading"></span></div>
  24. <input id="fuFile" name="fuFile" type="file" onchange="calculateMd5();" class="annytab-textbox" /><br /><br />
  25. <div>Select algorithm</div>
  26. <select id="selectSignatureAlgorithm" name="selectSignatureAlgorithm" class="annytab-textbox">
  27. <option value="SHA-1" selected>SHA-1</option>
  28. <option value="SHA-256">SHA-256</option>
  29. <option value="SHA-384">SHA-384</option>
  30. <option value="SHA-512">SHA-512</option>
  31. </select><br /><br />
  32. <div>Select padding</div>
  33. <select name="selectSignaturePadding" class="annytab-textbox">
  34. <option value="Pkcs1" selected>Pkcs1</option>
  35. </select><br /><br />
  36. <div>Signature data</div>
  37. <textarea id="txtSignatureData" name="txtSignatureData" class="annytab-textarea"></textarea><br /><br />
  38. <div>Certificate</div>
  39. <textarea id="txtSignatureCertificate" name="txtSignatureCertificate" class="annytab-textarea"></textarea><br /><br />
  40. <div>Signature value</div>
  41. <textarea id="txtSignatureValue" name="txtSignatureValue" class="annytab-textarea"></textarea><br /><br />
  42. <input type="button" value="Sign file" class="btn-disablable" onclick="createSignature()" disabled />
  43. <input type="button" value="Validate signature" class="btn-disablable" onclick="validateSignature()" disabled />
  44. </form>
  45. </div>
  46. <!-- Style and scripts -->
  47. <link href="/css/annytab.notifier.css" rel="stylesheet" />
  48. <script src="/js/font-awesome/all.min.js"></script>
  49. <script src="/js/annytab.effects.js"></script>
  50. <script src="/js/annytab.notifier.js"></script>
  51. <script src="/js/crypto/spark-md5.js"></script>
  52. <script src="/js/crypto/hwcrypto.js"></script>
  53. <script src="/js/crypto/hex2base.js"></script>
  54. <script>
  55. // Set default focus
  56. document.querySelector('#fuFile').focus();
  57. // Create a signature
  58. async function createSignature() {
  59. // Make sure that the request is secure (SSL)
  60. if (location.protocol !== 'https:') {
  61. annytab.notifier.show('error', 'You need a secure connection (SSL)!');
  62. return;
  63. }
  64. // Disable buttons
  65. disableButtons();
  66. // Get input data
  67. var data = document.querySelector('#txtSignatureData').value;
  68. var algorithm = document.querySelector('#selectSignatureAlgorithm').value;
  69. var hash = await getHash(data, algorithm);
  70. // Log selected algorithm and hash
  71. console.log('Algorithm: ' + algorithm);
  72. console.log('Hash: ' + hash);
  73. // Get the certificate
  74. window.hwcrypto.getCertificate({ lang: 'en' }).then(function (response) {
  75. // Get certificate
  76. certificate = hexToBase64(response.hex);
  77. document.querySelector('#txtSignatureCertificate').value = certificate;
  78. console.log('Using certificate:\n' + certificate);
  79. // Sign the hash
  80. window.hwcrypto.sign(response, { type: algorithm, hex: hash }, { lang: 'en' }).then(function (response) {
  81. // Get the signature value
  82. signature_value = hexToBase64(response.hex);
  83. document.querySelector('#txtSignatureValue').value = signature_value;
  84. annytab.notifier.show('success', 'Signature was successfully created!');
  85. // Enable buttons
  86. enableButtons();
  87. // Post the form
  88. }, function (err) {
  89. // Enable buttons
  90. enableButtons();
  91. if (err.message === 'no_implementation') {
  92. annytab.notifier.show('error', 'You need to install an extension for smart cards in your browser!');
  93. }
  94. else if (err.message === 'pin_blocked') {
  95. annytab.notifier.show('error', 'Your ID-card is blocked!');
  96. }
  97. else if (err.message === 'no_certificates') {
  98. annytab.notifier.show('error', 'We could not find any certificates, check your smart card reader.');
  99. }
  100. else if (err.message === 'technical_error') {
  101. annytab.notifier.show('error', 'The file could not be signed, your ID-card might not support {0}.'.replace('{0}', algorithm));
  102. }
  103. });
  104. }, function (err) {
  105. // Enable buttons
  106. enableButtons();
  107. if (err.message === 'no_implementation') {
  108. annytab.notifier.show('error', 'You need to install an extension for smart cards in your browser!');
  109. }
  110. else if (err.message === 'pin_blocked') {
  111. annytab.notifier.show('error', 'Your ID-card is blocked!');
  112. }
  113. else if (err.message === 'no_certificates') {
  114. annytab.notifier.show('error', 'We could not find any certificates, check your smart card reader.');
  115. }
  116. else if (err.message === 'technical_error') {
  117. annytab.notifier.show('error', 'The file could not be signed, your ID-card might not support {0}.'.replace('{0}', algorithm));
  118. }
  119. });
  120. } // End of the createSignature method
  121. // Validate signature
  122. function validateSignature() {
  123. // Disable buttons
  124. disableButtons();
  125. // Create form data
  126. var fd = new FormData(document.querySelector('#inputForm'));
  127. // Post form data
  128. postFormData('/eidsmartcard/validate', fd, function (data) {
  129. if (data.success === true) {
  130. annytab.notifier.show('success', data.message);
  131. }
  132. else {
  133. annytab.notifier.show('error', data.message);
  134. }
  135. // Enable buttons
  136. enableButtons();
  137. }, function (data) {
  138. annytab.notifier.show('error', data.message);
  139. // Enable buttons
  140. enableButtons();
  141. });
  142. } // End of the validateSignature method
  143. // Get a hash of a message
  144. async function getHash(data, algorithm) {
  145. // Hash data
  146. var hashBuffer = await crypto.subtle.digest(algorithm, new TextEncoder().encode(data));
  147. // Convert buffer to byte array
  148. var hashArray = Array.from(new Uint8Array(hashBuffer));
  149. // Convert bytes to hex string
  150. var hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
  151. // Return hash as hex string
  152. return hashHex;
  153. } // End of the getHash method
  154. // #region MD5
  155. // Convert Md5 to C# version
  156. function convertMd5(str) {
  157. return btoa(String.fromCharCode.apply(null,
  158. str.replace(/\r|\n/g, "").replace(/([\da-fA-F]{2}) ?/g, "0x$1 ").replace(/ +$/, "").split(" "))
  159. );
  160. } // End of the convertMd5 method
  161. // Calculate a MD5 value of a file
  162. async function calculateMd5() {
  163. // Get the controls
  164. var data = document.querySelector("#txtSignatureData");
  165. var loading = document.querySelector("#loading");
  166. // Get the file
  167. var file = document.querySelector("#fuFile").files[0];
  168. // Make sure that a file is selected
  169. if (typeof file === 'undefined' || file === null) {
  170. return;
  171. }
  172. // Add a loading animation
  173. loading.innerHTML = '- 0 %';
  174. // Variables
  175. var block_size = 4 * 1024 * 1024; // 4 MiB
  176. var offset = 0;
  177. // Create a spark object
  178. var spark = new SparkMD5.ArrayBuffer();
  179. var reader = new FileReader();
  180. // Create blocks
  181. while (offset < file.size) {
  182. // Get the start and end indexes
  183. var start = offset;
  184. var end = Math.min(offset + block_size, file.size);
  185. await loadToMd5(spark, reader, file.slice(start, end));
  186. loading.innerHTML = '- ' + Math.round((offset / file.size) * 100) + ' %';
  187. // Modify the offset and increment the index
  188. offset = end;
  189. }
  190. // Get todays date
  191. var today = new Date();
  192. var dd = String(today.getDate()).padStart(2, '0');
  193. var mm = String(today.getMonth() + 1).padStart(2, '0');
  194. var yyyy = today.getFullYear();
  195. // Output signature data
  196. data.value = yyyy + '-' + mm + '-' + dd + ',' + convertMd5(spark.end());
  197. loading.innerHTML = '- 100 %';
  198. // Enable buttons
  199. enableButtons();
  200. } // End of the calculateMd5 method
  201. // Load to md5
  202. async function loadToMd5(spark, reader, chunk) {
  203. return new Promise((resolve, reject) => {
  204. reader.readAsArrayBuffer(chunk);
  205. reader.onload = function (e) {
  206. resolve(spark.append(e.target.result));
  207. };
  208. reader.onerror = function () {
  209. reject(reader.abort());
  210. };
  211. });
  212. } // End of the loadToMd5 method
  213. // #endregion
  214. // #region Form methods
  215. // Post form data
  216. function postFormData(url, fd, successCallback, errorCallback) {
  217. var xhr = new XMLHttpRequest();
  218. xhr.open('POST', url, true);
  219. xhr.onload = function () {
  220. if (xhr.status === 200) {
  221. // Get response
  222. var data = JSON.parse(xhr.response);
  223. // Check success status
  224. if (data.success === true) {
  225. // Callback success
  226. if (successCallback !== null) { successCallback(data); }
  227. }
  228. else {
  229. // Callback error
  230. if (errorCallback !== null) { errorCallback(data); }
  231. }
  232. }
  233. else {
  234. // Callback error information
  235. data = { success: false, id: '', message: xhr.status + " - " + xhr.statusText };
  236. if (errorCallback !== null) { errorCallback(data); }
  237. }
  238. };
  239. xhr.onerror = function () {
  240. // Callback error information
  241. data = { success: false, id: '', message: xhr.status + " - " + xhr.statusText };
  242. if (errorCallback !== null) { errorCallback(data); }
  243. };
  244. xhr.send(fd);
  245. } // End of the postFormData method
  246. // Disable buttons
  247. function disableButtons() {
  248. var buttons = document.getElementsByClassName('btn-disablable');
  249. for (var i = 0; i < buttons.length; i++) {
  250. buttons[i].setAttribute('disabled', true);
  251. }
  252. } // End of the disableButtons method
  253. // Enable buttons
  254. function enableButtons() {
  255. var buttons = document.getElementsByClassName('btn-disablable');
  256. for (var i = 0; i < buttons.length; i++) {
  257. setTimeout(function (button) { button.removeAttribute('disabled'); }, 1000, buttons[i]);
  258. }
  259. } // End of the enableButtons method
  260. // #endregion
  261. </script>
  262. </body>
  263. </html>

Leave a Reply

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