Project

General

Profile

Download (64.9 KB) Statistics
| Branch: | Tag: | Revision:
1
<?php
2
/*
3
 * certs.inc
4
 *
5
 * part of pfSense (https://www.pfsense.org)
6
 * Copyright (c) 2008-2013 BSD Perimeter
7
 * Copyright (c) 2013-2016 Electric Sheep Fencing
8
 * Copyright (c) 2014-2019 Rubicon Communications, LLC (Netgate)
9
 * Copyright (c) 2008 Shrew Soft Inc. All rights reserved.
10
 * All rights reserved.
11
 *
12
 * Licensed under the Apache License, Version 2.0 (the "License");
13
 * you may not use this file except in compliance with the License.
14
 * You may obtain a copy of the License at
15
 *
16
 * http://www.apache.org/licenses/LICENSE-2.0
17
 *
18
 * Unless required by applicable law or agreed to in writing, software
19
 * distributed under the License is distributed on an "AS IS" BASIS,
20
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21
 * See the License for the specific language governing permissions and
22
 * limitations under the License.
23
 */
24

    
25
define("OPEN_SSL_CONF_PATH", "/etc/ssl/openssl.cnf");
26

    
27
require_once("functions.inc");
28

    
29
global $openssl_digest_algs;
30
$openssl_digest_algs = array("sha1", "sha224", "sha256", "sha384", "sha512");
31

    
32
global $openssl_crl_status;
33
/* Numbers are set in the RFC: https://www.ietf.org/rfc/rfc5280.txt */
34
$openssl_crl_status = array(
35
	-1 => "No Status (default)",
36
	0  => "Unspecified",
37
	1 => "Key Compromise",
38
	2 => "CA Compromise",
39
	3 => "Affiliation Changed",
40
	4 => "Superseded",
41
	5 => "Cessation of Operation",
42
	6 => "Certificate Hold",
43
	9 => 'Privilege Withdrawn',
44
);
45

    
46
global $cert_altname_types;
47
$cert_altname_types = array(
48
	'DNS' => gettext('FQDN or Hostname'),
49
	'IP' => gettext('IP address'),
50
	'URI' => gettext('URI'),
51
	'email' => gettext('email address'),
52
);
53

    
54
global $cert_max_lifetime;
55
$cert_max_lifetime = 12000;
56

    
57
function & lookup_ca($refid) {
58
	global $config;
59

    
60
	if (is_array($config['ca'])) {
61
		foreach ($config['ca'] as & $ca) {
62
			if ($ca['refid'] == $refid) {
63
				return $ca;
64
			}
65
		}
66
	}
67

    
68
	return false;
69
}
70

    
71
function & lookup_ca_by_subject($subject) {
72
	global $config;
73

    
74
	if (is_array($config['ca'])) {
75
		foreach ($config['ca'] as & $ca) {
76
			$ca_subject = cert_get_subject($ca['crt']);
77
			if ($ca_subject == $subject) {
78
				return $ca;
79
			}
80
		}
81
	}
82

    
83
	return false;
84
}
85

    
86
function & lookup_cert($refid) {
87
	global $config;
88

    
89
	if (is_array($config['cert'])) {
90
		foreach ($config['cert'] as & $cert) {
91
			if ($cert['refid'] == $refid) {
92
				return $cert;
93
			}
94
		}
95
	}
96

    
97
	return false;
98
}
99

    
100
function & lookup_cert_by_name($name) {
101
	global $config;
102
	if (is_array($config['cert'])) {
103
		foreach ($config['cert'] as & $cert) {
104
			if ($cert['descr'] == $name) {
105
				return $cert;
106
			}
107
		}
108
	}
109
}
110

    
111
function & lookup_crl($refid) {
112
	global $config;
113

    
114
	if (is_array($config['crl'])) {
115
		foreach ($config['crl'] as & $crl) {
116
			if ($crl['refid'] == $refid) {
117
				return $crl;
118
			}
119
		}
120
	}
121

    
122
	return false;
123
}
124

    
125
function ca_chain_array(& $cert) {
126
	if ($cert['caref']) {
127
		$chain = array();
128
		$crt = lookup_ca($cert['caref']);
129
		$chain[] = $crt;
130
		while ($crt) {
131
			$caref = $crt['caref'];
132
			if ($caref) {
133
				$crt = lookup_ca($caref);
134
			} else {
135
				$crt = false;
136
			}
137
			if ($crt) {
138
				$chain[] = $crt;
139
			}
140
		}
141
		return $chain;
142
	}
143
	return false;
144
}
145

    
146
function ca_chain(& $cert) {
147
	if ($cert['caref']) {
148
		$ca = "";
149
		$cas = ca_chain_array($cert);
150
		if (is_array($cas)) {
151
			foreach ($cas as & $ca_cert) {
152
				$ca .= base64_decode($ca_cert['crt']);
153
				$ca .= "\n";
154
			}
155
		}
156
		return $ca;
157
	}
158
	return "";
159
}
160

    
161
function ca_import(& $ca, $str, $key = "", $serial = "") {
162
	global $config;
163

    
164
	$ca['crt'] = base64_encode($str);
165
	if (!empty($key)) {
166
		$ca['prv'] = base64_encode($key);
167
	}
168
	if (empty($serial)) {
169
		$ca['serial'] = 0;
170
	} else {
171
		$ca['serial'] = $serial;
172
	}
173
	$subject = cert_get_subject($str, false);
174
	$issuer = cert_get_issuer($str, false);
175

    
176
	// Find my issuer unless self-signed
177
	if ($issuer <> $subject) {
178
		$issuer_crt =& lookup_ca_by_subject($issuer);
179
		if ($issuer_crt) {
180
			$ca['caref'] = $issuer_crt['refid'];
181
		}
182
	}
183

    
184
	/* Correct if child certificate was loaded first */
185
	if (is_array($config['ca'])) {
186
		foreach ($config['ca'] as & $oca) {
187
			$issuer = cert_get_issuer($oca['crt']);
188
			if ($ca['refid'] <> $oca['refid'] && $issuer == $subject) {
189
				$oca['caref'] = $ca['refid'];
190
			}
191
		}
192
	}
193
	if (is_array($config['cert'])) {
194
		foreach ($config['cert'] as & $cert) {
195
			$issuer = cert_get_issuer($cert['crt']);
196
			if ($issuer == $subject) {
197
				$cert['caref'] = $ca['refid'];
198
			}
199
		}
200
	}
201
	return true;
202
}
203

    
204
function ca_create(& $ca, $keylen, $lifetime, $dn, $digest_alg = "sha256", $keytype = "RSA", $ecname = "brainpoolP256r1") {
205

    
206
	$args = array(
207
		"x509_extensions" => "v3_ca",
208
		"digest_alg" => $digest_alg,
209
		"encrypt_key" => false);
210
	if ($keytype == 'ECDSA') {
211
		$args["curve_name"] = $ecname;
212
		$args["private_key_type"] = OPENSSL_KEYTYPE_EC;
213
	} else {
214
		$args["private_key_bits"] = (int)$keylen;
215
		$args["private_key_type"] = OPENSSL_KEYTYPE_RSA;
216
	}
217

    
218
	// generate a new key pair
219
	$res_key = openssl_pkey_new($args);
220
	if (!$res_key) {
221
		return false;
222
	}
223

    
224
	// generate a certificate signing request
225
	$res_csr = openssl_csr_new($dn, $res_key, $args);
226
	if (!$res_csr) {
227
		return false;
228
	}
229

    
230
	// self sign the certificate
231
	$res_crt = openssl_csr_sign($res_csr, null, $res_key, $lifetime, $args);
232
	if (!$res_crt) {
233
		return false;
234
	}
235

    
236
	// export our certificate data
237
	if (!openssl_pkey_export($res_key, $str_key) ||
238
	    !openssl_x509_export($res_crt, $str_crt)) {
239
		return false;
240
	}
241

    
242
	// return our ca information
243
	$ca['crt'] = base64_encode($str_crt);
244
	$ca['prv'] = base64_encode($str_key);
245
	$ca['serial'] = 0;
246

    
247
	return true;
248
}
249

    
250
function ca_inter_create(& $ca, $keylen, $lifetime, $dn, $caref, $digest_alg = "sha256", $keytype = "RSA", $ecname = "brainpoolP256r1") {
251
	// Create Intermediate Certificate Authority
252
	$signing_ca =& lookup_ca($caref);
253
	if (!$signing_ca) {
254
		return false;
255
	}
256

    
257
	$signing_ca_res_crt = openssl_x509_read(base64_decode($signing_ca['crt']));
258
	$signing_ca_res_key = openssl_pkey_get_private(array(0 => base64_decode($signing_ca['prv']) , 1 => ""));
259
	if (!$signing_ca_res_crt || !$signing_ca_res_key) {
260
		return false;
261
	}
262
	$signing_ca_serial = ++$signing_ca['serial'];
263

    
264
	$args = array(
265
		"x509_extensions" => "v3_ca",
266
		"digest_alg" => $digest_alg,
267
		"encrypt_key" => false);
268
	if ($keytype == 'ECDSA') {
269
		$args["curve_name"] = $ecname;
270
		$args["private_key_type"] = OPENSSL_KEYTYPE_EC;
271
	} else {
272
		$args["private_key_bits"] = (int)$keylen;
273
		$args["private_key_type"] = OPENSSL_KEYTYPE_RSA;
274
	}
275

    
276
	// generate a new key pair
277
	$res_key = openssl_pkey_new($args);
278
	if (!$res_key) {
279
		return false;
280
	}
281

    
282
	// generate a certificate signing request
283
	$res_csr = openssl_csr_new($dn, $res_key, $args);
284
	if (!$res_csr) {
285
		return false;
286
	}
287

    
288
	// Sign the certificate
289
	$res_crt = openssl_csr_sign($res_csr, $signing_ca_res_crt, $signing_ca_res_key, $lifetime, $args, $signing_ca_serial);
290
	if (!$res_crt) {
291
		return false;
292
	}
293

    
294
	// export our certificate data
295
	if (!openssl_pkey_export($res_key, $str_key) ||
296
	    !openssl_x509_export($res_crt, $str_crt)) {
297
		return false;
298
	}
299

    
300
	// return our ca information
301
	$ca['crt'] = base64_encode($str_crt);
302
	$ca['prv'] = base64_encode($str_key);
303
	$ca['serial'] = 0;
304
	$ca['caref'] = $caref;
305

    
306
	return true;
307
}
308

    
309
function cert_import(& $cert, $crt_str, $key_str) {
310

    
311
	$cert['crt'] = base64_encode($crt_str);
312
	$cert['prv'] = base64_encode($key_str);
313

    
314
	$subject = cert_get_subject($crt_str, false);
315
	$issuer = cert_get_issuer($crt_str, false);
316

    
317
	// Find my issuer unless self-signed
318
	if ($issuer <> $subject) {
319
		$issuer_crt =& lookup_ca_by_subject($issuer);
320
		if ($issuer_crt) {
321
			$cert['caref'] = $issuer_crt['refid'];
322
		}
323
	}
324
	return true;
325
}
326

    
327
function cert_create(& $cert, $caref, $keylen, $lifetime, $dn, $type = "user", $digest_alg = "sha256", $keytype = "RSA", $ecname = "brainpoolP256r1") {
328

    
329
	$cert['type'] = $type;
330

    
331
	if ($type != "self-signed") {
332
		$cert['caref'] = $caref;
333
		$ca =& lookup_ca($caref);
334
		if (!$ca) {
335
			return false;
336
		}
337

    
338
		$ca_str_crt = base64_decode($ca['crt']);
339
		$ca_str_key = base64_decode($ca['prv']);
340
		$ca_res_crt = openssl_x509_read($ca_str_crt);
341
		$ca_res_key = openssl_pkey_get_private(array(0 => $ca_str_key, 1 => ""));
342
		if (!$ca_res_key) {
343
			return false;
344
		}
345

    
346
		/* Get the next available CA serial number. */
347
		$ca_serial = ca_get_next_serial($ca);
348
	}
349

    
350
	$cert_type = cert_type_config_section($type);
351

    
352
	// in case of using Subject Alternative Names use other sections (with postfix '_san')
353
	// pass subjectAltName over environment variable 'SAN'
354
	if ($dn['subjectAltName']) {
355
		putenv("SAN={$dn['subjectAltName']}"); // subjectAltName can be set _only_ via configuration file
356
		$cert_type .= '_san';
357
		unset($dn['subjectAltName']);
358
	}
359

    
360
	$args = array(
361
		"x509_extensions" => $cert_type,
362
		"digest_alg" => $digest_alg,
363
		"encrypt_key" => false);
364
	if ($keytype == 'ECDSA') {
365
		$args["curve_name"] = $ecname;
366
		$args["private_key_type"] = OPENSSL_KEYTYPE_EC;
367
	} else {
368
		$args["private_key_bits"] = (int)$keylen;
369
		$args["private_key_type"] = OPENSSL_KEYTYPE_RSA;
370
	}
371

    
372
	// generate a new key pair
373
	$res_key = openssl_pkey_new($args);
374
	if (!$res_key) {
375
		return false;
376
	}
377

    
378
	// If this is a self-signed cert, blank out the CA and sign with the cert's key
379
	if ($type == "self-signed") {
380
		$ca           = null;
381
		$ca_res_crt   = null;
382
		$ca_res_key   = $res_key;
383
		$ca_serial    = 0;
384
		$cert['type'] = "server";
385
	}
386

    
387
	// generate a certificate signing request
388
	$res_csr = openssl_csr_new($dn, $res_key, $args);
389
	if (!$res_csr) {
390
		return false;
391
	}
392

    
393
	// sign the certificate using an internal CA
394
	$res_crt = openssl_csr_sign($res_csr, $ca_res_crt, $ca_res_key, $lifetime,
395
				 $args, $ca_serial);
396
	if (!$res_crt) {
397
		return false;
398
	}
399

    
400
	// export our certificate data
401
	if (!openssl_pkey_export($res_key, $str_key) ||
402
	    !openssl_x509_export($res_crt, $str_crt)) {
403
		return false;
404
	}
405

    
406
	// return our certificate information
407
	$cert['crt'] = base64_encode($str_crt);
408
	$cert['prv'] = base64_encode($str_key);
409

    
410
	return true;
411
}
412

    
413
function csr_generate(& $cert, $keylen, $dn, $type = "user", $digest_alg = "sha256", $keytype = "RSA", $ecname = "brainpoolP256r1") {
414

    
415
	$cert_type = cert_type_config_section($type);
416

    
417
	// in case of using Subject Alternative Names use other sections (with postfix '_san')
418
	// pass subjectAltName over environment variable 'SAN'
419
	if ($dn['subjectAltName']) {
420
		putenv("SAN={$dn['subjectAltName']}"); // subjectAltName can be set _only_ via configuration file
421
		$cert_type .= '_san';
422
		unset($dn['subjectAltName']);
423
	}
424

    
425
	$args = array(
426
		"x509_extensions" => $cert_type,
427
		"req_extensions" => "req_{$cert_type}",
428
		"digest_alg" => $digest_alg,
429
		"encrypt_key" => false);
430
	if ($keytype == 'ECDSA') {
431
		$args["curve_name"] = $ecname;
432
		$args["private_key_type"] = OPENSSL_KEYTYPE_EC;
433
	} else {
434
		$args["private_key_bits"] = (int)$keylen;
435
		$args["private_key_type"] = OPENSSL_KEYTYPE_RSA;
436
	}
437

    
438
	// generate a new key pair
439
	$res_key = openssl_pkey_new($args);
440
	if (!$res_key) {
441
		return false;
442
	}
443

    
444
	// generate a certificate signing request
445
	$res_csr = openssl_csr_new($dn, $res_key, $args);
446
	if (!$res_csr) {
447
		return false;
448
	}
449

    
450
	// export our request data
451
	if (!openssl_pkey_export($res_key, $str_key) ||
452
	    !openssl_csr_export($res_csr, $str_csr)) {
453
		return false;
454
	}
455

    
456
	// return our request information
457
	$cert['csr'] = base64_encode($str_csr);
458
	$cert['prv'] = base64_encode($str_key);
459

    
460
	return true;
461
}
462

    
463
function csr_sign($csr, & $ca, $duration, $type = "user", $altnames, $digest_alg = "sha256") {
464
	global $config;
465
	$old_err_level = error_reporting(0);
466

    
467
	// Gather the information required for signed cert
468
	$ca_str_crt = base64_decode($ca['crt']);
469
	$ca_str_key = base64_decode($ca['prv']);
470
	$ca_res_key = openssl_pkey_get_private(array(0 => $ca_str_key, 1 => ""));
471
	if (!$ca_res_key) {
472
		return false;
473
	}
474

    
475
	/* Get the next available CA serial number. */
476
	$ca_serial = ca_get_next_serial($ca);
477

    
478
	$cert_type = cert_type_config_section($type);
479

    
480
	if (!empty($altnames)) {
481
		putenv("SAN={$altnames}"); // subjectAltName can be set _only_ via configuration file
482
		$cert_type .= '_san';
483
	}
484

    
485
	$args = array(
486
		"x509_extensions" => $cert_type,
487
		"digest_alg" => $digest_alg,
488
		"req_extensions" => "req_{$cert_type}"
489
	);
490

    
491
	// Sign the new cert and export it in x509 format
492
	openssl_x509_export(openssl_csr_sign($csr, $ca_str_crt, $ca_str_key, $duration, $args, $ca_serial), $n509);
493
	error_reporting($old_err_level);
494

    
495
	return $n509;
496
}
497

    
498
function csr_complete(& $cert, $str_crt) {
499
	$str_key = base64_decode($cert['prv']);
500
	cert_import($cert, $str_crt, $str_key);
501
	unset($cert['csr']);
502
	return true;
503
}
504

    
505
function csr_get_subject($str_crt, $decode = true) {
506

    
507
	if ($decode) {
508
		$str_crt = base64_decode($str_crt);
509
	}
510

    
511
	$components = openssl_csr_get_subject($str_crt);
512

    
513
	if (empty($components) || !is_array($components)) {
514
		return "unknown";
515
	}
516

    
517
	ksort($components);
518
	foreach ($components as $a => $v) {
519
		if (!strlen($subject)) {
520
			$subject = "{$a}={$v}";
521
		} else {
522
			$subject = "{$a}={$v}, {$subject}";
523
		}
524
	}
525

    
526
	return $subject;
527
}
528

    
529
function cert_get_subject($str_crt, $decode = true) {
530

    
531
	if ($decode) {
532
		$str_crt = base64_decode($str_crt);
533
	}
534

    
535
	$inf_crt = openssl_x509_parse($str_crt);
536
	$components = $inf_crt['subject'];
537

    
538
	if (empty($components) || !is_array($components)) {
539
		return "unknown";
540
	}
541

    
542
	ksort($components);
543
	foreach ($components as $a => $v) {
544
		if (is_array($v)) {
545
			ksort($v);
546
			foreach ($v as $w) {
547
				$asubject = "{$a}={$w}";
548
				$subject = (strlen($subject)) ? "{$asubject}, {$subject}" : $asubject;
549
			}
550
		} else {
551
			$asubject = "{$a}={$v}";
552
			$subject = (strlen($subject)) ? "{$asubject}, {$subject}" : $asubject;
553
		}
554
	}
555

    
556
	return $subject;
557
}
558

    
559
function cert_get_subject_array($crt) {
560
	$str_crt = base64_decode($crt);
561
	$inf_crt = openssl_x509_parse($str_crt);
562
	$components = $inf_crt['subject'];
563

    
564
	if (!is_array($components)) {
565
		return;
566
	}
567

    
568
	$subject_array = array();
569

    
570
	foreach ($components as $a => $v) {
571
		$subject_array[] = array('a' => $a, 'v' => $v);
572
	}
573

    
574
	return $subject_array;
575
}
576

    
577
function cert_get_subject_hash($crt) {
578
	$str_crt = base64_decode($crt);
579
	$inf_crt = openssl_x509_parse($str_crt);
580
	return $inf_crt['subject'];
581
}
582

    
583
function cert_get_sans($str_crt, $decode = true) {
584
	if ($decode) {
585
		$str_crt = base64_decode($str_crt);
586
	}
587
	$sans = array();
588
	$crt_details = openssl_x509_parse($str_crt);
589
	if (!empty($crt_details['extensions']['subjectAltName'])) {
590
		$sans = explode(',', $crt_details['extensions']['subjectAltName']);
591
	}
592
	return $sans;
593
}
594

    
595
function cert_get_issuer($str_crt, $decode = true) {
596

    
597
	if ($decode) {
598
		$str_crt = base64_decode($str_crt);
599
	}
600

    
601
	$inf_crt = openssl_x509_parse($str_crt);
602
	$components = $inf_crt['issuer'];
603

    
604
	if (empty($components) || !is_array($components)) {
605
		return "unknown";
606
	}
607

    
608
	ksort($components);
609
	foreach ($components as $a => $v) {
610
		if (is_array($v)) {
611
			ksort($v);
612
			foreach ($v as $w) {
613
				$aissuer = "{$a}={$w}";
614
				$issuer = (strlen($issuer)) ? "{$aissuer}, {$issuer}" : $aissuer;
615
			}
616
		} else {
617
			$aissuer = "{$a}={$v}";
618
			$issuer = (strlen($issuer)) ? "{$aissuer}, {$issuer}" : $aissuer;
619
		}
620
	}
621

    
622
	return $issuer;
623
}
624

    
625
/* Works for both RSA and ECC (crt) and key (prv) */
626
function cert_get_publickey($str_crt, $decode = true, $type = "crt") {
627
	if ($decode) {
628
		$str_crt = base64_decode($str_crt);
629
	}
630
	$certfn = tempnam('/tmp', 'CGPK');
631
	file_put_contents($certfn, $str_crt);
632
	switch ($type) {
633
		case 'prv':
634
			exec("/usr/bin/openssl pkey -in {$certfn} -pubout", $out);
635
			break;
636
		case 'crt':
637
			exec("/usr/bin/openssl x509 -in {$certfn} -inform pem -noout -pubkey", $out);
638
			break;
639
		case 'csr':
640
			exec("/usr/bin/openssl req -in {$certfn} -inform pem -noout -pubkey", $out);
641
			break;
642
		default:
643
			$out = array();
644
			break;
645
	}
646
	unlink($certfn);
647
	return implode("\n", $out);
648
}
649

    
650
function cert_get_purpose($str_crt, $decode = true) {
651
	$extended_oids = array(
652
		"1.3.6.1.5.5.8.2.2" => "IP Security IKE Intermediate",
653
	);
654
	if ($decode) {
655
		$str_crt = base64_decode($str_crt);
656
	}
657
	$crt_details = openssl_x509_parse($str_crt);
658
	$purpose = array();
659
	if (!empty($crt_details['extensions']['keyUsage'])) {
660
		$purpose['ku'] = explode(',', $crt_details['extensions']['keyUsage']);
661
		foreach ($purpose['ku'] as & $ku) {
662
			$ku = trim($ku);
663
			if (array_key_exists($ku, $extended_oids)) {
664
				$ku = $extended_oids[$ku];
665
			}
666
		}
667
	} else {
668
		$purpose['ku'] = array();
669
	}
670
	if (!empty($crt_details['extensions']['extendedKeyUsage'])) {
671
		$purpose['eku'] = explode(',', $crt_details['extensions']['extendedKeyUsage']);
672
		foreach ($purpose['eku'] as & $eku) {
673
			$eku = trim($eku);
674
			if (array_key_exists($eku, $extended_oids)) {
675
				$eku = $extended_oids[$eku];
676
			}
677
		}
678
	} else {
679
		$purpose['eku'] = array();
680
	}
681
	$purpose['ca'] = (stristr($crt_details['extensions']['basicConstraints'], 'CA:TRUE') === false) ? 'No': 'Yes';
682
	$purpose['server'] = (in_array('TLS Web Server Authentication', $purpose['eku'])) ? 'Yes': 'No';
683

    
684
	return $purpose;
685
}
686

    
687
function cert_get_ocspstaple($str_crt, $decode = true) {
688
	if ($decode) {
689
		$str_crt = base64_decode($str_crt);
690
	}
691
	$crt_details = openssl_x509_parse($str_crt);
692
	if (($crt_details['extensions']['tlsfeature'] == "status_request") ||
693
	    !empty($crt_details['extensions']['1.3.6.1.5.5.7.1.24'])) {
694
		return true;
695
	}
696
	return false;
697
}
698

    
699
function cert_get_dates($str_crt, $decode = true, $outputstring = true) {
700
	if ($decode) {
701
		$str_crt = base64_decode($str_crt);
702
	}
703
	$crt_details = openssl_x509_parse($str_crt);
704
	$now = new DateTime("now");
705
	$start = DateTime::createFromFormat('ymdHis', rtrim($crt_details['validFrom'], 'Z'), new DateTimeZone('Z'));
706
	if ($start !== false) {
707
		$start->setTimezone($now->getTimeZone());
708
		if ($outputstring) {
709
			$start = $start->format(DateTimeInterface::RFC2822);
710
		}
711
	}
712
	$end = DateTime::createFromFormat('ymdHis', rtrim($crt_details['validTo'], 'Z'), new DateTimeZone('Z'));
713
	if ($end !== false) {
714
		$end->setTimezone($now->getTimeZone());
715
		if ($outputstring) {
716
			$end = $end->format(DateTimeInterface::RFC2822);
717
		}
718
	}
719
	return array($start, $end);
720
}
721

    
722
function cert_get_serial($str_crt, $decode = true) {
723
	if ($decode) {
724
		$str_crt = base64_decode($str_crt);
725
	}
726
	$crt_details = openssl_x509_parse($str_crt);
727
	if (isset($crt_details['serialNumber'])) {
728
		return $crt_details['serialNumber'];
729
	} else {
730
		return NULL;
731
	}
732
}
733

    
734
function cert_get_sigtype($str_crt, $decode = true) {
735
	if ($decode) {
736
		$str_crt = base64_decode($str_crt);
737
	}
738
	$crt_details = openssl_x509_parse($str_crt);
739

    
740
	$signature = array();
741
	if (isset($crt_details['signatureTypeSN']) && !empty($crt_details['signatureTypeSN'])) {
742
		$signature['shortname'] = $crt_details['signatureTypeSN'];
743
	}
744
	if (isset($crt_details['signatureTypeLN']) && !empty($crt_details['signatureTypeLN'])) {
745
		$signature['longname'] = $crt_details['signatureTypeLN'];
746
	}
747
	if (isset($crt_details['signatureTypeNID']) && !empty($crt_details['signatureTypeNID'])) {
748
		$signature['nid'] = $crt_details['signatureTypeNID'];
749
	}
750

    
751
	return $signature;
752
}
753

    
754
function is_openvpn_server_ca($caref) {
755
	global $config;
756
	if (!is_array($config['openvpn']['openvpn-server'])) {
757
		return;
758
	}
759
	foreach ($config['openvpn']['openvpn-server'] as $ovpns) {
760
		if ($ovpns['caref'] == $caref) {
761
			return true;
762
		}
763
	}
764
	return false;
765
}
766

    
767
function is_openvpn_client_ca($caref) {
768
	global $config;
769
	if (!is_array($config['openvpn']['openvpn-client'])) {
770
		return;
771
	}
772
	foreach ($config['openvpn']['openvpn-client'] as $ovpnc) {
773
		if ($ovpnc['caref'] == $caref) {
774
			return true;
775
		}
776
	}
777
	return false;
778
}
779

    
780
function is_ipsec_peer_ca($caref) {
781
	global $config;
782
	if (!is_array($config['ipsec']['phase1'])) {
783
		return;
784
	}
785
	foreach ($config['ipsec']['phase1'] as $ipsec) {
786
		if ($ipsec['caref'] == $caref) {
787
			return true;
788
		}
789
	}
790
	return false;
791
}
792

    
793
function is_ldap_peer_ca($caref) {
794
	global $config;
795
	if (!is_array($config['system']['authserver'])) {
796
		return;
797
	}
798
	foreach ($config['system']['authserver'] as $authserver) {
799
		if ($authserver['ldap_caref'] == $caref) {
800
			return true;
801
		}
802
	}
803
	return false;
804
}
805

    
806
function ca_in_use($caref) {
807
	return (is_openvpn_server_ca($caref) ||
808
		is_openvpn_client_ca($caref) ||
809
		is_ipsec_peer_ca($caref) ||
810
		is_ldap_peer_ca($caref));
811
}
812

    
813
function is_user_cert($certref) {
814
	global $config;
815
	if (!is_array($config['system']['user'])) {
816
		return;
817
	}
818
	foreach ($config['system']['user'] as $user) {
819
		if (!is_array($user['cert'])) {
820
			continue;
821
		}
822
		foreach ($user['cert'] as $cert) {
823
			if ($certref == $cert) {
824
				return true;
825
			}
826
		}
827
	}
828
	return false;
829
}
830

    
831
function is_openvpn_server_cert($certref) {
832
	global $config;
833
	if (!is_array($config['openvpn']['openvpn-server'])) {
834
		return;
835
	}
836
	foreach ($config['openvpn']['openvpn-server'] as $ovpns) {
837
		if ($ovpns['certref'] == $certref) {
838
			return true;
839
		}
840
	}
841
	return false;
842
}
843

    
844
function is_openvpn_client_cert($certref) {
845
	global $config;
846
	if (!is_array($config['openvpn']['openvpn-client'])) {
847
		return;
848
	}
849
	foreach ($config['openvpn']['openvpn-client'] as $ovpnc) {
850
		if ($ovpnc['certref'] == $certref) {
851
			return true;
852
		}
853
	}
854
	return false;
855
}
856

    
857
function is_ipsec_cert($certref) {
858
	global $config;
859
	if (!is_array($config['ipsec']['phase1'])) {
860
		return;
861
	}
862
	foreach ($config['ipsec']['phase1'] as $ipsec) {
863
		if ($ipsec['certref'] == $certref) {
864
			return true;
865
		}
866
	}
867
	return false;
868
}
869

    
870
function is_webgui_cert($certref) {
871
	global $config;
872
	if (($config['system']['webgui']['ssl-certref'] == $certref) &&
873
	    ($config['system']['webgui']['protocol'] != "http")) {
874
		return true;
875
	}
876
}
877

    
878
function is_package_cert($certref) {
879
	$pluginparams = array();
880
	$pluginparams['type'] = 'certificates';
881
	$pluginparams['event'] = 'used_certificates';
882

    
883
	$certificates_used_by_packages = pkg_call_plugins('plugin_certificates', $pluginparams);
884

    
885
	/* Check if any package is using certificate */
886
	foreach ($certificates_used_by_packages as $name => $package) {
887
		if (is_array($package['certificatelist'][$certref]) &&
888
		    isset($package['certificatelist'][$certref]) > 0) {
889
			return true;
890
		}
891
	}
892
}
893

    
894
function is_captiveportal_cert($certref) {
895
	global $config;
896
	if (!is_array($config['captiveportal'])) {
897
		return;
898
	}
899
	foreach ($config['captiveportal'] as $portal) {
900
		if (isset($portal['enable']) && isset($portal['httpslogin']) && ($portal['certref'] == $certref)) {
901
			return true;
902
		}
903
	}
904
	return false;
905
}
906

    
907
function cert_in_use($certref) {
908

    
909
	return (is_webgui_cert($certref) ||
910
		is_user_cert($certref) ||
911
		is_openvpn_server_cert($certref) ||
912
		is_openvpn_client_cert($certref) ||
913
		is_ipsec_cert($certref) ||
914
		is_captiveportal_cert($certref) ||
915
		is_package_cert($certref));
916
}
917

    
918
function cert_usedby_description($refid, $certificates_used_by_packages) {
919
	$result = "";
920
	if (is_array($certificates_used_by_packages)) {
921
		foreach ($certificates_used_by_packages as $name => $package) {
922
			if (isset($package['certificatelist'][$refid])) {
923
				$hint = "" ;
924
				if (is_array($package['certificatelist'][$refid])) {
925
					foreach ($package['certificatelist'][$refid] as $cert_used) {
926
						$hint = $hint . $cert_used['usedby']."\n";
927
					}
928
				}
929
				$count = count($package['certificatelist'][$refid]);
930
				$result .= "<div title='".htmlspecialchars($hint)."'>";
931
				$result .= htmlspecialchars($package['pkgname'])." ($count)<br />";
932
				$result .= "</div>";
933
			}
934
		}
935
	}
936
	return $result;
937
}
938

    
939
/* Detect a rollover at 2038 on some platforms (e.g. ARM)
940
 * See: https://redmine.pfsense.org/issues/9098 */
