• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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