1 // Copyright 2016 Joyent, Inc.
3 module.exports = Certificate;
5 var assert = require('assert-plus');
6 var algs = require('./algs');
7 var crypto = require('crypto');
8 var Fingerprint = require('./fingerprint');
9 var Signature = require('./signature');
10 var errs = require('./errors');
11 var util = require('util');
12 var utils = require('./utils');
13 var Key = require('./key');
14 var PrivateKey = require('./private-key');
15 var Identity = require('./identity');
18 formats['openssh'] = require('./formats/openssh-cert');
19 formats['x509'] = require('./formats/x509');
20 formats['pem'] = require('./formats/x509-pem');
22 var CertificateParseError = errs.CertificateParseError;
23 var InvalidAlgorithmError = errs.InvalidAlgorithmError;
25 function Certificate(opts) {
26 assert.object(opts, 'options');
27 assert.arrayOfObject(opts.subjects, 'options.subjects');
28 utils.assertCompatible(opts.subjects[0], Identity, [1, 0],
30 utils.assertCompatible(opts.subjectKey, Key, [1, 0],
31 'options.subjectKey');
32 utils.assertCompatible(opts.issuer, Identity, [1, 0], 'options.issuer');
33 if (opts.issuerKey !== undefined) {
34 utils.assertCompatible(opts.issuerKey, Key, [1, 0],
37 assert.object(opts.signatures, 'options.signatures');
38 assert.buffer(opts.serial, 'options.serial');
39 assert.date(opts.validFrom, 'options.validFrom');
40 assert.date(opts.validUntil, 'optons.validUntil');
42 assert.optionalArrayOfString(opts.purposes, 'options.purposes');
46 this.subjects = opts.subjects;
47 this.issuer = opts.issuer;
48 this.subjectKey = opts.subjectKey;
49 this.issuerKey = opts.issuerKey;
50 this.signatures = opts.signatures;
51 this.serial = opts.serial;
52 this.validFrom = opts.validFrom;
53 this.validUntil = opts.validUntil;
54 this.purposes = opts.purposes;
57 Certificate.formats = formats;
59 Certificate.prototype.toBuffer = function (format, options) {
60 if (format === undefined)
62 assert.string(format, 'format');
63 assert.object(formats[format], 'formats[format]');
64 assert.optionalObject(options, 'options');
66 return (formats[format].write(this, options));
69 Certificate.prototype.toString = function (format, options) {
70 if (format === undefined)
72 return (this.toBuffer(format, options).toString());
75 Certificate.prototype.fingerprint = function (algo) {
76 if (algo === undefined)
78 assert.string(algo, 'algorithm');
81 hash: this.hash(algo),
84 return (new Fingerprint(opts));
87 Certificate.prototype.hash = function (algo) {
88 assert.string(algo, 'algorithm');
89 algo = algo.toLowerCase();
90 if (algs.hashAlgs[algo] === undefined)
91 throw (new InvalidAlgorithmError(algo));
93 if (this._hashCache[algo])
94 return (this._hashCache[algo]);
96 var hash = crypto.createHash(algo).
97 update(this.toBuffer('x509')).digest();
98 this._hashCache[algo] = hash;
102 Certificate.prototype.isExpired = function (when) {
103 if (when === undefined)
105 return (!((when.getTime() >= this.validFrom.getTime()) &&
106 (when.getTime() < this.validUntil.getTime())));
109 Certificate.prototype.isSignedBy = function (issuerCert) {
110 utils.assertCompatible(issuerCert, Certificate, [1, 0], 'issuer');
112 if (!this.issuer.equals(issuerCert.subjects[0]))
114 if (this.issuer.purposes && this.issuer.purposes.length > 0 &&
115 this.issuer.purposes.indexOf('ca') === -1) {
119 return (this.isSignedByKey(issuerCert.subjectKey));
122 Certificate.prototype.isSignedByKey = function (issuerKey) {
123 utils.assertCompatible(issuerKey, Key, [1, 2], 'issuerKey');
125 if (this.issuerKey !== undefined) {
126 return (this.issuerKey.
127 fingerprint('sha512').matches(issuerKey));
130 var fmt = Object.keys(this.signatures)[0];
131 var valid = formats[fmt].verify(this, issuerKey);
133 this.issuerKey = issuerKey;
137 Certificate.prototype.signWith = function (key) {
138 utils.assertCompatible(key, PrivateKey, [1, 2], 'key');
139 var fmts = Object.keys(formats);
141 for (var i = 0; i < fmts.length; ++i) {
142 if (fmts[i] !== 'pem') {
143 var ret = formats[fmts[i]].sign(this, key);
149 throw (new Error('Failed to sign the certificate for any ' +
150 'available certificate formats'));
154 Certificate.createSelfSigned = function (subjectOrSubjects, key, options) {
156 if (Array.isArray(subjectOrSubjects))
157 subjects = subjectOrSubjects;
159 subjects = [subjectOrSubjects];
161 assert.arrayOfObject(subjects);
162 subjects.forEach(function (subject) {
163 utils.assertCompatible(subject, Identity, [1, 0], 'subject');
166 utils.assertCompatible(key, PrivateKey, [1, 2], 'private key');
168 assert.optionalObject(options, 'options');
169 if (options === undefined)
171 assert.optionalObject(options.validFrom, 'options.validFrom');
172 assert.optionalObject(options.validUntil, 'options.validUntil');
173 var validFrom = options.validFrom;
174 var validUntil = options.validUntil;
175 if (validFrom === undefined)
176 validFrom = new Date();
177 if (validUntil === undefined) {
178 assert.optionalNumber(options.lifetime, 'options.lifetime');
179 var lifetime = options.lifetime;
180 if (lifetime === undefined)
181 lifetime = 10*365*24*3600;
182 validUntil = new Date();
183 validUntil.setTime(validUntil.getTime() + lifetime*1000);
185 assert.optionalBuffer(options.serial, 'options.serial');
186 var serial = options.serial;
187 if (serial === undefined)
188 serial = new Buffer('0000000000000001', 'hex');
190 var purposes = options.purposes;
191 if (purposes === undefined)
194 if (purposes.indexOf('signature') === -1)
195 purposes.push('signature');
197 /* Self-signed certs are always CAs. */
198 if (purposes.indexOf('ca') === -1)
200 if (purposes.indexOf('crl') === -1)
201 purposes.push('crl');
204 * If we weren't explicitly given any other purposes, do the sensible
205 * thing and add some basic ones depending on the subject type.
207 if (purposes.length <= 3) {
208 var hostSubjects = subjects.filter(function (subject) {
209 return (subject.type === 'host');
211 var userSubjects = subjects.filter(function (subject) {
212 return (subject.type === 'user');
214 if (hostSubjects.length > 0) {
215 if (purposes.indexOf('serverAuth') === -1)
216 purposes.push('serverAuth');
218 if (userSubjects.length > 0) {
219 if (purposes.indexOf('clientAuth') === -1)
220 purposes.push('clientAuth');
222 if (userSubjects.length > 0 || hostSubjects.length > 0) {
223 if (purposes.indexOf('keyAgreement') === -1)
224 purposes.push('keyAgreement');
225 if (key.type === 'rsa' &&
226 purposes.indexOf('encryption') === -1)
227 purposes.push('encryption');
231 var cert = new Certificate({
234 subjectKey: key.toPublic(),
235 issuerKey: key.toPublic(),
238 validFrom: validFrom,
239 validUntil: validUntil,
248 function (subjectOrSubjects, key, issuer, issuerKey, options) {
250 if (Array.isArray(subjectOrSubjects))
251 subjects = subjectOrSubjects;
253 subjects = [subjectOrSubjects];
255 assert.arrayOfObject(subjects);
256 subjects.forEach(function (subject) {
257 utils.assertCompatible(subject, Identity, [1, 0], 'subject');
260 utils.assertCompatible(key, Key, [1, 0], 'key');
261 if (PrivateKey.isPrivateKey(key))
262 key = key.toPublic();
263 utils.assertCompatible(issuer, Identity, [1, 0], 'issuer');
264 utils.assertCompatible(issuerKey, PrivateKey, [1, 2], 'issuer key');
266 assert.optionalObject(options, 'options');
267 if (options === undefined)
269 assert.optionalObject(options.validFrom, 'options.validFrom');
270 assert.optionalObject(options.validUntil, 'options.validUntil');
271 var validFrom = options.validFrom;
272 var validUntil = options.validUntil;
273 if (validFrom === undefined)
274 validFrom = new Date();
275 if (validUntil === undefined) {
276 assert.optionalNumber(options.lifetime, 'options.lifetime');
277 var lifetime = options.lifetime;
278 if (lifetime === undefined)
279 lifetime = 10*365*24*3600;
280 validUntil = new Date();
281 validUntil.setTime(validUntil.getTime() + lifetime*1000);
283 assert.optionalBuffer(options.serial, 'options.serial');
284 var serial = options.serial;
285 if (serial === undefined)
286 serial = new Buffer('0000000000000001', 'hex');
288 var purposes = options.purposes;
289 if (purposes === undefined)
292 if (purposes.indexOf('signature') === -1)
293 purposes.push('signature');
295 if (options.ca === true) {
296 if (purposes.indexOf('ca') === -1)
298 if (purposes.indexOf('crl') === -1)
299 purposes.push('crl');
302 var hostSubjects = subjects.filter(function (subject) {
303 return (subject.type === 'host');
305 var userSubjects = subjects.filter(function (subject) {
306 return (subject.type === 'user');
308 if (hostSubjects.length > 0) {
309 if (purposes.indexOf('serverAuth') === -1)
310 purposes.push('serverAuth');
312 if (userSubjects.length > 0) {
313 if (purposes.indexOf('clientAuth') === -1)
314 purposes.push('clientAuth');
316 if (userSubjects.length > 0 || hostSubjects.length > 0) {
317 if (purposes.indexOf('keyAgreement') === -1)
318 purposes.push('keyAgreement');
319 if (key.type === 'rsa' &&
320 purposes.indexOf('encryption') === -1)
321 purposes.push('encryption');
324 var cert = new Certificate({
328 issuerKey: issuerKey.toPublic(),
331 validFrom: validFrom,
332 validUntil: validUntil,
335 cert.signWith(issuerKey);
340 Certificate.parse = function (data, format, options) {
341 if (typeof (data) !== 'string')
342 assert.buffer(data, 'data');
343 if (format === undefined)
345 assert.string(format, 'format');
346 if (typeof (options) === 'string')
347 options = { filename: options };
348 assert.optionalObject(options, 'options');
349 if (options === undefined)
351 assert.optionalString(options.filename, 'options.filename');
352 if (options.filename === undefined)
353 options.filename = '(unnamed)';
355 assert.object(formats[format], 'formats[format]');
358 var k = formats[format].read(data, options);
361 throw (new CertificateParseError(options.filename, format, e));
365 Certificate.isCertificate = function (obj, ver) {
366 return (utils.isCompatible(obj, Certificate, ver));
370 * API versions for Certificate:
371 * [1,0] -- initial ver
373 Certificate.prototype._sshpkApiVersion = [1, 0];
375 Certificate._oldVersionDetect = function (obj) {