Electronic Signature Using The WebCrypto API

Sometimes we need to let users sign something electronically. Often people understand that as placing your handwritten signature on the screen somehow. Depending on the jurisdiction, that may be fine, or it may not be sufficient to just store the image. In Europe, for example, there’s the Regulation 910/2014 which defines what electronic signature are. As it can be expected from a legal text, the definition is rather vague:

‘electronic signature’ means data in electronic form which is attached to or logically associated with other data in electronic form and which is used by the signatory to sign;

Yes, read it a few more times, say “wat” a few more times, and let’s discuss what that means. And it can bean basically anything. It is technically acceptable to just attach an image of the drawn signature (e.g. using an html canvas) to the data and that may still count.

But when we get to the more specific types of electronic signature – the advanced and qualified electronic signatures, things get a little better:

An advanced electronic signature shall meet the following requirements:
(a) it is uniquely linked to the signatory;
(b) it is capable of identifying the signatory;
(c) it is created using electronic signature creation data that the signatory can, with a high level of confidence, use under his sole control; and
(d) it is linked to the data signed therewith in such a way that any subsequent change in the data is detectable.

That looks like a proper “digital signature” in the technical sense – e.g. using a private key to sign and a public key to verify the signature. The “qualified” signatures need to be issued by qualified provider that is basically a trusted Certificate Authority. The keys for placing qualified signatures have to be issued on secure devices (smart cards and HSMs) so that nobody but the owner can have access to the private key.

But the legal distinction between advanced and qualified signatures isn’t entirely clear – the Regulation explicitly states that non-qualified signatures also have legal value. Working with qualified signatures (with smartcards) in browsers is a horrifying user experience – in most cases it goes through a Java Applet, which works basically just on Internet Explorer and a special build of Firefox nowadays. Alternatives include desktop software and local service JWS applications that handles the signing, but smartcards are a big issue and offtopic at the moment.

So, how do we allow users to “place” an electronic signature? I had an idea that this could be done entirely using the WebCrypto API that’s more or less supported in browsers these days. The idea is as follows:

  • Let the user type in a password for the purpose of sining
  • Derive a key from the password (e.g. using PBKDF2)
  • Sign the contents of the form that the user is submitting with the derived key
  • Store the signature alongside the rest of the form data
  • Optionally, store the derived key for verification purposes

Here’s a javascript implementation of that flow:

<html>
<head>
<script type="text/javascript">
function sign(input, password) {
	// salt should be Uint8Array or ArrayBuffer
	var saltBuffer = str2ab('e85c53e7f119d41fd7895cdc9d7bb9dd');

	// don't use naive approaches for converting text, otherwise international
	// characters won't have the correct byte sequences. Use TextEncoder when
	// available or otherwise use relevant polyfills
	var passphraseKey = str2ab(password);

	// You should firstly import your passphrase Uint8array into a CryptoKey
	return window.crypto.subtle.importKey(
	  'raw', 
	  passphraseKey, 
	  {name: 'PBKDF2'},
	  false, 
	  ['deriveBits', 'deriveKey']
	).then(function(key) {
	  return window.crypto.subtle.deriveKey(
		{ "name": 'PBKDF2',
		  "salt": saltBuffer,
		  // don't get too ambitious, or at least remember
		  // that low-power phones will access your app
		  "iterations": 100,
		  "hash": 'SHA-256'
		},
		key,
		{ name: "HMAC", hash: {name: "SHA-256"}},

		// Whether or not the key is extractable (less secure) or not (more secure)
		// when false, the key can only be passed as a web crypto object, not inspected
		true,

		// this web crypto object will only be allowed for these functions
		[ "sign" ]
	  )
	}).then(function (webKey) {
		return window.crypto.subtle.sign(
			{
				name: "HMAC"
			},
			webKey,
			str2ab(input) //ArrayBuffer of data we want to sign
		)
		.then(function(signature){
			return {signature: signature, key: webKey};
		})
		.catch(function(err){
			console.error(err);
		});
	});
}

function verify(input, key, signature) {
	return window.crypto.subtle.verify(
		{
			name: "HMAC",
		},
		key,
		hex2buf(signature), //ArrayBuffer of the signature
		str2ab(input) //ArrayBuffer of the data
	).then(function(isvalid){
		return isvalid;
	}).catch(function(err){
		console.error(err);
	});
}

function str2ab(str) {
  var buf = new ArrayBuffer(str.length*2); // 2 bytes for each char
  var bufView = new Uint16Array(buf);
  for (var i=0, strLen=str.length; i<strLen; i++) {
    bufView[i] = str.charCodeAt(i);
  }
  return buf;
}

function buf2hex(buffer) { // buffer is an ArrayBuffer
  return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join('');
}

function hex2buf(hex) {
	var buffer = new ArrayBuffer(hex.length / 2);
	var array = new Uint8Array(buffer);
	var k = 0;
	for (var i = 0; i < hex.length; i +=2 ) {
		array[k] = parseInt(hex[i] + hex[i+1], 16);
		k++;
	}
	
	return buffer;
}

function arrayToBuffer(array) {
	var buffer = new ArrayBuffer(array.length);
	var backingArray = new Uint8Array(buffer);
	
	for (var i = 0; i < array.length; i ++) {
		backingArray[i] = array[i];
	}
	return buffer;
}
function signTest() {
	var input = document.getElementById("input").value;
	var password = document.getElementById("password").value;
	sign(input, password).then(function(result) {
		window.crypto.subtle.exportKey("raw", result.key).then(function(key) {
			document.getElementById("signature").value = buf2hex(result.signature);
			document.getElementById("key").value = buf2hex(key);
			document.getElementById("verifyInput").value = input;
		});
	})
}

function verifyTest() {
	var signature = document.getElementById("signature").value;
	var rawKey = document.getElementById("key").value;
	var input = document.getElementById("verifyInput").value;
	var keyBuffer = hex2buf(rawKey);
	
	window.crypto.subtle.importKey(
		"raw",
		keyBuffer,
		{   //this is the algorithm options
			name: "HMAC",
			hash: {name: "SHA-256"}, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512"
		},
		true, //whether the key is extractable (i.e. can be used in exportKey)
		["verify"] //can be any combination of "sign" and "verify"
	).then(function(key) {
		verify(input, key, signature).then(function(valid) {
			alert("Verification success: " + valid);
		});
	});
}
</script>
</head>
<body>
Text to sign: <input type="text" id="input" /><br />
Password: <input type="password" id="password" /><br />
<input type="button" onclick="signTest()" value="Sign test" />
<br /><br />
<hr />
Key: <input type="text" id="key" /><br />
Text to verify: <input type="text" id="verifyInput" /><br />
Signature: <input type="text" id="signature" /><br />
<input type="button" onclick="verifyTest()" value="Verify test";
</body>
</html>

Many of the pieces are taken from the very helpful webcrypto examples repo. The hex2buf, buf2hex and str2ab functions are utilities (that sadly are not standard in js).

What the code does is straightforward, even though it’s a bit verbose. All the operations are chained using promises and “then”, which to be honest is a big tedious to write and read (but inevitable I guess):

  • The password is loaded as a raw key (after transforming to an array buffer)
  • A secret key is derived using PBKDF2 (with 100 iterations)
  • The secret key is used to do an HMAC “signature” on the content filled in by the user
  • The signature and the key are stored (in the UI in this example)
  • Then the signature can be verified using: the data, the signature and the key

You can test it here: