certificate.js (11600B)
1 // Copyright 2016 Joyent, Inc. 2 3 module.exports = Certificate; 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 Fingerprint = require('./fingerprint'); 10 var Signature = require('./signature'); 11 var errs = require('./errors'); 12 var util = require('util'); 13 var utils = require('./utils'); 14 var Key = require('./key'); 15 var PrivateKey = require('./private-key'); 16 var Identity = require('./identity'); 17 18 var formats = {}; 19 formats['openssh'] = require('./formats/openssh-cert'); 20 formats['x509'] = require('./formats/x509'); 21 formats['pem'] = require('./formats/x509-pem'); 22 23 var CertificateParseError = errs.CertificateParseError; 24 var InvalidAlgorithmError = errs.InvalidAlgorithmError; 25 26 function 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 58 Certificate.formats = formats; 59 60 Certificate.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 70 Certificate.prototype.toString = function (format, options) { 71 if (format === undefined) 72 format = 'pem'; 73 return (this.toBuffer(format, options).toString()); 74 }; 75 76 Certificate.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 88 Certificate.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 103 Certificate.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 110 Certificate.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 123 Certificate.prototype.getExtension = function (keyOrOid) { 124 assert.string(keyOrOid, 'keyOrOid'); 125 var ext = this.getExtensions().filter(function (maybeExt) { 126 if (maybeExt.format === 'x509') 127 return (maybeExt.oid === keyOrOid); 128 if (maybeExt.format === 'openssh') 129 return (maybeExt.name === keyOrOid); 130 return (false); 131 })[0]; 132 return (ext); 133 }; 134 135 Certificate.prototype.getExtensions = function () { 136 var exts = []; 137 var x509 = this.signatures.x509; 138 if (x509 && x509.extras && x509.extras.exts) { 139 x509.extras.exts.forEach(function (ext) { 140 ext.format = 'x509'; 141 exts.push(ext); 142 }); 143 } 144 var openssh = this.signatures.openssh; 145 if (openssh && openssh.exts) { 146 openssh.exts.forEach(function (ext) { 147 ext.format = 'openssh'; 148 exts.push(ext); 149 }); 150 } 151 return (exts); 152 }; 153 154 Certificate.prototype.isSignedByKey = function (issuerKey) { 155 utils.assertCompatible(issuerKey, Key, [1, 2], 'issuerKey'); 156 157 if (this.issuerKey !== undefined) { 158 return (this.issuerKey. 159 fingerprint('sha512').matches(issuerKey)); 160 } 161 162 var fmt = Object.keys(this.signatures)[0]; 163 var valid = formats[fmt].verify(this, issuerKey); 164 if (valid) 165 this.issuerKey = issuerKey; 166 return (valid); 167 }; 168 169 Certificate.prototype.signWith = function (key) { 170 utils.assertCompatible(key, PrivateKey, [1, 2], 'key'); 171 var fmts = Object.keys(formats); 172 var didOne = false; 173 for (var i = 0; i < fmts.length; ++i) { 174 if (fmts[i] !== 'pem') { 175 var ret = formats[fmts[i]].sign(this, key); 176 if (ret === true) 177 didOne = true; 178 } 179 } 180 if (!didOne) { 181 throw (new Error('Failed to sign the certificate for any ' + 182 'available certificate formats')); 183 } 184 }; 185 186 Certificate.createSelfSigned = function (subjectOrSubjects, key, options) { 187 var subjects; 188 if (Array.isArray(subjectOrSubjects)) 189 subjects = subjectOrSubjects; 190 else 191 subjects = [subjectOrSubjects]; 192 193 assert.arrayOfObject(subjects); 194 subjects.forEach(function (subject) { 195 utils.assertCompatible(subject, Identity, [1, 0], 'subject'); 196 }); 197 198 utils.assertCompatible(key, PrivateKey, [1, 2], 'private key'); 199 200 assert.optionalObject(options, 'options'); 201 if (options === undefined) 202 options = {}; 203 assert.optionalObject(options.validFrom, 'options.validFrom'); 204 assert.optionalObject(options.validUntil, 'options.validUntil'); 205 var validFrom = options.validFrom; 206 var validUntil = options.validUntil; 207 if (validFrom === undefined) 208 validFrom = new Date(); 209 if (validUntil === undefined) { 210 assert.optionalNumber(options.lifetime, 'options.lifetime'); 211 var lifetime = options.lifetime; 212 if (lifetime === undefined) 213 lifetime = 10*365*24*3600; 214 validUntil = new Date(); 215 validUntil.setTime(validUntil.getTime() + lifetime*1000); 216 } 217 assert.optionalBuffer(options.serial, 'options.serial'); 218 var serial = options.serial; 219 if (serial === undefined) 220 serial = Buffer.from('0000000000000001', 'hex'); 221 222 var purposes = options.purposes; 223 if (purposes === undefined) 224 purposes = []; 225 226 if (purposes.indexOf('signature') === -1) 227 purposes.push('signature'); 228 229 /* Self-signed certs are always CAs. */ 230 if (purposes.indexOf('ca') === -1) 231 purposes.push('ca'); 232 if (purposes.indexOf('crl') === -1) 233 purposes.push('crl'); 234 235 /* 236 * If we weren't explicitly given any other purposes, do the sensible 237 * thing and add some basic ones depending on the subject type. 238 */ 239 if (purposes.length <= 3) { 240 var hostSubjects = subjects.filter(function (subject) { 241 return (subject.type === 'host'); 242 }); 243 var userSubjects = subjects.filter(function (subject) { 244 return (subject.type === 'user'); 245 }); 246 if (hostSubjects.length > 0) { 247 if (purposes.indexOf('serverAuth') === -1) 248 purposes.push('serverAuth'); 249 } 250 if (userSubjects.length > 0) { 251 if (purposes.indexOf('clientAuth') === -1) 252 purposes.push('clientAuth'); 253 } 254 if (userSubjects.length > 0 || hostSubjects.length > 0) { 255 if (purposes.indexOf('keyAgreement') === -1) 256 purposes.push('keyAgreement'); 257 if (key.type === 'rsa' && 258 purposes.indexOf('encryption') === -1) 259 purposes.push('encryption'); 260 } 261 } 262 263 var cert = new Certificate({ 264 subjects: subjects, 265 issuer: subjects[0], 266 subjectKey: key.toPublic(), 267 issuerKey: key.toPublic(), 268 signatures: {}, 269 serial: serial, 270 validFrom: validFrom, 271 validUntil: validUntil, 272 purposes: purposes 273 }); 274 cert.signWith(key); 275 276 return (cert); 277 }; 278 279 Certificate.create = 280 function (subjectOrSubjects, key, issuer, issuerKey, options) { 281 var subjects; 282 if (Array.isArray(subjectOrSubjects)) 283 subjects = subjectOrSubjects; 284 else 285 subjects = [subjectOrSubjects]; 286 287 assert.arrayOfObject(subjects); 288 subjects.forEach(function (subject) { 289 utils.assertCompatible(subject, Identity, [1, 0], 'subject'); 290 }); 291 292 utils.assertCompatible(key, Key, [1, 0], 'key'); 293 if (PrivateKey.isPrivateKey(key)) 294 key = key.toPublic(); 295 utils.assertCompatible(issuer, Identity, [1, 0], 'issuer'); 296 utils.assertCompatible(issuerKey, PrivateKey, [1, 2], 'issuer key'); 297 298 assert.optionalObject(options, 'options'); 299 if (options === undefined) 300 options = {}; 301 assert.optionalObject(options.validFrom, 'options.validFrom'); 302 assert.optionalObject(options.validUntil, 'options.validUntil'); 303 var validFrom = options.validFrom; 304 var validUntil = options.validUntil; 305 if (validFrom === undefined) 306 validFrom = new Date(); 307 if (validUntil === undefined) { 308 assert.optionalNumber(options.lifetime, 'options.lifetime'); 309 var lifetime = options.lifetime; 310 if (lifetime === undefined) 311 lifetime = 10*365*24*3600; 312 validUntil = new Date(); 313 validUntil.setTime(validUntil.getTime() + lifetime*1000); 314 } 315 assert.optionalBuffer(options.serial, 'options.serial'); 316 var serial = options.serial; 317 if (serial === undefined) 318 serial = Buffer.from('0000000000000001', 'hex'); 319 320 var purposes = options.purposes; 321 if (purposes === undefined) 322 purposes = []; 323 324 if (purposes.indexOf('signature') === -1) 325 purposes.push('signature'); 326 327 if (options.ca === true) { 328 if (purposes.indexOf('ca') === -1) 329 purposes.push('ca'); 330 if (purposes.indexOf('crl') === -1) 331 purposes.push('crl'); 332 } 333 334 var hostSubjects = subjects.filter(function (subject) { 335 return (subject.type === 'host'); 336 }); 337 var userSubjects = subjects.filter(function (subject) { 338 return (subject.type === 'user'); 339 }); 340 if (hostSubjects.length > 0) { 341 if (purposes.indexOf('serverAuth') === -1) 342 purposes.push('serverAuth'); 343 } 344 if (userSubjects.length > 0) { 345 if (purposes.indexOf('clientAuth') === -1) 346 purposes.push('clientAuth'); 347 } 348 if (userSubjects.length > 0 || hostSubjects.length > 0) { 349 if (purposes.indexOf('keyAgreement') === -1) 350 purposes.push('keyAgreement'); 351 if (key.type === 'rsa' && 352 purposes.indexOf('encryption') === -1) 353 purposes.push('encryption'); 354 } 355 356 var cert = new Certificate({ 357 subjects: subjects, 358 issuer: issuer, 359 subjectKey: key, 360 issuerKey: issuerKey.toPublic(), 361 signatures: {}, 362 serial: serial, 363 validFrom: validFrom, 364 validUntil: validUntil, 365 purposes: purposes 366 }); 367 cert.signWith(issuerKey); 368 369 return (cert); 370 }; 371 372 Certificate.parse = function (data, format, options) { 373 if (typeof (data) !== 'string') 374 assert.buffer(data, 'data'); 375 if (format === undefined) 376 format = 'auto'; 377 assert.string(format, 'format'); 378 if (typeof (options) === 'string') 379 options = { filename: options }; 380 assert.optionalObject(options, 'options'); 381 if (options === undefined) 382 options = {}; 383 assert.optionalString(options.filename, 'options.filename'); 384 if (options.filename === undefined) 385 options.filename = '(unnamed)'; 386 387 assert.object(formats[format], 'formats[format]'); 388 389 try { 390 var k = formats[format].read(data, options); 391 return (k); 392 } catch (e) { 393 throw (new CertificateParseError(options.filename, format, e)); 394 } 395 }; 396 397 Certificate.isCertificate = function (obj, ver) { 398 return (utils.isCompatible(obj, Certificate, ver)); 399 }; 400 401 /* 402 * API versions for Certificate: 403 * [1,0] -- initial ver 404 * [1,1] -- openssh format now unpacks extensions 405 */ 406 Certificate.prototype._sshpkApiVersion = [1, 1]; 407 408 Certificate._oldVersionDetect = function (obj) { 409 return ([1, 0]); 410 };