signature.js (7989B)
1 // Copyright 2015 Joyent, Inc. 2 3 module.exports = Signature; 4 5 var assert = require('assert-plus'); 6 var Buffer = require('safer-buffer').Buffer; 7 var algs = require('./algs'); 8 var crypto = require('crypto'); 9 var errs = require('./errors'); 10 var utils = require('./utils'); 11 var asn1 = require('asn1'); 12 var SSHBuffer = require('./ssh-buffer'); 13 14 var InvalidAlgorithmError = errs.InvalidAlgorithmError; 15 var SignatureParseError = errs.SignatureParseError; 16 17 function 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 35 Signature.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 138 Signature.prototype.toString = function (format) { 139 assert.optionalString(format, 'format'); 140 return (this.toBuffer(format).toString('base64')); 141 }; 142 143 Signature.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 182 function 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 225 function 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 237 function 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 252 function 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 296 Signature.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 */ 307 Signature.prototype._sshpkApiVersion = [2, 1]; 308 309 Signature._oldVersionDetect = function (obj) { 310 assert.func(obj.toBuffer); 311 if (obj.hasOwnProperty('hashAlgorithm')) 312 return ([2, 0]); 313 return ([1, 0]); 314 };