941
function cert_get_max_lifetime() {
942
	global $cert_max_lifetime;
943
	$max = $cert_max_lifetime;
944

    
945
	$current_time = time();
946
	while ((int)($current_time + ($max * 24 * 60 * 60)) < 0) {
947
		$max--;
948
	}
949
	return min($max, $cert_max_lifetime);
950
}
951

    
952
function crl_create(& $crl, $caref, $name, $serial = 0, $lifetime = 3650) {
953
	global $config;
954
	$max_lifetime = cert_get_max_lifetime();
955
	$ca =& lookup_ca($caref);
956
	if (!$ca) {
957
		return false;
958
	}
959
	$crl['descr'] = $name;
960
	$crl['caref'] = $caref;
961
	$crl['serial'] = $serial;
962
	$crl['lifetime'] = ($lifetime > $max_lifetime) ? $max_lifetime : $lifetime;
963
	$crl['cert'] = array();
964
	$config['crl'][] = $crl;
965
	return $crl;
966
}
967

    
968
function crl_update(& $crl) {
969
	require_once('ASN1.php');
970
	require_once('ASN1_UTF8STRING.php');
971
	require_once('ASN1_ASCIISTRING.php');
972
	require_once('ASN1_BITSTRING.php');
973
	require_once('ASN1_BOOL.php');
974
	require_once('ASN1_GENERALTIME.php');
975
	require_once('ASN1_INT.php');
976
	require_once('ASN1_ENUM.php');
977
	require_once('ASN1_NULL.php');
978
	require_once('ASN1_OCTETSTRING.php');
979
	require_once('ASN1_OID.php');
980
	require_once('ASN1_SEQUENCE.php');
981
	require_once('ASN1_SET.php');
982
	require_once('ASN1_SIMPLE.php');
983
	require_once('ASN1_TELETEXSTRING.php');
984
	require_once('ASN1_UTCTIME.php');
985
	require_once('OID.php');
986
	require_once('X509.php');
987
	require_once('X509_CERT.php');
988
	require_once('X509_CRL.php');
989

    
990
	global $config;
991
	$max_lifetime = cert_get_max_lifetime();
992
	$ca =& lookup_ca($crl['caref']);
993
	if (!$ca) {
994
		return false;
995
	}
996
	// If we have text but no certs, it was imported and cannot be updated.
997
	if (($crl["method"] != "internal") && (!empty($crl['text']) && empty($crl['cert']))) {
998
		return false;
999
	}
1000
	$crl['serial']++;
1001
	$ca_cert = \Ukrbublik\openssl_x509_crl\X509::pem2der(base64_decode($ca['crt']));
1002
	$ca_pkey = openssl_pkey_get_private(base64_decode($ca['prv']));
1003

    
1004
	$crlconf = array(
1005
		'no' => $crl['serial'],
1006
		'version' => 2,
1007
		'days' => ($crl['lifetime'] > $max_lifetime) ? $max_lifetime : $crl['lifetime'],
1008
		'alg' => OPENSSL_ALGO_SHA1,
1009
		'revoked' => array()
1010
	);
1011

    
1012
	if (is_array($crl['cert']) && (count($crl['cert']) > 0)) {
1013
		foreach ($crl['cert'] as $cert) {
1014
			/* Determine the serial number to revoke */
1015
			if (isset($cert['serial'])) {
1016
				$serial = $cert['serial'];
1017
			} elseif (isset($cert['crt'])) {
1018
				$serial = cert_get_serial($cert['crt'], true);
1019
			} else {
1020
				continue;
1021
			}
1022
			$crlconf['revoked'][] = array(
1023
				'serial' => $serial,
1024
				'rev_date' => $cert['revoke_time'],
1025
				'reason' => ($cert['reason'] == -1) ? null : (int) $cert['reason'],
1026
			);
1027
		}
1028
	}
1029

    
1030
	$crl_data = \Ukrbublik\openssl_x509_crl\X509_CRL::create($crlconf, $ca_pkey, $ca_cert);
1031
	$crl['text'] = base64_encode(\Ukrbublik\openssl_x509_crl\X509::der2pem4crl($crl_data));
1032

    
1033
	return $crl['text'];
1034
}
1035

    
1036
function cert_revoke($cert, & $crl, $reason = OCSP_REVOKED_STATUS_UNSPECIFIED) {
1037
	global $config;
1038
	if (is_cert_revoked($cert, $crl['refid'])) {
1039
		return true;
1040
	}
1041
	// If we have text but no certs, it was imported and cannot be updated.
1042
	if (!is_crl_internal($crl)) {
1043
		return false;
1044
	}
1045

    
1046
	if (!is_array($cert)) {
1047
		/* If passed a not an array but a serial string, set it up as an
1048
		 * array with the serial number defined */
1049
		$rcert = array();
1050
		$rcert['serial'] = $cert;
1051
	} else {
1052
		/* If passed a certificate entry, read out the serial and store
1053
		 * it separately. */
1054
		$rcert = $cert;
1055
		$rcert['serial'] = cert_get_serial($cert['crt']);
1056
	}
1057
	$rcert['reason'] = $reason;
1058
	$rcert['revoke_time'] = time();
1059
	$crl['cert'][] = $rcert;
1060
	crl_update($crl);
1061
	return true;
1062
}
1063

    
1064
function cert_unrevoke($cert, & $crl) {
1065
	global $config;
1066
	if (!is_crl_internal($crl)) {
1067
		return false;
1068
	}
1069

    
1070
	$serial = crl_get_entry_serial($cert);
1071

    
1072
	foreach ($crl['cert'] as $id => $rcert) {
1073
		/* Check for a match by refid, name, or serial number */
1074
		if (($rcert['refid'] == $cert['refid']) ||
1075
		    ($rcert['descr'] == $cert['descr']) ||
1076
		    (crl_get_entry_serial($rcert) == $serial)) {
1077
			unset($crl['cert'][$id]);
1078
			if (count($crl['cert']) == 0) {
1079
				// Protect against accidentally switching the type to imported, for older CRLs
1080
				if (!isset($crl['method'])) {
1081
					$crl['method'] = "internal";
1082
				}
1083
				crl_update($crl);
1084
			} else {
1085
				crl_update($crl);
1086
			}
1087
			return true;
1088
		}
1089
	}
1090
	return false;
1091
}
1092

    
1093
/* Compare two certificates to see if they match. */
1094
function cert_compare($cert1, $cert2) {
1095
	/* Ensure two certs are identical by first checking that their issuers match, then
1096
		subjects, then serial numbers, and finally the moduli. Anything less strict
1097
		could accidentally count two similar, but different, certificates as
1098
		being identical. */
1099
	$c1 = base64_decode($cert1['crt']);
1100
	$c2 = base64_decode($cert2['crt']);
1101
	if ((cert_get_issuer($c1, false) == cert_get_issuer($c2, false)) &&
1102
	    (cert_get_subject($c1, false) == cert_get_subject($c2, false)) &&
1103
	    (cert_get_serial($c1, false) == cert_get_serial($c2, false)) &&
1104
	    (cert_get_publickey($c1, false) == cert_get_publickey($c2, false))) {
1105
		return true;
1106
	}
1107
	return false;
1108
}
1109

    
1110
/****f* certs/crl_get_entry_serial
1111
 * NAME
1112
 *   crl_get_entry_serial - Take a CRL entry and determine the associated serial
1113
 * INPUTS
1114
 *   $entry: CRL certificate list entry to inspect, or serial string
1115
 * RESULT
1116
 *   The requested serial string, if present, or null if it cannot be determined.
1117
 ******/
1118

    
1119
function crl_get_entry_serial($entry) {
1120
	/* Check the passed entry several ways to determine the serial */
1121
	if (isset($entry['serial']) && (strlen($entry['serial']) > 0)) {
1122
		/* Entry is an array with a viable 'serial' element */
1123
		return $entry['serial'];
1124
	} elseif (isset($entry['crt'])) {
1125
		/* Entry is an array with certificate text which can be used to
1126
		 * determine the serial */
1127
		return cert_get_serial($entry['crt'], true);
1128
	} elseif (cert_validate_serial($entry, false, true) != null) {
1129
		/* Entry is a valid serial string */
1130
		return $entry;
1131
	}
1132
	/* Unable to find or determine a serial number */
1133
	return null;
1134
}
1135

    
1136
/****f* certs/cert_validate_serial
1137
 * NAME
1138
 *   cert_validate_serial - Validate a given string to test if it can be used as
1139
 *                          a certificate serial.
1140
 * INPUTS
1141
 *   $serial     : Serial number string to test
1142
 *   $returnvalue: Whether to return the parsed value or true/false
1143
 * RESULT
1144
 *   If $returnvalue is true, then the parsed ASN.1 integer value string for
1145
 *     $serial or null if invalid
1146
 *   If $returnvalue is false, then true/false based on whether or not $serial
1147
 *     is valid.
1148
 ******/
