diff --git a/Makefile b/Makefile index a6b761d..d4f4309 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,11 @@ JSL_FILES_NODE = $(JS_FILES) JSSTYLE_FILES = $(JS_FILES) JSSTYLE_FLAGS = -o indent=4,doxygen,unparenthesized-return=0 +# +# Tools +# +NPM_EXEC := npm +TAPE := ./node_modules/.bin/tape include ./tools/mk/Makefile.defs @@ -19,7 +24,11 @@ include ./tools/mk/Makefile.defs # .PHONY: all all: - npm install + $(NPM_EXEC) install + +.PHONY: test +test: all + $(TAPE) test/*.test.js include ./tools/mk/Makefile.deps include ./tools/mk/Makefile.targ diff --git a/README.md b/README.md index 5f2eb1b..7f83304 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,6 @@ Utility functions to sign http requests to SmartDC services. var signer = auth.privateKeySigner({ key: fs.readFileSync(process.env.HOME + '/.ssh/id_rsa', 'utf8'), - keyId: process.env.SDC_CLI_KEY_ID, user: process.env.SDC_CLI_ACCOUNT }); @@ -27,26 +26,72 @@ The `keyId` for SmartDC is always `/$your_joyent_login/keys/$ssh_fingerprint`, and the supported algorithms are: `rsa-sha1`, `rsa-sha256` and `dsa-sha1`. You then just append the base64 encoded signature. -Please, note that at the moment of writing this document, `dsa-sha1` algorithm -does not work with `sshAgentSigner` yet. - ## Authenticating Requests When creating a smartdc client, you'll need to pass in a callback function for -the `sign` parameter. smartdc-auth ships with three functions that will likely -suit your need: `cliSigner`, `privateKeySigner` and `sshAgentSigner`. All of -these callbacks will automatically do the correct crypto for authenticating -requests, the difference is that `privateKeySigner` expects (non-passphrase -protected) keys to be passed in directly (as a file name), whereas `cliSigner` -and `sshAgentSigner` will load your credentials on each request from the SSH -agent (if available). Both callbacks require you to set the account (login) -and keyId (SSH key fingerprint). +the `sign` parameter. smartdc-auth ships with three constructors that return +such functions, which may suit your need: `cliSigner`, `privateKeySigner` and +`sshAgentSigner`. + +### `privateKeySigner(options);` + +A basic signer which signs using a given PEM (PKCS#1) format private key only. +Ideal for simple use cases where the key is stored in a file on the filesystem +ready for use. + +- `options`: an Object containing properties: + - `key`: a String, PEM-format (PKCS#1) private key, for any supported algorithm + - `user`: a String, SDC login name to be used in the full keyId, above + - `subuser`: an optional String, SDC sub-user login name + - `keyId`: optional String, the fingerprint of the `key` (not the same as the + full keyId given to the server). Ignored unless it does not match + the given `key`, then an Error will be thrown. + +### `sshAgentSigner(options);` + +Signs requests using a key that is stored in the OpenSSH agent. Opens and manages +a connection to the current session's agent during operation. + +- `options`: an Object containing properties: + - `keyId`: a String, fingerprint of the key to retrieve from the agent + - `user`: a String, SDC login name to be used + - `subuser`: an optional String, SDC sub-user login name + - `sshAgentOpts`: an optional Object, any additional options to pass through + to the SSHAgent constructor (eg `timeout`) + +### `cliSigner(options);` + +Signs requests using a key located either in the OpenSSH agent, or found in +the filesystem under `$HOME/.ssh` (or its equivalent on your platform). + +This is generally intended for use with CLI utilities (eg the `sdc-listmachines` +tool and family), hence the name. + +- `options`: an Object containing properties: + - `keyId`: a String, fingerprint of the key to retrieve or find + - `user`: a String, SDC login name to be used + - `subuser`: an optional String, SDC sub-user login name + - `algorithm`: an optional String, the signing algorithm to use. If this + does not match up with the algorithm of the key (once it is + located), an Error will be thrown. + - `sshAgentOpts`: an optional Object, any additional options to pass through + to the SSHAgent constructor (eg `timeout`) + +The `keyId` fingerprint does not necessarily need to be the exact format +(hex MD5) as sent to the server -- it can be in any fingerprint format supported +by the [`sshpk`](https://github.com/arekinath/node-sshpk) library. + +As of version 2.0.0, an invalid fingerprint (one that can never match any key, +because, for example, it contains invalid characters) will produce an exception +immediately rather than returning a `sign` function. Note that the `cliSigner` and `sshAgentSigner` are not suitable for server applications, or any other system where the performance degradation necessary to interact with SSH is not acceptable; put another way, you should only use it for interactive tooling, such as the CLI that ships with node-smartdc. +### Writing your own signer + Should you wish to write a custom plugin, the expected implementation of the `sign` callback is a function of the form `function (string, callback)`. `string` is generated by node-smartdc (typically the value of the Date header), diff --git a/bin/sdc-curl b/bin/sdc-curl new file mode 100755 index 0000000..cb77931 --- /dev/null +++ b/bin/sdc-curl @@ -0,0 +1,103 @@ +#!/usr/bin/env node +// -*- mode: js -*- +// vim: set filetype=javascript : +// Copyright 2015 Joyent, Inc. All rights reserved. + +var dashdash = require('dashdash'); +var bunyan = require('bunyan'); +var spawn = require('child_process').spawn; +var auth = require('../lib/index'); +var sprintf = require('util').format; + +var options = [ + { + names: ['account'], + type: 'string', + help: 'account name', + env: 'SDC_ACCOUNT' + }, { + names: ['user'], + type: 'string', + help: 'account sub-user login', + env: 'SDC_USER' + }, { + names: ['keyId'], + type: 'string', + help: 'your ssh key fingerprint', + env: 'SDC_KEY_ID' + }, { + names: ['manta'], + type: 'bool', + help: 'use manta-style sub-user format' + }, { + names: ['help', 'h'], + type: 'bool', + help: 'print this help and exit' + } +]; + +if (require.main === module) { + var parser = dashdash.createParser({ + options: options, + interspersed: true, + allowUnknown: true + }); + + try { + var opts = parser.parse(process.argv); + } catch (e) { + console.error('sdc-curl: error: %s', e.message); + process.exit(1); + } + + if (opts.help || opts._args.length < 1) { + var help = parser.help({includeEnv: true}).trimRight(); + console.log( + 'sdc-curl: performs a signed curl request with the same auth\n' + + ' creds as the sdc-* family of tools'); + console.log('usage: sdc-curl [OPTIONS]\n' + + 'options:\n' + help); + console.log('any options other than these will be passed directly to ' + + 'curl'); + process.exit(1); + } + + var user = opts.account; + if (opts.user !== undefined) { + user = opts.account + '/user/' + opts.user; + if (opts.manta) + user = opts.account + '/' + opts.user; + } + + var sign = auth.cliSigner({ + user: user, + keyId: opts.keyId + }); + + var args = opts._args.slice(); + + var dateHeader = 'date: ' + new Date().toUTCString(); + + sign(dateHeader, function (err, obj) { + if (err) + throw (err); + + var authz = sprintf( + 'Signature keyId="/%s/keys/%s",headers="date",' + + 'algorithm="%s",signature="%s"', + obj.user, obj.keyId, obj.algorithm, obj.signature); + + args.push('-H'); + args.push(dateHeader); + args.push('-H'); + args.push('Authorization: ' + authz); + + var kid = spawn('curl', args); + kid.stdout.pipe(process.stdout); + kid.stderr.pipe(process.stderr); + process.stdin.pipe(kid.stdin); + kid.on('close', function (rc) { + process.exit(rc); + }); + }); +} diff --git a/lib/index.js b/lib/index.js index 493f86a..b786079 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,54 +1,156 @@ -// Copyright (c) 2014, Joyent, Inc. All rights reserved. +// Copyright (c) 2015, Joyent, Inc. All rights reserved. var crypto = require('crypto'); var EventEmitter = require('events').EventEmitter; var fs = require('fs'); var path = require('path'); +var util = require('util'); var assert = require('assert-plus'); var clone = require('clone'); var SSHAgentClient = require('ssh-agent'); var once = require('once'); var vasync = require('vasync'); +var sshpk = require('sshpk'); +function KeyNotFoundError(fp, srcs) { + assert.string(fp, 'fingerprint'); + assert.arrayOfString(srcs, 'sources'); + if (Error.captureStackTrace) + Error.captureStackTrace(this, KeyNotFoundError); + this.name = 'KeyNotFoundError'; + this.fingerprint = fp; + this.sources = srcs; + this.message = 'SSH key with fingerprint "' + fp + '" could not be ' + + 'located in ' + srcs.join(' or '); +} +util.inherits(KeyNotFoundError, Error); +KeyNotFoundError.join = function (errs) { + assert.arrayOfObject(errs, 'errors'); + var fp = errs[0].fingerprint; + var srcs = errs[0].sources; + for (var i = 1; i < errs.length; ++i) { + assert.ok(errs[i] instanceof KeyNotFoundError); + assert.strictEqual(errs[i].fingerprint, fp); + srcs = srcs.concat(errs[i].sources); + } + return (new KeyNotFoundError(fp, srcs)); +}; +function SignatureCache(opts) { + assert.optionalObject(opts, 'options'); + opts = opts || {}; + assert.optionalNumber(opts.expiry, 'options.expiry'); -// ---- Helpers + this.expiry = opts.expiry || 10000; + this.pending = new EventEmitter(); + this.pending.table = {}; + this.table = {}; + this.list = []; +} -function fingerprint(key) { - var digest; - var fp = ''; - var hash = crypto.createHash('md5'); +SignatureCache.prototype.get = function get(k, cb) { + assert.string(k, 'key'); + assert.func(cb, 'callback'); - hash.update(new Buffer(key, 'base64')); - digest = hash.digest('hex'); + cb = once(cb); + var found = false; + var self = this; - for (var i = 0; i < digest.length; i++) { - if (i && i % 2 === 0) - fp += ':'; + function cachedResponse() { + var val = self.table[k].value; + cb(val.err, val.value); + } - fp += digest[i]; + if (this.table[k]) { + found = true; + process.nextTick(cachedResponse); + } else if (this.pending.table[k]) { + found = true; + this.pending.once(k, cachedResponse); } - return (fp); + return (found); +}; + + +SignatureCache.prototype.put = function put(k, v) { + assert.string(k, 'key'); + assert.ok(v, 'value'); + + this.table[k] = { + time: new Date().getTime(), + value: v + }; + + if (this.pending.table[k]) + delete this.pending.table[k]; + + this.pending.emit(k, v); + this.purge(); +}; + + +SignatureCache.prototype.purge = function purge() { + var list = []; + var now = new Date().getTime(); + var self = this; + + Object.keys(this.table).forEach(function (k) { + if (self.table[k].time + self.expiry < now) + list.push(k); + }); + + list.forEach(function (k) { + if (self.table[k]) + delete self.table[k]; + }); +}; + + +SignatureCache.prototype.toString = function toString() { + var fmt = '[object SignatureCache]'; + return (util.format(fmt, this.pending.table, this.table)); +}; + + +function createCacheKey(opts) { + assert.object(opts, 'options'); + assert.object(opts.key, 'options.key'); + assert.string(opts.data, 'options.data'); + + return (opts.key.comment + '|' + opts.data); } +function canonicalKeyId(key) { + assert.object(key, 'key'); + return (key.fingerprint('md5').toString('hex')); +} function loadSSHKey(fp, cb) { - assert.string(fp, 'fingerprint'); + if (typeof (fp) === 'string') + fp = sshpk.parseFingerprint(fp); + assert.object(fp, 'fingerprint'); + assert.ok(fp instanceof sshpk.Fingerprint, + 'fingerprint instanceof sshpk.Fingerprint'); assert.func(cb, 'callback'); cb = once(cb); var p; - if (process.platform == 'win32') { + if (process.platform === 'win32') { p = process.env.USERPROFILE; } else { p = process.env.HOME; } + if (!p) { + cb(new Error('cannot find HOME dir (HOME/USERPROFILE is not set)')); + return; + } + p = path.join(p, '.ssh'); fs.readdir(p, function (err, files) { @@ -57,42 +159,153 @@ function loadSSHKey(fp, cb) { return; } - var keys = (files || []).filter(function (f) { - return (/\.pub$/.test(f)); + var keyFiles = []; + (files || []).forEach(function (f) { + /* If we have a .pub file and a matching private key */ + var m = f.match(/(.+)\.pub$/); + if (m && files.indexOf(m[1]) !== -1) { + keyFiles.push({public: f, private: m[1]}); + return; + } + /* + * If the name contains id_ (but doesn't end with .pub) and there + * is no matching public key + */ + var m2 = f.match(/(^|[^a-zA-Z])id_/); + if (!m && m2 && files.indexOf(f + '.pub') === -1) { + keyFiles.push({private: f}); + return; + } }); - if (keys.length === 0) { - cb(new Error('no public SSH keys in: ' + p)); - return; + /* + * When we have both a public and private key file, read in the + * .pub file first to do the fingerprint match. If that succeeds, + * read in and validate that the private key file matches it. + * + * This also ensures we fail early and give a sensible error if, + * e.g. the specified key is password-protected. + */ + function readPublicKey(keyFile, kcb) { + var fname = path.join(p, keyFile.public); + fs.readFile(fname, 'ascii', function (kerr, blob) { + if (kerr) { + kcb(kerr); + return; + } + + try { + var key = sshpk.parseKey(blob, 'ssh', fname); + } catch (e) { + kcb(e); + return; + } + + if (fp.matches(key)) { + /* + * At this point, readPrivateKey has to succeed. If it + * doesn't, its error should go all the way to the user + * rather than a KeyNotFoundError (we did find the key, + * but something is wrong with it) + */ + readPrivateKey(keyFile, function (pkerr, pk) { + cb(pkerr, pk); + kcb(null, pk); + }); + } else { + kcb(new KeyNotFoundError(fp.toString(), [fname])); + } + }); } - var done = false; - var finished = 0; - function _done(err2, _k) { - if (done) - return; + function readPrivateKey(keyFile, kcb) { + var fname = path.join(p, keyFile.private); + fs.readFile(fname, 'ascii', function (kerr, blob) { + if (kerr) { + kcb(kerr); + return; + } - done = true; - if (err2) { - cb(err2); - } else { - fs.readFile(_k, 'utf8', cb); - } + try { + var key = sshpk.parseKey(blob, 'pem', fname); + } catch (e) { + kcb(e); + return; + } + + /* + * NOTE: we call cb() here (which has been once()'d above) + * directly if we find a match. The actual forEachParallel cb + * only calls cb() in case nothing succeeds. + */ + if (fp.matches(key)) { + key.privatePem = blob; + cb(null, key); + kcb(null, key); + } else { + kcb(new KeyNotFoundError(fp.toString(), [fname])); + } + }); } - function _checkPublic(fname, err2, blob) { - if (err2) { - _done(err2); - } else if (fingerprint(blob.split(' ')[1]) === fp) { - _done(null, fname.split(/\.pub$/)[0]); - } else if (++finished === keys.length) { - _done(new Error(fp + ' not found in: ' + p)); + function processKey(keyFile, kcb) { + /* + * Stat the file first to ensure we don't read from any sockets + * or crazy huge files that ended up in $HOME/.ssh (it happens) + */ + var fname; + if (keyFile.public) { + fname = path.join(p, keyFile.public); + fs.stat(fname, function (serr, stats) { + if (serr) { + kcb(serr); + return; + } + if (stats.isFile() && stats.size < 65536) { + readPublicKey(keyFile, kcb); + } else { + kcb(new Error(fname + ' is not a regular file, or ' + + 'size is too big to be an SSH public key.')); + } + }); + } else { + fname = path.join(p, keyFile.private); + fs.stat(fname, function (serr, stats) { + if (serr) { + kcb(serr); + return; + } + if (stats.isFile() && stats.size < 131072) { + readPrivateKey(keyFile, kcb); + } else { + kcb(new Error(fname + ' is not a regular file, or ' + + 'size is too big to be an SSH private key.')); + } + }); } } - keys.forEach(function (f) { - var _p = p + '/' + f; - fs.readFile(_p, 'utf8', _checkPublic.bind(null, _p)); + var opts = { + inputs: keyFiles, + func: processKey + }; + vasync.forEachParallel(opts, function (errs, res) { + /* Only handle the not found case, see above. */ + if (res.successes.length === 0) { + var msg = 'dir ' + p; + if (errs) { + var fatals = []; + res.operations.forEach(function (op) { + if (op.err && !(op.err instanceof KeyNotFoundError)) + fatals.push(op.err.name + ': ' + + op.err.message); + }); + if (fatals.length > 0) + msg += ' [warnings: ' + fatals.join(' ; ') + ']'; + } + cb(new KeyNotFoundError(fp.toString(), [msg])); + return; + } }); }); } @@ -108,127 +321,159 @@ function rfc3986(str) { function sshAgentGetKey(client, fp, cb) { assert.object(client, 'sshAgentClient'); - assert.string(fp, 'fingerprint'); + if (typeof (fp) === 'string') + fp = sshpk.parseFingerprint(fp); + assert.object(fp, 'fingerprint'); + assert.ok(fp instanceof sshpk.Fingerprint, + 'fingerprint instanceof sshpk.Fingerprint'); assert.func(cb, 'callback'); + var cache = client._signCache; + var _key = 'requestIdentities ' + fp.toString(); + if (cache.get(_key, cb)) + return; + client.requestIdentities(function (err, keys) { - if (err) { - cb(err); - return; - } + var _val = { + err: null, + value: null + }; - var key = (keys || []).filter(function (k) { - // DSA over agent doesn't work - if (k.type === 'ssh-dss') - return (false); - return (fp === fingerprint(k.ssh_key)); - }).pop(); + if (err) { + _val.err = err; + } else { + var key; + for (var i = 0; i < keys.length; ++i) { + var k = keys[i]; + var kp = sshpk.parseKey(k._raw, 'rfc4253', + 'ssh-agent:' + k.comment); + kp.comment = k.comment; + kp._raw = k._raw; + + if (fp.matches(kp)) { + key = kp; + break; + } + } - if (!key) { - cb(new Error('no key ' + fp + ' in ssh agent')); - return; + if (!key) { + _val.err = new KeyNotFoundError(fp.toString(), ['ssh-agent (' + + keys.length + ' keys)']); + } else { + _val.value = key; + } } - cb(null, key); + cache.put(_key, _val); + cb(_val.err, _val.value); }); } -function createSSHAgent(cb) { - assert.func(cb, 'callback'); +function createSSHAgent(agentOpts) { + assert.optionalObject(agentOpts, 'agentOpts'); - var agent; + /* + * Return an error rather than throwing, so if our caller wants to ignore + * an issue with agent setup (eg no socket to connect to), they can do that + * without also ignoring a programmer error in SignatureCache (like a + * failed assertion). + */ try { - agent = new SSHAgentClient(); + var agent = new SSHAgentClient(agentOpts); } catch (e) { - cb(e); - return; + assert.ok(e instanceof Error); + return (e); } - agent._signCache = {}; - - cb(null, agent); + agent._signCache = new SignatureCache(); + return (agent); } function sshAgentSign(client, key, data, alg, cb) { assert.object(client, 'sshAgentClient'); assert.object(client._signCache, 'sshAgentClient'); assert.object(key, 'key'); - assert.object(data, 'data (Buffer)'); + assert.buffer(data, 'data'); if (typeof (alg) === 'function') { cb = alg; - alg = 'rsa-sha1'; + alg = key.type + '-sha1'; } + if (alg === undefined) + alg = key.type + '-sha1'; assert.string(alg, 'algorithm'); assert.func(cb, 'callback'); - var c = client._signCache; - if (c.key === key.comment && c.data && - c.data.toString() === data.toString()) { - if (c.signing) { - /* - * The cache has been tagged, but is currently being - * signed; add our callback to the queue to be serviced - * when the call completes. - */ - c.cbs.push({ func: cb, t: this }); - return; - } + var algType = alg.split('-')[0].toLowerCase(); + assert.strictEqual(algType, key.type, + 'key type ' + key.type + ' does not match signature algorithm ' + alg); - if (c.sig) { - cb(null, c.sig); - return; - } - } + var cache = client._signCache; - c = { - key: key.comment, - data: data, - signing: true, - sig: false, - cbs: [ { func: cb, t: this } ] - }; + var _key = createCacheKey({ + key: key, + data: data.toString() + }); - if (!client._signCache.signing) - client._signCache = c; + if (cache.get(_key, cb)) + return; client.sign(key, data, function (err, signature) { - c.signing = false; - var cbs = c.cbs; + var _val = {}; if (err) { - cbs.forEach(function (_cb) { - _cb.func.call(_cb.t, err); - }); - return; - } + _val.err = err; + cb(err); + } else { + _val.err = null; + _val.value = { + algorithm: alg, + signature: (signature || {}).signature + }; - var sig = { - algorithm: alg, - signature: signature.signature - }; + cb(null, _val.value); + } - c.sig = sig; - cbs.forEach(function (_cb) { - _cb.func.call(_cb.t, null, sig); - }); + cache.put(_key, _val); }); } - +function defaultSignAlgorithm(key) { + switch (key.type) { + case 'rsa': + return ('RSA-SHA256'); + case 'dsa': + return ('DSA-SHA1'); + case 'ecdsa': + /* NOTE: lowercase, because node-crypto is speshul */ + return ('ecdsa-SHA1'); + default: + throw (new Error('Unsupported key type: ' + key.type)); + } +} // ---- API function privateKeySigner(options) { assert.object(options, 'options'); assert.optionalString(options.algorithm, 'options.algorithm'); - assert.string(options.keyId, 'options.keyId'); + assert.optionalString(options.keyId, 'options.keyId'); assert.string(options.key, 'options.key'); assert.string(options.user, 'options.user'); + assert.optionalString(options.subuser, 'options.subuser'); + + var key = sshpk.parseKey(options.key, 'pem'); + if (options.keyId) { + var fp = sshpk.parseFingerprint(options.keyId); + assert.ok(fp.matches(key), 'keyId does not match the given key'); + } + var keyId = canonicalKeyId(key); - var algorithm = options.algorithm ? options.algorithm : - (/ DSA /.test(options.key) ? 'DSA-SHA1' : 'RSA-SHA256'); + var algorithm = options.algorithm; - algorithm = algorithm.toUpperCase(); + if (algorithm === undefined) + algorithm = defaultSignAlgorithm(key); + else + algorithm = algorithm.toUpperCase(); var opts = clone(options); @@ -236,21 +481,27 @@ function privateKeySigner(options) { assert.string(str, 'str'); assert.func(cb, 'callback'); - var signer = crypto.createSign(algorithm); + var signer = crypto.createSign(algorithm. + replace(/^ecdsa/, 'ecdsa-with')); signer.update(str); var res = { - algorithm: sign.algorithm, - keyId: opts.keyId, + algorithm: sign.algorithm.toLowerCase(), + keyId: keyId, signature: signer.sign(opts.key, 'base64'), - user: opts.user + user: opts.user, + subuser: opts.subuser }; cb(null, res); } sign.algorithm = algorithm.toLowerCase(); - sign.keyId = options.keyId; + sign.keyId = keyId; sign.user = options.user; + sign.subuser = options.subuser; + sign.getKey = function (cb) { + cb(null, key); + }; return (sign); } @@ -260,16 +511,23 @@ function sshAgentSigner(options) { assert.object(options, 'options'); assert.string(options.keyId, 'options.keyId'); assert.string(options.user, 'options.user'); + assert.optionalObject(options.sshAgentOpts, 'options.sshAgentOpts'); + assert.optionalString(options.subuser, 'options.subuser'); + + var agentOrErr = createSSHAgent(options.sshAgentOpts); + /* An agent signer is useless without an agent, so throw */ + if (agentOrErr instanceof Error) + throw (agentOrErr); + + var agent = agentOrErr; - var agent = new SSHAgentClient(); - agent._signCache = {}; - var keyId = options.keyId; + var fp = sshpk.parseFingerprint(options.keyId); function sign(str, cb) { assert.string(str, 'string'); assert.func(cb, 'callback'); - sshAgentGetKey(agent, keyId, function (err, key) { + sshAgentGetKey(agent, fp, function (err, key) { if (err) { cb(err); return; @@ -280,17 +538,27 @@ function sshAgentSigner(options) { if (err2) { cb(err2); } else { - res.keyId = keyId; + res.keyId = canonicalKeyId(key); res.user = options.user; + res.subuser = options.subuser; + sign.algorithm = res.algorithm; + sign.keyId = res.keyId; cb(null, res); } }); }); } - sign.algorithm = 'rsa-sha1'; sign.keyId = options.keyId; sign.user = options.user; + sign.subuser = options.subuser; + sign.getKey = function (cb) { + sshAgentGetKey(agent, fp, function (err, key) { + if (key) + sign.algorithm = key.type + '-sha1'; + cb(err, key); + }); + }; return (sign); } @@ -298,15 +566,25 @@ function sshAgentSigner(options) { function cliSigner(options) { assert.object(options, 'options'); + assert.string(options.keyId, 'options.keyId'); assert.string(options.user, 'options.user'); + assert.optionalString(options.subuser, 'options.subuser'); + assert.optionalString(options.algorithm, 'options.algorithm'); + assert.optionalObject(options.sshAgentOpts, 'options.sshAgentOpts'); + + var alg = options.algorithm; - var alg = options.algorithm ? options.algorithm.toLowerCase() : 'rsa-sha1'; var initOpts = new EventEmitter(); initOpts.setMaxListeners(Infinity); - var keyId = options.keyId; + var fp = sshpk.parseFingerprint(options.keyId); var user = options.user; + var agentOrErr = createSSHAgent(options.sshAgentOpts); + /* It's ok if we got an error, we can look at files instead */ + if (!(agentOrErr instanceof Error)) + initOpts.agent = agentOrErr; + // This pipeline is to perform setup ahead of time; we don't want to // recheck the agent, or reload private keys, etc., if we're in a nested // case, like mfind. We use 'initOpts' as a node hack, where we tack @@ -314,23 +592,6 @@ function cliSigner(options) { // invoked _before_ the setup work is done. vasync.pipeline({ funcs: [ - function createAgent(opts, cb) { - createSSHAgent(function (err, agent) { - if (err) { - cb(); - return; - } - - if (alg !== 'rsa-sha1') { - cb(new Error('SSH agent only supports RSA-SHA1')); - return; - } - - opts.agent = agent; - cb(); - }); - }, - function checkAgentForKey(opts, cb) { if (!opts.agent) { cb(); @@ -338,9 +599,14 @@ function cliSigner(options) { } var a = opts.agent; - sshAgentGetKey(a, keyId, function (err, key) { - if (!err) + sshAgentGetKey(a, fp, function (err, key) { + if (err && err instanceof KeyNotFoundError) + opts.agentErr = err; + + if (!err) { opts.key = key; + opts.alg = opts.algorithm = key.type + '-sha1'; + } cb(); }); @@ -353,20 +619,27 @@ function cliSigner(options) { return; } - loadSSHKey(keyId, function (err, key) { + loadSSHKey(fp, function (err, key) { + if (err && err instanceof KeyNotFoundError && opts.agentErr) + err = KeyNotFoundError.join([opts.agentErr, err]); + if (err) { cb(err); return; } - if (alg && / DSA /.test(key) && /rsa/.test(alg)) { - cb(new Error('RSA signing requested; DSA key loaded')); - return; + if (alg) { + var wantAlg = alg.split('-')[0].toLowerCase(); + if (wantAlg !== key.type) { + cb(new Error(wantAlg + ' signing requested; ' + + key.type + ' key loaded')); + return; + } } - opts.alg = opts.algorithm = alg || (/ DSA /.test(key) ? - 'DSA-SHA1' : - 'RSA-SHA256'); + if (!alg) + alg = defaultSignAlgorithm(key); + opts.alg = opts.algorithm = alg; opts.key = key; cb(); }); @@ -380,10 +653,27 @@ function cliSigner(options) { return; } + sign.algorithm = initOpts.alg.toLowerCase(); + initOpts.ready = true; initOpts.emit('ready'); }); + function waitForReady(opts, cb) { + cb = once(cb); + + if (initOpts.ready) { + cb(); + return; + } else if (initOpts.error) { + cb(initOpts.error); + return; + } + + initOpts.once('ready', cb); + initOpts.once('error', cb); + } + function sign(str, callback) { assert.string(str, 'string'); assert.func(callback, 'callback'); @@ -393,25 +683,11 @@ function cliSigner(options) { var arg = {}; vasync.pipeline({ funcs: [ - function waitForReady(opts, cb) { - cb = once(cb); - - if (initOpts.ready) { - cb(); - return; - } else if (initOpts.error) { - cb(initOpts.error); - return; - } - - initOpts.once('ready', cb); - initOpts.once('error', cb); - }, - + waitForReady, function agentSign(opts, cb) { if (!initOpts.agent || !initOpts.key || - typeof (initOpts.key) !== 'object') + typeof (initOpts.key._raw) !== 'object') { cb(); return; @@ -426,9 +702,9 @@ function cliSigner(options) { return; } - s.algorithm = alg; - s.keyId = keyId; + s.keyId = canonicalKeyId(k); s.user = options.user; + s.subuser = options.subuser; opts.res = s; cb(); }); @@ -441,16 +717,18 @@ function cliSigner(options) { } - var a = initOpts.algorithm.toUpperCase(); + var a = initOpts.algorithm; var k = initOpts.key; - var s = crypto.createSign(a); + var s = crypto.createSign(a. + replace(/^ecdsa/, 'ecdsa-with')); s.update(str); - var sig = s.sign(k, 'base64'); + var sig = s.sign(k.privatePem, 'base64'); opts.res = { algorithm: a.toLowerCase(), - keyId: keyId, + keyId: canonicalKeyId(k), signature: sig, - user: user + user: user, + subuser: options.subuser }; cb(); @@ -461,14 +739,25 @@ function cliSigner(options) { if (err) { callback(err); } else { - sign.algorithm = arg.res.algorithm; - sign.keyId = keyId; + sign.algorithm = arg.res.algorithm.toLowerCase(); + sign.keyId = arg.res.keyId; sign.user = user; + sign.subuser = options.subuser; callback(null, arg.res); } }); } + function getKey(cb) { + waitForReady({}, function (err) { + if (err) + return cb(err); + return cb(null, initOpts.key); + }); + } + + sign.getKey = getKey; + return (sign); } @@ -479,6 +768,14 @@ function cliSigner(options) { * Invoke with a signing callback (like other client APIs) and the keys/et al * needed to actually form a valid presigned request. * + * Parameters: + * - host, keyId, user: see other client APIs + * - sign: needs to have a .getKey() (all the provided signers in smartdc-auth + * are fine) + * - path: path to the Manta object to sign + * - query: optional HTTP query parameters to include on the URL + * - expires: the expire time of the URL, in seconds since the Unix epoch + * - manta: set to true if using sub-users with Manta */ function signUrl(opts, cb) { assert.object(opts, 'options'); @@ -488,8 +785,18 @@ function signUrl(opts, cb) { assert.string(opts.user, 'options.user'); assert.string(opts.path, 'options.path'); assert.optionalObject(opts.query, 'options.query'); + assert.optionalArrayOfString(opts.role, 'options.role'); + assert.optionalArrayOfString(opts['role-tag'], 'options[\'role-tag\']'); + assert.optionalString(opts.subuser, 'opts.subuser'); assert.func(opts.sign, 'options.sign'); + assert.func(opts.sign.getKey, 'options.sign.getKey'); assert.func(cb, 'callback'); + assert.optionalBool(opts.manta, 'options.manta'); + + if (opts.manta && opts.subuser !== undefined) + opts.user = opts.user + '/' + opts.subuser; + else if (opts.subuser !== undefined) + opts.user = opts.user + '/user/' + opts.subuser; if (opts.method !== undefined) { if (Array.isArray(opts.method)) { @@ -508,37 +815,52 @@ function signUrl(opts, cb) { var method = opts.method.join(','); var q = clone(opts.query || {}); - q.algorithm = opts.algorithm || opts.sign.algorithm; q.expires = (opts.expires || Math.floor(((Date.now() + (1000 * 300))/1000))); - q.keyId = '/' + opts.user + '/keys/' + opts.keyId; + + if (opts.role) + q.role = opts.role.join(','); + + if (opts['role-tag']) + q['role-tag'] = opts['role-tag'].join(','); if (opts.method.length > 1) q.method = method; - var line = - method + '\n' + - opts.host + '\n' + - opts.path + '\n'; - var str = Object.keys(q).sort(function (a, b) { - return (a.localeCompare(b)); - }).map(function (k) { - return (rfc3986(k) + '=' + rfc3986(q[k])); - }).join('&'); - line += str; - - if (opts.log) - opts.log.debug('signUrl: signing -->\n%s', line); - - opts.sign(line, function onSignature(err, obj) { + opts.sign.getKey(function (err, key) { if (err) { cb(err); - } else { - var u = opts.path + '?' + - str + - '&signature=' + rfc3986(obj.signature); - cb(null, u); + return; } + + var fp = canonicalKeyId(key); + q.keyId = '/' + opts.user + '/keys/' + fp; + q.algorithm = (opts.algorithm || opts.sign.algorithm).toUpperCase(); + + var line = + method + '\n' + + opts.host + '\n' + + opts.path + '\n'; + var str = Object.keys(q).sort(function (a, b) { + return (a.localeCompare(b)); + }).map(function (k) { + return (rfc3986(k) + '=' + rfc3986(q[k])); + }).join('&'); + line += str; + + if (opts.log) + opts.log.debug('signUrl: signing -->\n%s', line); + + opts.sign(line, function onSignature(serr, obj) { + if (serr) { + cb(serr); + } else { + var u = opts.path + '?' + + str + + '&signature=' + rfc3986(obj.signature); + cb(null, u); + } + }); }); } @@ -550,5 +872,6 @@ module.exports = { privateKeySigner: privateKeySigner, sshAgentSigner: sshAgentSigner, loadSSHKey: loadSSHKey, - signUrl: signUrl + signUrl: signUrl, + KeyNotFoundError: KeyNotFoundError }; diff --git a/package.json b/package.json index e7428bc..aac88d2 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,11 @@ "url": "git://github.com/joyent/node-smartdc-auth.git" }, "main": "lib/index.js", + "scripts": { + "test": "tape test/*.test.js" + }, "directories": { + "bin": "./bin", "lib": "./lib" }, "engines": { @@ -23,10 +27,16 @@ }, "dependencies": { "assert-plus": "0.1.2", + "bunyan": "1.3.4", "clone": "0.1.5", - "ssh-agent": "0.2.4", + "dashdash": "1.7.3", "once": "1.3.0", + "ssh-agent": "0.2.4", + "sshpk": "~1.0.4", "vasync": "1.4.3" }, - "devDependencies": {} + "devDependencies": { + "tape": "^4.2.0", + "temp": "^0.8.0" + } } diff --git a/test/agent-keys.test.js b/test/agent-keys.test.js new file mode 100644 index 0000000..406d3bf --- /dev/null +++ b/test/agent-keys.test.js @@ -0,0 +1,7 @@ +// Copyright 2015 Joyent, Inc. All rights reserved. + +var test = require('tape').test; + +var spawn = require('child_process').spawn; +var auth = require('../lib/index'); + diff --git a/test/fs-keys.test.js b/test/fs-keys.test.js new file mode 100644 index 0000000..34a0398 --- /dev/null +++ b/test/fs-keys.test.js @@ -0,0 +1,113 @@ +// Copyright 2015 Joyent, Inc. All rights reserved. + +var test = require('tape').test; + +var temp = require('temp'); +var fs = require('fs'); +var path = require('path'); +var sshpk = require('sshpk'); +var vasync = require('vasync'); +var auth = require('../lib/index'); + +/* automatically clean up temp dir at exit */ +temp.track(); + +var testDir = __dirname; +var tmpDir; +var ID_RSA_FP = 'SHA256:29GY+6bxcBkcNNUzTnEcTdTv1W3d3PN/OxyplcYSoX4'; +var ID_RSA2_FP = 'SHA256:FWEns/VvPZdbSPtoVDUlUpewdP/LgC/4+l/V42Oltpw'; + +function copyAsset(name, dst, cb) { + var rd = fs.createReadStream(path.join(testDir, name)); + var wr = fs.createWriteStream(path.join(tmpDir, dst)); + wr.on('close', cb); + rd.pipe(wr); +} + +test('setup', function (t) { + temp.mkdir('smartdc-auth.fs-keys.test', function (err, tmp) { + t.error(err); + tmpDir = tmp; + fs.mkdirSync(path.join(tmpDir, '.ssh')); + + vasync.parallel({ + funcs: [ + copyAsset.bind(this, 'id_rsa', path.join('.ssh', 'id_rsa')), + copyAsset.bind(this, 'id_rsa.pub', path.join('.ssh', 'id_rsa.pub')) + ] + }, function (err, res) { + t.error(err); + process.env['HOME'] = tmpDir; + delete process.env['SSH_AUTH_SOCK']; + delete process.env['SSH_AGENT_PID']; + t.end(); + }); + }); +}); + +test('loadSSHKey full pair', function (t) { + auth.loadSSHKey(ID_RSA_FP, function (err, key) { + t.error(err); + t.equal(key.type, 'rsa'); + t.equal(key.size, 1024); + t.end(); + }); +}); + +test('loadSSHKey public only', function (t) { + fs.unlinkSync(path.join(tmpDir, '.ssh', 'id_rsa')); + auth.loadSSHKey(ID_RSA_FP, function (err) { + t.ok(err); + t.ok(err instanceof auth.KeyNotFoundError); + t.end(); + }); +}); + +test('loadSSHKey private only', function (t) { + fs.unlinkSync(path.join(tmpDir, '.ssh', 'id_rsa.pub')); + copyAsset('id_rsa', path.join('.ssh', 'id_rsa'), function () { + auth.loadSSHKey(ID_RSA_FP, function (err) { + t.error(err); + t.end(); + }); + }); +}); + +test('setup encrypted', function (t) { + vasync.parallel({ + funcs: [ + copyAsset.bind(this, 'id_rsa2', path.join('.ssh', 'id_rsa2')), + copyAsset.bind(this, 'id_rsa2.pub', path.join('.ssh', 'id_rsa2.pub')) + ] + }, function (err, res) { + t.error(err); + t.end(); + }); +}); + +test('loadSSHKey enc-private full pair', function (t) { + auth.loadSSHKey(ID_RSA2_FP, function (err) { + t.ok(err); + t.ok(err instanceof sshpk.KeyParseError); + t.notStrictEqual(err.message.indexOf('encrypted'), -1); + t.end(); + }); +}); + +test('loadSSHKey enc-private private only', function (t) { + fs.unlinkSync(path.join(tmpDir, '.ssh', 'id_rsa2.pub')); + auth.loadSSHKey(ID_RSA2_FP, function (err) { + t.ok(err); + t.ok(err instanceof auth.KeyNotFoundError); + t.notStrictEqual(err.message.indexOf('encrypted'), -1); + t.end(); + }); +}); + +test('loadSSHKey enc-private other key', function (t) { + auth.loadSSHKey(ID_RSA_FP, function (err, key) { + t.error(err); + t.end(); + }); +}) + diff --git a/test/id_rsa b/test/id_rsa new file mode 100644 index 0000000..cdefd29 --- /dev/null +++ b/test/id_rsa @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQDAI8AowC+1bDa//i3A/FO6bbLpKIcJAr0p4u2KKNXvE/eQvgXZ +sil1QJb7ycT2iA9mHCl9kLkQ6vv7X2q48u/achKNw+Vf4VfUihrKJq5PXeBEU0CK +8FxilfjfiUPwaE4UDy5XJzrAM9Ve1NMqjNbrzrLg/BSwQ+mQLqKxBixr6QIDAQAB +AoGADYz36mfTdYoSOmwkse2ZwhYmfgcbrukAikm00v+aRugzl4OvSfEkt148x7kt +KO3jmCH4UyC3zJel+c566lxHyhLjzkhmdB9mQ7ubAdKntOGms05rTNXqEmec4Shz +OHP339WydAOZWn0J5j6SzSwdO2BMPBR8ZY+LRI7wNYJmxpkCQQDhrY4mL6IrM9E9 +mbw7ZOv4DQGTSv+8vruj/lM2o+jSaffKJUzmPFIeCqlnm26eEugzfkA57p82WKFK +NE/Sh8OPAkEA2fSbr51XeYaKHmGD6aavpW7YkNisQYXtv2T1tNx2Z64QOA8JsnZI +gsXDeAQEN75kj1/PIg2VNdeaBw+khNM9BwJATlDjTp8jIOj3iPAL4XSxasBgtpPF +UePCzDNa/1A8YKDDi9QL7q4qNSCwDzNiXNrk19HNSg1kFQEG3/BtbvsMQwJAdgdS +r91C25qR/TXNm6AaijnmqTnMvobqYrUnodOpgyftvI3YMH6Bcd/qpHl4Vz+RcVru +7n/wh4HD9YLxEsTZzQJBAIzXKaT4WvKOo2LS/qR8v8UHbcvJoh+dRZ7HmcFqwrtM +LEITY7B+2cMJqtKG2i4IGVML5s2GBU1FPhsPW1Mn8Mg= +-----END RSA PRIVATE KEY----- diff --git a/test/id_rsa.enc b/test/id_rsa.enc new file mode 100644 index 0000000..19c0e36 --- /dev/null +++ b/test/id_rsa.enc @@ -0,0 +1,18 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,481A19D8D7F0E1EF6DE0384C59517549 + +/sG5Z/oGwGRg9dJ3FzobIPkUjspoRlCME4Tc36W4HsSwMHVymk7m8laCKwz9F966 +V4qbJ9OsJYC0cklzYWo/jJ/yEJwwvuZ3x0vY0fwAS+3n6hwik7+eyVF3PJwbZR/+ +AFp8MxX5GajKL+SD2u44vQsuiSChBa/2b8Fu1SvIDQmCR2FQKbVvgUdw5q3EEZgb +a6Cml1FpvYVXVCNKLdXLAcA+dH6xR+78lkuLlbhQ8q4pqluJqRF+sv95x3RJtqKh +79iZxUO/Z+Lsj5nGUGSuECPKVxBlMBYPLSBUuBbjV8dxw/J4aNd7AHy26v/teayn +hv5tQ2S82Qf+SVkoD53xZaq2awC4YAANYNjfSfrFTw/Q+3ufX+3y8jQ4S+DVNN+5 +sybr20zf8fheD6nKQ4b1oYqAOA6MRZ1afbzTadLfWhCpsRudWkmFsYeqkdzCS4Pe +2FNjTrQGoA4IlihockN4olriImRFdKfMoR8byUSzaRtVWAqvPfSkL2HT0qtkPP+m +7iAORXS1KkWXXX8Hg/wVuYBWpxsiJKBOiUBKZr5jcxbVL7V9bxs232vj4nYOO+xn +Hi8btusMk4yOccsJmqQ4k2lIcjl2UgjOailJ3DIEW7yONU1JupGaOH53RixnM5PT +esf8YrSbbNGVjMKyGjdTzg0J4ESMYKMh4tLv28GMEQCMadqvETjOQwE4ndhn7R/m +ir88cWHVp9VlSWnls/GyXt8ICp8CsKsOyBuEg19Zbw2ZuAqiDGgz9Rx2J85DdghR +W4mDG0i436klt1CyYJX2xqaT2xwRfCYE31viZc3VLSVS1LDvAR/pS4cnrUCtGgPb +-----END RSA PRIVATE KEY----- diff --git a/test/id_rsa.pub b/test/id_rsa.pub new file mode 100644 index 0000000..382a3c8 --- /dev/null +++ b/test/id_rsa.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDAI8AowC+1bDa//i3A/FO6bbLpKIcJAr0p4u2KKNXvE/eQvgXZsil1QJb7ycT2iA9mHCl9kLkQ6vv7X2q48u/achKNw+Vf4VfUihrKJq5PXeBEU0CK8FxilfjfiUPwaE4UDy5XJzrAM9Ve1NMqjNbrzrLg/BSwQ+mQLqKxBixr6Q== alex.wilson@awilson-mbp.local diff --git a/test/id_rsa2 b/test/id_rsa2 new file mode 100644 index 0000000..221b077 --- /dev/null +++ b/test/id_rsa2 @@ -0,0 +1,30 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,D821DB5867AF824DD44A4EBB8CE18A0D + +cdNnQFfBtLXnRbu+noV5c3LcdekJXw0RCEeD4PLNF5BJutVNkRzqFk69Zs821XCu +acvi0fx6TbiQE/iGMLx1223QvIMobwePTULLCGgxE/cgKuab9yAuFyaZrpd68BS8 +AA8t+kq6/clThTx9sObfaT+46r/OjWDTmAAYmKk95g0rvy91H/wGm5rvJe9HdEB1 +6zqMYdbW7FH3zijjVRbFvnfJJigweX3mndlQIq27/UU3TRTvHTffQuQ9gIy3FAAg +hzpXwFa6eCDZ8TZHUJ2k9hoTEJ5rGyS0YuQqLhl4wvZd9a6s1gddNmog+6ISEEWG +byC8TxkJkyMAdXsWYeLaR1qkSliOAYSVehsboW1Xr1HG8s6BcU9tCe9SM6k2YXx5 +ZQy66Yq6ZDN+sBkIvRTWjn8BD+EDaeH4M123GUqUv0ptWCQqFGEF6S58sgemLzR+ +p7n5UDhlaZ14CAJ+N3XmISa+ivWjmkKhNE6e7wQP+dMcW5gMHs5phsNockHzN5GT +DZbKPNlleAyGsOkhKx71OmtrZasRFnnydcqXijn+DJ2Nl0kuetUGEaZ+pyMs3534 +9iUqJlxdQBj8Aq82SU1jHrbUsiZY+Vfrdj+3/+w39pbHOu8DSRAf+goP+0ts8KbT +oCgVrxVFyox4dp9VQg6/KaUhTk011UuALootS/Kd941CQZ+TzXENlzH3AtLB084R +W8GvD6alfI1vPerqhj74pLVtdzqkycI9uVgrrXxtFtgmdWi5zHPbaxDCiLFaH7+/ +2BvlMuR3tzZ4EFd5T3f0TfyYA36LMdOtF+ZgXcbJadXTPW0PuSbp79ci01j/QQp4 +kJi7oI+7Gk7NOB7t6W34W3BnxiccZwRmLitSTJIwVVAx2tL3E9GrT3VNK40MonrK +TRWqZiLydQD/qpqh6GFtIEdP1O/2CpEyPzdNUZ+iLVfGI9SfZdE5Tvgdka2Hhl0x +iTS2CfR/QW+NOm/boaJ5O0jy3l5A9J8fstFzqa6nzoAcoNdu3d6cQsmh40V8B3Bn +tHZtSj5f521M4mDAMACiiToD7uo/7bzKqxFBKm8kE8cswqCnbiOsojtF5LnrUq68 +uKCyFrq2fYY4BTAlXfyUOMqOHeLKIAzsCGOtlQI/N1b8707yqoQFQA7AYDmWYTE2 +zZkH4VjS8IbXJigZmXmii5SNUb9r2dgvm/M60KM4PCis4lssXoHENn1u+nJaCVmT +pNvXTl4b/impgBvuVKVakNOltYBGwehAwMjR9H7Uk8PywB8+Ms0d9IOg46xASN7y +Rw+HVmG2/Tp9nAcF2sAS1bQX6HDRvvKBcwDwrXTxfdPQCJuLMc6j8jMeotzpGZ6Y +dnrMRQL/w0uJvfSQD1FFLc3MNVco7EHLMQOjw+ud/cMAZRZQhBcSeBiiZMSgfygE +fr7jYibRD/WGAt5yxkPQalBwBDjcWbnLEd1bjnXtjbPJPN0wwWL8UHFFldDysR7v +wNgI986QaUpRDwf8bkuSiYYoIFtsTj2CIvLyxMBVn0LmNGVPuZaVNBl2jx8u/p2N +uYV92bCBTaGN2itXZzxtg3l5H9+9S19OdwOxMFCpDHM1txcSWpXTikI8bseM2+re +-----END RSA PRIVATE KEY----- diff --git a/test/id_rsa2.pub b/test/id_rsa2.pub new file mode 100644 index 0000000..07a2b16 --- /dev/null +++ b/test/id_rsa2.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDeKkCHKuaK9kpHbXs733LRRmOAZSRnyF16L7wlwQ5ROcoXtkIR6gtMDzxx6qWtDEAmQ0rZ7+1UkmWnlUr9eYCq/ppmPR128/PpfEesCgaeDH9xOZs3S0zai9uPENZqWhA5S7W0vbj+DwZe/q+/dwp+7twfn6n70HEwWQdZW6aqijK//Cj/YdrOeBk66HxH+qIbUxTD7T0m8iP7poJ2gQ7Olu+wi/ZNOOfN6clZp+qeDiBxMd9zCBefOdQCgORy9dCmyhB/DMTmGO7bQ5dY/Oqo+akqHQ5v++shaqY+Ucsw+hUSWO4umixWb1ume3OX4z56r+b91yfIJYedR/VFijgr alex.wilson@awilson-mbp.local diff --git a/test/signers.test.js b/test/signers.test.js new file mode 100644 index 0000000..616b55f --- /dev/null +++ b/test/signers.test.js @@ -0,0 +1,117 @@ +// Copyright 2015 Joyent, Inc. All rights reserved. + +var test = require('tape').test; + +var temp = require('temp'); +var fs = require('fs'); +var path = require('path'); +var sshpk = require('sshpk'); +var vasync = require('vasync'); +var auth = require('../lib/index'); + +/* automatically clean up temp dir at exit */ +temp.track(); + +var testDir = __dirname; +var tmpDir; +var ID_RSA_FP = 'SHA256:29GY+6bxcBkcNNUzTnEcTdTv1W3d3PN/OxyplcYSoX4'; +var ID_RSA_MD5 = 'fa:56:a1:6b:cc:04:97:fe:e2:98:54:c4:2e:0d:26:c6'; +var ID_RSA2_FP = 'SHA256:FWEns/VvPZdbSPtoVDUlUpewdP/LgC/4+l/V42Oltpw'; + +var SIG_SHA256 = 'KX1okEE5wWjgrDYM35z9sO49WRk/DeZy7QeSNCFdOsn45BO6rVOIH5v' + + 'V7WD25/VWyGCiN86Pml/Eulhx3Xx4ZUEHHc18K0BAKU5CSu/jCRI0dEFt4q1bXCyM7aK' + + 'FlAXpk7CJIM0Gx91CJEXcZFuUddngoqljyt9hu4dpMhrjVFA='; + +var SIG_SHA1 = 'parChQDdkj8wFY75IUW/W7KN9q5FFTPYfcAf+W7PmN8yxnRJB884NHYNT' + + 'hl/TjZB2s0vt+kkfX3nldi54heTKbDKFwCOoDmVWQ2oE2ZrJPPFiUHReUAIRvwD0V/q7' + + '4c/DiRR6My7FEa8Szce27DBrjBmrMvMcmd7/jDbhaGusy4='; + +function copyAsset(name, dst, cb) { + var rd = fs.createReadStream(path.join(testDir, name)); + var wr = fs.createWriteStream(path.join(tmpDir, dst)); + wr.on('close', cb); + rd.pipe(wr); +} + +test('setup fs only', function (t) { + temp.mkdir('smartdc-auth.signers.test', function (err, tmp) { + t.error(err); + tmpDir = tmp; + fs.mkdirSync(path.join(tmpDir, '.ssh')); + + vasync.parallel({ + funcs: [ + copyAsset.bind(this, 'id_rsa', path.join('.ssh', 'id_rsa')), + copyAsset.bind(this, 'id_rsa.pub', path.join('.ssh', 'id_rsa.pub')) + ] + }, function (err, res) { + t.error(err); + process.env['HOME'] = tmpDir; + delete process.env['SSH_AUTH_SOCK']; + delete process.env['SSH_AGENT_PID']; + t.end(); + }); + }); +}); + +test('basic cliSigner', function (t) { + var sign = auth.cliSigner({ + keyId: ID_RSA_FP, + user: 'foo' + }); + t.ok(sign); + sign('foobar', function (err, sigData) { + t.error(err); + t.strictEqual(sigData.keyId, ID_RSA_MD5); + t.strictEqual(sigData.user, 'foo'); + t.strictEqual(sigData.signature, SIG_SHA256); + t.end(); + }); +}); + +test('basic cliSigner with algorithm and subuser', function (t) { + var sign = auth.cliSigner({ + keyId: ID_RSA_MD5, + user: 'foo', + algorithm: 'RSA-SHA1', + subuser: 'bar' + }); + t.ok(sign); + sign('foobar', function (err, sigData) { + t.error(err); + t.strictEqual(sigData.keyId, ID_RSA_MD5); + t.strictEqual(sigData.user, 'foo'); + t.strictEqual(sigData.subuser, 'bar'); + t.strictEqual(sigData.signature, SIG_SHA1); + t.end(); + }); +}); + +test('cliSigner unknown fp', function (t) { + var sign = auth.cliSigner({ + keyId: ID_RSA2_FP, + user: 'foo' + }); + t.ok(sign); + sign('foobar', function (err, sigData) { + t.ok(err); + t.ok(err instanceof auth.KeyNotFoundError); + t.end(); + }); +}); + +test('cliSigner invalid fp', function (t) { + t.throws(function () { + var sign = auth.cliSigner({ + keyId: '!!!!', + user: 'foo' + }); + }, sshpk.FingerprintFormatError); + t.throws(function () { + var sign = auth.cliSigner({ + keyId: ID_RSA_MD5 + 'aaaaa', + user: 'foo' + }); + }, sshpk.FingerprintFormatError); + t.end(); +});