u2f
Version:
U2F 2-factor authentication library
223 lines (186 loc) • 8.49 kB
JavaScript
var crypto = require('crypto');
// Convert binary certificate or public key to an OpenSSL-compatible PEM text format.
function convertCertToPEM(cert) {
if (!Buffer.isBuffer(cert))
throw new Error("convertCertToPEM: cert must be buffer.")
var type;
if (cert.length == 65 && cert[0] == 0x04) {
// If needed, we encode raw public key to ASN structure, adding metadata:
// SEQUENCE {
// SEQUENCE {
// OBJECTIDENTIFIER 1.2.840.10045.2.1 (ecPublicKey)
// OBJECTIDENTIFIER 1.2.840.10045.3.1.7 (P-256)
// }
// BITSTRING <raw public key>
// }
// Luckily, to do that, we just need to prefix it with constant 26 bytes (metadata is constant).
cert = Buffer.concat([
new Buffer("3059301306072a8648ce3d020106082a8648ce3d030107034200", "hex"),
cert]);
type = "PUBLIC KEY";
} else {
type = "CERTIFICATE";
}
// 2. To get PEM string, ASN structure then must be base64-encoded, split to
// lines of 64 chars each and prefixed/postfixed with ---BEGIN/END PUBLIC KEY--- etc.
var pemStr = "-----BEGIN "+type+"-----\n";
for (var certStr = cert.toString('base64'); certStr.length > 64; certStr = certStr.slice(64))
pemStr += certStr.slice(0, 64) + '\n';
pemStr += certStr + '\n';
pemStr += "-----END "+type+"-----\n";
return pemStr;
}
// Check ECDSA+SHA256 signature of given data.
// cert is buffer containing ASN encoded certificate or raw publicKey of len 65
// signature is buffer (ASN encoded: SEQUENCE of 2 ec points)
// returns: true/false
function checkECDSASignature(data, cert, signature) {
if (!Buffer.isBuffer(signature) || asnLen(signature) != signature.length)
throw new Error("checkSignature: signature must be buffer of valid ASN/DER structure.");
return crypto.createVerify("RSA-SHA256") // The actual signature alg is ECDSA and determined
.update(data) // by ASN/DER data in public key. SHA256 is what we set here.
.verify(convertCertToPEM(cert), signature);
}
// Our hash is always SHA256. Returns buffer.
function hash(data) {
return crypto.createHash('SHA256').update(data).digest();
}
// Decode initial bytes of buffer as ASN and return the length of the encoded structure.
// See http://en.wikipedia.org/wiki/X.690
// Only SEQUENCE top-level identifier is supported (which covers all certs luckily)
function asnLen(buf) {
if (buf.length < 2 || buf[0] != 0x30)
throw new Error("Invalid data: Not a SEQUENCE ASN/DER structure");
var len = buf[1];
if (len & 0x80) { // long form
var bytesCnt = len & 0x7F;
if (buf.length < 2+bytesCnt)
throw new Error("Invalid data: ASN structure not fully represented");
len = 0;
for (var i = 0; i < bytesCnt; i++)
len = len*0x100 + buf[2+i];
len += bytesCnt; // add bytes for length itself.
}
return len + 2; // add 2 initial bytes: type and length.
}
function toWebsafeBase64(buf) {
return buf.toString('base64').replace(/\//g,'_').replace(/\+/g,'-').replace(/=/g, '');
}
//==============================================================================
// Main API
// Generate request for client. Basically the same for registration and signature, except for the keyHandle.
function request(appId, keyHandle) {
if (typeof appId !== 'string')
throw new Error("U2F request(): appId must be provided.");
var res = {
version: "U2F_V2",
appId: appId,
challenge: toWebsafeBase64(crypto.randomBytes(32))
};
if (keyHandle)
res.keyHandle = keyHandle;
return res;
}
// Check registration data. We're checking correct challenge and certificate signature.
// request: {version, appId, challenge} - from user session, kept on server.
// registerData: {clientData, registrationData} - result of u2f.register
function checkRegistration(request, registerData) {
if (typeof registerData !== 'object')
return {errorMessage: "Invalid response from U2F token."};
// Check registration error
if (registerData.errorCode && registerData.errorCode != 0)
return {
errorMessage: registerData.errorMessage || "Error registering U2F token.",
errorCode: registerData.errorCode,
};
// Unpack and check clientData, challenge.
var clientData = new Buffer(registerData.clientData, 'base64');
try {
var clientDataObj = JSON.parse(clientData.toString('utf8'));
}
catch (e) {
return {errorMessage: "Invalid clientData: not a valid JSON object"}
}
if (clientDataObj.challenge !== request.challenge)
return {errorMessage: "Invalid challenge: not the one provided"};
// Parse registrationData.
var buf = new Buffer(registerData.registrationData, 'base64');
var reserved = buf[0]; buf = buf.slice(1);
var publicKey = buf.slice(0, 65); buf = buf.slice(65);
var keyHandleLen = buf[0]; buf = buf.slice(1);
var keyHandle = buf.slice(0, keyHandleLen); buf = buf.slice(keyHandleLen);
var certLen = asnLen(buf);
var certificate = buf.slice(0, certLen); buf = buf.slice(certLen);
var signLen = asnLen(buf);
var signature = buf.slice(0, signLen); buf = buf.slice(signLen);
if (buf.length !== 0)
console.error("U2F Registration Warning: registrationData has extra bytes: "+buf.toString('hex'));
var reservedByte = new Buffer('00', 'hex');
var appIdHash = hash(request.appId);
var clientDataHash = hash(clientData);
var signatureBase = Buffer.concat([reservedByte, appIdHash, clientDataHash, keyHandle, publicKey]);
if (checkECDSASignature(signatureBase, certificate, signature))
return {
successful: true,
publicKey: toWebsafeBase64(publicKey),
keyHandle: toWebsafeBase64(keyHandle),
certificate: certificate
};
else
return {errorMessage: "Invalid signature."};
}
// Check signature data.
// request: {version, appId, challenge, keyHandle} - from user session, kept on server.
// signResult: {clientData, signatureData} - result of u2f.sign on client.
// publicKey: string from user account.
function checkSignature(request, signResult, publicKey) {
if (typeof signResult !== 'object')
return {errorMessage: "Invalid response from U2F token."};
// Check registration error
if (signResult.errorCode && signResult.errorCode != 0)
return {
errorMessage: signResult.errorMessage || "Error getting signature from U2F token.",
errorCode: signResult.errorCode,
};
// Unpack and check clientData, challenge.
var clientData = new Buffer(signResult.clientData, 'base64');
try {
var clientDataObj = JSON.parse(clientData.toString('utf8'));
}
catch (e) {
return {errorMessage: "Invalid clientData: not a valid JSON object"}
}
if (clientDataObj.challenge !== request.challenge)
return {errorMessage: "Invalid challenge: not the one provided"};
// Parse signatureData
var buf = new Buffer(signResult.signatureData, 'base64');
var userPresenceFlag = buf.slice(0, 1); buf = buf.slice(1);
var counter = buf.slice(0, 4); buf = buf.slice(4);
var signLen = asnLen(buf);
var signature = buf.slice(0, signLen); buf = buf.slice(signLen);
if (buf.length !== 0)
console.error("U2F Authentication Warning: signatureData has extra bytes: "+buf.toString('hex'));
var appIdHash = hash(request.appId);
var clientDataHash = hash(clientData);
var signatureBase = Buffer.concat([appIdHash, userPresenceFlag, counter, clientDataHash]);
var cert = new Buffer(publicKey, 'base64');
if (checkECDSASignature(signatureBase, cert, signature))
return {
successful: true,
userPresent: (userPresenceFlag[0] & 1) === 1,
counter: counter.readUInt32BE(0)
};
else
return {errorMessage: "Invalid signature."};
}
// Set up appId as a convenience.
module.exports = {
// Main API
request: request,
checkRegistration: checkRegistration,
checkSignature: checkSignature,
// Supplemental API, mostly for testing.
_hash: hash,
_checkECDSASignature: checkECDSASignature,
_toWebsafeBase64: toWebsafeBase64,
}