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>