1149

    
1150
function cert_validate_serial($serial, $returnvalue = false, $allowlarge = false) {
1151
	require_once('ASN1.php');
1152
	require_once('ASN1_INT.php');
1153
	/* The ASN.1 parsing function will throw an exception if the value is
1154
	 * invalid, so take advantage of that to catch other error as well. */
1155
	try {
1156
		/* If the serial is not a string, then do not bother with
1157
		 * further tests. */
1158
		if (!is_string($serial)) {
1159
			throw new Exception('Not a string');
1160
		}
1161
		/* Process a hex string */
1162
		if ((substr($serial, 0, 2) == '0x')) {
1163
			/* If the string is hex, then it must contain only
1164
			 * valid hex digits */
1165
			if (!ctype_xdigit(substr($serial, 2))) {
1166
				throw new Exception('Not a valid hex string');
1167
			}
1168
			/* Convert to decimal */
1169
			$serial = base_convert($serial, 16, 10);
1170
		}
1171

    
1172
		/* Unfortunately, PHP openssl_csr_sign() limits serial numbers to a
1173
		 * PHP integer, so we cannot generate large numbers up to the maximum
1174
		 * allowed ASN.1 size (2^159). We are limited to PHP_INT_MAX --
1175
		 * As such, numbers larger than that limit should be rejected */
1176
		if ($serial > PHP_INT_MAX) {
1177
			throw new Exception('Serial too large for PHP OpenSSL');
1178
		}
1179

    
1180
		/* Attempt to create an ASN.1 integer, if it fails, an exception will be thrown */
1181
		$asn1serial = new \Ukrbublik\openssl_x509_crl\ASN1_INT( $serial );
1182
		return ($returnvalue) ? $asn1serial->content : true;
1183
	} catch (Exception $ex) {
1184
		/* No mattter what the error is, return null or false depending
1185
		 * on what was requested. */
1186
		return ($returnvalue) ? null : false;
1187
	}
1188
}
1189

    
1190
/****f* certs/cert_generate_serial
1191
 * NAME
1192
 *   cert_generate_serial - Generate a random positive integer usable as a
1193
 *                          certificate serial number
1194
 * INPUTS
1195
 *   None
1196
 * RESULT
1197
 *   Integer representing an ASN.1 compatible certificate serial number.
1198
 ******/
1199

    
1200
function cert_generate_serial() {
1201
	/* Use a separate function for this to make it easier to use a better
1202
	 * randomization function in the future. */
1203

    
1204
	/* Unfortunately, PHP openssl_csr_sign() limits serial numbers to a
1205
	 * PHP integer, so we cannot generate large numbers up to the maximum
1206
	 * allowed ASN.1 size (2^159). We are limited to PHP_INT_MAX */
1207
	return random_int(1, PHP_INT_MAX);
1208
}
1209

    
1210
/****f* certs/ca_has_serial
1211
 * NAME
1212
 *   ca_has_serial - Check if a serial number is used by any certificate in a given CA
1213
 * INPUTS
1214
 *   $ca    : Certificate Authority to check
1215
 *   $serial: Serial number to check
1216
 * RESULT
1217
 *   true if the serial number is in use by a certificate issued by this CA,
1218
 *   false otherwise.
1219
 ******/
1220

    
1221
function ca_has_serial($caref, $serial) {
1222
	global $config;
1223

    
1224
	foreach ($config['cert'] as $cert) {
1225
		if (($cert['caref'] == $caref) &&
1226
		    (cert_get_serial($cert['crt'], true) == $serial)) {
1227
			/* If this certificate is issued by the CA in question
1228
			 * and has a matching serial number, stop processing
1229
			 * and return true. */
1230
			return true;
1231
		}
1232
	}
1233

    
1234
	return false;
1235
}
1236

    
1237
/****f* certs/cert_get_random_serial
1238
 * NAME
1239
 *   cert_get_random_serial - Generate a random certificate serial unique in a CA
1240
 * INPUTS
1241
 *   $caref : Certificate Authority refid to test for serial uniqueness.
1242
 * RESULT
1243
 *   Random serial number which is not in use by any known certificate in a CA
1244
 ******/
