// Copyright 2018 Joyent, Inc.
|
|
|
|
module.exports = Key;
|
|
|
|
var assert = require('assert-plus');
|
|
var algs = require('./algs');
|
|
var crypto = require('crypto');
|
|
var Fingerprint = require('./fingerprint');
|
|
var Signature = require('./signature');
|
|
var DiffieHellman = require('./dhe').DiffieHellman;
|
|
var errs = require('./errors');
|
|
var utils = require('./utils');
|
|
var PrivateKey = require('./private-key');
|
|
var edCompat;
|
|
|
|
try {
|
|
edCompat = require('./ed-compat');
|
|
} catch (e) {
|
|
/* Just continue through, and bail out if we try to use it. */
|
|
}
|
|
|
|
var InvalidAlgorithmError = errs.InvalidAlgorithmError;
|
|
var KeyParseError = errs.KeyParseError;
|
|
|
|
var formats = {};
|
|
formats['auto'] = require('./formats/auto');
|
|
formats['pem'] = require('./formats/pem');
|
|
formats['pkcs1'] = require('./formats/pkcs1');
|
|
formats['pkcs8'] = require('./formats/pkcs8');
|
|
formats['rfc4253'] = require('./formats/rfc4253');
|
|
formats['ssh'] = require('./formats/ssh');
|
|
formats['ssh-private'] = require('./formats/ssh-private');
|
|
formats['openssh'] = formats['ssh-private'];
|
|
formats['dnssec'] = require('./formats/dnssec');
|
|
formats['putty'] = require('./formats/putty');
|
|
formats['ppk'] = formats['putty'];
|
|
|
|
function Key(opts) {
|
|
assert.object(opts, 'options');
|
|
assert.arrayOfObject(opts.parts, 'options.parts');
|
|
assert.string(opts.type, 'options.type');
|
|
assert.optionalString(opts.comment, 'options.comment');
|
|
|
|
var algInfo = algs.info[opts.type];
|
|
if (typeof (algInfo) !== 'object')
|
|
throw (new InvalidAlgorithmError(opts.type));
|
|
|
|
var partLookup = {};
|
|
for (var i = 0; i < opts.parts.length; ++i) {
|
|
var part = opts.parts[i];
|
|
partLookup[part.name] = part;
|
|
}
|
|
|
|
this.type = opts.type;
|
|
this.parts = opts.parts;
|
|
this.part = partLookup;
|
|
this.comment = undefined;
|
|
this.source = opts.source;
|
|
|
|
/* for speeding up hashing/fingerprint operations */
|
|
this._rfc4253Cache = opts._rfc4253Cache;
|
|
this._hashCache = {};
|
|
|
|
var sz;
|
|
this.curve = undefined;
|
|
if (this.type === 'ecdsa') {
|
|
var curve = this.part.curve.data.toString();
|
|
this.curve = curve;
|
|
sz = algs.curves[curve].size;
|
|
} else if (this.type === 'ed25519' || this.type === 'curve25519') {
|
|
sz = 256;
|
|
this.curve = 'curve25519';
|
|
} else {
|
|
var szPart = this.part[algInfo.sizePart];
|
|
sz = szPart.data.length;
|
|
sz = sz * 8 - utils.countZeros(szPart.data);
|
|
}
|
|
this.size = sz;
|
|
}
|
|
|
|
Key.formats = formats;
|
|
|
|
Key.prototype.toBuffer = function (format, options) {
|
|
if (format === undefined)
|
|
format = 'ssh';
|
|
assert.string(format, 'format');
|
|
assert.object(formats[format], 'formats[format]');
|
|
assert.optionalObject(options, 'options');
|
|
|
|
if (format === 'rfc4253') {
|
|
if (this._rfc4253Cache === undefined)
|
|
this._rfc4253Cache = formats['rfc4253'].write(this);
|
|
return (this._rfc4253Cache);
|
|
}
|
|
|
|
return (formats[format].write(this, options));
|
|
};
|
|
|
|
Key.prototype.toString = function (format, options) {
|
|
return (this.toBuffer(format, options).toString());
|
|
};
|
|
|
|
Key.prototype.hash = function (algo, type) {
|
|
assert.string(algo, 'algorithm');
|
|
assert.optionalString(type, 'type');
|
|
if (type === undefined)
|
|
type = 'ssh';
|
|
algo = algo.toLowerCase();
|
|
if (algs.hashAlgs[algo] === undefined)
|
|
throw (new InvalidAlgorithmError(algo));
|
|
|
|
var cacheKey = algo + '||' + type;
|
|
if (this._hashCache[cacheKey])
|
|
return (this._hashCache[cacheKey]);
|
|
|
|
var buf;
|
|
if (type === 'ssh') {
|
|
buf = this.toBuffer('rfc4253');
|
|
} else if (type === 'spki') {
|
|
buf = formats.pkcs8.pkcs8ToBuffer(this);
|
|
} else {
|
|
throw (new Error('Hash type ' + type + ' not supported'));
|
|
}
|
|
var hash = crypto.createHash(algo).update(buf).digest();
|
|
this._hashCache[cacheKey] = hash;
|
|
return (hash);
|
|
};
|
|
|
|
Key.prototype.fingerprint = function (algo, type) {
|
|
if (algo === undefined)
|
|
algo = 'sha256';
|
|
if (type === undefined)
|
|
type = 'ssh';
|
|
assert.string(algo, 'algorithm');
|
|
assert.string(type, 'type');
|
|
var opts = {
|
|
type: 'key',
|
|
hash: this.hash(algo, type),
|
|
algorithm: algo,
|
|
hashType: type
|
|
};
|
|
return (new Fingerprint(opts));
|
|
};
|
|
|
|
Key.prototype.defaultHashAlgorithm = function () {
|
|
var hashAlgo = 'sha1';
|
|
if (this.type === 'rsa')
|
|
hashAlgo = 'sha256';
|
|
if (this.type === 'dsa' && this.size > 1024)
|
|
hashAlgo = 'sha256';
|
|
if (this.type === 'ed25519')
|
|
hashAlgo = 'sha512';
|
|
if (this.type === 'ecdsa') {
|
|
if (this.size <= 256)
|
|
hashAlgo = 'sha256';
|
|
else if (this.size <= 384)
|
|
hashAlgo = 'sha384';
|
|
else
|
|
hashAlgo = 'sha512';
|
|
}
|
|
return (hashAlgo);
|
|
};
|
|
|
|
Key.prototype.createVerify = function (hashAlgo) {
|
|
if (hashAlgo === undefined)
|
|
hashAlgo = this.defaultHashAlgorithm();
|
|
assert.string(hashAlgo, 'hash algorithm');
|
|
|
|
/* ED25519 is not supported by OpenSSL, use a javascript impl. */
|
|
if (this.type === 'ed25519' && edCompat !== undefined)
|
|
return (new edCompat.Verifier(this, hashAlgo));
|
|
if (this.type === 'curve25519')
|
|
throw (new Error('Curve25519 keys are not suitable for ' +
|
|
'signing or verification'));
|
|
|
|
var v, nm, err;
|
|
try {
|
|
nm = hashAlgo.toUpperCase();
|
|
v = crypto.createVerify(nm);
|
|
} catch (e) {
|
|
err = e;
|
|
}
|
|
if (v === undefined || (err instanceof Error &&
|
|
err.message.match(/Unknown message digest/))) {
|
|
nm = 'RSA-';
|
|
nm += hashAlgo.toUpperCase();
|
|
v = crypto.createVerify(nm);
|
|
}
|
|
assert.ok(v, 'failed to create verifier');
|
|
var oldVerify = v.verify.bind(v);
|
|
var key = this.toBuffer('pkcs8');
|
|
var curve = this.curve;
|
|
var self = this;
|
|
v.verify = function (signature, fmt) {
|
|
if (Signature.isSignature(signature, [2, 0])) {
|
|
if (signature.type !== self.type)
|
|
return (false);
|
|
if (signature.hashAlgorithm &&
|
|
signature.hashAlgorithm !== hashAlgo)
|
|
return (false);
|
|
if (signature.curve && self.type === 'ecdsa' &&
|
|
signature.curve !== curve)
|
|
return (false);
|
|
return (oldVerify(key, signature.toBuffer('asn1')));
|
|
|
|
} else if (typeof (signature) === 'string' ||
|
|
Buffer.isBuffer(signature)) {
|
|
return (oldVerify(key, signature, fmt));
|
|
|
|
/*
|
|
* Avoid doing this on valid arguments, walking the prototype
|
|
* chain can be quite slow.
|
|
*/
|
|
} else if (Signature.isSignature(signature, [1, 0])) {
|
|
throw (new Error('signature was created by too old ' +
|
|
'a version of sshpk and cannot be verified'));
|
|
|
|
} else {
|
|
throw (new TypeError('signature must be a string, ' +
|
|
'Buffer, or Signature object'));
|
|
}
|
|
};
|
|
return (v);
|
|
};
|
|
|
|
Key.prototype.createDiffieHellman = function () {
|
|
if (this.type === 'rsa')
|
|
throw (new Error('RSA keys do not support Diffie-Hellman'));
|
|
|
|
return (new DiffieHellman(this));
|
|
};
|
|
Key.prototype.createDH = Key.prototype.createDiffieHellman;
|
|
|
|
Key.parse = function (data, format, options) {
|
|
if (typeof (data) !== 'string')
|
|
assert.buffer(data, 'data');
|
|
if (format === undefined)
|
|
format = 'auto';
|
|
assert.string(format, 'format');
|
|
if (typeof (options) === 'string')
|
|
options = { filename: options };
|
|
assert.optionalObject(options, 'options');
|
|
if (options === undefined)
|
|
options = {};
|
|
assert.optionalString(options.filename, 'options.filename');
|
|
if (options.filename === undefined)
|
|
options.filename = '(unnamed)';
|
|
|
|
assert.object(formats[format], 'formats[format]');
|
|
|
|
try {
|
|
var k = formats[format].read(data, options);
|
|
if (k instanceof PrivateKey)
|
|
k = k.toPublic();
|
|
if (!k.comment)
|
|
k.comment = options.filename;
|
|
return (k);
|
|
} catch (e) {
|
|
if (e.name === 'KeyEncryptedError')
|
|
throw (e);
|
|
throw (new KeyParseError(options.filename, format, e));
|
|
}
|
|
};
|
|
|
|
Key.isKey = function (obj, ver) {
|
|
return (utils.isCompatible(obj, Key, ver));
|
|
};
|
|
|
|
/*
|
|
* API versions for Key:
|
|
* [1,0] -- initial ver, may take Signature for createVerify or may not
|
|
* [1,1] -- added pkcs1, pkcs8 formats
|
|
* [1,2] -- added auto, ssh-private, openssh formats
|
|
* [1,3] -- added defaultHashAlgorithm
|
|
* [1,4] -- added ed support, createDH
|
|
* [1,5] -- first explicitly tagged version
|
|
* [1,6] -- changed ed25519 part names
|
|
* [1,7] -- spki hash types
|
|
*/
|
|
Key.prototype._sshpkApiVersion = [1, 7];
|
|
|
|
Key._oldVersionDetect = function (obj) {
|
|
assert.func(obj.toBuffer);
|
|
assert.func(obj.fingerprint);
|
|
if (obj.createDH)
|
|
return ([1, 4]);
|
|
if (obj.defaultHashAlgorithm)
|
|
return ([1, 3]);
|
|
if (obj.formats['auto'])
|
|
return ([1, 2]);
|
|
if (obj.formats['pkcs1'])
|
|
return ([1, 1]);
|
|
return ([1, 0]);
|
|
};
|