Index: build/chrome/Makefile ================================================================== --- build/chrome/Makefile +++ build/chrome/Makefile @@ -30,11 +30,11 @@ endif export NACL_SDK_ROOT all: cackey.zip -cackey.zip: $(CACKEY_EXECUTABLES) cackey.nmf manifest.json cackey.js google-pcsc.js pin.html pin.js pin-icon.png icon.png ui.html ui.js jsrsasign.js +cackey.zip: $(CACKEY_EXECUTABLES) cackey.nmf manifest.json cackey.js ssh-agent.js google-pcsc.js pin.html pin.js pin-icon.png icon.png ui.html ui.js jsrsasign.js rm -f cackey.zip zip cackey.zip.new $^ mv cackey.zip.new cackey.zip cackey.bc: cackey-chrome-pkcs11.o cackey-chrome-plugin.o lib/libcackey.a lib/libpcsc.a lib/libz.a Index: build/chrome/manifest.json.in ================================================================== --- build/chrome/manifest.json.in +++ build/chrome/manifest.json.in @@ -12,10 +12,12 @@ "app": { "background": { "scripts": [ "google-pcsc.js", + "jsrsasign.js", + "ssh-agent.js", "cackey.js" ], "persistent": false } }, ADDED build/chrome/ssh-agent.js Index: build/chrome/ssh-agent.js ================================================================== --- build/chrome/ssh-agent.js +++ build/chrome/ssh-agent.js @@ -0,0 +1,497 @@ +/* + * CACKey SSH Agent for ChromeOS + */ + +cackeySSHAgentApprovedApps = [ + "pnhechapfaindjhompbnflcldabbghjo" +]; + +/* + * SSH Element Encoding/Decoding + */ +function cackeySSHAgentEncodeInt(uint32) { + var result; + + result = [ + (uint32 >> 24) & 0xff, + (uint32 >> 16) & 0xff, + (uint32 >> 8) & 0xff, + uint32 & 0xff + ]; + + return(result); +} + +function cackeySSHAgentDecodeInt(input) { + var result; + + result = 0; + result |= (input[0] << 24); + result |= (input[1] << 16); + result |= (input[2] << 8); + result |= input[3]; + + return({ + value: result, + output: input.slice(4) + }); +} + +function cackeySSHAgentEncodeBigInt(bigInt) { + var result = []; + + switch (typeof(bigInt)) { + case "number": + while (bigInt) { + result.push(bigInt & 0xff); + bigInt = bigInt >> 8; + } + result.reverse(); + break; + case "object": + result = []; + new Uint8Array(bigInt.toByteArray()).forEach(function(e) { + result.push(e); + }); + break; + } + + result = cackeySSHAgentEncodeLV(result); + + return(result); +} + +function cackeySSHAgentEncodeLV(input) { + var result; + + result = cackeySSHAgentEncodeInt(input.length); + result = result.concat(input); + + return(result); +} + +function cackeySSHAgentDecodeLV(input) { + var result, info; + + info = cackeySSHAgentDecodeInt(input); + if (info.value >= input.length) { + throw(new Error("Invalid data")); + } + + input = info.output; + + result = input.slice(0, info.value); + + return({ + value: result, + output: input.slice(info.value) + }); +} + +function cackeySSHAgentEncodeTLV(tag, array) { + var result; + + result = []; + + result.push(tag & 0xff); + + result = result.concat(cackeySSHAgentEncodeLV(array)); + + return(result); +} + +function cackeySSHAgentEncodeToUTF8Array(str) { + var utf8 = []; + + if (typeof(str) === "string") { + str = str.split("").map(function(c) { + return(c.charCodeAt(0)); + }); + } + + for (var i = 0; i < str.length; i++) { + var charcode = str[i]; + + if (charcode < 0x80) { + utf8.push(charcode); + } else if (charcode < 0x800) { + utf8.push(0xc0 | (charcode >> 6), + 0x80 | (charcode & 0x3f)); + } else if (charcode < 0xd800 || charcode >= 0xe000) { + utf8.push(0xe0 | (charcode >> 12), + 0x80 | ((charcode >> 6) & 0x3f), + 0x80 | (charcode & 0x3f)); + } else { + // surrogate pair + i++; + // UTF-16 encodes 0x10000-0x10FFFF by + // subtracting 0x10000 and splitting the + // 20 bits of 0x0-0xFFFFF into two halves + charcode = 0x10000 + (((charcode & 0x3ff) << 10) + | (str[i] & 0x3ff)); + + utf8.push(0xf0 | (charcode >>18), + 0x80 | ((charcode >> 12) & 0x3f), + 0x80 | ((charcode >> 6) & 0x3f), + 0x80 | (charcode & 0x3f)); + } + } + + return utf8; +} + +function cackeySSHAgentEncodeString(string) { + var result; + + result = cackeySSHAgentEncodeLV(cackeySSHAgentEncodeToUTF8Array(string)); + + return(result); +} + +function cackeySSHAgentEncodeBinaryToHex(binaryString) { + var buffer; + + switch (typeof(binaryString)) { + case "string": + buffer = binaryString.split("").map(function(c) { + return(c.charCodeAt(0).toString(16).padStart(2, '0')); + }).join(""); + break; + default: + buffer = []; + new Uint8Array(binaryString).map(function(c) { + buffer.push(c.toString(16).padStart(2, '0')); + }); + buffer = buffer.join(""); + break; + } + + return(buffer); +} + +function cackeySSHAgentEncodeCertToKeyAndID(cert) { + var result = null, resultKey = null; + var certObj; + var publicKey; + + certObj = new X509; + if (!certObj) { + return(result); + } + + certObj.readCertHex(cackeySSHAgentEncodeBinaryToHex(cert)); + + publicKey = certObj.getPublicKey(); + + switch (publicKey.type) { + case "RSA": + resultKey = cackeySSHAgentEncodeString("ssh-rsa"); + resultKey = resultKey.concat(cackeySSHAgentEncodeBigInt(publicKey.e)); + resultKey = resultKey.concat(cackeySSHAgentEncodeBigInt(publicKey.n)); + break; + default: + console.log("[cackeySSH] Unsupported public key type:", publicKey.type, "-- ignoring."); + } + + if (resultKey) { + result = { + id: certObj.getSubjectString(), + key: resultKey + }; + } + + return(result); +} + +/* + * Command Handlers + */ +async function cackeySSHAgentCommandRequestIdentity(request) { + var response; + var certs = []; + var keys = []; + + /* + * Get a list of certificates + */ + certs = await cackeyListCertificates(); + + /* + * Convert each certificate to an SSH key blob + */ + certs.forEach(function(cert) { + var key; + + key = cackeySSHAgentEncodeCertToKeyAndID(cert.certificate); + + if (key) { + keys.push(key); + } + }); + + /* + * Encode response + */ + response = []; + + response.push(cackeySSHAgentMessage.SSH_AGENT_IDENTITIES_ANSWER); + response = response.concat(cackeySSHAgentEncodeInt(keys.length)); + keys.forEach(function(key) { + response = response.concat(cackeySSHAgentEncodeLV(key.key)); + response = response.concat(cackeySSHAgentEncodeString("CACKey: " + key.id)); + }); + + return(response); +} + +async function cackeySSHAgentCommandSignRequest(request) { + var keyInfo, data, flags; + var certs, certToUse; + var hashMethod, signedData, signedDataHeader, signRequest; + var response; + var flagMeaning = { + SSH_AGENT_RSA_SHA2_256: 2, + SSH_AGENT_RSA_SHA2_512: 4 + }; + + /* + * Strip off the command + */ + request = request.slice(1); + + /* + * Get certificate to sign using + */ + keyInfo = cackeySSHAgentDecodeLV(request); + request = keyInfo.output; + keyInfo = keyInfo.value; + + /* + * Get the data to sign + */ + data = cackeySSHAgentDecodeLV(request); + request = data.output; + data = data.value; + + /* + * Get the flags + */ + flags = cackeySSHAgentDecodeInt(request); + request = flags.output; + flags = flags.value; + + /* + * Find the certificate that matches the requested key + */ + certs = await cackeyListCertificates(); + certToUse = null; + certs.forEach(function(cert) { + var key; + + key = cackeySSHAgentEncodeCertToKeyAndID(cert.certificate); + + if (key.key.join() == keyInfo.join()) { + certToUse = cert; + } + }); + + /* + * If no certificate is found, return an error + */ + if (!certToUse) { + console.info("[cackeySSH] Unable to find a certificate to match the requested key:", keyInfo); + + return(null); + } + + /* + * Perform hashing of the data as specified by the flags + */ + if ((flags & flagMeaning.SSH_AGENT_RSA_SHA2_512) == flagMeaning.SSH_AGENT_RSA_SHA2_512) { + hashMethod = "SHA512"; + data = await crypto.subtle.digest("SHA-512", new Uint8Array(data)); + } else if ((flags & flagMeaning.SSH_AGENT_RSA_SHA2_256) == flagMeaning.SSH_AGENT_RSA_SHA2_256) { + hashMethod = "SHA256"; + data = await crypto.subtle.digest("SHA-256", new Uint8Array(data)); + } else if (flags == 1) { + hashMethod = "SHA1"; + data = await crypto.subtle.digest("SHA-1", new Uint8Array(data)); + } else { + console.info("[cackeySSH] Sign request with flags set to", flags, "which is unsupported, failing the request."); + + return(null); + } + + /* + * Sign the data + */ + signRequest = { + hash: hashMethod, + digest: new Uint8Array(data) + }; + signedData = await cackeySignMessage(signRequest); + signedData = Array.from(new Uint8Array(signedData)); + + /* + * Encode signature + */ + switch (hashMethod) { + case "SHA1": + signedDataHeader = cackeySSHAgentEncodeString("ssh-rsa"); + break; + case "SHA256": + signedDataHeader = cackeySSHAgentEncodeString("rsa-sha2-256"); + break; + case "SHA512": + signedDataHeader = cackeySSHAgentEncodeString("rsa-sha2-512"); + break; + default: + signedDataHeader = []; + break; + } + signedData = signedDataHeader.concat(cackeySSHAgentEncodeLV(signedData)); + + /* + * Encode response + */ + response = []; + + response.push(cackeySSHAgentMessage.SSH_AGENT_SIGN_RESPONSE); + response = response.concat(cackeySSHAgentEncodeLV(signedData)); + + return(response); +} + +/* + * Session handling + */ +async function cackeySSHAgentHandleMessage(socket, request) { + var sshRequestID, sshRequest, response, sshResponse; + var sshHandlerError; + + if (!request.type || request.type !== "auth-agent@openssh.com") { + return; + } + + if (!request.data || request.data.length < 1) { + return; + } + + sshRequestID = request.data[0]; + sshRequest = {}; + if (sshRequestID < cackeySSHAgentCommands.length) { + sshRequest = cackeySSHAgentCommands[sshRequestID]; + } + + response = null; + if (!sshRequest.name) { + console.log("[cackeySSH] Unsupported request: ", request, "; from: ", socket.sender.id); + } else { + if (goog.DEBUG) { + console.log("[cackeySSH] Request: ", sshRequest.name, "; from: ", socket.sender.id); + } + + try { + response = await sshRequest.handler(request.data); + } catch (sshHandlerError) { + response = null; + + console.error("[cackeySSH] Request:", sshRequest.name, "(", request, ") ERROR:", sshHandlerError); + } + } + + if (!response) { + response = [cackeySSHAgentMessage.SSH_AGENT_FAILURE]; + } + + sshResponse = { + type: "auth-agent@openssh.com", + data: response + }; + + if (goog.DEBUG) { + console.log("[cackeySSH] Response: ", sshResponse); + } + + socket.postMessage(sshResponse); + + return; +} + +function cackeySSHAgentAcceptConnection(socket) { + if (!socket) { + return; + } + + /* + * Only accept connections from approved apps + */ + if (!socket.sender || !socket.sender.id || !cackeySSHAgentApprovedApps.includes(socket.sender.id)) { + console.log("[cackeySSH] Disconnecting unapproved app: ", socket.sender); + + socket.disconnect(); + + return; + } + + console.log("[cackeySSH] Accepted connection from: ", socket.sender.id); + socket.onMessage.addListener(function(request) { + cackeySSHAgentHandleMessage(socket, request); + }); +} + +function cackeySSHAgentInit() { + chrome.runtime.onConnectExternal.addListener(cackeySSHAgentAcceptConnection); +} + +function cackeySSHAgentUninit() { + chrome.runtime.onConnectExternal.removeListener(cackeySSHAgentAcceptConnection); +} + +cackeySSHAgentCommands = [ + { /* 0: Not implemented */ }, + { /* 1: Not implemented */ }, + { /* 2: Not implemented */ }, + { /* 3: Not implemented */ }, + { /* 4: Not implemented */ }, + { /* 5: Not implemented */ }, + { /* 6: Not implemented */ }, + { /* 7: Not implemented */ }, + { /* 8: Not implemented */ }, + { /* 9: Not implemented */ }, + { /* 10: Not implemented */ }, + { + name: "requestIdentities", + handler: cackeySSHAgentCommandRequestIdentity + }, + { /* 12: Not implemented */ }, + { + name: "signRequest", + handler: cackeySSHAgentCommandSignRequest + }, + { /* 14: Not implemented */ }, + { /* 15: Not implemented */ }, + { /* 16: Not implemented */ }, + { /* 17: Not implemented */ }, + { /* 18: Not implemented */ }, + { /* 19: Not implemented */ }, + { /* 20: Not implemented */ }, + { /* 21: Not implemented */ }, + { /* 22: Not implemented */ }, + { /* 23: Not implemented */ }, + { /* 24: Not implemented */ }, + { /* 25: Not implemented */ }, + { /* 26: Not implemented */ }, + { /* 27: Not implemented */ }, + { /* 28: Not implemented */ } +]; + +cackeySSHAgentMessage = { + SSH_AGENT_FAILURE: 5, + SSH_AGENT_SUCCESS: 6, + SSH_AGENT_EXTENSION_FAILURE: 28, + SSH_AGENT_IDENTITIES_ANSWER: 12, + SSH_AGENT_SIGN_RESPONSE: 14 +}; + +cackeySSHAgentInit();