Skip to content

Commit

Permalink
PUBAPI-1146 Divorce wanted between *_KEY_ID env vars and keyId actual…
Browse files Browse the repository at this point in the history
…ly sent to server
  • Loading branch information
arekinath committed Oct 5, 2015
1 parent b6b1396 commit 6dfa3df
Show file tree
Hide file tree
Showing 2 changed files with 15 additions and 329 deletions.
338 changes: 12 additions & 326 deletions lib/imgapi.js
Expand Up @@ -69,6 +69,7 @@ var restify = require('restify');
var SSHAgentClient = require('ssh-agent');
var mod_url = require('url');
var backoff = require('backoff');
var auth = require('smartdc-auth');


// ---- globals
Expand Down Expand Up @@ -2485,331 +2486,6 @@ IMGAPI.prototype.channelAddImage = function channelAddImage(opts, cb) {
};



// ---- http-signature auth signing

var FINGERPRINT_RE = /^([a-f0-9]{2}:){15}[a-f0-9]{2}$/;


/**
* Calculate the fingerprint of the given ssh public key data.
*/
function fingerprintFromSshpubkey(sshpubkey) {
assert.string(sshpubkey, 'sshpubkey');

// Let's accept either:
// - just the base64 encoded data part, e.g.
// 'AAAAB3NzaC1yc2EAAAABIwAA...2l24uq9Lfw=='
// - the full ssh pub key file content, e.g.:
// 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAA...2l24uq9Lfw== my comment'
if (/^ssh-[rd]sa /.test(sshpubkey)) {
sshpubkey = sshpubkey.split(/\s+/, 2)[1];
}

var fingerprint = '';
var hash = crypto.createHash('md5');
hash.update(new Buffer(sshpubkey, 'base64'));
var digest = hash.digest('hex');
for (var i = 0; i < digest.length; i++) {
if (i && i % 2 === 0)
fingerprint += ':';
fingerprint += digest[i];
}

return (fingerprint);
}


/**
* Get an ssh public key fingerprint, e.g.:
* 28:21:57:10:fd:f4:a4:2f:0c:4a:86:39:07:a5:de:72
* from the given keyId.
*
* @param keyId {String} An ssh public key fingerprint or ssh private key path.
* @param callback {Function} `function (err, fingerprint)`
*/
function fingerprintFromKeyId(keyId, callback) {
if (FINGERPRINT_RE.test(keyId)) {
callback(null, keyId);
return;
}

// Try to get it from .pub public key file beside the ssh private key
// path.
var pubKeyPath = keyId + '.pub';
fs.exists(pubKeyPath, function (exists) {
if (!exists) {
callback(new Error(format(
'no SSH public key file, "%s", for keyId "%s"',
pubKeyPath, keyId)));
return;
}
fs.readFile(pubKeyPath, 'ascii', function (err, data) {
if (err) {
callback(err);
return;
}
callback(err, fingerprintFromSshpubkey(data));
});
});
}


/**
* Get a key object from ssh-agent for the given keyId.
*
* @param agent {SSHAgentClient}
* @param keyId {String} An ssh public key fingerprint or ssh private key path.
* @param callback {Function} `function (err, agentKey)`
*/
function agentKeyFromKeyId(agent, keyId, callback) {
assert.object(agent, 'agent');
assert.string(keyId, 'keyId');
assert.func(callback, 'callback');

fingerprintFromKeyId(keyId, function (err, fingerprint) {
if (err) {
callback(err);
return;
}

agent.requestIdentities(function (err, keys) {
if (err) {
callback(err);
return;
}

/**
* A key looks like this:
* { type: 'ssh-rsa',
* ssh_key: 'AAAAB3Nz...',
* comment: '/Users/bob/.ssh/foo.id_rsa',
* _raw: < Buffer 00 00 00 07 ... > },
*/
var key = (keys || []).filter(function (k) {
// DSA over agent doesn't work
if (k.type === 'ssh-dss')
return (false);
// console.log("%s == %s ? %s [keyId=%s]",
// fingerprint, fingerprintFromSshpubkey(k.ssh_key),
// (fingerprint === fingerprintFromSshpubkey(k.ssh_key)),
// keyId);
return (fingerprint === fingerprintFromSshpubkey(k.ssh_key));
}).pop();

if (!key) {
callback(new Error('no key ' + fingerprint + ' in ssh agent'));
return;
}

callback(null, key);
});
});
}


function sshAgentSign(agent, agentKey, data, callback) {
assert.object(agent, 'agent');
assert.object(agentKey, 'agentKey');
assert.object(data, 'data (Buffer)');
assert.func(callback, 'callback');

agent.sign(agentKey, data, function (err, sig) {
if (err) {
callback(err);
return;
}
callback(null, {algorithm: 'rsa-sha1', signature: sig.signature});
});
}