1245

    
1246
function cert_get_random_serial($caref) {
1247
	/* Number of attempts to generate a usable serial. Multiple attempts
1248
	 *  are necessary to ensure that the number is usable and unique. */
1249
	$attempts = 10;
1250

    
1251
	/* Default value, -1 indicates an error */
1252
	$serial = -1;
1253

    
1254
	for ($i=0; $i < $attempts; $i++) {
1255
		/* Generate a random serial */
1256
		$serial = cert_generate_serial();
1257
		/* Check that the serial number is usable and unique:
1258
		 *  * Cannot be 0
1259
		 *  * Must be a valid ASN.1 serial number
1260
		 *  * Cannot be used by any other certificate on this CA */
1261
		if (($serial != 0) &&
1262
		    cert_validate_serial($serial) &&
1263
		    !ca_has_serial($caref, $serial)) {
1264
			/* If all conditions are met, we have a good serial, so stop. */
1265
			break;
1266
		}
1267
	}
1268
	return $serial;
1269
}
1270

    
1271
/****f* certs/ca_get_next_serial
1272
 * NAME
1273
 *   ca_get_next_serial - Get the next available serial number for a CA
1274
 * INPUTS
1275
 *   $ca: Reference to a CA entry
1276
 * RESULT
1277
 *   A randomized serial number (if enabled for a CA) or the next sequential value.
1278
 ******/
1279

    
1280
function ca_get_next_serial(& $ca) {
1281
	$ca_serial = null;
1282
	/* Get a randomized serial if enabled */
1283
	if ($ca['randomserial'] == 'enabled') {
1284
		$ca_serial = cert_get_random_serial($ca['refid']);
1285
	}
1286
	/* Initialize the sequential serial to be safe */
1287
	if (empty($ca['serial'])) {
1288
		$ca['serial'] = 0;
1289
	}
1290
	/* If not using a randomized serial, or randomizing the serial
1291
	 * failed, then fall back to sequential serials. */
1292
	return (empty($ca_serial) || ($ca_serial == -1)) ? ++$ca['serial'] : $ca_serial;
1293
}
1294

    
1295
/****f* certs/crl_contains_cert
1296
 * NAME
1297
 *   crl_contains_cert - Check if a certificate is present in a CRL
1298
 * INPUTS
1299
 *   $crl : CRL to check
1300
 *   $cert: Certificate to test
1301
 * RESULT
1302
 *   true if the CRL contains the certificate, false otherwise
1303
 ******/
1304

    
1305
function crl_contains_cert($crl, $cert) {
1306
	global $config;
1307
	if (!is_array($config['crl']) ||
1308
	    !is_array($crl['cert'])) {
1309
		return false;
1310
	}
1311

    
1312
	/* Find the issuer of this CRL */
1313
	$ca = lookup_ca($crl['caref']);
1314
	$crlissuer = is_array($cert) ? cert_get_issuer($ca['crt']) : null;
1315
	$serial = crl_get_entry_serial($cert);
1316

    
1317
	/* Skip issuer match when sarching by serial instead of certificate */
1318
	$issuer = is_array($cert) ? cert_get_issuer($cert['crt']) : null;
1319

    
1320
	/* If the requested certificate was not issued by the
1321
	 * same CA as the CRL, then do not bother checking this
1322
	 * CRL. */
1323
	if ($issuer != $crlissuer) {
1324
		return false;
1325
	}
1326

    
1327
	/* Check CRL entries to see if the certificate serial is revoked */
1328
	foreach ($crl['cert'] as $rcert) {
1329
		if (crl_get_entry_serial($rcert) == $serial) {
1330
			return true;
1331
		}
1332
	}
1333

    
1334
	/* Certificate was not found in the CRL */
1335
	return false;
1336
}
1337

    
1338
/****f* certs/is_cert_revoked
1339
 * NAME
1340
 *   is_cert_revoked - Test if a given certificate or serial is revoked
1341
 * INPUTS
1342
 *   $cert  : Certificate entry or serial number to test
1343
 *   $crlref: CRL to check for revoked entries, or empty to check all CRLs
1344
 * RESULT
1345
 *   true if the requested entry is revoked
1346
 *   false if the requested entry is not revoked
1347
 ******/
1348

    
1349
function is_cert_revoked($cert, $crlref = "") {
1350
	global $config;
1351
	if (!is_array($config['crl'])) {
1352
		return false;
1353
	}
1354

    
1355
	if (!empty($crlref)) {
1356
		$crl = lookup_crl($crlref);
1357
		return crl_contains_cert($crl, $cert);
1358
	} else {
1359
		if (!is_array($cert)) {
1360
			/* If passed a serial, then it cannot be definitively
1361
			 * matched in this way since we do not know the CA
1362
			 * associated with the bare serial. */
1363
			return null;
1364
		}
1365

    
1366
		/* Check every CRL in the configuration for a match */
1367
		foreach ($config['crl'] as $crl) {
1368
			if (!is_array($crl['cert'])) {
1369
				continue;
1370
			}
1371
			if (crl_contains_cert($crl, $cert)) {
1372
				return true;
1373
			}
1374
		}
1375
	}
1376
	return false;
1377
}
1378

    
1379
function is_openvpn_server_crl($crlref) {
1380
	global $config;
1381
	if (!is_array($config['openvpn']['openvpn-server'])) {
1382
		return;
1383
	}
1384
	foreach ($config['openvpn']['openvpn-server'] as $ovpns) {
1385
		if (!empty($ovpns['crlref']) && ($ovpns['crlref'] == $crlref)) {
1386
			return true;
1387
		}
1388
	}
1389
	return false;
1390
}
1391

    
1392
// Keep this general to allow for future expansion. See cert_in_use() above.
1393
function crl_in_use($crlref) {
1394
	return (is_openvpn_server_crl($crlref));
1395
}
1396

    
1397
function is_crl_internal($crl) {
1398
	return (!(!empty($crl['text']) && empty($crl['cert'])) || ($crl["method"] == "internal"));
1399
}
1400

    
1401
function cert_get_cn($crt, $isref = false) {
1402
	/* If this is a certref, not an actual cert, look up the cert first */
1403
	if ($isref) {
1404
		$cert = lookup_cert($crt);
1405
		/* If it's not a valid cert, bail. */
1406
		if (!(is_array($cert) && !empty($cert['crt']))) {
1407
			return "";
1408
		}
1409
		$cert = $cert['crt'];
1410
	} else {
1411
		$cert = $crt;
1412
	}
1413
	$sub = cert_get_subject_array($cert);
1414
	if (is_array($sub)) {
1415
		foreach ($sub as $s) {
1416
			if (strtoupper($s['a']) == "CN") {
1417
				return $s['v'];
1418
			}
1419
		}
1420
	}
1421
	return "";
1422
}
1423

    
1424
function cert_escape_x509_chars($str, $reverse = false) {
1425
	/* Characters which need escaped when present in x.509 fields.
1426
	 * See https://www.ietf.org/rfc/rfc4514.txt
1427
	 *
1428
	 * The backslash (\) must be listed first in these arrays!
1429
	 */
1430
	$cert_directory_string_special_chars = array('\\', '"', '#', '+', ',', ';', '<', '=', '>');
1431
	$cert_directory_string_special_chars_esc = array('\\\\', '\"', '\#', '\+', '\,', '\;', '\<', '\=', '\>');
1432
	if ($reverse) {
1433
		return str_replace($cert_directory_string_special_chars_esc, $cert_directory_string_special_chars, $str);
1434
	} else {
1435
		/* First unescape and then escape again, to prevent possible double escaping. */
1436
		return str_replace($cert_directory_string_special_chars, $cert_directory_string_special_chars_esc, cert_escape_x509_chars($str, true));
1437
	}
1438
}
1439

    
1440
function cert_add_altname_type($str) {
1441
	$type = "";
1442
	if (is_ipaddr($str)) {
1443
		$type = "IP";
1444
	} elseif (is_hostname($str, true)) {
1445
		$type = "DNS";
1446
	} elseif (is_URL($str)) {
1447
		$type = "URI";
1448
	} elseif (filter_var($str, FILTER_VALIDATE_EMAIL)) {
1449
		$type = "email";
1450
	}
1451
	if (!empty($type)) {
1452
		return "{$type}:" . cert_escape_x509_chars($str);
1453
	} else {
1454
		return null;
1455
	}
1456
}
1457

    
1458
function cert_type_config_section($type) {
1459
	switch ($type) {
1460
		case "ca":
1461
			$cert_type = "v3_ca";
1462
			break;
1463
		case "server":
1464
		case "self-signed":
1465
			$cert_type = "server";
1466
			break;
1467
		default:
1468
			$cert_type = "usr_cert";
1469
			break;
1470
	}
1471
	return $cert_type;
1472
}
1473

    
1474
/****f* certs/is_cert_locally_renewable
1475
 * NAME
1476
 *   is_cert_locally_renewable - Check to see if an existing certificate can be
1477
 *                               renewed by a local internal CA.
1478
 * INPUTS
1479
 *   $cert : The certificate to be tested
1480
 * RESULT
1481
 *   true if the certificate can be locally renewed, false otherwise.
1482
 ******/
1483

    
1484
function is_cert_locally_renewable($cert) {
1485
	/* If there is no certificate or private key string, this entry is either
1486
	 * invalid or cannot be renewed. */
1487
	if (empty($cert['crt']) || empty($cert['prv'])) {
1488
		return false;
1489
	}
1490

    
1491
	/* Get subject and issuer values to test for self-signed state */
1492
	$subj = cert_get_subject($cert['crt']);
1493
	$issuer = cert_get_issuer($cert['crt']);
1494

    
1495
	/* Lookup CA for this certificate */
1496
	$ca = array();
1497
	if (!empty($cert['caref'])) {
1498
		$ca = lookup_ca($cert['caref']);
1499
	}
1500

    
1501
	/* If the CA exists and we have the private key, or if the cert is
1502
	 *  self-signed, then it can be locally renewed. */
1503
	return ((!empty($ca) && !empty($ca['prv'])) || ($subj == $issuer));
1504
}
1505

    
1506
/* Strict certificate requirements based on
1507
 * https://redmine.pfsense.org/issues/9825
1508
 */
1509
global $cert_strict_values;
1510
$cert_strict_values = array(
1511
	'max_server_cert_lifetime' => 825,
1512
	'digest_blacklist' => array('md4', 'RSA-MD4',  'md5', 'RSA-MD5', 'md5-sha1',
1513
					'mdc2', 'RSA-MDC2', 'sha1', 'RSA-SHA1',
1514
					'RSA-SHA1-2'),
1515
	'min_private_key_bits' => 2048,
1516
);
1517

    
1518
/****f* certs/cert_renew
1519
 * NAME
1520
 *   cert_renew - Renew an existing internal CA or certificate
1521
 * INPUTS
1522
 *   $cert : The entry to be renewed (used as a reference so it can be altered directly)
1523
 *   $reusekey : Whether or not to reuse the existing key for the certificate
1524
 *      true: Reuse the existing key (Default)
1525
 *      false: Generate a new key based on current (or enforced minimum) parameters
1526
 *   $strictsecurity : Whether or not to enforce stricter security for specific attributes
1527
 *      true: Enforce maximum lifetime for server certs, minimum digest type, and
1528
 *            minimum private key size. See https://redmine.pfsense.org/issues/9825
1529
 *      false: Use existing values as-is (Default).
1530
 * RESULT
1531
 *   true if successful, false if failure.
1532
 * NOTES
1533
 *   See https://redmine.pfsense.org/issues/9842 for more information on behavior.
1534
 *   Does NOT run write_config(), that must be performed by the caller.
1535
 ******/
