You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					315 lines
				
				7.8 KiB
			
		
		
			
		
	
	
					315 lines
				
				7.8 KiB
			| 
											3 years ago
										 | // Copyright 2015 Joyent, Inc.
 | ||
|  | 
 | ||
|  | module.exports = Signature; | ||
|  | 
 | ||
|  | var assert = require('assert-plus'); | ||
|  | var Buffer = require('safer-buffer').Buffer; | ||
|  | var algs = require('./algs'); | ||
|  | var crypto = require('crypto'); | ||
|  | var errs = require('./errors'); | ||
|  | var utils = require('./utils'); | ||
|  | var asn1 = require('asn1'); | ||
|  | var SSHBuffer = require('./ssh-buffer'); | ||
|  | 
 | ||
|  | var InvalidAlgorithmError = errs.InvalidAlgorithmError; | ||
|  | var SignatureParseError = errs.SignatureParseError; | ||
|  | 
 | ||
|  | function Signature(opts) { | ||
|  | 	assert.object(opts, 'options'); | ||
|  | 	assert.arrayOfObject(opts.parts, 'options.parts'); | ||
|  | 	assert.string(opts.type, 'options.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.hashAlgorithm = opts.hashAlgo; | ||
|  | 	this.curve = opts.curve; | ||
|  | 	this.parts = opts.parts; | ||
|  | 	this.part = partLookup; | ||
|  | } | ||
|  | 
 | ||
|  | Signature.prototype.toBuffer = function (format) { | ||
|  | 	if (format === undefined) | ||
|  | 		format = 'asn1'; | ||
|  | 	assert.string(format, 'format'); | ||
|  | 
 | ||
|  | 	var buf; | ||
|  | 	var stype = 'ssh-' + this.type; | ||
|  | 
 | ||
|  | 	switch (this.type) { | ||
|  | 	case 'rsa': | ||
|  | 		switch (this.hashAlgorithm) { | ||
|  | 		case 'sha256': | ||
|  | 			stype = 'rsa-sha2-256'; | ||
|  | 			break; | ||
|  | 		case 'sha512': | ||
|  | 			stype = 'rsa-sha2-512'; | ||
|  | 			break; | ||
|  | 		case 'sha1': | ||
|  | 		case undefined: | ||
|  | 			break; | ||
|  | 		default: | ||
|  | 			throw (new Error('SSH signature ' + | ||
|  | 			    'format does not support hash ' + | ||
|  | 			    'algorithm ' + this.hashAlgorithm)); | ||
|  | 		} | ||
|  | 		if (format === 'ssh') { | ||
|  | 			buf = new SSHBuffer({}); | ||
|  | 			buf.writeString(stype); | ||
|  | 			buf.writePart(this.part.sig); | ||
|  | 			return (buf.toBuffer()); | ||
|  | 		} else { | ||
|  | 			return (this.part.sig.data); | ||
|  | 		} | ||
|  | 		break; | ||
|  | 
 | ||
|  | 	case 'ed25519': | ||
|  | 		if (format === 'ssh') { | ||
|  | 			buf = new SSHBuffer({}); | ||
|  | 			buf.writeString(stype); | ||
|  | 			buf.writePart(this.part.sig); | ||
|  | 			return (buf.toBuffer()); | ||
|  | 		} else { | ||
|  | 			return (this.part.sig.data); | ||
|  | 		} | ||
|  | 		break; | ||
|  | 
 | ||
|  | 	case 'dsa': | ||
|  | 	case 'ecdsa': | ||
|  | 		var r, s; | ||
|  | 		if (format === 'asn1') { | ||
|  | 			var der = new asn1.BerWriter(); | ||
|  | 			der.startSequence(); | ||
|  | 			r = utils.mpNormalize(this.part.r.data); | ||
|  | 			s = utils.mpNormalize(this.part.s.data); | ||
|  | 			der.writeBuffer(r, asn1.Ber.Integer); | ||
|  | 			der.writeBuffer(s, asn1.Ber.Integer); | ||
|  | 			der.endSequence(); | ||
|  | 			return (der.buffer); | ||
|  | 		} else if (format === 'ssh' && this.type === 'dsa') { | ||
|  | 			buf = new SSHBuffer({}); | ||
|  | 			buf.writeString('ssh-dss'); | ||
|  | 			r = this.part.r.data; | ||
|  | 			if (r.length > 20 && r[0] === 0x00) | ||
|  | 				r = r.slice(1); | ||
|  | 			s = this.part.s.data; | ||
|  | 			if (s.length > 20 && s[0] === 0x00) | ||
|  | 				s = s.slice(1); | ||
|  | 			if ((this.hashAlgorithm && | ||
|  | 			    this.hashAlgorithm !== 'sha1') || | ||
|  | 			    r.length + s.length !== 40) { | ||
|  | 				throw (new Error('OpenSSH only supports ' + | ||
|  | 				    'DSA signatures with SHA1 hash')); | ||
|  | 			} | ||
|  | 			buf.writeBuffer(Buffer.concat([r, s])); | ||
|  | 			return (buf.toBuffer()); | ||
|  | 		} else if (format === 'ssh' && this.type === 'ecdsa') { | ||
|  | 			var inner = new SSHBuffer({}); | ||
|  | 			r = this.part.r.data; | ||
|  | 			inner.writeBuffer(r); | ||
|  | 			inner.writePart(this.part.s); | ||
|  | 
 | ||
|  | 			buf = new SSHBuffer({}); | ||
|  | 			/* XXX: find a more proper way to do this? */ | ||
|  | 			var curve; | ||
|  | 			if (r[0] === 0x00) | ||
|  | 				r = r.slice(1); | ||
|  | 			var sz = r.length * 8; | ||
|  | 			if (sz === 256) | ||
|  | 				curve = 'nistp256'; | ||
|  | 			else if (sz === 384) | ||
|  | 				curve = 'nistp384'; | ||
|  | 			else if (sz === 528) | ||
|  | 				curve = 'nistp521'; | ||
|  | 			buf.writeString('ecdsa-sha2-' + curve); | ||
|  | 			buf.writeBuffer(inner.toBuffer()); | ||
|  | 			return (buf.toBuffer()); | ||
|  | 		} | ||
|  | 		throw (new Error('Invalid signature format')); | ||
|  | 	default: | ||
|  | 		throw (new Error('Invalid signature data')); | ||
|  | 	} | ||
|  | }; | ||
|  | 
 | ||
|  | Signature.prototype.toString = function (format) { | ||
|  | 	assert.optionalString(format, 'format'); | ||
|  | 	return (this.toBuffer(format).toString('base64')); | ||
|  | }; | ||
|  | 
 | ||
|  | Signature.parse = function (data, type, format) { | ||
|  | 	if (typeof (data) === 'string') | ||
|  | 		data = Buffer.from(data, 'base64'); | ||
|  | 	assert.buffer(data, 'data'); | ||
|  | 	assert.string(format, 'format'); | ||
|  | 	assert.string(type, 'type'); | ||
|  | 
 | ||
|  | 	var opts = {}; | ||
|  | 	opts.type = type.toLowerCase(); | ||
|  | 	opts.parts = []; | ||
|  | 
 | ||
|  | 	try { | ||
|  | 		assert.ok(data.length > 0, 'signature must not be empty'); | ||
|  | 		switch (opts.type) { | ||
|  | 		case 'rsa': | ||
|  | 			return (parseOneNum(data, type, format, opts)); | ||
|  | 		case 'ed25519': | ||
|  | 			return (parseOneNum(data, type, format, opts)); | ||
|  | 
 | ||
|  | 		case 'dsa': | ||
|  | 		case 'ecdsa': | ||
|  | 			if (format === 'asn1') | ||
|  | 				return (parseDSAasn1(data, type, format, opts)); | ||
|  | 			else if (opts.type === 'dsa') | ||
|  | 				return (parseDSA(data, type, format, opts)); | ||
|  | 			else | ||
|  | 				return (parseECDSA(data, type, format, opts)); | ||
|  | 
 | ||
|  | 		default: | ||
|  | 			throw (new InvalidAlgorithmError(type)); | ||
|  | 		} | ||
|  | 
 | ||
|  | 	} catch (e) { | ||
|  | 		if (e instanceof InvalidAlgorithmError) | ||
|  | 			throw (e); | ||
|  | 		throw (new SignatureParseError(type, format, e)); | ||
|  | 	} | ||
|  | }; | ||
|  | 
 | ||
|  | function parseOneNum(data, type, format, opts) { | ||
|  | 	if (format === 'ssh') { | ||
|  | 		try { | ||
|  | 			var buf = new SSHBuffer({buffer: data}); | ||
|  | 			var head = buf.readString(); | ||
|  | 		} catch (e) { | ||
|  | 			/* fall through */ | ||
|  | 		} | ||
|  | 		if (buf !== undefined) { | ||
|  | 			var msg = 'SSH signature does not match expected ' + | ||
|  | 			    'type (expected ' + type + ', got ' + head + ')'; | ||
|  | 			switch (head) { | ||
|  | 			case 'ssh-rsa': | ||
|  | 				assert.strictEqual(type, 'rsa', msg); | ||
|  | 				opts.hashAlgo = 'sha1'; | ||
|  | 				break; | ||
|  | 			case 'rsa-sha2-256': | ||
|  | 				assert.strictEqual(type, 'rsa', msg); | ||
|  | 				opts.hashAlgo = 'sha256'; | ||
|  | 				break; | ||
|  | 			case 'rsa-sha2-512': | ||
|  | 				assert.strictEqual(type, 'rsa', msg); | ||
|  | 				opts.hashAlgo = 'sha512'; | ||
|  | 				break; | ||
|  | 			case 'ssh-ed25519': | ||
|  | 				assert.strictEqual(type, 'ed25519', msg); | ||
|  | 				opts.hashAlgo = 'sha512'; | ||
|  | 				break; | ||
|  | 			default: | ||
|  | 				throw (new Error('Unknown SSH signature ' + | ||
|  | 				    'type: ' + head)); | ||
|  | 			} | ||
|  | 			var sig = buf.readPart(); | ||
|  | 			assert.ok(buf.atEnd(), 'extra trailing bytes'); | ||
|  | 			sig.name = 'sig'; | ||
|  | 			opts.parts.push(sig); | ||
|  | 			return (new Signature(opts)); | ||
|  | 		} | ||
|  | 	} | ||
|  | 	opts.parts.push({name: 'sig', data: data}); | ||
|  | 	return (new Signature(opts)); | ||
|  | } | ||
|  | 
 | ||
|  | function parseDSAasn1(data, type, format, opts) { | ||
|  | 	var der = new asn1.BerReader(data); | ||
|  | 	der.readSequence(); | ||
|  | 	var r = der.readString(asn1.Ber.Integer, true); | ||
|  | 	var s = der.readString(asn1.Ber.Integer, true); | ||
|  | 
 | ||
|  | 	opts.parts.push({name: 'r', data: utils.mpNormalize(r)}); | ||
|  | 	opts.parts.push({name: 's', data: utils.mpNormalize(s)}); | ||
|  | 
 | ||
|  | 	return (new Signature(opts)); | ||
|  | } | ||
|  | 
 | ||
|  | function parseDSA(data, type, format, opts) { | ||
|  | 	if (data.length != 40) { | ||
|  | 		var buf = new SSHBuffer({buffer: data}); | ||
|  | 		var d = buf.readBuffer(); | ||
|  | 		if (d.toString('ascii') === 'ssh-dss') | ||
|  | 			d = buf.readBuffer(); | ||
|  | 		assert.ok(buf.atEnd(), 'extra trailing bytes'); | ||
|  | 		assert.strictEqual(d.length, 40, 'invalid inner length'); | ||
|  | 		data = d; | ||
|  | 	} | ||
|  | 	opts.parts.push({name: 'r', data: data.slice(0, 20)}); | ||
|  | 	opts.parts.push({name: 's', data: data.slice(20, 40)}); | ||
|  | 	return (new Signature(opts)); | ||
|  | } | ||
|  | 
 | ||
|  | function parseECDSA(data, type, format, opts) { | ||
|  | 	var buf = new SSHBuffer({buffer: data}); | ||
|  | 
 | ||
|  | 	var r, s; | ||
|  | 	var inner = buf.readBuffer(); | ||
|  | 	var stype = inner.toString('ascii'); | ||
|  | 	if (stype.slice(0, 6) === 'ecdsa-') { | ||
|  | 		var parts = stype.split('-'); | ||
|  | 		assert.strictEqual(parts[0], 'ecdsa'); | ||
|  | 		assert.strictEqual(parts[1], 'sha2'); | ||
|  | 		opts.curve = parts[2]; | ||
|  | 		switch (opts.curve) { | ||
|  | 		case 'nistp256': | ||
|  | 			opts.hashAlgo = 'sha256'; | ||
|  | 			break; | ||
|  | 		case 'nistp384': | ||
|  | 			opts.hashAlgo = 'sha384'; | ||
|  | 			break; | ||
|  | 		case 'nistp521': | ||
|  | 			opts.hashAlgo = 'sha512'; | ||
|  | 			break; | ||
|  | 		default: | ||
|  | 			throw (new Error('Unsupported ECDSA curve: ' + | ||
|  | 			    opts.curve)); | ||
|  | 		} | ||
|  | 		inner = buf.readBuffer(); | ||
|  | 		assert.ok(buf.atEnd(), 'extra trailing bytes on outer'); | ||
|  | 		buf = new SSHBuffer({buffer: inner}); | ||
|  | 		r = buf.readPart(); | ||
|  | 	} else { | ||
|  | 		r = {data: inner}; | ||
|  | 	} | ||
|  | 
 | ||
|  | 	s = buf.readPart(); | ||
|  | 	assert.ok(buf.atEnd(), 'extra trailing bytes'); | ||
|  | 
 | ||
|  | 	r.name = 'r'; | ||
|  | 	s.name = 's'; | ||
|  | 
 | ||
|  | 	opts.parts.push(r); | ||
|  | 	opts.parts.push(s); | ||
|  | 	return (new Signature(opts)); | ||
|  | } | ||
|  | 
 | ||
|  | Signature.isSignature = function (obj, ver) { | ||
|  | 	return (utils.isCompatible(obj, Signature, ver)); | ||
|  | }; | ||
|  | 
 | ||
|  | /* | ||
|  |  * API versions for Signature: | ||
|  |  * [1,0] -- initial ver | ||
|  |  * [2,0] -- support for rsa in full ssh format, compat with sshpk-agent | ||
|  |  *          hashAlgorithm property | ||
|  |  * [2,1] -- first tagged version | ||
|  |  */ | ||
|  | Signature.prototype._sshpkApiVersion = [2, 1]; | ||
|  | 
 | ||
|  | Signature._oldVersionDetect = function (obj) { | ||
|  | 	assert.func(obj.toBuffer); | ||
|  | 	if (obj.hasOwnProperty('hashAlgorithm')) | ||
|  | 		return ([2, 0]); | ||
|  | 	return ([1, 0]); | ||
|  | }; |