Check-in [5d6a50ef48]
Overview
Comment:Added SSH agent support
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA1:5d6a50ef4881fe38baa504f04eb4d7353692d8bd
User & Date: rkeene on 2019-01-31 07:16:21
Other Links: manifest | tags
Context
2019-01-31
07:23
Improved SSH agent check-in: 43f92f8f98 user: rkeene tags: trunk
07:16
Added SSH agent support check-in: 5d6a50ef48 user: rkeene tags: trunk
07:15
Added SHA512 support and support for returning a promise when signing check-in: a5d56f2277 user: rkeene tags: trunk
Changes

Modified build/chrome/Makefile from [2b4e0751d3] to [388b08b05b].

    28     28   ifeq (,${NACL_SDK_ROOT})
    29     29   $(error "Please set NACL_SDK_ROOT")
    30     30   endif
    31     31   export NACL_SDK_ROOT
    32     32   
    33     33   all: cackey.zip
    34     34   
    35         -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
           35  +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
    36     36   	rm -f cackey.zip
    37     37   	zip cackey.zip.new $^
    38     38   	mv cackey.zip.new cackey.zip
    39     39   
    40     40   cackey.bc: cackey-chrome-pkcs11.o cackey-chrome-plugin.o lib/libcackey.a lib/libpcsc.a lib/libz.a
    41     41   	$(CXX) $(CXXFLAGS) $(LDFLAGS) -o cackey.bc.new cackey-chrome-pkcs11.o cackey-chrome-plugin.o $(LIBS)
    42     42   	mv cackey.bc.new cackey.bc

Modified build/chrome/manifest.json.in from [3e4f620248] to [46508e25a5].

    10     10   		"128": "icon.png"
    11     11   	},
    12     12   
    13     13   	"app": {
    14     14   		"background": {
    15     15   			"scripts": [
    16     16   				"google-pcsc.js",
           17  +				"jsrsasign.js",
           18  +				"ssh-agent.js",
    17     19   				"cackey.js"
    18     20   			],
    19     21   			"persistent": false
    20     22   		}
    21     23   	},
    22     24   
    23     25   	"permissions": [
    24     26   		"certificateProvider",
    25     27   		"alwaysOnTopWindows"
    26     28   	]
    27     29   }

