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();