1536

    
1537
function cert_renew(& $cert, $reusekey = true, $strictsecurity = false) {
1538
	global $cert_strict_values;
1539

    
1540
	/* If there is no certificate or private key string, this entry is either
1541
	 *  invalid or cannot be renewed by this function. */
1542
	if (empty($cert['crt']) || empty($cert['prv'])) {
1543
		return false;
1544
	}
1545

    
1546
	/* Read certificate information necessary to create a new request */
1547
	$cert_details = openssl_x509_parse(base64_decode($cert['crt']));
1548

    
1549
	/* No details, must not be valid in some way */
1550
	if (!array($cert_details) || empty($cert_details)) {
1551
		return false;
1552
	}
1553

    
1554
	$subj = cert_get_subject($cert['crt']);
1555
	$issuer = cert_get_issuer($cert['crt']);
1556

    
1557
	$res_key = openssl_pkey_get_private(base64_decode($cert['prv']));
1558
	$key_details = openssl_pkey_get_details($res_key);
1559

    
1560
	/* Form a new Distinguished Name from the existing values.
1561
	 * Note: Deprecated/unsupported DN fields may not be carried forward, but
1562
	 *       may be preserved to avoid altering a subject.
1563
	 */
1564
	$subject_map = array(
1565
		'CN' => 'commonName',
1566
		'C' => 'countryName',
1567
		'ST' => 'stateOrProvinceName',
1568
		'L' => 'localityName',
1569
		'O' => 'organizationName',
1570
		'OU' => 'organizationalUnitName',
1571
		'emailAddress' => 'emailAddress', /* deprecated, but commonly found in older entries */
1572
	);
1573
	$dn = array();
1574
	/* This is necessary to ensure the order of subject components is
1575
	 * identical on the old and new certificate. */
1576
	foreach ($cert_details['subject'] as $p => $v) {
1577
		if (array_key_exists($p, $subject_map)) {
1578
			$dn[$subject_map[$p]] = $v;
1579
		}
1580
	}
1581

    
1582
	/* Test for self-signed or signed by a CA */
1583
	$selfsigned = ($subj == $issuer);
1584

    
1585
	/* Determine the type if it is not specified directly */
1586
	if (array_key_exists('serial', $cert)) {
1587
		/* If a serial value is present, this must be a CA */
1588
		$cert['type'] = 'ca';
1589
	} elseif (empty($cert['type'])) {
1590
		/* Assume server certificate if empty & non-CA (e.g. self-signed) */
1591
		$cert['type'] = 'server';
1592
	}
1593

    
1594
	/* Convert the internal certificate type to an openssl.cnf section name */
1595
	$cert_type = cert_type_config_section($cert['type']);
1596
	if ($cert['type'] != 'ca') {
1597
		$cert_type .= '_san';
1598
	}
1599

    
1600
	/* Reuse lifetime (convert seconds to days) */
1601
	$lifetime = (int) round(($cert_details['validTo_time_t'] - $cert_details['validFrom_time_t']) / 86400);
1602

    
1603
	/* If we are enforcing strict security, then cap the lifetime for server certificates */
1604
	if (($cert_type == 'server_san') && $strictsecurity &&
1605
	    ($lifetime > $cert_strict_values['max_server_cert_lifetime'])) {
1606
		$lifetime = $cert_strict_values['max_server_cert_lifetime'];
1607
	}
1608

    
1609
	/* Reuse SAN list, or, if empty, add CN as SAN. */
1610
	$sans = $cert_details['extensions']['subjectAltName'];
1611
	if (empty($sans)) {
1612
		$sans = cert_add_altname_type($dn['commonName']);
1613
	}
1614

    
1615
	/* subjectAltName can be set _only_ via configuration file, so put the
1616
	 * value into the environment where it will be read from the configuration */
1617
	putenv("SAN={$sans}");
1618

    
1619
	/* If we are enforcing strict security, then check the digest against a
1620
	 * blacklist of insecure digest methods. */
1621
	$digest_alg = $cert_details['signatureTypeSN'];
1622
	if ($strictsecurity &&
1623
	    (in_array($digest_alg, $cert_strict_values['digest_blacklist']))) {
1624
		$digest_alg = 'sha256';
1625
	}
1626

    
1627
	/* If we are enforcing strict security, then ensure the private key size
1628
	 * is at least 2048 bits. */
1629
	$private_key_bits = $key_details['bits'];
1630
	if ($strictsecurity &&
1631
	    ($private_key_bits < $cert_strict_values['min_private_key_bits'])) {
1632
		$private_key_bits = $cert_strict_values['min_private_key_bits'];
1633
		$reusekey = false;
1634
	}
1635

    
1636
	/* Validate key type, assume RSA if it cannot be read. */
1637
	if (is_array($key_details) && array_key_exists('type', $key_details)) {
1638
		$private_key_type = $key_details['type'];
1639
	} else {
1640
		$private_key_type = OPENSSL_KEYTYPE_RSA;
1641
	}
1642

    
1643
	/* Setup certificate and key arguments */
1644
	$args = array(
1645
		"x509_extensions" => $cert_type,
1646
		"digest_alg" => $digest_alg,
1647
		"private_key_bits" => (int)$private_key_bits,
1648
		"private_key_type" => $private_key_type,
1649
		"encrypt_key" => false);
1650

    
1651
	/* Adjust argumnents to account for ECDSA keys. */
1652
	if (($key_details['type'] ==  OPENSSL_KEYTYPE_EC) &&
1653
	    (!empty($key_details['ec']['curve_name']))) {
1654
		$args['curve_name'] = $key_details['ec']['curve_name'];
1655
	}
1656

    
1657
	/* Make a new key if necessary */
1658
	if (!$res_key || !$reusekey) {
1659
		$res_key = openssl_pkey_new($args);
1660
		if (!$res_key) {
1661
			return false;
1662
		}
1663
	}
1664

    
1665
	/* Create a new CSR from derived parameters and key */
1666
	$res_csr = openssl_csr_new($dn, $res_key, $args);
1667
	/* If the CSR could not be created, bail */
1668
	if (!$res_csr) {
1669
		return false;
1670
	}
1671

    
1672
	if (!empty($cert['caref'])) {
1673
		/* The certificate was signed by a CA, so read the CA details. */
1674
		$ca = & lookup_ca($cert['caref']);
1675
		/* If the referenced CA cannot be found, bail. */
1676
		if (!$ca) {
1677
			return false;
1678
		}
1679
		$ca_str_crt = base64_decode($ca['crt']);
1680
		$ca_str_key = base64_decode($ca['prv']);
1681
		$ca_res_crt = openssl_x509_read($ca_str_crt);
1682
		$ca_res_key = openssl_pkey_get_private(array(0 => $ca_str_key, 1 => ""));
1683
		if (!$ca_res_key) {
1684
			/* If the CA key cannot be read, bail. */
1685
			return false;
1686
		}
1687
		/* If the CA does not have a serial number, assume 0. */
1688
		if (empty($ca['serial'])) {
1689
			$ca['serial'] = 0;
1690
		}
1691
		/* Get the next available CA serial number. */
1692
		$ca_serial = ca_get_next_serial($ca);
1693
	} elseif ($selfsigned) {
1694
		/* For self-signed CAs & certificates, set the CA details to self and
1695
		 * use the key for this entry to sign itself.
1696
		 */
1697
		$ca_res_crt   = null;
1698
		$ca_res_key   = $res_key;
1699
		$ca_serial    = 0; /* TODO: Check if we should increment from previous */
1700
	}
1701

    
1702
	/* Sign the CSR */
1703
	$res_crt = openssl_csr_sign($res_csr, $ca_res_crt, $ca_res_key, $lifetime,
1704
				 $args, $ca_serial);
1705
	/* If the CSR could not be signed, bail */
1706
	if (!$res_crt) {
1707
		return false;
1708
	}
1709

    
1710
	/* Attempt to read the key and certificate and if that fails, bail */
1711
	if (!openssl_pkey_export($res_key, $str_key) ||
1712
	    !openssl_x509_export($res_crt, $str_crt)) {
1713
		return false;
1714
	}
1715

    
1716
	/* Load the new certificate string and key into the configuration */
1717
	$cert['crt'] = base64_encode($str_crt);
1718
	$cert['prv'] = base64_encode($str_key);
1719

    
1720
	return true;
1721
}
1722

    
1723
/****f* certs/cert_get_all_services
1724
 * NAME
1725
 *   cert_get_all_services - Locate services using a given certificate
1726
 * INPUTS
1727
 *   $refid: The refid of a certificate to check
1728
 * RESULT
1729
 *   array containing the services which use this certificate, including:
1730
 *     webgui: Present and true if the WebGUI uses this certificate. Unset otherwise.
1731
 *     services: Array of service definitions using this certificate, with:
1732
 *       name: Name of the service
1733
 *       extras: Extra information needed by some services, such as OpenVPN or Captive Portal.
1734
 *     packages: Array containing package names using this certificate.
1735
 ******/