Added build/chrome/ssh-agent.js version [d68e90b355].

            1  +/*
            2  + * CACKey SSH Agent for ChromeOS
            3  + */
            4  +
            5  +cackeySSHAgentApprovedApps = [
            6  +	"pnhechapfaindjhompbnflcldabbghjo"
            7  +];
            8  +
            9  +/*
           10  + * SSH Element Encoding/Decoding
           11  + */
           12  +function cackeySSHAgentEncodeInt(uint32) {
           13  +	var result;
           14  +
           15  +	result = [
           16  +		(uint32 >> 24) & 0xff,
           17  +		(uint32 >> 16) & 0xff,
           18  +		(uint32 >>  8) & 0xff,
           19  +		 uint32        & 0xff
           20  +	];
           21  +
           22  +	return(result);
           23  +}
           24  +
           25  +function cackeySSHAgentDecodeInt(input) {
           26  +	var result;
           27  +
           28  +	result = 0;
           29  +	result |= (input[0] << 24);
           30  +	result |= (input[1] << 16);
           31  +	result |= (input[2] << 8);
           32  +	result |=  input[3];
           33  +
           34  +	return({
           35  +		value: result,
           36  +		output: input.slice(4)
           37  +	});
           38  +}
           39  +
           40  +function cackeySSHAgentEncodeBigInt(bigInt) {
           41  +	var result = [];
           42  +
           43  +	switch (typeof(bigInt)) {
           44  +		case "number":
           45  +			while (bigInt) {
           46  +				result.push(bigInt & 0xff);
           47  +				bigInt = bigInt >> 8;
           48  +			}
           49  +			result.reverse();
           50  +			break;
           51  +		case "object":
           52  +			result = [];
           53  +			new Uint8Array(bigInt.toByteArray()).forEach(function(e) {
           54  +				result.push(e);
           55  +			});
           56  +			break;
           57  +	}
           58  +
           59  +	result = cackeySSHAgentEncodeLV(result);
           60  +
           61  +	return(result);
           62  +}
           63  +
           64  +function cackeySSHAgentEncodeLV(input) {
           65  +	var result;
           66  +
           67  +	result = cackeySSHAgentEncodeInt(input.length);
           68  +	result = result.concat(input);
           69  +
           70  +	return(result);
           71  +}
           72  +
           73  +function cackeySSHAgentDecodeLV(input) {
           74  +	var result, info;
           75  +
           76  +	info = cackeySSHAgentDecodeInt(input);
           77  +	if (info.value >= input.length) {
           78  +		throw(new Error("Invalid data"));
           79  +	}
           80  +
           81  +	input = info.output;
           82  +
           83  +	result = input.slice(0, info.value);
           84  +
           85  +	return({
           86  +		value: result,
           87  +		output: input.slice(info.value)
           88  +	});
           89  +}
           90  +
           91  +function cackeySSHAgentEncodeTLV(tag, array) {
           92  +	var result;
           93  +
           94  +	result = [];
           95  +
           96  +	result.push(tag & 0xff);
           97  +
           98  +	result = result.concat(cackeySSHAgentEncodeLV(array));
           99  +
          100  +	return(result);
          101  +}
          102  +
          103  +function cackeySSHAgentEncodeToUTF8Array(str) {
          104  +	var utf8 = [];
          105  +
          106  +	if (typeof(str) === "string") {
          107  +		str = str.split("").map(function(c) {
          108  +			return(c.charCodeAt(0));
          109  +		});
          110  +	}
          111  +
          112  +	for (var i = 0; i < str.length; i++) {
          113  +		var charcode = str[i];
          114  +
          115  +		if (charcode < 0x80) {
          116  +			utf8.push(charcode);
          117  +		} else if (charcode < 0x800) {
          118  +			utf8.push(0xc0 | (charcode >> 6), 
          119  +			          0x80 | (charcode & 0x3f));
          120  +		} else if (charcode < 0xd800 || charcode >= 0xe000) {
          121  +			utf8.push(0xe0 | (charcode >> 12), 
          122  +			          0x80 | ((charcode >> 6) & 0x3f), 
          123  +			          0x80 | (charcode & 0x3f));
          124  +		} else {
          125  +			// surrogate pair
          126  +			i++;
          127  +			// UTF-16 encodes 0x10000-0x10FFFF by
          128  +			// subtracting 0x10000 and splitting the
          129  +			// 20 bits of 0x0-0xFFFFF into two halves
          130  +			charcode = 0x10000 + (((charcode & 0x3ff) << 10)
          131  +			           | (str[i] & 0x3ff));
          132  +
          133  +			utf8.push(0xf0 | (charcode >>18), 
          134  +			          0x80 | ((charcode >> 12) & 0x3f), 
          135  +			          0x80 | ((charcode >> 6) & 0x3f), 
          136  +			          0x80 | (charcode & 0x3f));
          137  +		}
          138  +	}
          139  +
          140  +	return utf8;
          141  +}
          142  +
          143  +function cackeySSHAgentEncodeString(string) {
          144  +	var result;
          145  +
          146  +	result = cackeySSHAgentEncodeLV(cackeySSHAgentEncodeToUTF8Array(string));
          147  +
          148  +	return(result);
          149  +}
          150  +
          151  +function cackeySSHAgentEncodeBinaryToHex(binaryString) {
          152  +	var buffer;
          153  +
          154  +	switch (typeof(binaryString)) {
          155  +		case "string":
          156  +			buffer = binaryString.split("").map(function(c) {
          157  +				return(c.charCodeAt(0).toString(16).padStart(2, '0'));
          158  +			}).join("");
          159  +			break;
          160  +		default:
          161  +			buffer = [];
          162  +			new Uint8Array(binaryString).map(function(c) {
          163  +				buffer.push(c.toString(16).padStart(2, '0'));
          164  +			});
          165  +			buffer = buffer.join("");
          166  +			break;
          167  +	}
          168  +
          169  +	return(buffer);
          170  +}
          171  +
          172  +function cackeySSHAgentEncodeCertToKeyAndID(cert) {
          173  +	var result = null, resultKey = null;
          174  +	var certObj;
          175  +	var publicKey;
          176  +
          177  +	certObj = new X509;
          178  +	if (!certObj) {
          179  +		return(result);
          180  +	}
          181  +
          182  +	certObj.readCertHex(cackeySSHAgentEncodeBinaryToHex(cert));
          183  +
          184  +	publicKey = certObj.getPublicKey();
          185  +
          186  +	switch (publicKey.type) {
          187  +		case "RSA":
          188  +			resultKey = cackeySSHAgentEncodeString("ssh-rsa");
          189  +			resultKey = resultKey.concat(cackeySSHAgentEncodeBigInt(publicKey.e));
          190  +			resultKey = resultKey.concat(cackeySSHAgentEncodeBigInt(publicKey.n));
          191  +			break;
          192  +		default:
          193  +			console.log("[cackeySSH] Unsupported public key type:", publicKey.type, "-- ignoring.");
          194  +	}
          195  +
          196  +	if (resultKey) {
          197  +		result = {
          198  +			id: certObj.getSubjectString(),
          199  +			key: resultKey
          200  +		};
          201  +	}
          202  +
          203  +	return(result);
          204  +}
          205  +
          206  +/*
          207  + * Command Handlers
          208  + */
          209  +async function cackeySSHAgentCommandRequestIdentity(request) {
          210  +	var response;
          211  +	var certs = [];
          212  +	var keys = [];
          213  +
          214  +	/*
          215  +	 * Get a list of certificates
          216  +	 */
          217  +	certs = await cackeyListCertificates();
          218  +
          219  +	/*
          220  +	 * Convert each certificate to an SSH key blob
          221  +	 */
          222  +	certs.forEach(function(cert) {
          223  +		var key;
          224  +
          225  +		key = cackeySSHAgentEncodeCertToKeyAndID(cert.certificate);
          226  +
          227  +		if (key) {
          228  +			keys.push(key);
          229  +		}
          230  +	});
          231  +
          232  +	/*
          233  +	 * Encode response
          234  +	 */
          235  +	response = [];
          236  +
          237  +	response.push(cackeySSHAgentMessage.SSH_AGENT_IDENTITIES_ANSWER);
          238  +	response = response.concat(cackeySSHAgentEncodeInt(keys.length));
          239  +	keys.forEach(function(key) {
          240  +		response = response.concat(cackeySSHAgentEncodeLV(key.key));
          241  +		response = response.concat(cackeySSHAgentEncodeString("CACKey: " + key.id));
          242  +	});
          243  +
          244  +	return(response);
          245  +}
          246  +
          247  +async function cackeySSHAgentCommandSignRequest(request) {
          248  +	var keyInfo, data, flags;
          249  +	var certs, certToUse;
          250  +	var hashMethod, signedData, signedDataHeader, signRequest;
          251  +	var response;
          252  +	var flagMeaning = {
          253  +		SSH_AGENT_RSA_SHA2_256: 2,
          254  +		SSH_AGENT_RSA_SHA2_512: 4
          255  +	};
          256  +
          257  +	/*
          258  +	 * Strip off the command
          259  +	 */
          260  +	request = request.slice(1);
          261  +
          262  +	/*
          263  +	 * Get certificate to sign using
          264  +	 */
          265  +	keyInfo = cackeySSHAgentDecodeLV(request);
          266  +	request = keyInfo.output;
          267  +	keyInfo = keyInfo.value;
          268  +
          269  +	/*
          270  +	 * Get the data to sign
          271  +	 */
          272  +	data = cackeySSHAgentDecodeLV(request);
          273  +	request = data.output;
          274  +	data = data.value;
          275  +
          276  +	/*
          277  +	 * Get the flags
          278  +	 */
          279  +	flags = cackeySSHAgentDecodeInt(request);
          280  +	request = flags.output;
          281  +	flags = flags.value;
          282  +
          283  +	/*
          284  +	 * Find the certificate that matches the requested key
          285  +	 */
          286  +	certs = await cackeyListCertificates();
          287  +	certToUse = null;
          288  +	certs.forEach(function(cert) {
          289  +		var key;
          290  +
          291  +		key = cackeySSHAgentEncodeCertToKeyAndID(cert.certificate);
          292  +
          293  +		if (key.key.join() == keyInfo.join()) {
          294  +			certToUse = cert;
          295  +		}
          296  +	});
          297  +
          298  +	/*
          299  +	 * If no certificate is found, return an error
          300  +	 */
          301  +	if (!certToUse) {
          302  +		console.info("[cackeySSH] Unable to find a certificate to match the requested key:", keyInfo);
          303  +
          304  +		return(null);
          305  +	}
          306  +
          307  +	/*
          308  +	 * Perform hashing of the data as specified by the flags
          309  +	 */
          310  +	if ((flags & flagMeaning.SSH_AGENT_RSA_SHA2_512) == flagMeaning.SSH_AGENT_RSA_SHA2_512) {
          311  +		hashMethod = "SHA512";
          312  +		data = await crypto.subtle.digest("SHA-512", new Uint8Array(data));
          313  +	} else if ((flags & flagMeaning.SSH_AGENT_RSA_SHA2_256) == flagMeaning.SSH_AGENT_RSA_SHA2_256) {
          314  +		hashMethod = "SHA256";
          315  +		data = await crypto.subtle.digest("SHA-256", new Uint8Array(data));
          316  +	} else if (flags == 1) {
          317  +		hashMethod = "SHA1";
          318  +		data = await crypto.subtle.digest("SHA-1", new Uint8Array(data));
          319  +	} else {
          320  +		console.info("[cackeySSH] Sign request with flags set to", flags, "which is unsupported, failing the request.");
          321  +
          322  +		return(null);
          323  +	}
          324  +
          325  +	/*
          326  +	 * Sign the data
          327  +	 */
          328  +	signRequest = {
          329  +		hash: hashMethod,
          330  +		digest: new Uint8Array(data)
          331  +	};
          332  +	signedData = await cackeySignMessage(signRequest);
          333  +	signedData = Array.from(new Uint8Array(signedData));
          334  +
          335  +	/*
          336  +	 * Encode signature
          337  +	 */
          338  +	switch (hashMethod) {
          339  +		case "SHA1":
          340  +			signedDataHeader = cackeySSHAgentEncodeString("ssh-rsa");
          341  +			break;
          342  +		case "SHA256":
          343  +			signedDataHeader = cackeySSHAgentEncodeString("rsa-sha2-256");
          344  +			break;
          345  +		case "SHA512":
          346  +			signedDataHeader = cackeySSHAgentEncodeString("rsa-sha2-512");
          347  +			break;
          348  +		default:
          349  +			signedDataHeader = [];
          350  +			break;
          351  +	}
          352  +	signedData = signedDataHeader.concat(cackeySSHAgentEncodeLV(signedData));
          353  +
          354  +	/*
          355  +	 * Encode response
          356  +	 */
          357  +	response = [];
          358  +
          359  +	response.push(cackeySSHAgentMessage.SSH_AGENT_SIGN_RESPONSE);
          360  +	response = response.concat(cackeySSHAgentEncodeLV(signedData));
          361  +
          362  +	return(response);
          363  +}
          364  +
          365  +/*
          366  + * Session handling
          367  + */
          368  +async function cackeySSHAgentHandleMessage(socket, request) {
          369  +	var sshRequestID, sshRequest, response, sshResponse;
          370  +	var sshHandlerError;
          371  +
          372  +	if (!request.type || request.type !== "auth-agent@openssh.com") {
          373  +		return;
          374  +	}
          375  +
          376  +	if (!request.data || request.data.length < 1) {
          377  +		return;
          378  +	}
          379  +
          380  +	sshRequestID = request.data[0];
          381  +	sshRequest = {};
          382  +	if (sshRequestID < cackeySSHAgentCommands.length) {
          383  +		sshRequest = cackeySSHAgentCommands[sshRequestID];
          384  +	}
          385  +
          386  +	response = null;
          387  +	if (!sshRequest.name) {
          388  +		console.log("[cackeySSH] Unsupported request: ", request, "; from: ", socket.sender.id);
          389  +	} else {
          390  +		if (goog.DEBUG) {
          391  +			console.log("[cackeySSH] Request: ", sshRequest.name, "; from: ", socket.sender.id);
          392  +		}
          393  +
          394  +		try {
          395  +			response = await sshRequest.handler(request.data);
          396  +		} catch (sshHandlerError) {
          397  +			response = null;
          398  +
          399  +			console.error("[cackeySSH] Request:", sshRequest.name, "(", request, ") ERROR:", sshHandlerError);
          400  +		}
          401  +	}
          402  +
          403  +	if (!response) {
          404  +		response = [cackeySSHAgentMessage.SSH_AGENT_FAILURE];
          405  +	}
          406  +
          407  +	sshResponse = {
          408  +		type: "auth-agent@openssh.com",
          409  +		data: response
          410  +	};
          411  +
          412  +	if (goog.DEBUG) {
          413  +		console.log("[cackeySSH] Response: ", sshResponse);
          414  +	}
          415  +
          416  +	socket.postMessage(sshResponse);
          417  +
          418  +	return;
          419  +}
          420  +
          421  +function cackeySSHAgentAcceptConnection(socket) {
          422  +	if (!socket) {
          423  +		return;
          424  +	}
          425  +
          426  +	/*
          427  +	 * Only accept connections from approved apps
          428  +	 */
          429  +	if (!socket.sender || !socket.sender.id || !cackeySSHAgentApprovedApps.includes(socket.sender.id)) {
          430  +		console.log("[cackeySSH] Disconnecting unapproved app: ", socket.sender);
          431  +
          432  +		socket.disconnect();
          433  +
          434  +		return;
          435  +	}
          436  +
          437  +	console.log("[cackeySSH] Accepted connection from: ", socket.sender.id);
          438  +	socket.onMessage.addListener(function(request) {
          439  +		cackeySSHAgentHandleMessage(socket, request);
          440  +	});
          441  +}
          442  +
          443  +function cackeySSHAgentInit() {
          444  +	chrome.runtime.onConnectExternal.addListener(cackeySSHAgentAcceptConnection);
          445  +}
          446  +
          447  +function cackeySSHAgentUninit() {
          448  +	chrome.runtime.onConnectExternal.removeListener(cackeySSHAgentAcceptConnection);
          449  +}
          450  +
          451  +cackeySSHAgentCommands = [
          452  +	{ /* 0: Not implemented */ },
          453  +	{ /* 1: Not implemented */ },
          454  +	{ /* 2: Not implemented */ },
          455  +	{ /* 3: Not implemented */ },
          456  +	{ /* 4: Not implemented */ },
          457  +	{ /* 5: Not implemented */ },
          458  +	{ /* 6: Not implemented */ },
          459  +	{ /* 7: Not implemented */ },
          460  +	{ /* 8: Not implemented */ },
          461  +	{ /* 9: Not implemented */ },
          462  +	{ /* 10: Not implemented */ },
          463  +	{
          464  +		name: "requestIdentities",
          465  +		handler: cackeySSHAgentCommandRequestIdentity
          466  +	},
          467  +	{ /* 12: Not implemented */ },
          468  +	{
          469  +		name: "signRequest",
          470  +		handler: cackeySSHAgentCommandSignRequest
          471  +	},
          472  +	{ /* 14: Not implemented */ },
          473  +	{ /* 15: Not implemented */ },
          474  +	{ /* 16: Not implemented */ },
          475  +	{ /* 17: Not implemented */ },
          476  +	{ /* 18: Not implemented */ },
          477  +	{ /* 19: Not implemented */ },
          478  +	{ /* 20: Not implemented */ },
          479  +	{ /* 21: Not implemented */ },
          480  +	{ /* 22: Not implemented */ },
          481  +	{ /* 23: Not implemented */ },
          482  +	{ /* 24: Not implemented */ },
          483  +	{ /* 25: Not implemented */ },
          484  +	{ /* 26: Not implemented */ },
          485  +	{ /* 27: Not implemented */ },
          486  +	{ /* 28: Not implemented */ }
          487  +];
          488  +
          489  +cackeySSHAgentMessage = {
          490  +	SSH_AGENT_FAILURE: 5,
          491  +	SSH_AGENT_SUCCESS: 6,
          492  +	SSH_AGENT_EXTENSION_FAILURE: 28,
          493  +	SSH_AGENT_IDENTITIES_ANSWER: 12,
          494  +	SSH_AGENT_SIGN_RESPONSE: 14
          495  +};
          496  +
          497  +cackeySSHAgentInit();