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) {
700
	if ($decode) {
701
		$str_crt = base64_decode($str_crt);
702
	}
703
	$crt_details = openssl_x509_parse($str_crt);
704
	if ($crt_details['validFrom_time_t'] > 0) {
705
		$start = date('r', $crt_details['validFrom_time_t']);
706
	} else {
707
		$dt = DateTime::createFromFormat('ymdHis', rtrim($crt_details['validFrom'], 'Z'));
708
		if ($dt !== false) {
709
			$start = $dt->format(DateTimeInterface::RFC2822);
710
		}
711
	}
712
	if ($crt_details['validTo_time_t'] > 0) {
713
		$end = date('r', $crt_details['validTo_time_t']);
714
	} else {
715
		$dt = DateTime::createFromFormat('ymdHis', rtrim($crt_details['validTo'], 'Z'));
716
		if ($dt !== false) {
717
			$end = $dt->format(DateTimeInterface::RFC2822);
718
		}
719
	}
720
	return array($start, $end);
721
}
722

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

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

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

    
752
	return $signature;
753
}
754

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
908
function cert_in_use($certref) {
909

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1071
	$serial = crl_get_entry_serial($cert);
1072

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1235
	return false;
1236
}
1237

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1721
	return true;
1722
}
1723

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

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

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

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

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

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

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

    
1806
	return $services;
1807
}
1808

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

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

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

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

    
1877
	return $services;
1878
}
1879

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

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

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

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

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

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

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

    
1947
function cert_get_lifetime($cert, $untilexpire = false) {
1948
	/* If the certificate is not valid, bail. */
1949
	if (!is_array($cert) || empty($cert['crt'])) {
1950
		return null;
1951
	}
1952
	/* Read certificate details */
1953
	$cert_details = openssl_x509_parse(base64_decode($cert['crt']));
1954
	/* Determine which start time to use (now, or cert start) */
1955
	$fromtime = ($untilexpire) ? time() : $cert_details['validFrom_time_t'];
1956
	/* Calculate and return the requested duration, converting from seconds to days. */
1957
	return (int) round(($cert_details['validTo_time_t'] - $fromtime) / 86400);
1958
}
1959

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
2189
function cert_notify_expiring() {
2190
	global $config;
2191

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

    
2198
	$notifications = array();
2199

    
2200
	/* Check all CA and Cert entries at once */
2201
	init_config_arr(array('ca'));
2202
	init_config_arr(array('cert'));
2203
	$all_certs = array_merge_recursive_unique($config['ca'], $config['cert']);
2204

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

    
2228
/****f* certs/ca_setup_trust_store
2229
 * NAME
2230
 *   ca_setup_trust_store - Setup local CA trust store so that CA entries in the
2231
 *                          configuration may be trusted by the operating system.
2232
 * INPUTS
2233
 *   None
2234
 * RESULT
2235
 *   CAs marked as trusted in the configuration will be setup in the OS trust store.
2236
 ******/
2237

    
2238
function ca_setup_trust_store() {
2239
	global $config;
2240

    
2241
	/* This directory is trusted by OpenSSL on FreeBSD by default */
2242
	$trust_store_directory = '/etc/ssl/certs';
2243

    
2244
	/* Create the directory if it does not already exist, and clean it up if it does. */
2245
	safe_mkdir($trust_store_directory);
2246
	unlink_if_exists("{$trust_store_directory}/*.0");
2247

    
2248
	init_config_arr(array('ca'));
2249
	foreach ($config['ca'] as $ca) {
2250
		/* If the entry is invalid or is not trusted, skip it. */
2251
		if (!is_array($ca) ||
2252
		    empty($ca['crt']) ||
2253
		    !isset($ca['trust']) ||
2254
		    ($ca['trust'] != "enabled")) {
2255
			continue;
2256
		}
2257

    
2258
		/* Decode the certificate contents */
2259
		$cert_contents = base64_decode($ca['crt']);
2260
		/* Get hash value to use for filename */
2261
		$cert_details = openssl_x509_parse($cert_contents);
2262
		$ca_filename = "{$trust_store_directory}/{$cert_details['hash']}.0";
2263

    
2264
		/* Write CA to trust store and ensure it has correct permissions. */
2265
		file_put_contents($ca_filename, $cert_contents);
2266
		chmod($ca_filename, 0644);
2267
		chown($ca_filename, 'root');
2268
		chgrp($ca_filename, 'wheel');
2269
	}
2270
}
2271

    
2272
?>
(7-7/60)