1736

    
1737
function cert_get_all_services($refid) {
1738
	global $config;
1739
	$services = array();
1740
	$services['services'] = array();
1741
	$services['packages'] = array();
1742

    
1743
	/* Only set if true, otherwise leave unset. */
1744
	if (is_webgui_cert($refid)) {
1745
		$services['webgui'] = true;
1746
	}
1747

    
1748
	init_config_arr(array('openvpn', 'openvpn-server'));
1749
	init_config_arr(array('openvpn', 'openvpn-client'));
1750
	/* Find all OpenVPN clients and servers which use this certificate */
1751
	foreach(array('server', 'client') as $mode) {
1752
		foreach ($config['openvpn']["openvpn-{$mode}"] as $ovpn) {
1753
			if ($ovpn['certref'] == $refid) {
1754
				/* OpenVPN instances are restarted individually,
1755
				 * so we need to note the mode and ID. */
1756
				$services['services'][] = array(
1757
					'name' => 'openvpn',
1758
					'extras' => array(
1759
						'vpnmode' => $mode,
1760
						'id' => $ovpn['vpnid']
1761
					)
1762
				);
1763
			}
1764
		}
1765
	}
1766

    
1767
	/* If any one IPsec tunnel uses this certificate then the whole service
1768
	 * needs a bump. */
1769
	init_config_arr(array('ipsec', 'phase1'));
1770
	foreach ($config['ipsec']['phase1'] as $ipsec) {
1771
		if (($ipsec['authentication_method'] == 'rsasig') &&
1772
		    ($ipsec['certref'] == $refid)) {
1773
			$services['services'][] = array('name' => 'ipsec');
1774
			/* Stop after finding one, no need to search for more. */
1775
			break;
1776
		}
1777
	}
1778

    
1779
	/* Check to see if any HTTPS-enabled Captive Portal zones use this
1780
	 * certificate. */
1781
	init_config_arr(array('captiveportal'));
1782
	foreach ($config['captiveportal'] as $zone => $portal) {
1783
		if (isset($portal['enable']) && isset($portal['httpslogin']) &&
1784
		    ($portal['certref'] == $refid)) {
1785
			/* Captive Portal zones are restarted individually, so
1786
			 * we need to note the zone name. */
1787
			$services['services'][] = array(
1788
				'name' => 'captiveportal',
1789
				'extras' => array(
1790
					'zone' => $zone,
1791
				)
1792
			);
1793
		}
1794
	}
1795

    
1796
	/* Locate any packages using this certificate */
1797
	$pkgcerts = pkg_call_plugins('plugin_certificates', array('type' => 'certificates', 'event' => 'used_certificates'));
1798
	foreach ($pkgcerts as $name => $package) {
1799
		if (is_array($package['certificatelist'][$refid]) &&
1800
		    isset($package['certificatelist'][$refid]) > 0) {
1801
			$services['packages'][] = $name;
1802
		}
1803
	}
1804

    
1805
	return $services;
1806
}
1807

    
1808
/****f* certs/ca_get_all_services
1809
 * NAME
1810
 *   ca_get_all_services - Locate services using a given certificate authority or its decendents
1811
 * INPUTS
1812
 *   $refid: The refid of a certificate authority to check
1813
 * RESULT
1814
 *   array containing the services which use this certificate authority, including:
1815
 *     webgui: Present and true if the WebGUI uses this certificate. Unset otherwise.
1816
 *     services: Array of service definitions using this certificate, with:
1817
 *       name: Name of the service
1818
 *       extras: Extra information needed by some services, such as OpenVPN or Captive Portal.
1819
 *     packages: Array containing package names using this certificate.
1820
 * NOTES
1821
 *   This searches recursively to find entries using this CA as well as intermediate
1822
 *   CAs and certificates signed by this CA, and returns a single set of all services.
1823
 *   This avoids restarting affected services multiple times when there is overlapping
1824
 *   usage.
1825
 ******/
1826
function ca_get_all_services($refid) {
1827
	global $config;
1828
	$services = array();
1829
	$services['services'] = array();
1830

    
1831
	init_config_arr(array('openvpn', 'openvpn-server'));
1832
	init_config_arr(array('openvpn', 'openvpn-client'));
1833
	foreach(array('server', 'client') as $mode) {
1834
		foreach ($config['openvpn']["openvpn-{$mode}"] as $ovpn) {
1835
			if ($ovpn['caref'] == $refid) {
1836
				$services['services'][] = array(
1837
					'name' => 'openvpn',
1838
					'extras' => array(
1839
						'vpnmode' => $mode,
1840
						'id' => $ovpn['vpnid']
1841
					)
1842
				);
1843
			}
1844
		}
1845
	}
1846
	init_config_arr(array('ipsec', 'phase1'));
1847
	foreach ($config['ipsec']['phase1'] as $ipsec) {
1848
		if ($ipsec['certref'] == $refid) {
1849
			break;
1850
		}
1851
	}
1852
	foreach ($config['ipsec']['phase1'] as $ipsec) {
1853
		if (($ipsec['authentication_method'] == 'rsasig') &&
1854
		    ($ipsec['caref'] == $refid)) {
1855
			$services['services'][] = array('name' => 'ipsec');
1856
			break;
1857
		}
1858
	}
1859

    
1860
	/* Loop through all certs and get their services as well */
1861
	init_config_arr(array('cert'));
1862
	foreach ($config['cert'] as $cert) {
1863
		if ($cert['caref'] == $refid) {
1864
			$services = array_merge_recursive_unique($services, cert_get_all_services($cert['refid']));
1865
		}
1866
	}
1867

    
1868
	/* Look for intermediate certs and services */
1869
	init_config_arr(array('ca'));
1870
	foreach ($config['ca'] as $cert) {
1871
		if ($cert['caref'] == $refid) {
1872
			$services = array_merge_recursive_unique($services, ca_get_all_services($cert['refid']));
1873
		}
1874
	}
1875

    
1876
	return $services;
1877
}
1878

    
1879
/****f* certs/cert_restart_services
1880
 * NAME
1881
 *   cert_restart_services - Restarts services specific to CA/Certificate usage
1882
 * INPUTS
1883
 *   $services: An array of services returned by cert_get_all_services or ca_get_all_services
1884
 * RESULT
1885
 *   Services in the given array are restarted
1886
 *   returns false if the input is invalid
1887
 *   returns true at the end of execution
1888
 ******/
1889

    
1890
function cert_restart_services($services) {
1891
	/* If the input is not an array, it is invalid. */
1892
	if (!is_array($services)) {
1893
		return false;
1894
	}
1895

    
1896
	/* Base string to log when restarting a service */
1897
	$restart_string = gettext('Restarting %s %s due to certificate change');
1898

    
1899
	/* Restart GUI: */
1900
	if ($services['webgui']) {
1901
		ob_flush();
1902
		flush();
1903
		log_error(sprintf($restart_string, gettext('service'), 'WebGUI'));
1904
		send_event("service restart webgui");
1905
	}
1906

    
1907
	/* Restart other base services: */
1908
	if (is_array($services['services'])) {
1909
		foreach ($services['services'] as $service) {
1910
			switch ($service['name']) {
1911
				case 'openvpn':
1912
					$service_name = "{$service['name']} {$service['extras']['vpnmode']} {$service['extras']['id']}";
1913
					break;
1914
				case 'captiveportal':
1915
					$service_name = "{$service['name']} zone {$service['extras']['zone']}";
1916
					break;
1917
				default:
1918
					$service_name = $service['name'];
1919
			}
1920
			log_error(sprintf($restart_string, gettext('service'), $service_name));
1921
			service_control_restart($service['name'], $service['extras']);
1922
		}
1923
	}
1924

    
1925
	/* Restart Packages: */
1926
	if (is_array($services['packages'])) {
1927
		foreach ($services['packages'] as $service) {
1928
			log_error(sprintf($restart_string, gettext('package'), $service));
1929
			restart_service($service);
1930
		}
1931
	}
1932
	return true;
1933
}
1934

    
1935
/****f* certs/cert_get_lifetime
1936
 * NAME
1937
 *   cert_get_lifetime - Returns the number of days the certificate is valid
1938
 * INPUTS
1939
 *   $untilexpire: Boolean
1940
 *     true: The number of days returned is from now until the certificate expiration.
1941
 *     false (default): The number of days returned is the total lifetime of the certificate.
1942
 * RESULT
1943
 *   Integer number of days in the certificate total or remaining lifetime
1944
 ******/
1945

    
1946
function cert_get_lifetime($cert, $untilexpire = false) {
1947
	/* If the certificate is not valid, bail. */
1948
	if (!is_array($cert) || empty($cert['crt'])) {
1949
		return null;
1950
	}
1951
	/* Read certificate details */
1952
	list($startdate, $enddate) = cert_get_dates($cert['crt'], true, false);
1953

    
1954
	/* Determine which start time to use (now, or cert start) */
1955
	$startdate = ($untilexpire) ? new DateTime("now") : $startdate;
1956

    
1957
	/* Calculate the requested intervals */
1958
	$interval = $startdate->diff($enddate);
1959

    
1960
	return $interval->days;
1961
}
1962

    
1963
/****f* certs/cert_analyze_lifetime
1964
 * NAME
1965
 *   cert_analyze_lifetime - Analyze a certificate lifetime for expiration notices
1966
 * INPUTS
1967
 *   $expiredays: Number of days until the certificate expires (See cert_get_lifetime())
1968
 * RESULT
1969
 *   An array of two entries:
1970
 *   0/$lrclass: A bootstrap name for use with classes like text-<x>
1971
 *   1/$expstring: A text analysis describing the expiration timeframe.
1972
 ******/
1973

    
1974
function cert_analyze_lifetime($expiredays) {
1975
	global $config, $g;
1976
	/* Number of days at which to warn of expiration. */
1977
	init_config_arr(array('notifications', 'certexpire'));
1978
	if (!isset($config['notifications']['certexpire']['expiredays']) ||
1979
	    empty($config['notifications']['certexpire']['expiredays'])) {
1980
		$warning_days = $g['default_cert_expiredays'];
1981
	} else {
1982
		$warning_days = $config['notifications']['certexpire']['expiredays'];
1983
	}
1984

    
1985
	if ($expiredays > $warning_days) {
1986
		/* Not expiring soon */
1987
		$lrclass = 'normal';
1988
		$expstring = gettext("%d %s until expiration");
1989
	} elseif ($expiredays > 0) {
1990
		/* Still valid but expiring soon */
1991
		$lrclass = 'warning';
1992
		$expstring = gettext("Expiring soon, in %d %s");
1993
	} else {
1994
		/* Certificate has expired */
1995
		$lrclass = 'danger';
1996
		$expstring = gettext("Expired %d %s ago");
1997
	}
1998
	$days = (abs($expiredays) == 1) ? gettext('day') : gettext('days');
1999
	$expstring = sprintf($expstring, abs($expiredays), $days);
2000
	return array($lrclass, $expstring);
2001
}
2002

    
2003
/****f* certs/cert_print_dates
2004
 * NAME
2005
 *   cert_print_dates - Print the start and end timestamps for the given certificate
2006
 * INPUTS
2007
 *   $cert: CA or Cert entry for which the dates will be printed
2008
 * RESULT
2009
 *   Returns null if the passed entry is invalid
2010
 *   Otherwise, outputs the dates to the user with formatting.
2011
 ******/
2012

    
2013
function cert_print_dates($cert) {
2014
	/* If the certificate is not valid, bail. */
2015
	if (!is_array($cert) || empty($cert['crt'])) {
2016
		return null;
2017
	}
2018
	/* Attempt to extract the dates from the certificate */
2019
	list($startdate, $enddate) = cert_get_dates($cert['crt']);
2020
	/* If either of the timestamps are empty, then do not print anything.
2021
	 * The entry may not be valid or it may just be missing date information */
2022
	if (empty($startdate) || empty($enddate)) {
2023
		return null;
2024
	}
2025
	/* Get the expiration days */
2026
	$expiredays = cert_get_lifetime($cert, true);
2027
	/* Analyze the lifetime value */
2028
	list($lrclass, $expstring) = cert_analyze_lifetime($expiredays);
2029
	/* Output the dates, with a tooltip showing days until expiration, and
2030
	 * a visual indication of warning/expired status. */
2031
	?>
2032
	<br />
2033
	<small>
2034
	<?=gettext("Valid From")?>: <b><?=$startdate ?></b><br />
2035
	<?=gettext("Valid Until")?>:
2036
	<span class="text-<?=$lrclass?>" data-toggle="tooltip" data-placement="bottom" title="<?= $expstring ?>">
2037
	<b><?=$enddate ?></b>
2038
	</span>
2039
	</small>
2040
	<?php
2041
}
2042

    
2043
/****f* certs/cert_print_infoblock
2044
 * NAME
2045
 *   cert_print_infoblock - Print an information block containing certificate details
2046
 * INPUTS
2047
 *   $cert: CA or Cert entry for which the information will be printed
2048
 * RESULT
2049
 *   Returns null if the passed entry is invalid
2050
 *   Otherwise, outputs information to the user with formatting.
2051
 ******/