/**
* Load a local ssh private key (in PEM format). PEM format is the OpenSSH
* default format for private keys.
*
* @param keyId {String} An ssh public key fingerprint or ssh private key path.
* @param log {Bunyan Logger}
* @param callback {Function} `function (err, sshPrivKeyData)`
*/
function loadSSHKey(keyId, log, callback) {
// If `keyId` is already a private key path, then just read it and return.
if (!FINGERPRINT_RE.test(keyId)) {
fs.readFile(keyId, 'utf8', callback);
return;
}

// Else, look at all priv keys in "~/.ssh" for a matching fingerprint.
var fingerprint = keyId;
var keyDir = process.env.HOME + '/.ssh';
fs.readdir(keyDir, function (readdirErr, filenames) {
if (readdirErr) {
callback(readdirErr);
return;
}

var match = null;
async.forEachSeries(
filenames || [],
function oneFile(pubKeyFilename, next) {
if (match || /\.pub$/.test(pubKeyFilename)) {
next();
return;
}

var pubKeyPath = keyDir + '/' + pubKeyFilename;
fs.readFile(pubKeyPath, 'utf8', function (readErr, data) {
if (readErr) {
log.debug(readErr, 'could not read "%s"', pubKeyPath);
} else if (fingerprintFromSshpubkey(data) === fingerprint) {
match = pubKeyPath;
}
next();
});
},
function done(err) {
if (err) {
callback(err);
return;
} else if (match) {
var privKeyPath = match.split(/\.pub$/)[0];
fs.readFile(privKeyPath, 'utf8', callback);
}
});
});
}



/**
* Create an IMGAPI request signer for the http-signature auth scheme
* approriate for use with a CLI tool. This handles integrate with ssh keys
* and ssh-agent.
*
* @param options {Object}
* - `user` {String} The user name.
* - `keyId` or `keyIds` {String} One or more key ids with which to
* sign. A key id is either an ssh key *fingerprint* or a path to
* a private ssh key.
* - `log` {Bunyan Logger} Optional.
*/
function cliSigner(options) {
assert.object(options, 'options');
assert.optionalObject(options.log, 'options.log');
assert.string(options.user, 'options.user');
assert.optionalString(options.keyId, 'options.keyId');
assert.optionalArrayOfString(options.keyIds, 'options.keyIds');
assert.ok(options.keyId || options.keyIds &&
!(options.keyId && options.keyIds),
'one of "options.keyId" or "options.keyIds"');

var log = options.log || new BunyanNoopLogger();
var keyIds = (options.keyId ? [options.keyId] : options.keyIds);
var user = options.user;

// Limitation. TODO: remove this limit
assert.ok(keyIds.length === 1, 'only a single keyId currently supported');
var keyId = keyIds[0];

function sign(str, callback) {
assert.string(str, 'string');
assert.func(callback, 'callback');

var arg = {};
vasync.pipeline({
arg: arg,
funcs: [
function tryAgent(arg, next) {
log.debug('looking for %s in agent', keyId);
try {
arg.agent = new SSHAgentClient();
} catch (e) {
log.debug(e, 'unable to create agent');
next();
return;
}
agentKeyFromKeyId(arg.agent, keyId, function (err, key) {
if (err) {
log.debug(err, 'key not in agent');
} else {
log.debug({key: key.ssh_key}, 'key in agent');
arg.key = key;
}
next();
});
},
function agentSign(arg, next) {
if (!arg.key) {
next();
return;
}

log.debug('signing with agent');
var data = new Buffer(str);
sshAgentSign(arg.agent, arg.key, data, function (err, res) {
if (err) {
log.debug(err, 'agent sign fail');
} else {
res.keyId = keyId;
res.user = user;
arg.res = res;
}
next();
});
},
function loadKey(arg, next) {
if (arg.res) {
next();
return;
}

log.debug('loading private key');
loadSSHKey(keyId, log, function (err, key) {
if (err) {
log.debug(err, 'loading private key failed');
next(err);
return;
}

var alg = / DSA /.test(key) ? 'DSA-SHA1' : 'RSA-SHA256';
log.debug({algorithm: alg}, 'loaded private key');
arg.algorithm = alg;
arg.key = key;
next();
});
},
function keySign(arg, next) {
if (arg.res) {
next();
return;
}

var s = crypto.createSign(arg.algorithm);
s.update(str);
var signature = s.sign(arg.key, 'base64');
arg.res = {
algorithm: arg.algorithm.toLowerCase(),
keyId: keyId,
signature: signature,
user: user
};
next();
}
]
}, function (err) {
if (err) {
callback(err);
} else {
callback(null, arg.res);
}
});
}

return (sign);
}



// ---- exports

module.exports = IMGAPI;
Expand All @@ -2821,7 +2497,17 @@ module.exports.createClient = function createClient(options) {
return new IMGAPI(options);
};

module.exports.cliSigner = cliSigner;
module.exports.cliSigner = function (opts, cb) {
/* API backwards compatibility */
if (opts.keyIds !== undefined) {
if (!Array.isArray(opts.keyIds) || opts.keyIds.length !== 1)
throw (new Error('options.keyIds must be an array with a single ' +
'element'));
opts.keyId = opts.keyIds[0];
delete (opts.keyIds);
}
return (auth.cliSigner(opts, cb));
};

// A useful utility that must be used on a stream passed into the
// `addImageFile` API to not lose leading chunks.
Expand Down
6 changes: 3 additions & 3 deletions package.json
@@ -1,7 +1,7 @@
{
"name": "sdc-clients",
"description": "Contains node.js client libraries for SDC REST APIs.",
"version": "8.1.5",
"version": "9.0.0",
"homepage": "http://www.joyent.com",
"private": true,
"repository": {
Expand All @@ -20,9 +20,9 @@
"libuuid": "0.1.2",
"once": "^1.3.1",
"restify": "git://github.com/joyent/node-restify.git#fd5d5b5",
"ssh-agent": "0.2.1",
"vasync": "^1.6.2",
"verror": "^1.6.0"
"verror": "^1.6.0",
"smartdc-auth": "2.1.2"
},
"devDependencies": {
"nodeunit": "0.8.0"
Expand Down

0 comments on commit 6dfa3df

Please sign in to comment.