• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2015 Joyent, Inc.
2
3module.exports = Signature;
4
5var assert = require('assert-plus');
6var Buffer = require('safer-buffer').Buffer;
7var algs = require('./algs');
8var crypto = require('crypto');
9var errs = require('./errors');
10var utils = require('./utils');
11var asn1 = require('asn1');
12var SSHBuffer = require('./ssh-buffer');
13
14var InvalidAlgorithmError = errs.InvalidAlgorithmError;
15var SignatureParseError = errs.SignatureParseError;
16
17function Signature(opts) {
18	assert.object(opts, 'options');
19	assert.arrayOfObject(opts.parts, 'options.parts');
20	assert.string(opts.type, 'options.type');
21
22	var partLookup = {};
23	for (var i = 0; i < opts.parts.length; ++i) {
24		var part = opts.parts[i];
25		partLookup[part.name] = part;
26	}
27
28	this.type = opts.type;
29	this.hashAlgorithm = opts.hashAlgo;
30	this.curve = opts.curve;
31	this.parts = opts.parts;
32	this.part = partLookup;
33}
34
35Signature.prototype.toBuffer = function (format) {
36	if (format === undefined)
37		format = 'asn1';
38	assert.string(format, 'format');
39
40	var buf;
41	var stype = 'ssh-' + this.type;
42
43	switch (this.type) {
44	case 'rsa':
45		switch (this.hashAlgorithm) {
46		case 'sha256':
47			stype = 'rsa-sha2-256';
48			break;
49		case 'sha512':
50			stype = 'rsa-sha2-512';
51			break;
52		case 'sha1':
53		case undefined:
54			break;
55		default:
56			throw (new Error('SSH signature ' +
57			    'format does not support hash ' +
58			    'algorithm ' + this.hashAlgorithm));
59		}
60		if (format === 'ssh') {
61			buf = new SSHBuffer({});
62			buf.writeString(stype);
63			buf.writePart(this.part.sig);
64			return (buf.toBuffer());
65		} else {
66			return (this.part.sig.data);
67		}
68		break;
69
70	case 'ed25519':
71		if (format === 'ssh') {
72			buf = new SSHBuffer({});
73			buf.writeString(stype);
74			buf.writePart(this.part.sig);
75			return (buf.toBuffer());
76		} else {
77			return (this.part.sig.data);
78		}
79		break;
80
81	case 'dsa':
82	case 'ecdsa':
83		var r, s;
84		if (format === 'asn1') {
85			var der = new asn1.BerWriter();
86			der.startSequence();
87			r = utils.mpNormalize(this.part.r.data);
88			s = utils.mpNormalize(this.part.s.data);
89			der.writeBuffer(r, asn1.Ber.Integer);
90			der.writeBuffer(s, asn1.Ber.Integer);
91			der.endSequence();
92			return (der.buffer);
93		} else if (format === 'ssh' && this.type === 'dsa') {
94			buf = new SSHBuffer({});
95			buf.writeString('ssh-dss');
96			r = this.part.r.data;
97			if (r.length > 20 && r[0] === 0x00)
98				r = r.slice(1);
99			s = this.part.s.data;
100			if (s.length > 20 && s[0] === 0x00)
101				s = s.slice(1);
102			if ((this.hashAlgorithm &&
103			    this.hashAlgorithm !== 'sha1') ||
104			    r.length + s.length !== 40) {
105				throw (new Error('OpenSSH only supports ' +
106				    'DSA signatures with SHA1 hash'));
107			}
108			buf.writeBuffer(Buffer.concat([r, s]));
109			return (buf.toBuffer());
110		} else if (format === 'ssh' && this.type === 'ecdsa') {
111			var inner = new SSHBuffer({});
112			r = this.part.r.data;
113			inner.writeBuffer(r);
114			inner.writePart(this.part.s);
115
116			buf = new SSHBuffer({});
117			/* XXX: find a more proper way to do this? */
118			var curve;
119			if (r[0] === 0x00)
120				r = r.slice(1);
121			var sz = r.length * 8;
122			if (sz === 256)
123				curve = 'nistp256';
124			else if (sz === 384)
125				curve = 'nistp384';
126			else if (sz === 528)
127				curve = 'nistp521';
128			buf.writeString('ecdsa-sha2-' + curve);
129			buf.writeBuffer(inner.toBuffer());
130			return (buf.toBuffer());
131		}
132		throw (new Error('Invalid signature format'));
133	default:
134		throw (new Error('Invalid signature data'));
135	}
136};
137
138Signature.prototype.toString = function (format) {
139	assert.optionalString(format, 'format');
140	return (this.toBuffer(format).toString('base64'));
141};
142
143Signature.parse = function (data, type, format) {
144	if (typeof (data) === 'string')
145		data = Buffer.from(data, 'base64');
146	assert.buffer(data, 'data');
147	assert.string(format, 'format');
148	assert.string(type, 'type');
149
150	var opts = {};
151	opts.type = type.toLowerCase();
152	opts.parts = [];
153
154	try {
155		assert.ok(data.length > 0, 'signature must not be empty');
156		switch (opts.type) {
157		case 'rsa':
158			return (parseOneNum(data, type, format, opts));
159		case 'ed25519':
160			return (parseOneNum(data, type, format, opts));
161
162		case 'dsa':
163		case 'ecdsa':
164			if (format === 'asn1')
165				return (parseDSAasn1(data, type, format, opts));
166			else if (opts.type === 'dsa')
167				return (parseDSA(data, type, format, opts));
168			else
169				return (parseECDSA(data, type, format, opts));
170
171		default:
172			throw (new InvalidAlgorithmError(type));
173		}
174
175	} catch (e) {
176		if (e instanceof InvalidAlgorithmError)
177			throw (e);
178		throw (new SignatureParseError(type, format, e));
179	}
180};
181
182function parseOneNum(data, type, format, opts) {
183	if (format === 'ssh') {
184		try {
185			var buf = new SSHBuffer({buffer: data});
186			var head = buf.readString();
187		} catch (e) {
188			/* fall through */
189		}
190		if (buf !== undefined) {
191			var msg = 'SSH signature does not match expected ' +
192			    'type (expected ' + type + ', got ' + head + ')';
193			switch (head) {
194			case 'ssh-rsa':
195				assert.strictEqual(type, 'rsa', msg);
196				opts.hashAlgo = 'sha1';
197				break;
198			case 'rsa-sha2-256':
199				assert.strictEqual(type, 'rsa', msg);
200				opts.hashAlgo = 'sha256';
201				break;
202			case 'rsa-sha2-512':
203				assert.strictEqual(type, 'rsa', msg);
204				opts.hashAlgo = 'sha512';
205				break;
206			case 'ssh-ed25519':
207				assert.strictEqual(type, 'ed25519', msg);
208				opts.hashAlgo = 'sha512';
209				break;
210			default:
211				throw (new Error('Unknown SSH signature ' +
212				    'type: ' + head));
213			}
214			var sig = buf.readPart();
215			assert.ok(buf.atEnd(), 'extra trailing bytes');
216			sig.name = 'sig';
217			opts.parts.push(sig);
218			return (new Signature(opts));
219		}
220	}
221	opts.parts.push({name: 'sig', data: data});
222	return (new Signature(opts));
223}
224
225function parseDSAasn1(data, type, format, opts) {
226	var der = new asn1.BerReader(data);
227	der.readSequence();
228	var r = der.readString(asn1.Ber.Integer, true);
229	var s = der.readString(asn1.Ber.Integer, true);
230
231	opts.parts.push({name: 'r', data: utils.mpNormalize(r)});
232	opts.parts.push({name: 's', data: utils.mpNormalize(s)});
233
234	return (new Signature(opts));
235}
236
237function parseDSA(data, type, format, opts) {
238	if (data.length != 40) {
239		var buf = new SSHBuffer({buffer: data});
240		var d = buf.readBuffer();
241		if (d.toString('ascii') === 'ssh-dss')
242			d = buf.readBuffer();
243		assert.ok(buf.atEnd(), 'extra trailing bytes');
244		assert.strictEqual(d.length, 40, 'invalid inner length');
245		data = d;
246	}
247	opts.parts.push({name: 'r', data: data.slice(0, 20)});
248	opts.parts.push({name: 's', data: data.slice(20, 40)});
249	return (new Signature(opts));
250}
251
252function parseECDSA(data, type, format, opts) {
253	var buf = new SSHBuffer({buffer: data});
254
255	var r, s;
256	var inner = buf.readBuffer();
257	var stype = inner.toString('ascii');
258	if (stype.slice(0, 6) === 'ecdsa-') {
259		var parts = stype.split('-');
260		assert.strictEqual(parts[0], 'ecdsa');
261		assert.strictEqual(parts[1], 'sha2');
262		opts.curve = parts[2];
263		switch (opts.curve) {
264		case 'nistp256':
265			opts.hashAlgo = 'sha256';
266			break;
267		case 'nistp384':
268			opts.hashAlgo = 'sha384';
269			break;
270		case 'nistp521':
271			opts.hashAlgo = 'sha512';
272			break;
273		default:
274			throw (new Error('Unsupported ECDSA curve: ' +
275			    opts.curve));
276		}
277		inner = buf.readBuffer();
278		assert.ok(buf.atEnd(), 'extra trailing bytes on outer');
279		buf = new SSHBuffer({buffer: inner});
280		r = buf.readPart();
281	} else {
282		r = {data: inner};
283	}
284
285	s = buf.readPart();
286	assert.ok(buf.atEnd(), 'extra trailing bytes');
287
288	r.name = 'r';
289	s.name = 's';
290
291	opts.parts.push(r);
292	opts.parts.push(s);
293	return (new Signature(opts));
294}
295
296Signature.isSignature = function (obj, ver) {
297	return (utils.isCompatible(obj, Signature, ver));
298};
299
300/*
301 * API versions for Signature:
302 * [1,0] -- initial ver
303 * [2,0] -- support for rsa in full ssh format, compat with sshpk-agent
304 *          hashAlgorithm property
305 * [2,1] -- first tagged version
306 */
307Signature.prototype._sshpkApiVersion = [2, 1];
308
309Signature._oldVersionDetect = function (obj) {
310	assert.func(obj.toBuffer);
311	if (obj.hasOwnProperty('hashAlgorithm'))
312		return ([2, 0]);
313	return ([1, 0]);
314};
315