Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial Creation

  • Loading branch information...
commit f6314c141a01674feb1688dce80f58cff9752c3d 0 parents
Mark Cavage authored
2  .gitignore
@@ -0,0 +1,2 @@
+node_modules
+*.log
19 LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2011 Mark Cavage, All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE
41 README.md
@@ -0,0 +1,41 @@
+node-ssh-agent is a client binding to the SSH Agent protocol, written in "pure"
+node.js. For now, the operations supported are "list keys" and "sign data"
+(which in SSH parlance is `requestIdentities` and `sign`.
+
+## Usage
+
+ var SSHAgentClient = require('ssh-agent');
+
+ var client = new SSHAgentClient();
+ var data = new Buffer('Hello World');
+
+ // Try to sign data with an RSA key (will generate
+ // an RSA-SHA1 signature).
+ client.requestIdentities(function(err, keys) {
+ var key = null;
+ for (var i = 0; i < keys.length; i++) {
+ if (keys[i].type === 'ssh-rsa') {
+ key = keys[i];
+ break;
+ }
+ }
+ if (!key)
+ return;
+
+ client.sign(key, data, function(err, signature) {
+ console.log('Signature: ' + signature.signature);
+ });
+ });
+
+
+## Installation
+
+ npm install ssh-agent
+
+## License
+
+MIT.
+
+## Bugs
+
+See <https://github.com/mcavage/node-ssh-agent/issues>.
301 lib/ssh_agent_client.js
@@ -0,0 +1,301 @@
+// Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved.
+var assert = require('assert');
+var net = require('net');
+
+var ctype = require('ctype');
+
+
+
+///--- Globals
+
+var PROTOCOL = {
+ SSH_AGENTC_REQUEST_RSA_IDENTITIES: 11,
+ SSH_AGENT_IDENTITIES_ANSWER: 12,
+ SSH2_AGENTC_SIGN_REQUEST: 13,
+ SSH2_AGENT_SIGN_RESPONSE: 14,
+ SSH_AGENT_FAILURE: 5,
+ SSH_AGENT_SUCCESS: 6
+};
+
+
+
+///--- Specific Errors
+
+function MissingEnvironmentVariableError(variable) {
+ this.name = 'MissingEnvironmentVariableError';
+ this.variable = variable;
+ this.message = variable + ' was not found in your environment';
+ this.stack = (new Error()).stack;
+}
+MissingEnvironmentVariableError.prototype = new Error();
+
+
+function TimeoutError(message) {
+ this.name = 'TimeoutError';
+ this.message = message;
+ this.stack = (new Error()).stack;
+}
+TimeoutError.prototype = new Error();
+
+
+function InvalidProtocolError(message) {
+ this.name = 'InvalidProtocolError';
+ this.message = message;
+ this.stack = (new Error()).stack;
+}
+InvalidProtocolError.prototype = new Error();
+
+
+
+///--- Internal Helpers
+
+function _newBuffer(buffers, additional) {
+ assert.ok(buffers);
+
+ var len = 5; // length + tag
+ for (var i = 0; i < buffers.length; i++)
+ len += 4 + buffers[i].length;
+
+ if (additional)
+ len += additional;
+
+ return new Buffer(len);
+}
+
+
+function _readString(buffer, offset) {
+ assert.ok(buffer);
+ assert.ok(offset !== undefined);
+
+ var i = 0;
+
+ var len = ctype.ruint32(buffer, 'big', offset);
+ offset += 4;
+
+ var str = new Buffer(len);
+ buffer.copy(str, 0, offset, offset + len);
+
+ return str;
+}
+
+
+function _writeString(request, buffer, offset) {
+ assert.ok(request);
+ assert.ok(buffer);
+ assert.ok(offset !== undefined);
+
+ ctype.wuint32(buffer.length, 'big', request, offset);
+ offset += 4;
+ buffer.copy(request, offset);
+
+ return offset + buffer.length;
+}
+
+
+function _readHeader(response, expect) {
+ assert.ok(response);
+
+ var len = ctype.ruint32(response, 'big', 0);
+ var type = ctype.ruint8(response, 'big', 4);
+
+ return (expect === type ? len : -1);
+}
+
+
+function _writeHeader(request, tag) {
+ ctype.wuint32(request.length - 4, 'big', request, 0);
+ ctype.wuint8(tag, 'big', request, 4);
+ return 5;
+}
+
+
+
+///--- API
+
+/**
+ * Creates a new SSHAgentClient.
+ *
+ * Note that the environment variable SSH_AUTH_SOCK must be set, else
+ * this will throw.
+ *
+ * @param {Object} options (optional) only supported flag is timeout (in ms).
+ * @throws {MissingEnvironmentVariableError} on SSH_AUTH_SOCK not being set.
+ * @constructor
+ */
+function SSHAgentClient(options) {
+ if (options) {
+ this.timeout = options.timeout || 1000;
+ }
+
+ this.sockFile = process.env.SSH_AUTH_SOCK;
+ if (!this.sockFile)
+ throw new MissingEnvironmentVariableError('SSH_AUTH_SOCK');
+}
+
+
+/**
+ * Lists all SSH keys available under this session.
+ *
+ * This returns an array of objects of the form:
+ * {
+ * type: 'ssh-rsa',
+ * ssh_key: '<base64 string>',
+ * comment: '/Users/mark/.ssh/id_rsa'
+ * }
+ *
+ * @param {Function} callback of the form f(err, keys).
+ * @throws {TypeError} on invalid arguments.
+ */
+SSHAgentClient.prototype.requestIdentities = function(callback) {
+ if (!callback || typeof(callback) !== 'function')
+ throw new TypeError('callback (function) is required');
+
+ function requestIdentities() {
+ var request = new Buffer(4 + 1);
+ _writeHeader(request, PROTOCOL.SSH_AGENTC_REQUEST_RSA_IDENTITIES);
+ return request;
+ }
+
+ function identitiesAnswer(response) {
+ assert.ok(response);
+
+ var numKeys = ctype.ruint32(response, 'big', 0);
+
+ var offset = 4;
+ var keys = [];
+ for (var i = 0; i < numKeys; i++) {
+ var key = _readString(response, offset);
+ offset += 4 + key.length;
+ var comment = _readString(response, offset);
+ offset += 4 + comment.length;
+ var type = _readString(key, 0);
+
+ keys.push({
+ type: type.toString('ascii'),
+ ssh_key: key.toString('base64'),
+ comment: comment.toString('utf8'),
+ _raw: key
+ });
+ }
+
+ return callback(null, keys);
+ }
+
+ return this._request(requestIdentities,
+ identitiesAnswer,
+ PROTOCOL.SSH_AGENT_IDENTITIES_ANSWER,
+ callback);
+};
+
+
+/**
+ * Asks the SSH Agent to sign some data given a key.
+ *
+ * The key object MUST be the object retrieved from
+ * requestIdentities. Data is a Buffer. The response
+ * you get back is an object of the form:
+ *
+ * {
+ * type: 'ssh-rsa',
+ * signature: 'base64 string'
+ * }
+ *
+ * @param {Object} key a key from requestIdentities.
+ * @param {Object} data a Buffer.
+ * @param {Function} callback of the form f(err, signature).
+ * @throws {TypeError} on invalid arguments.
+ */
+SSHAgentClient.prototype.sign = function(key, data, callback) {
+ if (!key || typeof(key) !== 'object')
+ throw new TypeError('key (object) required');
+ if (!data || typeof(data) !== 'object')
+ throw new TypeError('key (buffer) required');
+ if (!callback || typeof(callback) !== 'function')
+ throw new TypeError('callback (function) is required');
+
+ function signRequest() {
+ // Length + tag + 2 length prefixed strings + trailing flags(NULL)
+ var request = new Buffer(4 + 1 + 4 + key._raw.length + 4 + data.length + 4);
+ var offset = _writeHeader(request, PROTOCOL.SSH2_AGENTC_SIGN_REQUEST);
+ offset = _writeString(request, key._raw, offset);
+ offset = _writeString(request, data, offset);
+ ctype.wuint32(0, 'big', request, offset);
+ return request;
+ }
+
+ function signatureResponse(response) {
+ assert.ok(response);
+
+ var blob = _readString(response, 0);
+ var type = _readString(blob, 0);
+ var signature = _readString(blob, type.length + 4);
+
+ return callback(null, {
+ type: type,
+ signature: signature.toString('base64'),
+ _raw: signature
+ });
+ }
+
+ return this._request(signRequest,
+ signatureResponse,
+ PROTOCOL.SSH2_AGENT_SIGN_RESPONSE,
+ callback);
+};
+
+
+
+///--- Private Methods
+
+SSHAgentClient.prototype._request = function(getRequest,
+ parseResponse,
+ messageType,
+ callback) {
+ assert.ok(getRequest && typeof(getRequest) === 'function');
+ assert.ok(parseResponse && typeof(parseResponse) === 'function');
+ assert.ok(messageType && typeof(messageType) === 'number');
+ assert.ok(callback && typeof(callback) === 'function');
+
+ var self = this;
+ var socket = net.createConnection(this.sockFile);
+
+ socket.on('data', function(data) {
+ var len = ctype.ruint32(data, 'big', 0);
+ if (len !== data.length - 4) {
+ return callback(new InvalidProtocolError('Expected length: ' +
+ len + ' but got: ' +
+ data.length));
+ }
+
+ var type = ctype.ruint8(data, 'big', 4);
+ if (type !== messageType) {
+ return callback(new InvalidProtocolError('Expected message type: ' +
+ messageType +
+ ' but got: ' + type));
+ }
+
+ socket.end();
+ return parseResponse(data.slice(5));
+ });
+
+ socket.on('connect', function() {
+ socket.write(getRequest());
+ });
+
+ socket.on('error', function(err) {
+ return callback(err);
+ });
+
+ socket.setTimeout(this.timeout, function() {
+ socket.end();
+ var e = new TimeoutError('request timed out after: ' + self.timeout);
+ return callback(e);
+ });
+
+ return socket;
+};
+
+
+
+module.exports = SSHAgentClient;
+
24 package.json
@@ -0,0 +1,24 @@
+{
+ "author": "Mark Cavage <mcavage@gmail.com>",
+ "name": "ssh-agent",
+ "description": "An API for interacting with the SSH Agent.",
+ "version": "0.1.0",
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/mcavage/node-ssh-agent.git"
+ },
+ "main": "lib/ssh_agent_client",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "dependencies": {
+ "ctype": ">=0.0.2"
+ },
+ "devDependencies": {
+ "whiskey": ">=0.4.0"
+ },
+ "scripts": {
+ "pretest": "which gjslint; if [[ \"$?\" = 0 ]] ; then gjslint --nojsdoc -r lib; else echo \"Missing gjslint. Skipping lint\"; fi",
+ "test": "SSH_PRIVATE_KEY=$HOME/.ssh/id_rsa ./node_modules/.bin/whiskey -t \"`find tst -name *.test.js | xargs`\""
+ }
+}
77 tst/agent.test.js
@@ -0,0 +1,77 @@
+// Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved.
+
+var crypto = require('crypto');
+var fs = require('fs');
+var util = require('util');
+
+var SSHAgentClient = require('../lib/ssh_agent_client');
+
+
+
+///--- Globals
+
+var client = null;
+var privateKey = null;
+
+
+///--- Start Tests
+
+exports.setUp = function(test, assert) {
+ client = new SSHAgentClient();
+ assert.ok(client);
+
+ if (process.env.SSH_PRIVATE_KEY)
+ privateKey = fs.readFileSync(process.env.SSH_PRIVATE_KEY, 'ascii');
+
+ test.finish();
+};
+
+
+exports.test_request_identities = function(test, assert) {
+ client.requestIdentities(function(err, keys) {
+ assert.ifError(err);
+ assert.ok(keys);
+ assert.ok(keys.length);
+ assert.ok(keys[0].type);
+ assert.ok(keys[0].ssh_key);
+ assert.ok(keys[0].comment);
+ assert.ok(keys[0]._raw);
+ test.finish();
+ });
+};
+
+
+exports.test_sign = function(test, assert) {
+ client.requestIdentities(function(err, keys) {
+ assert.ifError(err);
+ assert.ok(keys);
+ assert.ok(keys.length);
+
+ var key = keys[0];
+ for (var i = 0; i < keys.length; i++) {
+ if (keys[i].type === 'ssh-rsa') {
+ key = keys[i];
+ break;
+ }
+ }
+
+ var data = new Buffer('Hello World');
+ client.sign(key, data, function(err, signature) {
+ assert.ifError(err);
+ assert.ok(signature);
+
+ if (privateKey) {
+ var signer = crypto.createSign('RSA-SHA1');
+ signer.update(data);
+ assert.equal(signature.signature, signer.sign(privateKey, 'base64'));
+ }
+
+ test.finish();
+ });
+ });
+};
+
+
+exports.tearDown = function(test, assert) {
+ test.finish();
+};
Please sign in to comment.
Something went wrong with that request. Please try again.