virtual-u2f
Version:
Virtual U2F token for automated testing of U2F/Fido APIs
720 lines (596 loc) • 21.4 kB
JavaScript
'use strict';
var KJUR = require('jsrsasign');
var crypto = require('crypto');
/**
* Internal Token Logic
*/
var u2f = {};
/**
* Message types for messsages to/from the extension
* @const
* @type {{U2F_REGISTER_REQUEST: string, U2F_SIGN_REQUEST: string, U2F_REGISTER_RESPONSE: string, U2F_SIGN_RESPONSE: string}}
*/
u2f.MessageTypes = {
'U2F_REGISTER_REQUEST': 'u2f_register_request',
'U2F_SIGN_REQUEST': 'u2f_sign_request',
'U2F_REGISTER_RESPONSE': 'u2f_register_response',
'U2F_SIGN_RESPONSE': 'u2f_sign_response'
};
/**
* Response status codes
* @const
* @type {{OK: number, OTHER_ERROR: number, BAD_REQUEST: number, CONFIGURATION_UNSUPPORTED: number, DEVICE_INELIGIBLE: number, TIMEOUT: number}}
*/
u2f.ErrorCodes = {
"OK": 0,
"OTHER_ERROR": 1,
"BAD_REQUEST": 2,
"CONFIGURATION_UNSUPPORTED": 3,
"DEVICE_INELIGIBLE": 4,
"TIMEOUT": 5
};
/**
* Crypto Configuration
*/
// Generated through jsrsasign library (hex):
// var secp256r1 = new KJUR.crypto.ECDSA({'curve': 'secp256r1'});
// secp256r1.generateKeyPairHex();
/**
* ECDSA-secp256r1 Attestation Keys
* @const
*/
var ATTESTATION_KEY = {
"private": "d30c9cac7da2b4a7d71b002a40a3b59a96ca508ba9c7dc617d982c4b11d952e6",
"public": "04c3c91f252e20107b5e8deab1902098f7287071e45418b898ce5ff17ca725ae78c33cc701c0746011cbbbb58b08b61d20c05e75d501a3f8f7a1673fbe3263aebe"
};
/*
* SHA256withECDSA Attestation Certificate
*/
// Generated through jsrsasign library:
// var ecdsa = new KJUR.crypto.ECDSA({
// "curve": "secp256r1"
// });
//
// ecdsa.setPrivateKeyHex("d30c9cac7da2b4a7d71b002a40a3b59a96ca508ba9c7dc617d982c4b11d952e6");
// ecdsa.setPublicKeyHex("04c3c91f252e20107b5e8deab1902098f7287071e45418b898ce5ff17ca725ae78c33cc701c0746011cbbbb58b08b61d20c05e75d501a3f8f7a1673fbe3263aebe");
//
// var tbsc = new KJUR.asn1.x509.TBSCertificate();
// tbsc.setSerialNumberByParam({
// "int": 1
// });
// tbsc.setSignatureAlgByParam({
// "name": "SHA256withECDSA"
// });
// tbsc.setIssuerByParam({
// "str": "/C=DE/O=Untrustworthy CA Organisation/ST=Berlin/CN=Untrustworthy CA"
// });
// tbsc.setNotBeforeByParam({
// "str": "20140924120000Z"
// });
// tbsc.setNotAfterByParam({
// "str": "21140924120000Z"
// });
// tbsc.setSubjectByParam({
// "str": "/C=DE/O=virtual-u2f-manufacturer/ST=Berlin/CN=virtual-u2f-v0.0.1"
// });
//
// tbsc.setSubjectPublicKeyByGetKey(ecdsa);
//
// var cert = new KJUR.asn1.x509.Certificate({
// "tbscertobj": tbsc,
// "prvkeyobj" : ecdsa
// });
//
// cert.sign();
// console.log(cert.getPEMString());
// Resulting PEM String
//
// -----BEGIN CERTIFICATE-----
// MIIBtDCCAVigAwIBAgIBATAMBggqhkjOPQQDAgUAMGExCzAJBgNVBAYTAkRFMSYw
// JAYDVQQKDB1VbnRydXN0d29ydGh5IENBIE9yZ2FuaXNhdGlvbjEPMA0GA1UECAwG
// QmVybGluMRkwFwYDVQQDDBBVbnRydXN0d29ydGh5IENBMCIYDzIwMTQwOTI0MTIw
// MDAwWhgPMjExNDA5MjQxMjAwMDBaMF4xCzAJBgNVBAYTAkRFMSEwHwYDVQQKDBh2
// aXJ0dWFsLXUyZi1tYW51ZmFjdHVyZXIxDzANBgNVBAgMBkJlcmxpbjEbMBkGA1UE
// AwwSdmlydHVhbC11MmYtdjAuMC4xMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
// w8kfJS4gEHtejeqxkCCY9yhwceRUGLiYzl/xfKclrnjDPMcBwHRgEcu7tYsIth0g
// wF511QGj+PehZz++MmOuvjAMBggqhkjOPQQDAgUAA0gAMEUCIQCOuSBXofNBTxt5
// GljmB6ukZhyTYfvEuollXIo77BBo2gIgFZCodvCAR99gjiOyKqCq0ksNScl1MwCv
// MraQc/ChpNs=
// -----END CERTIFICATE-----
// Resulting human readable certificate representation
// (created by openSLL: openssl x509 -noout -text -in u2f.pem)
//
// Data:
// Version: 3 (0x2)
// Serial Number: 1 (0x1)
// Signature Algorithm: ecdsa-with-SHA256
// Issuer: C=DE, O=Untrustworthy CA Organisation, ST=Berlin, CN=Untrustworthy CA
// Validity
// Not Before: Sep 24 12:00:00 2014 GMT
// Not After : Sep 24 12:00:00 2114 GMT
// Subject: C=DE, O=virtual-u2f-manufacturer, ST=Berlin, CN=virtual-u2f-v0.0.1
// Subject Public Key Info:
// Public Key Algorithm: id-ecPublicKey
// Public-Key: (256 bit)
// pub:
// 04:c3:c9:1f:25:2e:20:10:7b:5e:8d:ea:b1:90:20:
// 98:f7:28:70:71:e4:54:18:b8:98:ce:5f:f1:7c:a7:
// 25:ae:78:c3:3c:c7:01:c0:74:60:11:cb:bb:b5:8b:
// 08:b6:1d:20:c0:5e:75:d5:01:a3:f8:f7:a1:67:3f:
// be:32:63:ae:be
// ASN1 OID: prime256v1
// Signature Algorithm: ecdsa-with-SHA256
// 30:45:02:21:00:8e:b9:20:57:a1:f3:41:4f:1b:79:1a:58:e6:
// 07:ab:a4:66:1c:93:61:fb:c4:ba:89:65:5c:8a:3b:ec:10:68:
// da:02:20:15:90:a8:76:f0:80:47:df:60:8e:23:b2:2a:a0:aa:
// d2:4b:0d:49:c9:75:33:00:af:32:b6:90:73:f0:a1:a4:db
// Resulting ASN.1 DER certificate
// (created by openSLL: openssl x509 -outform der -in u2f.pem -out u2f.der)
//
// 30 82 01 B4 30 82 01 58 A0 03 02 01 02 02 01 01
// 30 0C 06 08 2A 86 48 CE 3D 04 03 02 05 00 30 61
// 31 0B 30 09 06 03 55 04 06 13 02 44 45 31 26 30
// 24 06 03 55 04 0A 0C 1D 55 6E 74 72 75 73 74 77
// 6F 72 74 68 79 20 43 41 20 4F 72 67 61 6E 69 73
// 61 74 69 6F 6E 31 0F 30 0D 06 03 55 04 08 0C 06
// 42 65 72 6C 69 6E 31 19 30 17 06 03 55 04 03 0C
// 10 55 6E 74 72 75 73 74 77 6F 72 74 68 79 20 43
// 41 30 22 18 0F 32 30 31 34 30 39 32 34 31 32 30
// 30 30 30 5A 18 0F 32 31 31 34 30 39 32 34 31 32
// 30 30 30 30 5A 30 5E 31 0B 30 09 06 03 55 04 06
// 13 02 44 45 31 21 30 1F 06 03 55 04 0A 0C 18 76
// 69 72 74 75 61 6C 2D 75 32 66 2D 6D 61 6E 75 66
// 61 63 74 75 72 65 72 31 0F 30 0D 06 03 55 04 08
// 0C 06 42 65 72 6C 69 6E 31 1B 30 19 06 03 55 04
// 03 0C 12 76 69 72 74 75 61 6C 2D 75 32 66 2D 76
// 30 2E 30 2E 31 30 59 30 13 06 07 2A 86 48 CE 3D
// 02 01 06 08 2A 86 48 CE 3D 03 01 07 03 42 00 04
// C3 C9 1F 25 2E 20 10 7B 5E 8D EA B1 90 20 98 F7
// 28 70 71 E4 54 18 B8 98 CE 5F F1 7C A7 25 AE 78
// C3 3C C7 01 C0 74 60 11 CB BB B5 8B 08 B6 1D 20
// C0 5E 75 D5 01 A3 F8 F7 A1 67 3F BE 32 63 AE BE
// 30 0C 06 08 2A 86 48 CE 3D 04 03 02 05 00 03 48
// 00 30 45 02 21 00 8E B9 20 57 A1 F3 41 4F 1B 79
// 1A 58 E6 07 AB A4 66 1C 93 61 FB C4 BA 89 65 5C
// 8A 3B EC 10 68 DA 02 20 15 90 A8 76 F0 80 47 DF
// 60 8E 23 B2 2A A0 AA D2 4B 0D 49 C9 75 33 00 AF
// 32 B6 90 73 F0 A1 A4 DB
/**
* ASN.1 DER hexadecimal certificate representation
* @type {string}
* @const
*/
var ATTESTATION_CERTIFICATE = "308201b430820158a003020102020101300c06082a8648ce3d04030205003061310b300906035504061302444531263024060355040a0c1d556e7472757374776f72746879204341204f7267616e69736174696f6e310f300d06035504080c064265726c696e3119301706035504030c10556e7472757374776f727468792043413022180f32303134303932343132303030305a180f32313134303932343132303030305a305e310b30090603550406130244453121301f060355040a0c187669727475616c2d7532662d6d616e756661637475726572310f300d06035504080c064265726c696e311b301906035504030c127669727475616c2d7532662d76302e302e313059301306072a8648ce3d020106082a8648ce3d03010703420004c3c91f252e20107b5e8deab1902098f7287071e45418b898ce5ff17ca725ae78c33cc701c0746011cbbbb58b08b61d20c05e75d501a3f8f7a1673fbe3263aebe300c06082a8648ce3d040302050003480030450221008eb92057a1f3414f1b791a58e607aba4661c9361fbc4ba89655c8a3bec1068da02201590a876f08047df608e23b22aa0aad24b0d49c9753300af32b69073f0a1a4db";
/**
* Name of the key store string in local storage
* @type {string}
* @const
*/
var KEY_STORE_NAME = "virtual-u2f-key-store-0.0.1";
/**
* The "future use" byte to add to a message
* @type {string}
* @const
*/
var FUTURE_USE_BYTE = '00';
/**
* The "reserved" byte to add to a register request
* @type {string}
* @const
*/
var RESERVED_BYTE = '05';
/**
* The control byte for "enforce-user-presence-and-sign"
* @type {string}
* @const
*/
var CONTROL_BYTE = '03';
/**
* The byte indicating user presence
* @type {string}
* @const
*/
var USER_PRESENCE_BYTE = '01';
/**
* The storage for the current request
*/
var _currentRequest = {};
function hextob64(data) {
// Pad out as required
if (data.length % 2 != 0) {
data = data + "0";
}
// Create standard b64 encoding
var b64 = new Buffer(data, 'hex').toString('base64');
// Format to web safe b64
return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
function b64tohex(data) {
return new Buffer(data, 'base64').toString('hex');
}
module.exports = class U2FToken {
constructor(keys) {
this.keys = keys || [];
return this;
}
// Save a key to the device
SaveKey(applicationId, keyHandle, keyPair) {
var key = {
"generated" : (new Date()),
"appId" : applicationId,
"keyHandle" : keyHandle,
"public" : keyPair.ecpubhex,
"private" : keyPair.ecprvhex,
"counter" : 0
};
this.keys.push(key);
}
GetKeyByHandle(keyHandle) {
return this.keys.find(function(key) {
return key.keyHandle == keyHandle;
}) || null;
}
GetKeyByAppId(appId) {
return this.keys.find(function(key) {
return key.appId == appId;
}) || null;
}
ExportKeys() {
return this.keys;
}
ImportKeys(keys) {
this.keys = keys;
}
/**
* Determines whether a provided key handle belongs to a key that may be used by the app with the provided app id.
* @param keyHandle
* @param appId
* @returns {boolean}
*/
IsValidKeyHandleForAppId(keyHandle, appId) {
var key = this.GetKeyByHandle(keyHandle);
if (key === null) {
return false;
}
if (key.appId === appId) {
return true;
} else {
return false;
}
};
// Handle a registration request
HandleRegisterRequest(request) {
//Check if appId is unique
var existingKey = this.GetKeyByAppId(request.appId);
if(existingKey != null) {
return Promise.reject("Application key already exists");
}
var keyPair = generateKeyPair();
var clientData = getClientDataStringFromRequest(request);
var clientDataHash = sha256Digest(clientData);
var applicationIdHash = sha256Digest(request.appId);
var keyHandle = generateKeyHandle();
var keyHandleLength = getKeyHandleLengthString(keyHandle);
var registrationBaseString = getRegistrationSignatureBaseString(applicationIdHash, clientDataHash, keyHandle, keyPair.ecpubhex);
var signature = signHex(ATTESTATION_KEY.private, registrationBaseString);
var response = RESERVED_BYTE + keyPair.ecpubhex + keyHandleLength + keyHandle + ATTESTATION_CERTIFICATE + signature;
//var sessionID = getSessionIdFromRequest(request);
this.SaveKey(request.appId, keyHandle, keyPair);
/*
* fido-u2f-javascript-api-v1.0-rd-20140209.pdf ll. 175-182
*/
return Promise.resolve({
// websafe-base64(raw registration response message)
registrationData: hextob64(response),
// websafe-base64(UTF8(stringified(client data)))
clientData: new Buffer(clientData).toString('base64'),
// Unencoded key handle for convenience
keyHandle: keyHandle
});
};
register(appId, registerRequests, registeredKeys, registerCallback, timeout) {
HandleRegisterRequest({
appId: appId,
registerRequests: registerRequests,
registeredKeys: registeredKeys
}).then(function(res) {
registerCallback(res)
}, function(err) {
registerCallback({errorCode: err})
});
};
/**
* Handles a sign request
* @param request
* @param sender
* @param sendResponse
*/
HandleSignRequest(request) {
var usedKey = request.registeredKeys.find(function(item) {
return this.IsValidKeyHandleForAppId(b64tohex(item.keyHandle), request.appId);
}.bind(this)) || null;
if(usedKey == null) {
return Promise.reject({
errorCode: u2f.ErrorCodes.DEVICE_INELIGIBLE,
errorMessage: "Not a valid device for this key handle/app id combination"
});
}
var key = this.GetKeyByHandle(b64tohex(usedKey.keyHandle));
if (key.appId != request.appId) {
return Promise.reject({
errorCode: u2f.ErrorCodes.DEVICE_INELIGIBLE,
errorMessage: "keyHandle and appId mismatch"
});
}
var clientData = getClientDataStringFromRequest(request);
var clientDataHash = sha256Digest(clientData);
var applicationId = getApplicationIdFromRequest(request);
var applicationIdHash = sha256Digest(applicationId);
//var sessionID = getSessionIdFromRequest(request);
var challenge = getChallengeFromRequest(request);
var counterHex = counterPadding(key.counter);
var signature = signHex(key.private, getSignSignatureBaseString(applicationIdHash, counterHex, clientDataHash));
var signatureData = hextob64(USER_PRESENCE_BYTE + counterHex + signature);
if (key.counter >= 65535) {
key.counter = 0;
} else {
key.counter ++;
}
/*
* fido-u2f-javascript-api-v1.0-rd-20140209.pdf ll.254 - 265
*/
return Promise.resolve({
// websafe-base64(client data)
clientData : new Buffer(clientData).toString('base64'),
// websafe-base64(raw response from U2F device)
signatureData : signatureData,
// challenge originally passed to handleSignRequest
challenge : challenge,
// session id originally passed to handleSignRequest
//sessionId : sessionID,
// application id originally passed to handleSignRequest
appId : applicationId,
// Unencoded key handle for convenience
keyHandle: hextob64(key.keyHandle)
});
};
/**
* Handle a registration request
* of the Google's Ref Code format
* @param request
*/
HandleRefCodeRegisterRequest(request) {
request.type = u2f.MessageTypes.U2F_REGISTER_REQUEST;
request.registerRequests = [{
challenge: request.challenge,
}];
return this.HandleRegisterRequest(request);
}
/**
* Handles a sign request object
* of the Google's Ref Code format
* @param request
*/
HandleRefCodeSignRequest(request) {
request.type = u2f.MessageTypes.U2F_SIGN_REQUEST;
request.registeredKeys = [{
keyHandle: request.keyHandle,
}];
return this.HandleSignRequest(request);
}
};
/**
* Padds an integer for counter byte use
* @param num
* @returns {string}
*/
var counterPadding = function (num) {
return ("00000000" + num.toString(16)).substr(-8);
}
/**
* Generates a new random generateKeyPair
* @returns {Array} associative array of hexadecimal string of private and public key
*/
var generateKeyPair = function () {
/**
*
* @type {KJUR.crypto.ECDSA}
*/
var secp256r1 = new KJUR.crypto.ECDSA({'curve': 'secp256r1'});
return secp256r1.generateKeyPairHex();
};
/**
* Signs a given message with a given private key
* Example at http://kjur.github.io/jsrsasign/sample-ecdsa.html
*
* @param privateKey The private key to sign the message with
* @param message The message to sign
* @returns {String} the signature bytes as a hexadecimal string
*/
var signHex = function (privateKey, message) {
/**
* The signature object to sign a message with a given private key.
* @type {KJUR.crypto.Signature}
*/
var sig = new KJUR.crypto.Signature({
'alg': 'SHA256withECDSA',
'prov': 'cryptojs/jsrsa'
});
sig.init(new KJUR.crypto.ECDSA({
prv: privateKey,
courve: 'secp256r1',
}));
sig.updateHex(message);
return sig.sign();
};
/**
* Gets a signature base String for registration
*
* @param applicationParameter
* @param challengeParameter
* @param keyHandle
* @param userPublicKey
* @returns {string} The signature base string
*/
var getRegistrationSignatureBaseString = function (applicationParameter, challengeParameter, keyHandle, userPublicKey) {
return FUTURE_USE_BYTE + applicationParameter + challengeParameter + keyHandle + userPublicKey;
};
/**
* Gets a signature base String for signin
*
* @param applicationParameter
* @param challengeParameter
* @param keyHandle
* @param userPublicKey
* @returns {string} The signature base string
*/
var getSignSignatureBaseString = function (applicationParameter, counter, challenge) {
return applicationParameter + USER_PRESENCE_BYTE + counter + challenge;
};
/**
* Dispatches the user presence event
*/
var handleButtonPress = function () {
window.dispatchEvent(userPresenceTest);
return;
};
var handleSignIn = function () {
currentRequest.sendResponse({
"success": "sign"
});
return;
};
/**
* Converts a decimal number < 256 to a heaxadecimal byte representation.
* @param {Integer} dec Decimal number < 255
* @returns {string}
*/
var decimalNumberToHexByte = function (dec) {
if (dec > 255) {
throw new Error("Number exceeds a byte.");
}
return (dec + 0x10000).toString(16).substr(-2);
};
/**
* Creates a SHA256 has of a string
* @param {String} s String do hash
* @returns {String} Hexadecimal digest
*/
var sha256Digest = function (s) {
var sha = new KJUR.crypto.MessageDigest({
alg: 'sha256',
prov: 'cryptojs'
});
sha.updateString(s)
return sha.digest();
};
var arrayBufferToB64 = function (arrayBuffer) {
return btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)));
};
var Uint8ToHex = function (uint) {
var s = '';
for (var i = 0; i < uint.length; i++) {
s += uint[i].toString(16);
}
return s;
};
var stringToUint = function (s) {
var uint = new Uint8Array(s.length);
for (var i = 0, j = s.length; i < j; ++i) {
uint[i] = s.charCodeAt(i);
}
return uint;
};
var prepareSignableData = function (appId, challenge, callback) {
window.crypto.subtle.digest({
name: "SHA-256"
}, stringToUint(appId)).then(function (appIdDigest) {
window.crypto.subtle.digest({
name: "SHA-256"
}, stringToUint(challenge)).then(function (challengeDigest) {
callback(appIdDigest, challengeDigest);
}, function (error) {
throw new Error("Can't Digest Challenge")
});
}, function (error) {
throw new Error("Can't Digest App ID")
});
return;
};
var prepareChallengeSha256 = function (challenge, callback) {
window.crypto.subtle.digest({
name: "SHA-256"
}, stringToUint(challenge)).then(function (fullChallengeDigest) {
callback(fullChallengeDigest);
}, function (error) {
throw new Error("Can't Digest Full Challenge")
});
return;
};
var generateKeyHandle = function () {
return crypto.randomBytes(16).toString('hex');
};
var getPrivateAttestationKey = function () {
return ATTESTATION_KEY.private;
};
var getPublicAttestationKey = function () {
return ATTESTATION_KEY.public;
};
var getAttestationCertificate = function () {
return ATTESTATION_CERTIFICATE;
};
var getSessionIdFromRequest = function (request) {
switch (request.type) {
case u2f.MessageTypes.U2F_REGISTER_REQUEST:
return request.registerRequests[0].sessionId;
break;
case u2f.MessageTypes.U2F_SIGN_REQUEST:
return request.signRequests[0].sessionId;
break;
default:
throw new Error("Invalid SessionID");
break;
}
};
var getClientDataStringFromRequest = function (request) {
switch (request.type) {
case u2f.MessageTypes.U2F_REGISTER_REQUEST:
return JSON.stringify({challenge: request.registerRequests[0].challenge});
break;
case u2f.MessageTypes.U2F_SIGN_REQUEST:
return JSON.stringify({challenge: request.challenge});
break;
default:
throw new Error("Invalid Request Type");
break;
}
};
var getChallengeFromRequest = function (request) {
return getClientDataStringFromRequest(request);
};
var getApplicationIdFromRequest = function (request) {
switch (request.type) {
case u2f.MessageTypes.U2F_REGISTER_REQUEST:
return request.registerRequests[0].appId;
break;
case u2f.MessageTypes.U2F_SIGN_REQUEST:
return request.appId;
break;
default:
throw new Error("Invalid Request Type");
break;
}
};
var getKeyHandleFromRequest = function (request) {
switch (request.type) {
case u2f.MessageTypes.U2F_SIGN_REQUEST:
return request.registeredKeys[0].keyHandle;
break;
default:
throw new Error("Invalid Request Type");
break;
}
};
var getKeyHandleLengthString = function (keyHandle) {
return decimalNumberToHexByte(keyHandle.length / 2);
};