2052

    
2053
function cert_print_infoblock($cert) {
2054
	/* If the certificate is not valid, bail. */
2055
	if (!is_array($cert) || empty($cert['crt'])) {
2056
		return null;
2057
	}
2058
	/* Variable to hold the formatted information */
2059
	$certextinfo = "";
2060

    
2061
	/* Serial number */
2062
	$cert_details = openssl_x509_parse(base64_decode($cert['crt']));
2063
	if (isset($cert_details['serialNumber']) && (strlen($cert_details['serialNumber']) > 0)) {
2064
		$certextinfo .= '<b>' . gettext("Serial: ") . '</b> ';
2065
		$certextinfo .= htmlspecialchars(cert_escape_x509_chars($cert_details['serialNumber'], true));
2066
		$certextinfo .= '<br/>';
2067
	}
2068

    
2069
	/* Digest type */
2070
	$certsig = cert_get_sigtype($cert['crt']);
2071
	if (is_array($certsig) && !empty($certsig) && !empty($certsig['shortname'])) {
2072
		$certextinfo .= '<b>' . gettext("Signature Digest: ") . '</b> ';
2073
		$certextinfo .= htmlspecialchars(cert_escape_x509_chars($certsig['shortname'], true));
2074
		$certextinfo .= '<br/>';
2075
	}
2076

    
2077
	/* Subject Alternative Name (SAN) list */
2078
	$sans = cert_get_sans($cert['crt']);
2079
	if (is_array($sans) && !empty($sans)) {
2080
		$certextinfo .= '<b>' . gettext("SAN: ") . '</b> ';
2081
		$certextinfo .= htmlspecialchars(implode(', ', cert_escape_x509_chars($sans, true)));
2082
		$certextinfo .= '<br/>';
2083
	}
2084

    
2085
	/* Key usage */
2086
	$purpose = cert_get_purpose($cert['crt']);
2087
	if (is_array($purpose) && !empty($purpose['ku'])) {
2088
		$certextinfo .= '<b>' . gettext("KU: ") . '</b> ';
2089
		$certextinfo .= htmlspecialchars(implode(', ', $purpose['ku']));
2090
		$certextinfo .= '<br/>';
2091
	}
2092

    
2093
	/* Extended key usage */
2094
	if (is_array($purpose) && !empty($purpose['eku'])) {
2095
		$certextinfo .= '<b>' . gettext("EKU: ") . '</b> ';
2096
		$certextinfo .= htmlspecialchars(implode(', ', $purpose['eku']));
2097
		$certextinfo .= '<br/>';
2098
	}
2099

    
2100
	/* OCSP / Must Staple */
2101
	if (cert_get_ocspstaple($cert['crt'])) {
2102
		$certextinfo .= '<b>' . gettext("OCSP: ") . '</b> ';
2103
		$certextinfo .= gettext("Must Staple");
2104
		$certextinfo .= '<br/>';
2105
	}
2106

    
2107
	/* Private key information */
2108
	if (!empty($cert['prv'])) {
2109
		$res_key = openssl_pkey_get_private(base64_decode($cert['prv']));
2110
		$key_details = openssl_pkey_get_details($res_key);
2111

    
2112
		/* Key type (RSA or EC) */
2113
		$certextinfo .= '<b>' . gettext("Key Type: ") . '</b> ';
2114
		if ($key_details['type'] == OPENSSL_KEYTYPE_RSA) {
2115
			/* RSA Key size */
2116
			$certextinfo .= 'RSA<br/>';
2117
			$certextinfo .= '<b>' . gettext("Key Size: ") . '</b> ';
2118
			$certextinfo .= $key_details['bits'] . '<br/>';
2119
		} else {
2120
			/* Elliptic curve (EC) key curve name */
2121
			$certextinfo .= 'ECDSA<br/>';
2122
			$certextinfo .= '<b>' . gettext("Elliptic curve name: ") . '</b>';
2123
			$certextinfo .= $key_details['ec']['curve_name'] . '<br/>';
2124
		}
2125
	}
2126

    
2127
	/* Distinguished name (DN) */
2128
	if (!empty($cert_details['name'])) {
2129
		$certextinfo .= '<b>' . gettext("DN: ") . '</b> ';
2130
		$certextinfo .= htmlspecialchars(cert_escape_x509_chars($cert_details['name'], true));
2131
		$certextinfo .= '<br/>';
2132
	}
2133

    
2134
	/* Hash value */
2135
	if (!empty($cert_details['hash'])) {
2136
		$certextinfo .= '<b>' . gettext("Hash: ") . '</b> ';
2137
		$certextinfo .= htmlspecialchars(cert_escape_x509_chars($cert_details['hash'], true));
2138
		$certextinfo .= '<br/>';
2139
	}
2140

    
2141
	/* Subject Key Identifier (SKID) */
2142
	if (is_array($cert_details["extensions"]) && !empty($cert_details["extensions"]["subjectKeyIdentifier"])) {
2143
		$certextinfo .= '<b>' . gettext("Subject Key ID: ") . '</b> ';
2144
		$certextinfo .= htmlspecialchars(cert_escape_x509_chars($cert_details["extensions"]["subjectKeyIdentifier"], true));
2145
		$certextinfo .= '<br/>';
2146
	}
2147

    
2148
	/* Authority Key Identifier (AKID) */
2149
	if (is_array($cert_details["extensions"]) && !empty($cert_details["extensions"]["authorityKeyIdentifier"])) {
2150
		$certextinfo .= '<b>' . gettext("Authority Key ID: ") . '</b> ';
2151
		$certextinfo .= str_replace("\n", '<br/>', htmlspecialchars(cert_escape_x509_chars($cert_details["extensions"]["authorityKeyIdentifier"], true)));
2152
		$certextinfo .= '<br/>';
2153
	}
2154

    
2155
	/* Total Lifetime (days from cert start to end) */
2156
	$lifetime = cert_get_lifetime($cert);
2157
	$certextinfo .= '<b>' . gettext("Total Lifetime: ") . '</b> ';
2158
	$certextinfo .= sprintf("%d %s", $lifetime, (abs($lifetime) == 1) ? gettext('day') : gettext('days'));
2159
	$certextinfo .= '<br/>';
2160

    
2161
	/* Lifetime before certificate expires (days from now to end) */
2162
	$expiredays = cert_get_lifetime($cert, true);
2163
	list($lrclass, $expstring) = cert_analyze_lifetime($expiredays);
2164
	$certextinfo .= '<b>' . gettext("Lifetime Remaining: ") . '</b> ';
2165
	$certextinfo .= "<span class=\"text-{$lrclass}\">{$expstring}</span>";
2166
	$certextinfo .= '<br/>';
2167

    
2168
	/* CA Trust store presence */
2169
	$certextinfo .= '<b>' . gettext("Trust Store: ") . '</b> ';
2170
	$certextinfo .= (isset($cert['trust']) && ($cert['trust'] == "enabled")) ? gettext('Included') : gettext('Excluded');
2171
	$certextinfo .= '<br/>';
2172

    
2173
	/* Output the infoblock */
2174
	if (!empty($certextinfo)) { ?>
2175
		<div class="infoblock">
2176
		<? print_info_box($certextinfo, 'info', false); ?>
2177
		</div>
2178
	<?php
2179
	}
2180
}
2181

    
2182
/****f* certs/cert_notify_expiring
2183
 * NAME
2184
 *   cert_notify_expiring - Notify admin about expiring certificates
2185
 * INPUTS
2186
 *   None
2187
 * RESULT
2188
 *   File a notice containing expiring certificate information, which is then
2189
 *   logged, displayed in the GUI, and sent via e-mail (if enabled).
2190
 ******/
2191

    
2192
function cert_notify_expiring() {
2193
	global $config;
2194

    
2195
	/* If certificate expiration notifications are disabled, there is nothing to do. */
2196
	init_config_arr(array('notifications', 'certexpire'));
2197
	if ($config['notifications']['certexpire']['enable'] == "disabled") {
2198
		return;
2199
	}
2200

    
2201
	$notifications = array();
2202

    
2203
	/* Check all CA and Cert entries at once */
2204
	init_config_arr(array('ca'));
2205
	init_config_arr(array('cert'));
2206
	$all_certs = array_merge_recursive($config['ca'], $config['cert']);
2207

    
2208
	foreach ($all_certs as $cert) {
2209
		if (empty($cert)) {
2210
			continue;
2211
		}
2212
		/* Fetch and analyze expiration */
2213
		$expiredays = cert_get_lifetime($cert, true);
2214
		/* If the result is null, then the lifetime data is missing, so skip the invalid entry. */
2215
		if ($expiredays == null) {
2216
			continue;
2217
		}
2218
		list($lrclass, $expstring) = cert_analyze_lifetime($expiredays);
2219
		/* Only notify if the certificate is expiring soon, or has
2220
		 * already expired */
2221
		if ($lrclass != 'normal') {
2222
			$notify_string = (array_key_exists('serial', $cert)) ? gettext('Certificate Authority') : gettext('Certificate');
2223
			$notify_string .= ": {$cert['descr']} ({$cert['refid']}): {$expstring}";
2224
			$notifications[] = $notify_string;
2225
		}
2226
	}
2227
	if (!empty($notifications)) {
2228
		$message = gettext("The following CA/Certificate entries are expiring:") . "\n" .
2229
			implode("\n", $notifications);
2230
		file_notice("Certificate Expiration", $message, "Certificate Manager");
2231
	}
2232
}
2233

    
2234
/****f* certs/ca_setup_trust_store
2235
 * NAME
2236
 *   ca_setup_trust_store - Setup local CA trust store so that CA entries in the
2237
 *                          configuration may be trusted by the operating system.
2238
 * INPUTS
2239
 *   None
2240
 * RESULT
2241
 *   CAs marked as trusted in the configuration will be setup in the OS trust store.
2242
 ******/
2243

    
2244
function ca_setup_trust_store() {
2245
	global $config;
2246

    
2247
	/* This directory is trusted by OpenSSL on FreeBSD by default */
2248
	$trust_store_directory = '/etc/ssl/certs';
2249

    
2250
	/* Create the directory if it does not already exist, and clean it up if it does. */
2251
	safe_mkdir($trust_store_directory);
2252
	unlink_if_exists("{$trust_store_directory}/*.0");
2253

    
2254
	init_config_arr(array('ca'));
2255
	foreach ($config['ca'] as $ca) {
2256
		/* If the entry is invalid or is not trusted, skip it. */
2257
		if (!is_array($ca) ||
2258
		    empty($ca['crt']) ||
2259
		    !isset($ca['trust']) ||
2260
		    ($ca['trust'] != "enabled")) {
2261
			continue;
2262
		}
2263

    
2264
		/* Decode the certificate contents */
2265
		$cert_contents = base64_decode($ca['crt']);
2266
		/* Get hash value to use for filename */
2267
		$cert_details = openssl_x509_parse($cert_contents);
2268
		$ca_filename = "{$trust_store_directory}/{$cert_details['hash']}.0";
2269

    
2270
		/* Write CA to trust store and ensure it has correct permissions. */
2271
		file_put_contents($ca_filename, $cert_contents);
2272
		chmod($ca_filename, 0644);
2273
		chown($ca_filename, 'root');
2274
		chgrp($ca_filename, 'wheel');
2275
	}
2276
}
2277

    
2278
?>
(7-7/60)