1// Copyright 2016 Joyent, Inc. 2 3module.exports = Certificate; 4 5var assert = require('assert-plus'); 6var Buffer = require('safer-buffer').Buffer; 7var algs = require('./algs'); 8var crypto = require('crypto'); 9var Fingerprint = require('./fingerprint'); 10var Signature = require('./signature'); 11var errs = require('./errors'); 12var util = require('util'); 13var utils = require('./utils'); 14var Key = require('./key'); 15var PrivateKey = require('./private-key'); 16var Identity = require('./identity'); 17 18var formats = {}; 19formats['openssh'] = require('./formats/openssh-cert'); 20formats['x509'] = require('./formats/x509'); 21formats['pem'] = require('./formats/x509-pem'); 22 23var CertificateParseError = errs.CertificateParseError; 24var InvalidAlgorithmError = errs.InvalidAlgorithmError; 25 26function Certificate(opts) { 27 assert.object(opts, 'options'); 28 assert.arrayOfObject(opts.subjects, 'options.subjects'); 29 utils.assertCompatible(opts.subjects[0], Identity, [1, 0], 30 'options.subjects'); 31 utils.assertCompatible(opts.subjectKey, Key, [1, 0], 32 'options.subjectKey'); 33 utils.assertCompatible(opts.issuer, Identity, [1, 0], 'options.issuer'); 34 if (opts.issuerKey !== undefined) { 35 utils.assertCompatible(opts.issuerKey, Key, [1, 0], 36 'options.issuerKey'); 37 } 38 assert.object(opts.signatures, 'options.signatures'); 39 assert.buffer(opts.serial, 'options.serial'); 40 assert.date(opts.validFrom, 'options.validFrom'); 41 assert.date(opts.validUntil, 'optons.validUntil'); 42 43 assert.optionalArrayOfString(opts.purposes, 'options.purposes'); 44 45 this._hashCache = {}; 46 47 this.subjects = opts.subjects; 48 this.issuer = opts.issuer; 49 this.subjectKey = opts.subjectKey; 50 this.issuerKey = opts.issuerKey; 51 this.signatures = opts.signatures; 52 this.serial = opts.serial; 53 this.validFrom = opts.validFrom; 54 this.validUntil = opts.validUntil; 55 this.purposes = opts.purposes; 56} 57 58Certificate.formats = formats; 59 60Certificate.prototype.toBuffer = function (format, options) { 61 if (format === undefined) 62 format = 'x509'; 63 assert.string(format, 'format'); 64 assert.object(formats[format], 'formats[format]'); 65 assert.optionalObject(options, 'options'); 66 67 return (formats[format].write(this, options)); 68}; 69 70Certificate.prototype.toString = function (format, options) { 71 if (format === undefined) 72 format = 'pem'; 73 return (this.toBuffer(format, options).toString()); 74}; 75 76Certificate.prototype.fingerprint = function (algo) { 77 if (algo === undefined) 78 algo = 'sha256'; 79 assert.string(algo, 'algorithm'); 80 var opts = { 81 type: 'certificate', 82 hash: this.hash(algo), 83 algorithm: algo 84 }; 85 return (new Fingerprint(opts)); 86}; 87 88Certificate.prototype.hash = function (algo) { 89 assert.string(algo, 'algorithm'); 90 algo = algo.toLowerCase(); 91 if (algs.hashAlgs[algo] === undefined) 92 throw (new InvalidAlgorithmError(algo)); 93 94 if (this._hashCache[algo]) 95 return (this._hashCache[algo]); 96 97 var hash = crypto.createHash(algo). 98 update(this.toBuffer('x509')).digest(); 99 this._hashCache[algo] = hash; 100 return (hash); 101}; 102 103Certificate.prototype.isExpired = function (when) { 104 if (when === undefined) 105 when = new Date(); 106 return (!((when.getTime() >= this.validFrom.getTime()) && 107 (when.getTime() < this.validUntil.getTime()))); 108}; 109 110Certificate.prototype.isSignedBy = function (issuerCert) { 111 utils.assertCompatible(issuerCert, Certificate, [1, 0], 'issuer'); 112 113 if (!this.issuer.equals(issuerCert.subjects[0])) 114 return (false); 115 if (this.issuer.purposes && this.issuer.purposes.length > 0 && 116 this.issuer.purposes.indexOf('ca') === -1) { 117 return (false); 118 } 119 120 return (this.isSignedByKey(issuerCert.subjectKey)); 121}; 122 123Certificate.prototype.isSignedByKey = function (issuerKey) { 124 utils.assertCompatible(issuerKey, Key, [1, 2], 'issuerKey'); 125 126 if (this.issuerKey !== undefined) { 127 return (this.issuerKey. 128 fingerprint('sha512').matches(issuerKey)); 129 } 130 131 var fmt = Object.keys(this.signatures)[0]; 132 var valid = formats[fmt].verify(this, issuerKey); 133 if (valid) 134 this.issuerKey = issuerKey; 135 return (valid); 136}; 137 138Certificate.prototype.signWith = function (key) { 139 utils.assertCompatible(key, PrivateKey, [1, 2], 'key'); 140 var fmts = Object.keys(formats); 141 var didOne = false; 142 for (var i = 0; i < fmts.length; ++i) { 143 if (fmts[i] !== 'pem') { 144 var ret = formats[fmts[i]].sign(this, key); 145 if (ret === true) 146 didOne = true; 147 } 148 } 149 if (!didOne) { 150 throw (new Error('Failed to sign the certificate for any ' + 151 'available certificate formats')); 152 } 153}; 154 155Certificate.createSelfSigned = function (subjectOrSubjects, key, options) { 156 var subjects; 157 if (Array.isArray(subjectOrSubjects)) 158 subjects = subjectOrSubjects; 159 else 160 subjects = [subjectOrSubjects]; 161 162 assert.arrayOfObject(subjects); 163 subjects.forEach(function (subject) { 164 utils.assertCompatible(subject, Identity, [1, 0], 'subject'); 165 }); 166 167 utils.assertCompatible(key, PrivateKey, [1, 2], 'private key'); 168 169 assert.optionalObject(options, 'options'); 170 if (options === undefined) 171 options = {}; 172 assert.optionalObject(options.validFrom, 'options.validFrom'); 173 assert.optionalObject(options.validUntil, 'options.validUntil'); 174 var validFrom = options.validFrom; 175 var validUntil = options.validUntil; 176 if (validFrom === undefined) 177 validFrom = new Date(); 178 if (validUntil === undefined) { 179 assert.optionalNumber(options.lifetime, 'options.lifetime'); 180 var lifetime = options.lifetime; 181 if (lifetime === undefined) 182 lifetime = 10*365*24*3600; 183 validUntil = new Date(); 184 validUntil.setTime(validUntil.getTime() + lifetime*1000); 185 } 186 assert.optionalBuffer(options.serial, 'options.serial'); 187 var serial = options.serial; 188 if (serial === undefined) 189 serial = Buffer.from('0000000000000001', 'hex'); 190 191 var purposes = options.purposes; 192 if (purposes === undefined) 193 purposes = []; 194 195 if (purposes.indexOf('signature') === -1) 196 purposes.push('signature'); 197 198 /* Self-signed certs are always CAs. */ 199 if (purposes.indexOf('ca') === -1) 200 purposes.push('ca'); 201 if (purposes.indexOf('crl') === -1) 202 purposes.push('crl'); 203 204 /* 205 * If we weren't explicitly given any other purposes, do the sensible 206 * thing and add some basic ones depending on the subject type. 207 */ 208 if (purposes.length <= 3) { 209 var hostSubjects = subjects.filter(function (subject) { 210 return (subject.type === 'host'); 211 }); 212 var userSubjects = subjects.filter(function (subject) { 213 return (subject.type === 'user'); 214 }); 215 if (hostSubjects.length > 0) { 216 if (purposes.indexOf('serverAuth') === -1) 217 purposes.push('serverAuth'); 218 } 219 if (userSubjects.length > 0) { 220 if (purposes.indexOf('clientAuth') === -1) 221 purposes.push('clientAuth'); 222 } 223 if (userSubjects.length > 0 || hostSubjects.length > 0) { 224 if (purposes.indexOf('keyAgreement') === -1) 225 purposes.push('keyAgreement'); 226 if (key.type === 'rsa' && 227 purposes.indexOf('encryption') === -1) 228 purposes.push('encryption'); 229 } 230 } 231 232 var cert = new Certificate({ 233 subjects: subjects, 234 issuer: subjects[0], 235 subjectKey: key.toPublic(), 236 issuerKey: key.toPublic(), 237 signatures: {}, 238 serial: serial, 239 validFrom: validFrom, 240 validUntil: validUntil, 241 purposes: purposes 242 }); 243 cert.signWith(key); 244 245 return (cert); 246}; 247 248Certificate.create = 249 function (subjectOrSubjects, key, issuer, issuerKey, options) { 250 var subjects; 251 if (Array.isArray(subjectOrSubjects)) 252 subjects = subjectOrSubjects; 253 else 254 subjects = [subjectOrSubjects]; 255 256 assert.arrayOfObject(subjects); 257 subjects.forEach(function (subject) { 258 utils.assertCompatible(subject, Identity, [1, 0], 'subject'); 259 }); 260 261 utils.assertCompatible(key, Key, [1, 0], 'key'); 262 if (PrivateKey.isPrivateKey(key)) 263 key = key.toPublic(); 264 utils.assertCompatible(issuer, Identity, [1, 0], 'issuer'); 265 utils.assertCompatible(issuerKey, PrivateKey, [1, 2], 'issuer key'); 266 267 assert.optionalObject(options, 'options'); 268 if (options === undefined) 269 options = {}; 270 assert.optionalObject(options.validFrom, 'options.validFrom'); 271 assert.optionalObject(options.validUntil, 'options.validUntil'); 272 var validFrom = options.validFrom; 273 var validUntil = options.validUntil; 274 if (validFrom === undefined) 275 validFrom = new Date(); 276 if (validUntil === undefined) { 277 assert.optionalNumber(options.lifetime, 'options.lifetime'); 278 var lifetime = options.lifetime; 279 if (lifetime === undefined) 280 lifetime = 10*365*24*3600; 281 validUntil = new Date(); 282 validUntil.setTime(validUntil.getTime() + lifetime*1000); 283 } 284 assert.optionalBuffer(options.serial, 'options.serial'); 285 var serial = options.serial; 286 if (serial === undefined) 287 serial = Buffer.from('0000000000000001', 'hex'); 288 289 var purposes = options.purposes; 290 if (purposes === undefined) 291 purposes = []; 292 293 if (purposes.indexOf('signature') === -1) 294 purposes.push('signature'); 295 296 if (options.ca === true) { 297 if (purposes.indexOf('ca') === -1) 298 purposes.push('ca'); 299 if (purposes.indexOf('crl') === -1) 300 purposes.push('crl'); 301 } 302 303 var hostSubjects = subjects.filter(function (subject) { 304 return (subject.type === 'host'); 305 }); 306 var userSubjects = subjects.filter(function (subject) { 307 return (subject.type === 'user'); 308 }); 309 if (hostSubjects.length > 0) { 310 if (purposes.indexOf('serverAuth') === -1) 311 purposes.push('serverAuth'); 312 } 313 if (userSubjects.length > 0) { 314 if (purposes.indexOf('clientAuth') === -1) 315 purposes.push('clientAuth'); 316 } 317 if (userSubjects.length > 0 || hostSubjects.length > 0) { 318 if (purposes.indexOf('keyAgreement') === -1) 319 purposes.push('keyAgreement'); 320 if (key.type === 'rsa' && 321 purposes.indexOf('encryption') === -1) 322 purposes.push('encryption'); 323 } 324 325 var cert = new Certificate({ 326 subjects: subjects, 327 issuer: issuer, 328 subjectKey: key, 329 issuerKey: issuerKey.toPublic(), 330 signatures: {}, 331 serial: serial, 332 validFrom: validFrom, 333 validUntil: validUntil, 334 purposes: purposes 335 }); 336 cert.signWith(issuerKey); 337 338 return (cert); 339}; 340 341Certificate.parse = function (data, format, options) { 342 if (typeof (data) !== 'string') 343 assert.buffer(data, 'data'); 344 if (format === undefined) 345 format = 'auto'; 346 assert.string(format, 'format'); 347 if (typeof (options) === 'string') 348 options = { filename: options }; 349 assert.optionalObject(options, 'options'); 350 if (options === undefined) 351 options = {}; 352 assert.optionalString(options.filename, 'options.filename'); 353 if (options.filename === undefined) 354 options.filename = '(unnamed)'; 355 356 assert.object(formats[format], 'formats[format]'); 357 358 try { 359 var k = formats[format].read(data, options); 360 return (k); 361 } catch (e) { 362 throw (new CertificateParseError(options.filename, format, e)); 363 } 364}; 365 366Certificate.isCertificate = function (obj, ver) { 367 return (utils.isCompatible(obj, Certificate, ver)); 368}; 369 370/* 371 * API versions for Certificate: 372 * [1,0] -- initial ver 373 */ 374Certificate.prototype._sshpkApiVersion = [1, 0]; 375 376Certificate._oldVersionDetect = function (obj) { 377 return ([1, 0]); 378}; 379