Project

General

Profile

Download (76.8 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-2022 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
	$serialNumber = cert_get_serial($str, false);
176

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

    
185
	/* Correct if child certificate was loaded first */
186
	if (is_array($config['ca'])) {
187
		foreach ($config['ca'] as & $oca) {
188
			// check by serial number if CA already exists
189
			$osn = cert_get_serial($oca['crt']);
190
			if (($ca['refid'] <> $oca['refid']) && ($serialNumber == $osn)) {
191
				return false;
192
			}
193
			$issuer = cert_get_issuer($oca['crt']);
194
			if (($ca['refid'] <> $oca['refid']) && ($issuer == $subject)) {
195
				$oca['caref'] = $ca['refid'];
196
			}
197
		}
198
	}
199
	if (is_array($config['cert'])) {
200
		foreach ($config['cert'] as & $cert) {
201
			$issuer = cert_get_issuer($cert['crt']);
202
			if ($issuer == $subject) {
203
				$cert['caref'] = $ca['refid'];
204
			}
205
		}
206
	}
207
	return true;
208
}
209

    
210
function ca_create(& $ca, $keylen, $lifetime, $dn, $digest_alg = "sha256", $keytype = "RSA", $ecname = "prime256v1") {
211

    
212
	$args = array(
213
		"x509_extensions" => "v3_ca",
214
		"digest_alg" => $digest_alg,
215
		"encrypt_key" => false);
216
	if ($keytype == 'ECDSA') {
217
		$args["curve_name"] = $ecname;
218
		$args["private_key_type"] = OPENSSL_KEYTYPE_EC;
219
	} else {
220
		$args["private_key_bits"] = (int)$keylen;
221
		$args["private_key_type"] = OPENSSL_KEYTYPE_RSA;
222
	}
223

    
224
	// generate a new key pair
225
	$res_key = openssl_pkey_new($args);
226
	if (!$res_key) {
227
		return false;
228
	}
229

    
230
	// generate a certificate signing request
231
	$res_csr = openssl_csr_new($dn, $res_key, $args);
232
	if (!$res_csr) {
233
		return false;
234
	}
235

    
236
	// self sign the certificate
237
	$res_crt = openssl_csr_sign($res_csr, null, $res_key, $lifetime, $args, cert_get_random_serial());
238
	if (!$res_crt) {
239
		return false;
240
	}
241

    
242
	// export our certificate data
243
	if (!openssl_pkey_export($res_key, $str_key) ||
244
	    !openssl_x509_export($res_crt, $str_crt)) {
245
		return false;
246
	}
247

    
248
	// return our ca information
249
	$ca['crt'] = base64_encode($str_crt);
250
	$ca['prv'] = base64_encode($str_key);
251
	$ca['serial'] = 0;
252

    
253
	return true;
254
}
255

    
256
function ca_inter_create(& $ca, $keylen, $lifetime, $dn, $caref, $digest_alg = "sha256", $keytype = "RSA", $ecname = "prime256v1") {
257
	// Create Intermediate Certificate Authority
258
	$signing_ca =& lookup_ca($caref);
259
	if (!$signing_ca) {
260
		return false;
261
	}
262

    
263
	$signing_ca_res_crt = openssl_x509_read(base64_decode($signing_ca['crt']));
264
	$signing_ca_res_key = openssl_pkey_get_private(array(0 => base64_decode($signing_ca['prv']) , 1 => ""));
265
	if (!$signing_ca_res_crt || !$signing_ca_res_key) {
266
		return false;
267
	}
268
	$signing_ca_serial = ++$signing_ca['serial'];
269

    
270
	$args = array(
271
		"x509_extensions" => "v3_ca",
272
		"digest_alg" => $digest_alg,
273
		"encrypt_key" => false);
274
	if ($keytype == 'ECDSA') {
275
		$args["curve_name"] = $ecname;
276
		$args["private_key_type"] = OPENSSL_KEYTYPE_EC;
277
	} else {
278
		$args["private_key_bits"] = (int)$keylen;
279
		$args["private_key_type"] = OPENSSL_KEYTYPE_RSA;
280
	}
281

    
282
	// generate a new key pair
283
	$res_key = openssl_pkey_new($args);
284
	if (!$res_key) {
285
		return false;
286
	}
287

    
288
	// generate a certificate signing request
289
	$res_csr = openssl_csr_new($dn, $res_key, $args);
290
	if (!$res_csr) {
291
		return false;
292
	}
293

    
294
	// Sign the certificate
295
	$res_crt = openssl_csr_sign($res_csr, $signing_ca_res_crt, $signing_ca_res_key, $lifetime, $args, $signing_ca_serial);
296
	if (!$res_crt) {
297
		return false;
298
	}
299

    
300
	// export our certificate data
301
	if (!openssl_pkey_export($res_key, $str_key) ||
302
	    !openssl_x509_export($res_crt, $str_crt)) {
303
		return false;
304
	}
305

    
306
	// return our ca information
307
	$ca['crt'] = base64_encode($str_crt);
308
	$ca['prv'] = base64_encode($str_key);
309
	$ca['serial'] = 0;
310
	$ca['caref'] = $caref;
311

    
312
	return true;
313
}
314

    
315
function cert_import(& $cert, $crt_str, $key_str) {
316

    
317
	$cert['crt'] = base64_encode($crt_str);
318
	$cert['prv'] = base64_encode($key_str);
319

    
320
	$subject = cert_get_subject($crt_str, false);
321
	$issuer = cert_get_issuer($crt_str, false);
322

    
323
	// Find my issuer unless self-signed
324
	if ($issuer <> $subject) {
325
		$issuer_crt =& lookup_ca_by_subject($issuer);
326
		if ($issuer_crt) {
327
			$cert['caref'] = $issuer_crt['refid'];
328
		}
329
	}
330
	return true;
331
}
332

    
333
function cert_create(& $cert, $caref, $keylen, $lifetime, $dn, $type = "user", $digest_alg = "sha256", $keytype = "RSA", $ecname = "prime256v1") {
334

    
335
	$cert['type'] = $type;
336

    
337
	if ($type != "self-signed") {
338
		$cert['caref'] = $caref;
339
		$ca =& lookup_ca($caref);
340
		if (!$ca) {
341
			return false;
342
		}
343

    
344
		$ca_str_crt = base64_decode($ca['crt']);
345
		$ca_str_key = base64_decode($ca['prv']);
346
		$ca_res_crt = openssl_x509_read($ca_str_crt);
347
		$ca_res_key = openssl_pkey_get_private(array(0 => $ca_str_key, 1 => ""));
348
		if (!$ca_res_key) {
349
			return false;
350
		}
351

    
352
		/* Get the next available CA serial number. */
353
		$ca_serial = ca_get_next_serial($ca);
354
	}
355

    
356
	$cert_type = cert_type_config_section($type);
357

    
358
	// in case of using Subject Alternative Names use other sections (with postfix '_san')
359
	// pass subjectAltName over environment variable 'SAN'
360
	if ($dn['subjectAltName']) {
361
		putenv("SAN={$dn['subjectAltName']}"); // subjectAltName can be set _only_ via configuration file
362
		$cert_type .= '_san';
363
		unset($dn['subjectAltName']);
364
	}
365

    
366
	$args = array(
367
		"x509_extensions" => $cert_type,
368
		"digest_alg" => $digest_alg,
369
		"encrypt_key" => false);
370
	if ($keytype == 'ECDSA') {
371
		$args["curve_name"] = $ecname;
372
		$args["private_key_type"] = OPENSSL_KEYTYPE_EC;
373
	} else {
374
		$args["private_key_bits"] = (int)$keylen;
375
		$args["private_key_type"] = OPENSSL_KEYTYPE_RSA;
376
	}
377

    
378
	// generate a new key pair
379
	$res_key = openssl_pkey_new($args);
380
	if (!$res_key) {
381
		return false;
382
	}
383

    
384
	// If this is a self-signed cert, blank out the CA and sign with the cert's key
385
	if ($type == "self-signed") {
386
		$ca           = null;
387
		$ca_res_crt   = null;
388
		$ca_res_key   = $res_key;
389
		$ca_serial    = cert_get_random_serial();
390
		$cert['type'] = "server";
391
	}
392

    
393
	// generate a certificate signing request
394
	$res_csr = openssl_csr_new($dn, $res_key, $args);
395
	if (!$res_csr) {
396
		return false;
397
	}
398

    
399
	// sign the certificate using an internal CA
400
	$res_crt = openssl_csr_sign($res_csr, $ca_res_crt, $ca_res_key, $lifetime,
401
				 $args, $ca_serial);
402
	if (!$res_crt) {
403
		return false;
404
	}
405

    
406
	// export our certificate data
407
	if (!openssl_pkey_export($res_key, $str_key) ||
408
	    !openssl_x509_export($res_crt, $str_crt)) {
409
		return false;
410
	}
411

    
412
	// return our certificate information
413
	$cert['crt'] = base64_encode($str_crt);
414
	$cert['prv'] = base64_encode($str_key);
415

    
416
	return true;
417
}
418

    
419
function csr_generate(& $cert, $keylen, $dn, $type = "user", $digest_alg = "sha256", $keytype = "RSA", $ecname = "prime256v1") {
420

    
421
	$cert_type = cert_type_config_section($type);
422

    
423
	// in case of using Subject Alternative Names use other sections (with postfix '_san')
424
	// pass subjectAltName over environment variable 'SAN'
425
	if ($dn['subjectAltName']) {
426
		putenv("SAN={$dn['subjectAltName']}"); // subjectAltName can be set _only_ via configuration file
427
		$cert_type .= '_san';
428
		unset($dn['subjectAltName']);
429
	}
430

    
431
	$args = array(
432
		"x509_extensions" => $cert_type,
433
		"req_extensions" => "req_{$cert_type}",
434
		"digest_alg" => $digest_alg,
435
		"encrypt_key" => false);
436
	if ($keytype == 'ECDSA') {
437
		$args["curve_name"] = $ecname;
438
		$args["private_key_type"] = OPENSSL_KEYTYPE_EC;
439
	} else {
440
		$args["private_key_bits"] = (int)$keylen;
441
		$args["private_key_type"] = OPENSSL_KEYTYPE_RSA;
442
	}
443

    
444
	// generate a new key pair
445
	$res_key = openssl_pkey_new($args);
446
	if (!$res_key) {
447
		return false;
448
	}
449

    
450
	// generate a certificate signing request
451
	$res_csr = openssl_csr_new($dn, $res_key, $args);
452
	if (!$res_csr) {
453
		return false;
454
	}
455

    
456
	// export our request data
457
	if (!openssl_pkey_export($res_key, $str_key) ||
458
	    !openssl_csr_export($res_csr, $str_csr)) {
459
		return false;
460
	}
461

    
462
	// return our request information
463
	$cert['csr'] = base64_encode($str_csr);
464
	$cert['prv'] = base64_encode($str_key);
465

    
466
	return true;
467
}
468

    
469
function csr_sign($csr, & $ca, $duration, $type = "user", $altnames, $digest_alg = "sha256") {
470
	global $config;
471
	$old_err_level = error_reporting(0);
472

    
473
	// Gather the information required for signed cert
474
	$ca_str_crt = base64_decode($ca['crt']);
475
	$ca_str_key = base64_decode($ca['prv']);
476
	$ca_res_key = openssl_pkey_get_private(array(0 => $ca_str_key, 1 => ""));
477
	if (!$ca_res_key) {
478
		return false;
479
	}
480

    
481
	/* Get the next available CA serial number. */
482
	$ca_serial = ca_get_next_serial($ca);
483

    
484
	$cert_type = cert_type_config_section($type);
485

    
486
	if (!empty($altnames)) {
487
		putenv("SAN={$altnames}"); // subjectAltName can be set _only_ via configuration file
488
		$cert_type .= '_san';
489
	}
490

    
491
	$args = array(
492
		"x509_extensions" => $cert_type,
493
		"digest_alg" => $digest_alg,
494
		"req_extensions" => "req_{$cert_type}"
495
	);
496

    
497
	// Sign the new cert and export it in x509 format
498
	openssl_x509_export(openssl_csr_sign($csr, $ca_str_crt, $ca_str_key, $duration, $args, $ca_serial), $n509);
499
	error_reporting($old_err_level);
500

    
501
	return $n509;
502
}
503

    
504
function csr_complete(& $cert, $str_crt) {
505
	$str_key = base64_decode($cert['prv']);
506
	cert_import($cert, $str_crt, $str_key);
507
	unset($cert['csr']);
508
	return true;
509
}
510

    
511
function csr_get_subject($str_crt, $decode = true) {
512

    
513
	if ($decode) {
514
		$str_crt = base64_decode($str_crt);
515
	}
516

    
517
	$components = openssl_csr_get_subject($str_crt);
518

    
519
	if (empty($components) || !is_array($components)) {
520
		return "unknown";
521
	}
522

    
523
	ksort($components);
524
	foreach ($components as $a => $v) {
525
		if (!strlen($subject)) {
526
			$subject = "{$a}={$v}";
527
		} else {
528
			$subject = "{$a}={$v}, {$subject}";
529
		}
530
	}
531

    
532
	return $subject;
533
}
534

    
535
function cert_get_subject($str_crt, $decode = true) {
536

    
537
	if ($decode) {
538
		$str_crt = base64_decode($str_crt);
539
	}
540

    
541
	$inf_crt = openssl_x509_parse($str_crt);
542
	$components = $inf_crt['subject'];
543

    
544
	if (empty($components) || !is_array($components)) {
545
		return "unknown";
546
	}
547

    
548
	ksort($components);
549
	foreach ($components as $a => $v) {
550
		if (is_array($v)) {
551
			ksort($v);
552
			foreach ($v as $w) {
553
				$asubject = "{$a}={$w}";
554
				$subject = (strlen($subject)) ? "{$asubject}, {$subject}" : $asubject;
555
			}
556
		} else {
557
			$asubject = "{$a}={$v}";
558
			$subject = (strlen($subject)) ? "{$asubject}, {$subject}" : $asubject;
559
		}
560
	}
561

    
562
	return $subject;
563
}
564

    
565
function cert_get_subject_array($crt) {
566
	$str_crt = base64_decode($crt);
567
	$inf_crt = openssl_x509_parse($str_crt);
568
	$components = $inf_crt['subject'];
569

    
570
	if (!is_array($components)) {
571
		return;
572
	}
573

    
574
	$subject_array = array();
575

    
576
	foreach ($components as $a => $v) {
577
		$subject_array[] = array('a' => $a, 'v' => $v);
578
	}
579

    
580
	return $subject_array;
581
}
582

    
583
function cert_get_subject_hash($crt) {
584
	$str_crt = base64_decode($crt);
585
	$inf_crt = openssl_x509_parse($str_crt);
586
	return $inf_crt['subject'];
587
}
588

    
589
function cert_get_sans($str_crt, $decode = true) {
590
	if ($decode) {
591
		$str_crt = base64_decode($str_crt);
592
	}
593
	$sans = array();
594
	$crt_details = openssl_x509_parse($str_crt);
595
	if (!empty($crt_details['extensions']['subjectAltName'])) {
596
		$sans = explode(',', $crt_details['extensions']['subjectAltName']);
597
	}
598
	return $sans;
599
}
600

    
601
function cert_get_issuer($str_crt, $decode = true) {
602

    
603
	if ($decode) {
604
		$str_crt = base64_decode($str_crt);
605
	}
606

    
607
	$inf_crt = openssl_x509_parse($str_crt);
608
	$components = $inf_crt['issuer'];
609

    
610
	if (empty($components) || !is_array($components)) {
611
		return "unknown";
612
	}
613

    
614
	ksort($components);
615
	foreach ($components as $a => $v) {
616
		if (is_array($v)) {
617
			ksort($v);
618
			foreach ($v as $w) {
619
				$aissuer = "{$a}={$w}";
620
				$issuer = (strlen($issuer)) ? "{$aissuer}, {$issuer}" : $aissuer;
621
			}
622
		} else {
623
			$aissuer = "{$a}={$v}";
624
			$issuer = (strlen($issuer)) ? "{$aissuer}, {$issuer}" : $aissuer;
625
		}
626
	}
627

    
628
	return $issuer;
629
}
630

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

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

    
690
	return $purpose;
691
}
692

    
693
function cert_get_ocspstaple($str_crt, $decode = true) {
694
	if ($decode) {
695
		$str_crt = base64_decode($str_crt);
696
	}
697
	$crt_details = openssl_x509_parse($str_crt);
698
	if (($crt_details['extensions']['tlsfeature'] == "status_request") ||
699
	    !empty($crt_details['extensions']['1.3.6.1.5.5.7.1.24'])) {
700
		return true;
701
	}
702
	return false;
703
}
704

    
705
function cert_format_date($validTS, $validTS_time_t, $outputstring = true) {
706
	$now = new DateTime("now");
707

    
708
	/* Try to create a DateTime object from the full time string */
709
	$date = DateTime::createFromFormat('ymdHis', rtrim($validTS, 'Z'), new DateTimeZone('Z'));
710
	/* If that failed, try using a four digit year */
711
	if ($date === false) {
712
		$date = DateTime::createFromFormat('YmdHis', rtrim($validTS, 'Z'), new DateTimeZone('Z'));
713
	}
714
	/* If that failed, try to create it from the UNIX timestamp */
715
	if (($date === false) && (!empty($validTS_time_t))) {
716
		$date = new DateTime('@' . $validTS_time_t, new DateTimeZone('Z'));
717
	}
718
	/* If we have a valid DateTime object, format it in a nice way */
719
	if ($date !== false) {
720
		$date->setTimezone($now->getTimeZone());
721
		if ($outputstring) {
722
			$date = $date->format(DateTimeInterface::RFC2822);
723
		}
724
	}
725
	return $date;
726
}
727

    
728
function cert_get_dates($str_crt, $decode = true, $outputstring = true) {
729
	if ($decode) {
730
		$str_crt = base64_decode($str_crt);
731
	}
732
	$crt_details = openssl_x509_parse($str_crt);
733

    
734
	$start = cert_format_date($crt_details['validFrom'], $crt_details['validFrom_time_t'], $outputstring);
735
	$end   = cert_format_date($crt_details['validTo'], $crt_details['validTo_time_t'], $outputstring);
736

    
737
	return array($start, $end);
738
}
739

    
740
function cert_get_serial($str_crt, $decode = true) {
741
	if ($decode) {
742
		$str_crt = base64_decode($str_crt);
743
	}
744
	$crt_details = openssl_x509_parse($str_crt);
745
	if (isset($crt_details['serialNumber'])) {
746
		return $crt_details['serialNumber'];
747
	} else {
748
		return NULL;
749
	}
750
}
751

    
752
function cert_get_sigtype($str_crt, $decode = true) {
753
	if ($decode) {
754
		$str_crt = base64_decode($str_crt);
755
	}
756
	$crt_details = openssl_x509_parse($str_crt);
757

    
758
	$signature = array();
759
	if (isset($crt_details['signatureTypeSN']) && !empty($crt_details['signatureTypeSN'])) {
760
		$signature['shortname'] = $crt_details['signatureTypeSN'];
761
	}
762
	if (isset($crt_details['signatureTypeLN']) && !empty($crt_details['signatureTypeLN'])) {
763
		$signature['longname'] = $crt_details['signatureTypeLN'];
764
	}
765
	if (isset($crt_details['signatureTypeNID']) && !empty($crt_details['signatureTypeNID'])) {
766
		$signature['nid'] = $crt_details['signatureTypeNID'];
767
	}
768

    
769
	return $signature;
770
}
771

    
772
function is_openvpn_server_ca($caref) {
773
	global $config;
774
	if (!is_array($config['openvpn']['openvpn-server'])) {
775
		return;
776
	}
777
	foreach ($config['openvpn']['openvpn-server'] as $ovpns) {
778
		if ($ovpns['caref'] == $caref) {
779
			return true;
780
		}
781
	}
782
	return false;
783
}
784

    
785
function is_openvpn_client_ca($caref) {
786
	global $config;
787
	if (!is_array($config['openvpn']['openvpn-client'])) {
788
		return;
789
	}
790
	foreach ($config['openvpn']['openvpn-client'] as $ovpnc) {
791
		if ($ovpnc['caref'] == $caref) {
792
			return true;
793
		}
794
	}
795
	return false;
796
}
797

    
798
function is_ipsec_peer_ca($caref) {
799
	global $config;
800
	if (!is_array($config['ipsec']['phase1'])) {
801
		return;
802
	}
803
	foreach ($config['ipsec']['phase1'] as $ipsec) {
804
		if ($ipsec['caref'] == $caref) {
805
			return true;
806
		}
807
	}
808
	return false;
809
}
810

    
811
function is_ldap_peer_ca($caref) {
812
	global $config;
813
	if (!is_array($config['system']['authserver'])) {
814
		return;
815
	}
816
	foreach ($config['system']['authserver'] as $authserver) {
817
		if (($authserver['ldap_caref'] == $caref) &&
818
		    ($authserver['ldap_urltype'] != 'Standard TCP')) {
819
			return true;
820
		}
821
	}
822
	return false;
823
}
824

    
825
function ca_in_use($caref) {
826
	return (is_openvpn_server_ca($caref) ||
827
		is_openvpn_client_ca($caref) ||
828
		is_ipsec_peer_ca($caref) ||
829
		is_ldap_peer_ca($caref));
830
}
831

    
832
function is_user_cert($certref) {
833
	global $config;
834
	if (!is_array($config['system']['user'])) {
835
		return;
836
	}
837
	foreach ($config['system']['user'] as $user) {
838
		if (!is_array($user['cert'])) {
839
			continue;
840
		}
841
		foreach ($user['cert'] as $cert) {
842
			if ($certref == $cert) {
843
				return true;
844
			}
845
		}
846
	}
847
	return false;
848
}
849

    
850
function is_openvpn_server_cert($certref) {
851
	global $config;
852
	if (!is_array($config['openvpn']['openvpn-server'])) {
853
		return;
854
	}
855
	foreach ($config['openvpn']['openvpn-server'] as $ovpns) {
856
		if ($ovpns['certref'] == $certref) {
857
			return true;
858
		}
859
	}
860
	return false;
861
}
862

    
863
function is_openvpn_client_cert($certref) {
864
	global $config;
865
	if (!is_array($config['openvpn']['openvpn-client'])) {
866
		return;
867
	}
868
	foreach ($config['openvpn']['openvpn-client'] as $ovpnc) {
869
		if ($ovpnc['certref'] == $certref) {
870
			return true;
871
		}
872
	}
873
	return false;
874
}
875

    
876
function is_ipsec_cert($certref) {
877
	global $config;
878
	if (!is_array($config['ipsec']['phase1'])) {
879
		return;
880
	}
881
	foreach ($config['ipsec']['phase1'] as $ipsec) {
882
		if ($ipsec['certref'] == $certref) {
883
			return true;
884
		}
885
	}
886
	return false;
887
}
888

    
889
function is_webgui_cert($certref) {
890
	global $config;
891
	if (($config['system']['webgui']['ssl-certref'] == $certref) &&
892
	    ($config['system']['webgui']['protocol'] != "http")) {
893
		return true;
894
	}
895
}
896

    
897
function is_package_cert($certref) {
898
	$pluginparams = array();
899
	$pluginparams['type'] = 'certificates';
900
	$pluginparams['event'] = 'used_certificates';
901

    
902
	$certificates_used_by_packages = pkg_call_plugins('plugin_certificates', $pluginparams);
903

    
904
	/* Check if any package is using certificate */
905
	foreach ($certificates_used_by_packages as $name => $package) {
906
		if (is_array($package['certificatelist'][$certref]) &&
907
		    isset($package['certificatelist'][$certref]) > 0) {
908
			return true;
909
		}
910
	}
911
}
912

    
913
function is_captiveportal_cert($certref) {
914
	global $config;
915
	if (!is_array($config['captiveportal'])) {
916
		return;
917
	}
918
	foreach ($config['captiveportal'] as $portal) {
919
		if (isset($portal['enable']) && isset($portal['httpslogin']) && ($portal['certref'] == $certref)) {
920
			return true;
921
		}
922
	}
923
	return false;
924
}
925

    
926
function is_unbound_cert($certref) {
927
	global $config;
928
	if (($config['unbound']['sslcertref'] == $certref) &&
929
	    isset($config['unbound']['enablessl']) &&
930
	    isset($config['unbound']['enable'])) {
931
		return true;
932
	}
933
}
934

    
935
function cert_in_use($certref) {
936

    
937
	return (is_webgui_cert($certref) ||
938
		is_user_cert($certref) ||
939
		is_openvpn_server_cert($certref) ||
940
		is_openvpn_client_cert($certref) ||
941
		is_ipsec_cert($certref) ||
942
		is_captiveportal_cert($certref) ||
943
		is_unbound_cert($certref) ||
944
		is_package_cert($certref));
945
}
946

    
947
function cert_usedby_description($refid, $certificates_used_by_packages) {
948
	$result = "";
949
	if (is_array($certificates_used_by_packages)) {
950
		foreach ($certificates_used_by_packages as $name => $package) {
951
			if (isset($package['certificatelist'][$refid])) {
952
				$hint = "" ;
953
				if (is_array($package['certificatelist'][$refid])) {
954
					foreach ($package['certificatelist'][$refid] as $cert_used) {
955
						$hint = $hint . $cert_used['usedby']."\n";
956
					}
957
				}
958
				$count = count($package['certificatelist'][$refid]);
959
				$result .= "<div title='".htmlspecialchars($hint)."'>";
960
				$result .= htmlspecialchars($package['pkgname'])." ($count)<br />";
961
				$result .= "</div>";
962
			}
963
		}
964
	}
965
	return $result;
966
}
967

    
968
/* Detect a rollover at 2038 on some platforms (e.g. ARM)
969
 * See: https://redmine.pfsense.org/issues/9098 */
970
function cert_get_max_lifetime() {
971
	global $cert_max_lifetime;
972
	$max = $cert_max_lifetime;
973

    
974
	$current_time = time();
975
	while ((int)($current_time + ($max * 24 * 60 * 60)) < 0) {
976
		$max--;
977
	}
978
	return min($max, $cert_max_lifetime);
979
}
980

    
981
function crl_create(& $crl, $caref, $name, $serial = 0, $lifetime = 3650) {
982
	global $config;
983
	$max_lifetime = cert_get_max_lifetime();
984
	$ca =& lookup_ca($caref);
985
	if (!$ca) {
986
		return false;
987
	}
988
	$crl['descr'] = $name;
989
	$crl['caref'] = $caref;
990
	$crl['serial'] = $serial;
991
	$crl['lifetime'] = ($lifetime > $max_lifetime) ? $max_lifetime : $lifetime;
992
	$crl['cert'] = array();
993
	$config['crl'][] = $crl;
994
	return $crl;
995
}
996

    
997
function crl_update(& $crl) {
998
	require_once('ASN1.php');
999
	require_once('ASN1_UTF8STRING.php');
1000
	require_once('ASN1_ASCIISTRING.php');
1001
	require_once('ASN1_BITSTRING.php');
1002
	require_once('ASN1_BOOL.php');
1003
	require_once('ASN1_GENERALTIME.php');
1004
	require_once('ASN1_INT.php');
1005
	require_once('ASN1_ENUM.php');
1006
	require_once('ASN1_NULL.php');
1007
	require_once('ASN1_OCTETSTRING.php');
1008
	require_once('ASN1_OID.php');
1009
	require_once('ASN1_SEQUENCE.php');
1010
	require_once('ASN1_SET.php');
1011
	require_once('ASN1_SIMPLE.php');
1012
	require_once('ASN1_TELETEXSTRING.php');
1013
	require_once('ASN1_UTCTIME.php');
1014
	require_once('OID.php');
1015
	require_once('X509.php');
1016
	require_once('X509_CERT.php');
1017
	require_once('X509_CRL.php');
1018

    
1019
	global $config;
1020
	$max_lifetime = cert_get_max_lifetime();
1021
	$ca =& lookup_ca($crl['caref']);
1022
	if (!$ca) {
1023
		return false;
1024
	}
1025
	// If we have text but no certs, it was imported and cannot be updated.
1026
	if (($crl["method"] != "internal") && (!empty($crl['text']) && empty($crl['cert']))) {
1027
		return false;
1028
	}
1029
	$crl['serial']++;
1030
	$ca_cert = \Ukrbublik\openssl_x509_crl\X509::pem2der(base64_decode($ca['crt']));
1031
	$ca_pkey = openssl_pkey_get_private(base64_decode($ca['prv']));
1032

    
1033
	$crlconf = array(
1034
		'no' => $crl['serial'],
1035
		'version' => 2,
1036
		'days' => ($crl['lifetime'] > $max_lifetime) ? $max_lifetime : $crl['lifetime'],
1037
		'alg' => OPENSSL_ALGO_SHA1,
1038
		'revoked' => array()
1039
	);
1040

    
1041
	if (is_array($crl['cert']) && (count($crl['cert']) > 0)) {
1042
		foreach ($crl['cert'] as $cert) {
1043
			/* Determine the serial number to revoke */
1044
			if (isset($cert['serial'])) {
1045
				$serial = $cert['serial'];
1046
			} elseif (isset($cert['crt'])) {
1047
				$serial = cert_get_serial($cert['crt'], true);
1048
			} else {
1049
				continue;
1050
			}
1051
			$crlconf['revoked'][] = array(
1052
				'serial' => $serial,
1053
				'rev_date' => $cert['revoke_time'],
1054
				'reason' => ($cert['reason'] == -1) ? null : (int) $cert['reason'],
1055
			);
1056
		}
1057
	}
1058

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

    
1062
	return $crl['text'];
1063
}
1064

    
1065
function cert_revoke($cert, & $crl, $reason = OCSP_REVOKED_STATUS_UNSPECIFIED) {
1066
	global $config;
1067
	if (is_cert_revoked($cert, $crl['refid'])) {
1068
		return true;
1069
	}
1070
	// If we have text but no certs, it was imported and cannot be updated.
1071
	if (!is_crl_internal($crl)) {
1072
		return false;
1073
	}
1074

    
1075
	if (!is_array($cert)) {
1076
		/* If passed a not an array but a serial string, set it up as an
1077
		 * array with the serial number defined */
1078
		$rcert = array();
1079
		$rcert['serial'] = $cert;
1080
	} else {
1081
		/* If passed a certificate entry, read out the serial and store
1082
		 * it separately. */
1083
		$rcert = $cert;
1084
		$rcert['serial'] = cert_get_serial($cert['crt']);
1085
	}
1086
	$rcert['reason'] = $reason;
1087
	$rcert['revoke_time'] = time();
1088
	$crl['cert'][] = $rcert;
1089
	crl_update($crl);
1090
	return true;
1091
}
1092

    
1093
function cert_unrevoke($cert, & $crl) {
1094
	global $config;
1095
	if (!is_crl_internal($crl)) {
1096
		return false;
1097
	}
1098

    
1099
	$serial = crl_get_entry_serial($cert);
1100

    
1101
	foreach ($crl['cert'] as $id => $rcert) {
1102
		/* Check for a match by refid, name, or serial number */
1103
		if (($rcert['refid'] == $cert['refid']) ||
1104
		    ($rcert['descr'] == $cert['descr']) ||
1105
		    (crl_get_entry_serial($rcert) == $serial)) {
1106
			unset($crl['cert'][$id]);
1107
			if (count($crl['cert']) == 0) {
1108
				// Protect against accidentally switching the type to imported, for older CRLs
1109
				if (!isset($crl['method'])) {
1110
					$crl['method'] = "internal";
1111
				}
1112
				crl_update($crl);
1113
			} else {
1114
				crl_update($crl);
1115
			}
1116
			return true;
1117
		}
1118
	}
1119
	return false;
1120
}
1121

    
1122
/* Compare two certificates to see if they match. */
1123
function cert_compare($cert1, $cert2) {
1124
	/* Ensure two certs are identical by first checking that their issuers match, then
1125
		subjects, then serial numbers, and finally the moduli. Anything less strict
1126
		could accidentally count two similar, but different, certificates as
1127
		being identical. */
1128
	$c1 = base64_decode($cert1['crt']);
1129
	$c2 = base64_decode($cert2['crt']);
1130
	if ((cert_get_issuer($c1, false) == cert_get_issuer($c2, false)) &&
1131
	    (cert_get_subject($c1, false) == cert_get_subject($c2, false)) &&
1132
	    (cert_get_serial($c1, false) == cert_get_serial($c2, false)) &&
1133
	    (cert_get_publickey($c1, false) == cert_get_publickey($c2, false))) {
1134
		return true;
1135
	}
1136
	return false;
1137
}
1138

    
1139
/****f* certs/crl_get_entry_serial
1140
 * NAME
1141
 *   crl_get_entry_serial - Take a CRL entry and determine the associated serial
1142
 * INPUTS
1143
 *   $entry: CRL certificate list entry to inspect, or serial string
1144
 * RESULT
1145
 *   The requested serial string, if present, or null if it cannot be determined.
1146
 ******/
1147

    
1148
function crl_get_entry_serial($entry) {
1149
	/* Check the passed entry several ways to determine the serial */
1150
	if (isset($entry['serial']) && (strlen($entry['serial']) > 0)) {
1151
		/* Entry is an array with a viable 'serial' element */
1152
		return $entry['serial'];
1153
	} elseif (isset($entry['crt'])) {
1154
		/* Entry is an array with certificate text which can be used to
1155
		 * determine the serial */
1156
		return cert_get_serial($entry['crt'], true);
1157
	} elseif (cert_validate_serial($entry, false, true) != null) {
1158
		/* Entry is a valid serial string */
1159
		return $entry;
1160
	}
1161
	/* Unable to find or determine a serial number */
1162
	return null;
1163
}
1164

    
1165
/****f* certs/cert_validate_serial
1166
 * NAME
1167
 *   cert_validate_serial - Validate a given string to test if it can be used as
1168
 *                          a certificate serial.
1169
 * INPUTS
1170
 *   $serial     : Serial number string to test
1171
 *   $returnvalue: Whether to return the parsed value or true/false
1172
 * RESULT
1173
 *   If $returnvalue is true, then the parsed ASN.1 integer value string for
1174
 *     $serial or null if invalid
1175
 *   If $returnvalue is false, then true/false based on whether or not $serial
1176
 *     is valid.
1177
 ******/
1178

    
1179
function cert_validate_serial($serial, $returnvalue = false, $allowlarge = false) {
1180
	require_once('ASN1.php');
1181
	require_once('ASN1_INT.php');
1182
	/* The ASN.1 parsing function will throw an exception if the value is
1183
	 * invalid, so take advantage of that to catch other error as well. */
1184
	try {
1185
		/* If the serial is not a string, then do not bother with
1186
		 * further tests. */
1187
		if (!is_string($serial)) {
1188
			throw new Exception('Not a string');
1189
		}
1190
		/* Process a hex string */
1191
		if ((substr($serial, 0, 2) == '0x')) {
1192
			/* If the string is hex, then it must contain only
1193
			 * valid hex digits */
1194
			if (!ctype_xdigit(substr($serial, 2))) {
1195
				throw new Exception('Not a valid hex string');
1196
			}
1197
			/* Convert to decimal */
1198
			$serial = base_convert($serial, 16, 10);
1199
		}
1200

    
1201
		/* Unfortunately, PHP openssl_csr_sign() limits serial numbers to a
1202
		 * PHP integer, so we cannot generate large numbers up to the maximum
1203
		 * allowed ASN.1 size (2^159). We are limited to PHP_INT_MAX --
1204
		 * As such, numbers larger than that limit should be rejected */
1205
		if ($serial > PHP_INT_MAX) {
1206
			throw new Exception('Serial too large for PHP OpenSSL');
1207
		}
1208

    
1209
		/* Attempt to create an ASN.1 integer, if it fails, an exception will be thrown */
1210
		$asn1serial = new \Ukrbublik\openssl_x509_crl\ASN1_INT( $serial );
1211
		return ($returnvalue) ? $asn1serial->content : true;
1212
	} catch (Exception $ex) {
1213
		/* No mattter what the error is, return null or false depending
1214
		 * on what was requested. */
1215
		return ($returnvalue) ? null : false;
1216
	}
1217
}
1218

    
1219
/****f* certs/cert_generate_serial
1220
 * NAME
1221
 *   cert_generate_serial - Generate a random positive integer usable as a
1222
 *                          certificate serial number
1223
 * INPUTS
1224
 *   None
1225
 * RESULT
1226
 *   Integer representing an ASN.1 compatible certificate serial number.
1227
 ******/
1228

    
1229
function cert_generate_serial() {
1230
	/* Use a separate function for this to make it easier to use a better
1231
	 * randomization function in the future. */
1232

    
1233
	/* Unfortunately, PHP openssl_csr_sign() limits serial numbers to a
1234
	 * PHP integer, so we cannot generate large numbers up to the maximum
1235
	 * allowed ASN.1 size (2^159). We are limited to PHP_INT_MAX */
1236
	return random_int(1, PHP_INT_MAX);
1237
}
1238

    
1239
/****f* certs/ca_has_serial
1240
 * NAME
1241
 *   ca_has_serial - Check if a serial number is used by any certificate in a given CA
1242
 * INPUTS
1243
 *   $ca    : Certificate Authority to check
1244
 *   $serial: Serial number to check
1245
 * RESULT
1246
 *   true if the serial number is in use by a certificate issued by this CA,
1247
 *   false otherwise.
1248
 ******/
1249

    
1250
function ca_has_serial($caref, $serial) {
1251
	global $config;
1252

    
1253
	/* Check certs first -- more likely to find a hit */
1254
	foreach ($config['cert'] as $cert) {
1255
		if (($cert['caref'] == $caref) &&
1256
		    (cert_get_serial($cert['crt'], true) == $serial)) {
1257
			/* If this certificate is issued by the CA in question
1258
			 * and has a matching serial number, stop processing
1259
			 * and return true. */
1260
			return true;
1261
		}
1262
	}
1263

    
1264
	/* Check the CA iteself */
1265
	$this_ca = lookup_ca($caref);
1266
	$this_serial = cert_get_serial($this_ca['crt'], true);
1267
	if ($serial == $this_serial) {
1268
		return true;
1269
	}
1270

    
1271
	/* Check other CAs for a match (intermediates signed by this CA) */
1272
	foreach ($config['ca'] as $ca) {
1273
		if (($ca['caref'] == $caref) &&
1274
		    (cert_get_serial($ca['crt'], true) == $serial)) {
1275
			/* If this CA is issued by the CA in question
1276
			 * and has a matching serial number, stop processing
1277
			 * and return true. */
1278
			return true;
1279
		}
1280
	}
1281

    
1282
	return false;
1283
}
1284

    
1285
/****f* certs/cert_get_random_serial
1286
 * NAME
1287
 *   cert_get_random_serial - Generate a random certificate serial unique in a CA
1288
 * INPUTS
1289
 *   $caref : Certificate Authority refid to test for serial uniqueness.
1290
 * RESULT
1291
 *   Random serial number which is not in use by any known certificate in a CA
1292
 ******/
1293

    
1294
function cert_get_random_serial($caref = '') {
1295
	/* Number of attempts to generate a usable serial. Multiple attempts
1296
	 *  are necessary to ensure that the number is usable and unique. */
1297
	$attempts = 10;
1298

    
1299
	/* Default value, -1 indicates an error */
1300
	$serial = -1;
1301

    
1302
	for ($i=0; $i < $attempts; $i++) {
1303
		/* Generate a random serial */
1304
		$serial = cert_generate_serial();
1305
		/* Check that the serial number is usable and unique:
1306
		 *  * Cannot be 0
1307
		 *  * Must be a valid ASN.1 serial number
1308
		 *  * Cannot be used by any other certificate on this CA */
1309
		if (($serial != 0) &&
1310
		    cert_validate_serial($serial) &&
1311
		    !ca_has_serial($caref, $serial)) {
1312
			/* If all conditions are met, we have a good serial, so stop. */
1313
			break;
1314
		}
1315
	}
1316
	return $serial;
1317
}
1318

    
1319
/****f* certs/ca_get_next_serial
1320
 * NAME
1321
 *   ca_get_next_serial - Get the next available serial number for a CA
1322
 * INPUTS
1323
 *   $ca: Reference to a CA entry
1324
 * RESULT
1325
 *   A randomized serial number (if enabled for a CA) or the next sequential value.
1326
 ******/
1327

    
1328
function ca_get_next_serial(& $ca) {
1329
	$ca_serial = null;
1330
	/* Get a randomized serial if enabled */
1331
	if ($ca['randomserial'] == 'enabled') {
1332
		$ca_serial = cert_get_random_serial($ca['refid']);
1333
	}
1334
	/* Initialize the sequential serial to be safe */
1335
	if (empty($ca['serial'])) {
1336
		$ca['serial'] = 0;
1337
	}
1338
	/* If not using a randomized serial, or randomizing the serial
1339
	 * failed, then fall back to sequential serials. */
1340
	return (empty($ca_serial) || ($ca_serial == -1)) ? ++$ca['serial'] : $ca_serial;
1341
}
1342

    
1343
/****f* certs/crl_contains_cert
1344
 * NAME
1345
 *   crl_contains_cert - Check if a certificate is present in a CRL
1346
 * INPUTS
1347
 *   $crl : CRL to check
1348
 *   $cert: Certificate to test
1349
 * RESULT
1350
 *   true if the CRL contains the certificate, false otherwise
1351
 ******/
1352

    
1353
function crl_contains_cert($crl, $cert) {
1354
	global $config;
1355
	if (!is_array($config['crl']) ||
1356
	    !is_array($crl['cert'])) {
1357
		return false;
1358
	}
1359

    
1360
	/* Find the issuer of this CRL */
1361
	$ca = lookup_ca($crl['caref']);
1362
	$crlissuer = is_array($cert) ? cert_get_subject($ca['crt']) : null;
1363
	$serial = crl_get_entry_serial($cert);
1364

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

    
1368
	/* If the requested certificate was not issued by the
1369
	 * same CA as the CRL, then do not bother checking this
1370
	 * CRL. */
1371
	if ($issuer != $crlissuer) {
1372
		return false;
1373
	}
1374

    
1375
	/* Check CRL entries to see if the certificate serial is revoked */
1376
	foreach ($crl['cert'] as $rcert) {
1377
		if (crl_get_entry_serial($rcert) == $serial) {
1378
			return true;
1379
		}
1380
	}
1381

    
1382
	/* Certificate was not found in the CRL */
1383
	return false;
1384
}
1385

    
1386
/****f* certs/is_cert_revoked
1387
 * NAME
1388
 *   is_cert_revoked - Test if a given certificate or serial is revoked
1389
 * INPUTS
1390
 *   $cert  : Certificate entry or serial number to test
1391
 *   $crlref: CRL to check for revoked entries, or empty to check all CRLs
1392
 * RESULT
1393
 *   true if the requested entry is revoked
1394
 *   false if the requested entry is not revoked
1395
 ******/
1396

    
1397
function is_cert_revoked($cert, $crlref = "") {
1398
	global $config;
1399
	if (!is_array($config['crl'])) {
1400
		return false;
1401
	}
1402

    
1403
	if (!empty($crlref)) {
1404
		$crl = lookup_crl($crlref);
1405
		return crl_contains_cert($crl, $cert);
1406
	} else {
1407
		if (!is_array($cert)) {
1408
			/* If passed a serial, then it cannot be definitively
1409
			 * matched in this way since we do not know the CA
1410
			 * associated with the bare serial. */
1411
			return null;
1412
		}
1413

    
1414
		/* Check every CRL in the configuration for a match */
1415
		foreach ($config['crl'] as $crl) {
1416
			if (!is_array($crl['cert'])) {
1417
				continue;
1418
			}
1419
			if (crl_contains_cert($crl, $cert)) {
1420
				return true;
1421
			}
1422
		}
1423
	}
1424
	return false;
1425
}
1426

    
1427
function is_openvpn_server_crl($crlref) {
1428
	global $config;
1429
	if (!is_array($config['openvpn']['openvpn-server'])) {
1430
		return;
1431
	}
1432
	foreach ($config['openvpn']['openvpn-server'] as $ovpns) {
1433
		if (!empty($ovpns['crlref']) && ($ovpns['crlref'] == $crlref)) {
1434
			return true;
1435
		}
1436
	}
1437
	return false;
1438
}
1439

    
1440
function is_package_crl($crlref) {
1441
	$pluginparams = array();
1442
	$pluginparams['type'] = 'certificates';
1443
	$pluginparams['event'] = 'used_crl';
1444

    
1445
	$certificates_used_by_packages = pkg_call_plugins('plugin_certificates', $pluginparams);
1446

    
1447
	/* Check if any package is using CRL */
1448
	foreach ($certificates_used_by_packages as $name => $package) {
1449
		if (is_array($package['certificatelist'][$crlref]) &&
1450
		    (count($package['certificatelist'][$crlref]) > 0)) {
1451
			return true;
1452
		}
1453
	}
1454
}
1455

    
1456
// Keep this general to allow for future expansion. See cert_in_use() above.
1457
function crl_in_use($crlref) {
1458
	return (is_openvpn_server_crl($crlref) ||
1459
		is_package_crl($crlref));
1460
}
1461

    
1462
function is_crl_internal($crl) {
1463
	return (!(!empty($crl['text']) && empty($crl['cert'])) || ($crl["method"] == "internal"));
1464
}
1465

    
1466
function cert_get_cn($crt, $isref = false) {
1467
	/* If this is a certref, not an actual cert, look up the cert first */
1468
	if ($isref) {
1469
		$cert = lookup_cert($crt);
1470
		/* If it's not a valid cert, bail. */
1471
		if (!(is_array($cert) && !empty($cert['crt']))) {
1472
			return "";
1473
		}
1474
		$cert = $cert['crt'];
1475
	} else {
1476
		$cert = $crt;
1477
	}
1478
	$sub = cert_get_subject_array($cert);
1479
	if (is_array($sub)) {
1480
		foreach ($sub as $s) {
1481
			if (strtoupper($s['a']) == "CN") {
1482
				return $s['v'];
1483
			}
1484
		}
1485
	}
1486
	return "";
1487
}
1488

    
1489
function cert_escape_x509_chars($str, $reverse = false) {
1490
	/* Characters which need escaped when present in x.509 fields.
1491
	 * See https://www.ietf.org/rfc/rfc4514.txt
1492
	 *
1493
	 * The backslash (\) must be listed first in these arrays!
1494
	 */
1495
	$cert_directory_string_special_chars = array('\\', '"', '#', '+', ',', ';', '<', '=', '>');
1496
	$cert_directory_string_special_chars_esc = array('\\\\', '\"', '\#', '\+', '\,', '\;', '\<', '\=', '\>');
1497
	if ($reverse) {
1498
		return str_replace($cert_directory_string_special_chars_esc, $cert_directory_string_special_chars, $str);
1499
	} else {
1500
		/* First unescape and then escape again, to prevent possible double escaping. */
1501
		return str_replace($cert_directory_string_special_chars, $cert_directory_string_special_chars_esc, cert_escape_x509_chars($str, true));
1502
	}
1503
}
1504

    
1505
function cert_add_altname_type($str) {
1506
	$type = "";
1507
	if (is_ipaddr($str)) {
1508
		$type = "IP";
1509
	} elseif (is_hostname($str, true)) {
1510
		$type = "DNS";
1511
	} elseif (is_URL($str)) {
1512
		$type = "URI";
1513
	} elseif (filter_var($str, FILTER_VALIDATE_EMAIL)) {
1514
		$type = "email";
1515
	}
1516
	if (!empty($type)) {
1517
		return "{$type}:" . cert_escape_x509_chars($str);
1518
	} else {
1519
		return null;
1520
	}
1521
}
1522

    
1523
function cert_type_config_section($type) {
1524
	switch ($type) {
1525
		case "ca":
1526
			$cert_type = "v3_ca";
1527
			break;
1528
		case "server":
1529
		case "self-signed":
1530
			$cert_type = "server";
1531
			break;
1532
		default:
1533
			$cert_type = "usr_cert";
1534
			break;
1535
	}
1536
	return $cert_type;
1537
}
1538

    
1539
/****f* certs/is_cert_locally_renewable
1540
 * NAME
1541
 *   is_cert_locally_renewable - Check to see if an existing certificate can be
1542
 *                               renewed by a local internal CA.
1543
 * INPUTS
1544
 *   $cert : The certificate to be tested
1545
 * RESULT
1546
 *   true if the certificate can be locally renewed, false otherwise.
1547
 ******/
1548

    
1549
function is_cert_locally_renewable($cert) {
1550
	/* If there is no certificate or private key string, this entry is either
1551
	 * invalid or cannot be renewed. */
1552
	if (empty($cert['crt']) || empty($cert['prv'])) {
1553
		return false;
1554
	}
1555

    
1556
	/* Get subject and issuer values to test for self-signed state */
1557
	$subj = cert_get_subject($cert['crt']);
1558
	$issuer = cert_get_issuer($cert['crt']);
1559

    
1560
	/* Lookup CA for this certificate */
1561
	$ca = array();
1562
	if (!empty($cert['caref'])) {
1563
		$ca = lookup_ca($cert['caref']);
1564
	}
1565

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

    
1571
/* Strict certificate requirements based on
1572
 * https://redmine.pfsense.org/issues/9825
1573
 */
1574
global $cert_strict_values;
1575
$cert_strict_values = array(
1576
	'max_server_cert_lifetime' => 398,
1577
	'digest_blacklist' => array('md4', 'RSA-MD4',  'md5', 'RSA-MD5', 'md5-sha1',
1578
					'mdc2', 'RSA-MDC2', 'sha1', 'RSA-SHA1',
1579
					'RSA-SHA1-2'),
1580
	'min_private_key_bits' => 2048,
1581
	'ec_curve' => 'prime256v1',
1582
);
1583

    
1584
/****f* certs/cert_renew
1585
 * NAME
1586
 *   cert_renew - Renew an existing internal CA or certificate
1587
 * INPUTS
1588
 *   $cert : The entry to be renewed (used as a reference so it can be altered directly)
1589
 *   $reusekey : Whether or not to reuse the existing key for the certificate
1590
 *      true: Reuse the existing key (Default)
1591
 *      false: Generate a new key based on current (or enforced minimum) parameters
1592
 *   $strictsecurity : Whether or not to enforce stricter security for specific attributes
1593
 *      true: Enforce maximum lifetime for server certs, minimum digest type, and
1594
 *            minimum private key size. See https://redmine.pfsense.org/issues/9825
1595
 *      false: Use existing values as-is (Default).
1596
 * RESULT
1597
 *   true if successful, false if failure.
1598
 * NOTES
1599
 *   See https://redmine.pfsense.org/issues/9842 for more information on behavior.
1600
 *   Does NOT run write_config(), that must be performed by the caller.
1601
 ******/
1602

    
1603
function cert_renew(& $cert, $reusekey = true, $strictsecurity = false, $reuseserial = false) {
1604
	global $cert_strict_values, $cert_curve_compatible, $curve_compatible_list;
1605

    
1606
	/* If there is no certificate or private key string, this entry is either
1607
	 *  invalid or cannot be renewed by this function. */
1608
	if (empty($cert['crt']) || empty($cert['prv'])) {
1609
		return false;
1610
	}
1611

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

    
1615
	/* No details, must not be valid in some way */
1616
	if (!array($cert_details) || empty($cert_details)) {
1617
		return false;
1618
	}
1619

    
1620
	$subj = cert_get_subject($cert['crt']);
1621
	$issuer = cert_get_issuer($cert['crt']);
1622
	$purpose = cert_get_purpose($cert['crt']);
1623

    
1624
	$res_key = openssl_pkey_get_private(base64_decode($cert['prv']));
1625
	$key_details = openssl_pkey_get_details($res_key);
1626

    
1627
	/* Form a new Distinguished Name from the existing values.
1628
	 * Note: Deprecated/unsupported DN fields may not be carried forward, but
1629
	 *       may be preserved to avoid altering a subject.
1630
	 */
1631
	$subject_map = array(
1632
		'CN' => 'commonName',
1633
		'C' => 'countryName',
1634
		'ST' => 'stateOrProvinceName',
1635
		'L' => 'localityName',
1636
		'O' => 'organizationName',
1637
		'OU' => 'organizationalUnitName',
1638
		'emailAddress' => 'emailAddress', /* deprecated, but commonly found in older entries */
1639
	);
1640
	$dn = array();
1641
	/* This is necessary to ensure the order of subject components is
1642
	 * identical on the old and new certificate. */
1643
	foreach ($cert_details['subject'] as $p => $v) {
1644
		if (array_key_exists($p, $subject_map)) {
1645
			$dn[$subject_map[$p]] = $v;
1646
		}
1647
	}
1648

    
1649
	/* Test for self-signed or signed by a CA */
1650
	$selfsigned = ($subj == $issuer);
1651

    
1652
	/* Determine the type if it is not specified directly */
1653
	if (array_key_exists('serial', $cert)) {
1654
		/* If a serial value is present, this must be a CA */
1655
		$cert['type'] = 'ca';
1656
	} elseif (empty($cert['type'])) {
1657
		/* Automatically determine certificate type if unset based on purpose value */
1658
		$cert['type'] = ($purpose['server'] == 'Yes') ? 'server' : 'user';
1659
	}
1660

    
1661
	/* Convert the internal certificate type to an openssl.cnf section name */
1662
	$cert_type = cert_type_config_section($cert['type']);
1663

    
1664
	/* Reuse lifetime (convert seconds to days) */
1665
	$lifetime = (int) round(($cert_details['validTo_time_t'] - $cert_details['validFrom_time_t']) / 86400);
1666

    
1667
	/* If we are enforcing strict security, then cap the lifetime for server certificates */
1668
	if (($cert_type == 'server') && $strictsecurity &&
1669
	    ($lifetime > $cert_strict_values['max_server_cert_lifetime'])) {
1670
		$lifetime = $cert_strict_values['max_server_cert_lifetime'];
1671
	}
1672

    
1673
	/* Reuse SAN list, or, if empty, add CN as SAN. */
1674
	$sans = str_replace("IP Address", "IP", $cert_details['extensions']['subjectAltName']);
1675
	if (empty($sans)) {
1676
		$sans = cert_add_altname_type($dn['commonName']);
1677
	}
1678

    
1679
	/* Do not setup SANs if the SAN list is empty (e.g. no SAN list and/or
1680
	 * CN cannot be mapped to a valid SAN type) */
1681
	if (!empty($sans)) {
1682
		if ($cert['type'] != 'ca') {
1683
			$cert_type .= '_san';
1684
		}
1685
		/* subjectAltName can be set _only_ via configuration file, so put the
1686
		 * value into the environment where it will be read from the configuration */
1687
		putenv("SAN={$sans}");
1688
	}
1689

    
1690
	/* If we are enforcing strict security, then check the digest against a
1691
	 * blacklist of insecure digest methods. */
1692
	$digest_alg = $cert_details['signatureTypeSN'];
1693
	if ($strictsecurity &&
1694
	    (in_array($digest_alg, $cert_strict_values['digest_blacklist']))) {
1695
		$digest_alg = 'sha256';
1696
	}
1697

    
1698
	/* Validate key type, assume RSA if it cannot be read. */
1699
	if (is_array($key_details) && array_key_exists('type', $key_details)) {
1700
		$private_key_type = $key_details['type'];
1701
	} else {
1702
		$private_key_type = OPENSSL_KEYTYPE_RSA;
1703
	}
1704

    
1705
	/* Setup certificate and key arguments */
1706
	$args = array(
1707
		"x509_extensions" => $cert_type,
1708
		"digest_alg" => $digest_alg,
1709
		"private_key_type" => $private_key_type,
1710
		"encrypt_key" => false);
1711

    
1712
	/* If we are enforcing strict security, then ensure the private key size
1713
	 * is at least 2048 bits or NIST P-256 elliptic curve*/
1714
	$private_key_bits = $key_details['bits'];
1715
	if ($strictsecurity) {
1716
		if (($key_details['type'] == OPENSSL_KEYTYPE_RSA) &&
1717
		    ($private_key_bits < $cert_strict_values['min_private_key_bits'])) {
1718
			$private_key_bits = $cert_strict_values['min_private_key_bits'];
1719
			$reusekey = false;
1720
		} else if (!in_array($key_details['ec']['curve_name'], $curve_compatible_list)) {
1721
			$ec_curve = $cert_strict_values['ec_curve'];
1722
			$reusekey = false;
1723
		}
1724
	}
1725

    
1726
	/* Set key parameters. */
1727
	if ($key_details['type'] ==  OPENSSL_KEYTYPE_RSA) {
1728
		$args['private_key_bits'] = (int)$private_key_bits;
1729
	} else if ($ec_curve) {
1730
		$args['curve_name'] = $ec_curve;
1731
	} else {
1732
		$args['curve_name'] = $key_details['ec']['curve_name'];
1733
	}
1734

    
1735
	/* Make a new key if necessary */
1736
	if (!$res_key || !$reusekey) {
1737
		$res_key = openssl_pkey_new($args);
1738
		if (!$res_key) {
1739
			return false;
1740
		}
1741
	}
1742

    
1743
	/* Create a new CSR from derived parameters and key */
1744
	$res_csr = openssl_csr_new($dn, $res_key, $args);
1745
	/* If the CSR could not be created, bail */
1746
	if (!$res_csr) {
1747
		return false;
1748
	}
1749

    
1750
	if (!empty($cert['caref'])) {
1751
		/* The certificate was signed by a CA, so read the CA details. */
1752
		$ca = & lookup_ca($cert['caref']);
1753
		/* If the referenced CA cannot be found, bail. */
1754
		if (!$ca) {
1755
			return false;
1756
		}
1757
		$ca_str_crt = base64_decode($ca['crt']);
1758
		$ca_str_key = base64_decode($ca['prv']);
1759
		$ca_res_crt = openssl_x509_read($ca_str_crt);
1760
		$ca_res_key = openssl_pkey_get_private(array(0 => $ca_str_key, 1 => ""));
1761
		if (!$ca_res_key) {
1762
			/* If the CA key cannot be read, bail. */
1763
			return false;
1764
		}
1765
		/* If the CA does not have a serial number, assume 0. */
1766
		if (empty($ca['serial'])) {
1767
			$ca['serial'] = 0;
1768
		}
1769
		/* Get the next available CA serial number. */
1770
		$ca_serial = ca_get_next_serial($ca);
1771
	} elseif ($selfsigned) {
1772
		/* For self-signed CAs & certificates, set the CA details to self and
1773
		 * use the key for this entry to sign itself.
1774
		 */
1775
		$ca_res_crt   = null;
1776
		$ca_res_key   = $res_key;
1777
		/* Use random serial from this CA/Self-Signed Cert */
1778
		$ca_serial    = cert_get_random_serial($cert['refid']);
1779
	}
1780

    
1781
	/* Did the user choose to keep the serial? */
1782
	$ca_serial = ($reuseserial) ? cert_get_serial($cert['crt']) : $ca_serial;
1783

    
1784
	/* Sign the CSR */
1785
	$res_crt = openssl_csr_sign($res_csr, $ca_res_crt, $ca_res_key, $lifetime,
1786
				 $args, $ca_serial);
1787
	/* If the CSR could not be signed, bail */
1788
	if (!$res_crt) {
1789
		return false;
1790
	}
1791

    
1792
	/* Attempt to read the key and certificate and if that fails, bail */
1793
	if (!openssl_pkey_export($res_key, $str_key) ||
1794
	    !openssl_x509_export($res_crt, $str_crt)) {
1795
		return false;
1796
	}
1797

    
1798
	/* Load the new certificate string and key into the configuration */
1799
	$cert['crt'] = base64_encode($str_crt);
1800
	$cert['prv'] = base64_encode($str_key);
1801

    
1802
	return true;
1803
}
1804

    
1805
/****f* certs/cert_get_all_services
1806
 * NAME
1807
 *   cert_get_all_services - Locate services using a given certificate
1808
 * INPUTS
1809
 *   $refid: The refid of a certificate to check
1810
 * RESULT
1811
 *   array containing the services which use this certificate, including:
1812
 *     webgui: Present and true if the WebGUI uses this certificate. Unset otherwise.
1813
 *     services: Array of service definitions using this certificate, with:
1814
 *       name: Name of the service
1815
 *       extras: Extra information needed by some services, such as OpenVPN or Captive Portal.
1816
 *     packages: Array containing package names using this certificate.
1817
 ******/
1818

    
1819
function cert_get_all_services($refid) {
1820
	global $config;
1821
	$services = array();
1822
	$services['services'] = array();
1823
	$services['packages'] = array();
1824

    
1825
	/* Only set if true, otherwise leave unset. */
1826
	if (is_webgui_cert($refid)) {
1827
		$services['webgui'] = true;
1828
	}
1829

    
1830
	init_config_arr(array('openvpn', 'openvpn-server'));
1831
	init_config_arr(array('openvpn', 'openvpn-client'));
1832
	/* Find all OpenVPN clients and servers which use this certificate */
1833
	foreach(array('server', 'client') as $mode) {
1834
		foreach ($config['openvpn']["openvpn-{$mode}"] as $ovpn) {
1835
			if ($ovpn['certref'] == $refid) {
1836
				/* OpenVPN instances are restarted individually,
1837
				 * so we need to note the mode and ID. */
1838
				$services['services'][] = array(
1839
					'name' => 'openvpn',
1840
					'extras' => array(
1841
						'vpnmode' => $mode,
1842
						'id' => $ovpn['vpnid']
1843
					)
1844
				);
1845
			}
1846
		}
1847
	}
1848

    
1849
	/* If any one IPsec tunnel uses this certificate then the whole service
1850
	 * needs a bump. */
1851
	init_config_arr(array('ipsec', 'phase1'));
1852
	foreach ($config['ipsec']['phase1'] as $ipsec) {
1853
		if (($ipsec['authentication_method'] == 'cert') &&
1854
		    ($ipsec['certref'] == $refid)) {
1855
			$services['services'][] = array('name' => 'ipsec');
1856
			/* Stop after finding one, no need to search for more. */
1857
			break;
1858
		}
1859
	}
1860

    
1861
	/* Check to see if any HTTPS-enabled Captive Portal zones use this
1862
	 * certificate. */
1863
	init_config_arr(array('captiveportal'));
1864
	foreach ($config['captiveportal'] as $zone => $portal) {
1865
		if (isset($portal['enable']) && isset($portal['httpslogin']) &&
1866
		    ($portal['certref'] == $refid)) {
1867
			/* Captive Portal zones are restarted individually, so
1868
			 * we need to note the zone name. */
1869
			$services['services'][] = array(
1870
				'name' => 'captiveportal',
1871
				'extras' => array(
1872
					'zone' => $zone,
1873
				)
1874
			);
1875
		}
1876
	}
1877

    
1878
	/* Locate any packages using this certificate */
1879
	$pkgcerts = pkg_call_plugins('plugin_certificates', array('type' => 'certificates', 'event' => 'used_certificates'));
1880
	foreach ($pkgcerts as $name => $package) {
1881
		if (is_array($package['certificatelist'][$refid]) &&
1882
		    isset($package['certificatelist'][$refid]) > 0) {
1883
			$services['packages'][] = $name;
1884
		}
1885
	}
1886

    
1887
	return $services;
1888
}
1889

    
1890
/****f* certs/ca_get_all_services
1891
 * NAME
1892
 *   ca_get_all_services - Locate services using a given certificate authority or its decendents
1893
 * INPUTS
1894
 *   $refid: The refid of a certificate authority to check
1895
 * RESULT
1896
 *   array containing the services which use this certificate authority, including:
1897
 *     webgui: Present and true if the WebGUI uses this certificate. Unset otherwise.
1898
 *     services: Array of service definitions using this certificate, with:
1899
 *       name: Name of the service
1900
 *       extras: Extra information needed by some services, such as OpenVPN or Captive Portal.
1901
 *     packages: Array containing package names using this certificate.
1902
 * NOTES
1903
 *   This searches recursively to find entries using this CA as well as intermediate
1904
 *   CAs and certificates signed by this CA, and returns a single set of all services.
1905
 *   This avoids restarting affected services multiple times when there is overlapping
1906
 *   usage.
1907
 ******/
1908
function ca_get_all_services($refid) {
1909
	global $config;
1910
	$services = array();
1911
	$services['services'] = array();
1912

    
1913
	init_config_arr(array('openvpn', 'openvpn-server'));
1914
	init_config_arr(array('openvpn', 'openvpn-client'));
1915
	foreach(array('server', 'client') as $mode) {
1916
		foreach ($config['openvpn']["openvpn-{$mode}"] as $ovpn) {
1917
			if ($ovpn['caref'] == $refid) {
1918
				$services['services'][] = array(
1919
					'name' => 'openvpn',
1920
					'extras' => array(
1921
						'vpnmode' => $mode,
1922
						'id' => $ovpn['vpnid']
1923
					)
1924
				);
1925
			}
1926
		}
1927
	}
1928
	init_config_arr(array('ipsec', 'phase1'));
1929
	foreach ($config['ipsec']['phase1'] as $ipsec) {
1930
		if ($ipsec['certref'] == $refid) {
1931
			break;
1932
		}
1933
	}
1934
	foreach ($config['ipsec']['phase1'] as $ipsec) {
1935
		if (($ipsec['authentication_method'] == 'cert') &&
1936
		    ($ipsec['caref'] == $refid)) {
1937
			$services['services'][] = array('name' => 'ipsec');
1938
			break;
1939
		}
1940
	}
1941

    
1942
	/* Loop through all certs and get their services as well */
1943
	init_config_arr(array('cert'));
1944
	foreach ($config['cert'] as $cert) {
1945
		if ($cert['caref'] == $refid) {
1946
			$services = array_merge_recursive_unique($services, cert_get_all_services($cert['refid']));
1947
		}
1948
	}
1949

    
1950
	/* Look for intermediate certs and services */
1951
	init_config_arr(array('ca'));
1952
	foreach ($config['ca'] as $cert) {
1953
		if ($cert['caref'] == $refid) {
1954
			$services = array_merge_recursive_unique($services, ca_get_all_services($cert['refid']));
1955
		}
1956
	}
1957

    
1958
	return $services;
1959
}
1960

    
1961
/****f* certs/cert_restart_services
1962
 * NAME
1963
 *   cert_restart_services - Restarts services specific to CA/Certificate usage
1964
 * INPUTS
1965
 *   $services: An array of services returned by cert_get_all_services or ca_get_all_services
1966
 * RESULT
1967
 *   Services in the given array are restarted
1968
 *   returns false if the input is invalid
1969
 *   returns true at the end of execution
1970
 ******/
1971

    
1972
function cert_restart_services($services) {
1973
	require_once("service-utils.inc");
1974
	/* If the input is not an array, it is invalid. */
1975
	if (!is_array($services)) {
1976
		return false;
1977
	}
1978

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

    
1982
	/* Restart GUI: */
1983
	if ($services['webgui']) {
1984
		ob_flush();
1985
		flush();
1986
		log_error(sprintf($restart_string, gettext('service'), 'WebGUI'));
1987
		send_event("service restart webgui");
1988
	}
1989

    
1990
	/* Restart other base services: */
1991
	if (is_array($services['services'])) {
1992
		foreach ($services['services'] as $service) {
1993
			switch ($service['name']) {
1994
				case 'openvpn':
1995
					$service_name = "{$service['name']} {$service['extras']['vpnmode']} {$service['extras']['id']}";
1996
					break;
1997
				case 'captiveportal':
1998
					$service_name = "{$service['name']} zone {$service['extras']['zone']}";
1999
					break;
2000
				default:
2001
					$service_name = $service['name'];
2002
			}
2003
			log_error(sprintf($restart_string, gettext('service'), $service_name));
2004
			service_control_restart($service['name'], $service['extras']);
2005
		}
2006
	}
2007

    
2008
	/* Restart Packages: */
2009
	if (is_array($services['packages'])) {
2010
		foreach ($services['packages'] as $service) {
2011
			log_error(sprintf($restart_string, gettext('package'), $service));
2012
			restart_service($service);
2013
		}
2014
	}
2015
	return true;
2016
}
2017

    
2018
/****f* certs/cert_get_lifetime
2019
 * NAME
2020
 *   cert_get_lifetime - Returns the number of days the certificate is valid
2021
 * INPUTS
2022
 *   $untilexpire: Boolean
2023
 *     true: The number of days returned is from now until the certificate expiration.
2024
 *     false (default): The number of days returned is the total lifetime of the certificate.
2025
 * RESULT
2026
 *   Integer number of days in the certificate total or remaining lifetime
2027
 ******/
2028

    
2029
function cert_get_lifetime($cert, $untilexpire = false) {
2030
	/* If the certificate is not valid, bail. */
2031
	if (!is_array($cert) || empty($cert['crt'])) {
2032
		return null;
2033
	}
2034
	/* Read certificate details */
2035
	list($startdate, $enddate) = cert_get_dates($cert['crt'], true, false);
2036

    
2037
	/* If either of the dates are invalid, there is nothing we can do here. */
2038
	if (($startdate === false) || ($enddate === false)) {
2039
		return false;
2040
	}
2041

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

    
2045
	/* Calculate the requested intervals */
2046
	$interval = $startdate->diff($enddate);
2047

    
2048
	/* DateTime diff is always positive, check if we need to negate the result. */
2049
	return ($startdate > $enddate) ? -1 * $interval->days : $interval->days;
2050
}
2051

    
2052
/****f* certs/cert_analyze_lifetime
2053
 * NAME
2054
 *   cert_analyze_lifetime - Analyze a certificate lifetime for expiration notices
2055
 * INPUTS
2056
 *   $expiredays: Number of days until the certificate expires (See cert_get_lifetime())
2057
 * RESULT
2058
 *   An array of two entries:
2059
 *   0/$lrclass: A bootstrap name for use with classes like text-<x>
2060
 *   1/$expstring: A text analysis describing the expiration timeframe.
2061
 ******/
2062

    
2063
function cert_analyze_lifetime($expiredays) {
2064
	global $config, $g;
2065
	/* Number of days at which to warn of expiration. */
2066
	init_config_arr(array('notifications', 'certexpire'));
2067
	if (!isset($config['notifications']['certexpire']['expiredays']) ||
2068
	    empty($config['notifications']['certexpire']['expiredays'])) {
2069
		$warning_days = $g['default_cert_expiredays'];
2070
	} else {
2071
		$warning_days = $config['notifications']['certexpire']['expiredays'];
2072
	}
2073

    
2074
	if ($expiredays > $warning_days) {
2075
		/* Not expiring soon */
2076
		$lrclass = 'normal';
2077
		$expstring = gettext("%d %s until expiration");
2078
	} elseif ($expiredays >= 0) {
2079
		/* Still valid but expiring soon */
2080
		$lrclass = 'warning';
2081
		$expstring = gettext("Expiring soon, in %d %s");
2082
	} else {
2083
		/* Certificate has expired */
2084
		$lrclass = 'danger';
2085
		$expstring = gettext("Expired %d %s ago");
2086
	}
2087
	$days = (abs($expiredays) == 1) ? gettext('day') : gettext('days');
2088
	$expstring = sprintf($expstring, abs($expiredays), $days);
2089
	return array($lrclass, $expstring);
2090
}
2091

    
2092
/****f* certs/cert_print_dates
2093
 * NAME
2094
 *   cert_print_dates - Print the start and end timestamps for the given certificate
2095
 * INPUTS
2096
 *   $cert: CA or Cert entry for which the dates will be printed
2097
 * RESULT
2098
 *   Returns null if the passed entry is invalid
2099
 *   Otherwise, outputs the dates to the user with formatting.
2100
 ******/
2101

    
2102
function cert_print_dates($cert) {
2103
	/* If the certificate is not valid, bail. */
2104
	if (!is_array($cert) || empty($cert['crt'])) {
2105
		return null;
2106
	}
2107
	/* Attempt to extract the dates from the certificate */
2108
	list($startdate, $enddate) = cert_get_dates($cert['crt']);
2109
	/* If either of the timestamps are empty, then do not print anything.
2110
	 * The entry may not be valid or it may just be missing date information */
2111
	if (empty($startdate) || empty($enddate)) {
2112
		return null;
2113
	}
2114
	/* Get the expiration days */
2115
	$expiredays = cert_get_lifetime($cert, true);
2116
	/* Analyze the lifetime value */
2117
	list($lrclass, $expstring) = cert_analyze_lifetime($expiredays);
2118
	/* Output the dates, with a tooltip showing days until expiration, and
2119
	 * a visual indication of warning/expired status. */
2120
	?>
2121
	<br />
2122
	<small>
2123
	<?=gettext("Valid From")?>: <b><?=$startdate ?></b><br />
2124
	<?=gettext("Valid Until")?>:
2125
	<span class="text-<?=$lrclass?>" data-toggle="tooltip" data-placement="bottom" title="<?= $expstring ?>">
2126
	<b><?=$enddate ?></b>
2127
	</span>
2128
	</small>
2129
	<?php
2130
}
2131

    
2132
/****f* certs/cert_print_infoblock
2133
 * NAME
2134
 *   cert_print_infoblock - Print an information block containing certificate details
2135
 * INPUTS
2136
 *   $cert: CA or Cert entry for which the information will be printed
2137
 * RESULT
2138
 *   Returns null if the passed entry is invalid
2139
 *   Otherwise, outputs information to the user with formatting.
2140
 ******/
2141

    
2142
function cert_print_infoblock($cert) {
2143
	/* If the certificate is not valid, bail. */
2144
	if (!is_array($cert) || empty($cert['crt'])) {
2145
		return null;
2146
	}
2147
	/* Variable to hold the formatted information */
2148
	$certextinfo = "";
2149

    
2150
	/* Serial number */
2151
	$cert_details = openssl_x509_parse(base64_decode($cert['crt']));
2152
	if (isset($cert_details['serialNumber']) && (strlen($cert_details['serialNumber']) > 0)) {
2153
		$certextinfo .= '<b>' . gettext("Serial: ") . '</b> ';
2154
		$certextinfo .= htmlspecialchars(cert_escape_x509_chars($cert_details['serialNumber'], true));
2155
		$certextinfo .= '<br/>';
2156
	}
2157

    
2158
	/* Digest type */
2159
	$certsig = cert_get_sigtype($cert['crt']);
2160
	if (is_array($certsig) && !empty($certsig) && !empty($certsig['shortname'])) {
2161
		$certextinfo .= '<b>' . gettext("Signature Digest: ") . '</b> ';
2162
		$certextinfo .= htmlspecialchars(cert_escape_x509_chars($certsig['shortname'], true));
2163
		$certextinfo .= '<br/>';
2164
	}
2165

    
2166
	/* Subject Alternative Name (SAN) list */
2167
	$sans = cert_get_sans($cert['crt']);
2168
	if (is_array($sans) && !empty($sans)) {
2169
		$certextinfo .= '<b>' . gettext("SAN: ") . '</b> ';
2170
		$certextinfo .= htmlspecialchars(implode(', ', cert_escape_x509_chars($sans, true)));
2171
		$certextinfo .= '<br/>';
2172
	}
2173

    
2174
	/* Key usage */
2175
	$purpose = cert_get_purpose($cert['crt']);
2176
	if (is_array($purpose) && !empty($purpose['ku'])) {
2177
		$certextinfo .= '<b>' . gettext("KU: ") . '</b> ';
2178
		$certextinfo .= htmlspecialchars(implode(', ', $purpose['ku']));
2179
		$certextinfo .= '<br/>';
2180
	}
2181

    
2182
	/* Extended key usage */
2183
	if (is_array($purpose) && !empty($purpose['eku'])) {
2184
		$certextinfo .= '<b>' . gettext("EKU: ") . '</b> ';
2185
		$certextinfo .= htmlspecialchars(implode(', ', $purpose['eku']));
2186
		$certextinfo .= '<br/>';
2187
	}
2188

    
2189
	/* OCSP / Must Staple */
2190
	if (cert_get_ocspstaple($cert['crt'])) {
2191
		$certextinfo .= '<b>' . gettext("OCSP: ") . '</b> ';
2192
		$certextinfo .= gettext("Must Staple");
2193
		$certextinfo .= '<br/>';
2194
	}
2195

    
2196
	/* Private key information */
2197
	if (!empty($cert['prv'])) {
2198
		$res_key = openssl_pkey_get_private(base64_decode($cert['prv']));
2199
		$certextinfo .= '<b>' . gettext("Key Type: ") . '</b> ';
2200
		if ($res_key) {
2201
			$key_details = openssl_pkey_get_details($res_key);
2202
			/* Key type (RSA or EC) */
2203
			if ($key_details['type'] == OPENSSL_KEYTYPE_RSA) {
2204
				/* RSA Key size */
2205
				$certextinfo .= 'RSA<br/>';
2206
				$certextinfo .= '<b>' . gettext("Key Size: ") . '</b> ';
2207
				$certextinfo .= $key_details['bits'] . '<br/>';
2208
			} else {
2209
				/* Elliptic curve (EC) key curve name */
2210
				$certextinfo .= 'ECDSA<br/>';
2211
				$curve = cert_get_pkey_curve($cert['prv']);
2212
				if (!empty($curve)) {
2213
					$certextinfo .= '<b>' . gettext("Elliptic curve name:") . ' </b>';
2214
					$certextinfo .= $curve . '<br/>';
2215
				}
2216
			}
2217
		} else {
2218
			$certextinfo .= '<i>' . gettext("Unknown (Key could not be parsed)") . '</i><br/>';
2219
		}
2220
	}
2221

    
2222
	/* Distinguished name (DN) */
2223
	if (!empty($cert_details['name'])) {
2224
		$certextinfo .= '<b>' . gettext("DN: ") . '</b> ';
2225
		/* UTF8 DN support, see https://redmine.pfsense.org/issues/12041 */
2226
		$certdnstring = preg_replace_callback('/\\\\x([0-9A-F]{2})/', function ($a) { return pack('H*', $a[1]); }, $cert_details['name']);
2227
		$certextinfo .= htmlspecialchars(cert_escape_x509_chars($certdnstring, true));
2228
		$certextinfo .= '<br/>';
2229
	}
2230

    
2231
	/* Hash value */
2232
	if (!empty($cert_details['hash'])) {
2233
		$certextinfo .= '<b>' . gettext("Hash: ") . '</b> ';
2234
		$certextinfo .= htmlspecialchars(cert_escape_x509_chars($cert_details['hash'], true));
2235
		$certextinfo .= '<br/>';
2236
	}
2237

    
2238
	/* Subject Key Identifier (SKID) */
2239
	if (is_array($cert_details["extensions"]) && !empty($cert_details["extensions"]["subjectKeyIdentifier"])) {
2240
		$certextinfo .= '<b>' . gettext("Subject Key ID: ") . '</b> ';
2241
		$certextinfo .= htmlspecialchars(cert_escape_x509_chars($cert_details["extensions"]["subjectKeyIdentifier"], true));
2242
		$certextinfo .= '<br/>';
2243
	}
2244

    
2245
	/* Authority Key Identifier (AKID) */
2246
	if (is_array($cert_details["extensions"]) && !empty($cert_details["extensions"]["authorityKeyIdentifier"])) {
2247
		$certextinfo .= '<b>' . gettext("Authority Key ID: ") . '</b> ';
2248
		$certextinfo .= str_replace("\n", '<br/>', htmlspecialchars(cert_escape_x509_chars($cert_details["extensions"]["authorityKeyIdentifier"], true)));
2249
		$certextinfo .= '<br/>';
2250
	}
2251

    
2252
	/* Total Lifetime (days from cert start to end) */
2253
	$lifetime = cert_get_lifetime($cert);
2254
	if ($lifetime !== false) {
2255
		$certextinfo .= '<b>' . gettext("Total Lifetime: ") . '</b> ';
2256
		$certextinfo .= sprintf("%d %s", $lifetime, (abs($lifetime) == 1) ? gettext('day') : gettext('days'));
2257
		$certextinfo .= '<br/>';
2258

    
2259
		/* Lifetime before certificate expires (days from now to end) */
2260
		$expiredays = cert_get_lifetime($cert, true);
2261
		list($lrclass, $expstring) = cert_analyze_lifetime($expiredays);
2262
		$certextinfo .= '<b>' . gettext("Lifetime Remaining: ") . '</b> ';
2263
		$certextinfo .= "<span class=\"text-{$lrclass}\">{$expstring}</span>";
2264
		$certextinfo .= '<br/>';
2265
	}
2266

    
2267
	if ($purpose['ca'] == 'Yes') {
2268
		/* CA Trust store presence */
2269
		$certextinfo .= '<b>' . gettext("Trust Store: ") . '</b> ';
2270
		$certextinfo .= (isset($cert['trust']) && ($cert['trust'] == "enabled")) ? gettext('Included') : gettext('Excluded');
2271
		$certextinfo .= '<br/>';
2272

    
2273
		if (!empty($cert['prv'])) {
2274
			/* CA Next/Randomize Serial */
2275
			$certextinfo .= '<b>' . gettext("Next Serial: ") . '</b> ';
2276
			$certextinfo .= (isset($cert['randomserial']) && ($cert['randomserial'] == "enabled")) ? gettext('Randomized') : $cert['serial'];
2277
			$certextinfo .= '<br/>';
2278
		}
2279
	}
2280

    
2281
	/* Output the infoblock */
2282
	if (!empty($certextinfo)) { ?>
2283
		<div class="infoblock">
2284
		<? print_info_box($certextinfo, 'info', false); ?>
2285
		</div>
2286
	<?php
2287
	}
2288
}
2289

    
2290
/****f* certs/cert_notify_expiring
2291
 * NAME
2292
 *   cert_notify_expiring - Notify admin about expiring certificates
2293
 * INPUTS
2294
 *   None
2295
 * RESULT
2296
 *   File a notice containing expiring certificate information, which is then
2297
 *   logged, displayed in the GUI, and sent via e-mail (if enabled).
2298
 ******/
2299

    
2300
function cert_notify_expiring() {
2301
	global $config;
2302

    
2303
	/* If certificate expiration notifications are disabled, there is nothing to do. */
2304
	init_config_arr(array('notifications', 'certexpire'));
2305
	if ($config['notifications']['certexpire']['enable'] == "disabled") {
2306
		return;
2307
	}
2308

    
2309
	$notifications = array();
2310

    
2311
	/* Check all CA and Cert entries at once */
2312
	init_config_arr(array('ca'));
2313
	init_config_arr(array('cert'));
2314
	$all_certs = array_merge_recursive($config['ca'], $config['cert']);
2315

    
2316
	foreach ($all_certs as $cert) {
2317
		if (empty($cert)) {
2318
			continue;
2319
		}
2320
		/* Proceed only for not revoked certificate if ignore setting enabled */
2321
		if (($config['notifications']['certexpire']['ignore_revoked'] == "enabled") && is_cert_revoked($cert)) {
2322
			continue;
2323
		}
2324
		/* Fetch and analyze expiration */
2325
		$expiredays = cert_get_lifetime($cert, true);
2326
		/* If the result is null, then the lifetime data is missing, so skip the invalid entry. */
2327
		if ($expiredays === null) {
2328
			continue;
2329
		}
2330
		list($lrclass, $expstring) = cert_analyze_lifetime($expiredays);
2331
		/* Only notify if the certificate is expiring soon, or has
2332
		 * already expired */
2333
		if ($lrclass != 'normal') {
2334
			$notify_string = (array_key_exists('serial', $cert)) ? gettext('Certificate Authority') : gettext('Certificate');
2335
			$notify_string .= ": {$cert['descr']} ({$cert['refid']}): {$expstring}";
2336
			$notifications[] = $notify_string;
2337
		}
2338
	}
2339
	if (!empty($notifications)) {
2340
		$message = gettext("The following CA/Certificate entries are expiring:") . "\n" .
2341
			implode("\n", $notifications);
2342
		file_notice("Certificate Expiration", $message, "Certificate Manager");
2343
	}
2344
}
2345

    
2346
/****f* certs/ca_setup_trust_store
2347
 * NAME
2348
 *   ca_setup_trust_store - Setup local CA trust store so that CA entries in the
2349
 *                          configuration may be trusted by the operating system.
2350
 * INPUTS
2351
 *   None
2352
 * RESULT
2353
 *   CAs marked as trusted in the configuration will be setup in the OS trust store.
2354
 ******/
2355

    
2356
function ca_setup_trust_store() {
2357
	global $config;
2358

    
2359
	/* This directory is trusted by OpenSSL on FreeBSD by default */
2360
	$trust_store_directory = '/etc/ssl/certs';
2361

    
2362
	/* Create the directory if it does not already exist, and clean it up if it does. */
2363
	safe_mkdir($trust_store_directory);
2364
	unlink_if_exists("{$trust_store_directory}/*.0");
2365

    
2366
	init_config_arr(array('ca'));
2367
	foreach ($config['ca'] as $ca) {
2368
		/* If the entry is invalid or is not trusted, skip it. */
2369
		if (!is_array($ca) ||
2370
		    empty($ca['crt']) ||
2371
		    !isset($ca['trust']) ||
2372
		    ($ca['trust'] != "enabled")) {
2373
			continue;
2374
		}
2375

    
2376
		ca_setup_capath($ca, $trust_store_directory);
2377
	}
2378
}
2379

    
2380
/****f* certs/ca_setup_capath
2381
 * NAME
2382
 *   ca_setup_capath - Setup CApath structure so that CA chains and related CRLs
2383
 *                     may be written and validated by the -CApath option in
2384
 *                     OpenSSL and other compatible validators.
2385
 * INPUTS
2386
 *   $ca     : A CA (not a refid) to write
2387
 *   $basedir: The directory which will contain the CA structure.
2388
 *   $crl    : A CRL (not a refid) associated with the CA to write.
2389
 *   $refresh: Refresh CRLs -- When true, perform no cleanup and increment suffix
2390
 * RESULT
2391
 *   $basedir is populated with CA and CRL files in a format usable by OpenSSL
2392
 *   CApath. This has the filenames as the CA hash with the CA named <hash>.0
2393
 *   and CRLs named <hash>.r0
2394
 ******/
2395

    
2396
function ca_setup_capath($ca, $basedir, $crl = "", $refresh = false) {
2397
	global $config;
2398
	/* Check for an invalid CA */
2399
	if (!$ca || !is_array($ca)) {
2400
		return false;
2401
	}
2402
	/* Check for an invalid CRL, but do not consider it fatal if it's wrong */
2403
	if (!$crl || !is_array($crl) || ($crl['caref'] != $ca['refid'])) {
2404
		unset($crl);
2405
	}
2406

    
2407
	/* Check for an empty base directory, which is invalid */
2408
	if (empty($basedir)) {
2409
		return false;
2410
	}
2411

    
2412
	/* Ensure that $basedir exists and is a directory */
2413
	if (!is_dir($basedir)) {
2414
		/* If it's a file, remove it, otherwise the directory cannot
2415
		 * be created. */
2416
		if (file_exists($basedir)) {
2417
			@unlink_if_exists($basedir);
2418
		}
2419
		@safe_mkdir($basedir);
2420
	}
2421
	/* Decode the certificate contents */
2422
	$cert_contents = base64_decode($ca['crt']);
2423
	/* Get hash value to use for filename */
2424
	$cert_details = openssl_x509_parse($cert_contents);
2425
	$fprefix = "{$basedir}/{$cert_details['hash']}";
2426

    
2427

    
2428
	$ca_filename = "{$fprefix}.0";
2429
	/* Cleanup old CA/CRL files for this hash */
2430
	@unlink_if_exists($ca_filename);
2431
	/* Write CA to base dir and ensure it has correct permissions. */
2432
	file_put_contents($ca_filename, $cert_contents);
2433
	chmod($ca_filename, 0644);
2434
	chown($ca_filename, 'root');
2435
	chgrp($ca_filename, 'wheel');
2436

    
2437
	/* If there is a CRL, process it. */
2438
	if ($crl) {
2439
		$crl_filename = "{$fprefix}.r";
2440
		if (!$refresh) {
2441
			/* Cleanup old CA/CRL files for this hash */
2442
			@unlink_if_exists("{$crl_filename}*");
2443
		}
2444
		/* Find next suffix based on how many existing files there are (start=0) */
2445
		$crl_filename .= count(glob("{$crl_filename}*"));
2446
		/* Write CRL to base dir and ensure it has correct permissions. */
2447
		file_put_contents($crl_filename, base64_decode($crl['text']));
2448
		chmod($crl_filename, 0644);
2449
		chown($crl_filename, 'root');
2450
		chgrp($crl_filename, 'wheel');
2451
	}
2452

    
2453
	return true;
2454
}
2455

    
2456
/****f* certs/cert_get_pkey_curve
2457
 * NAME
2458
 *   cert_get_pkey_curve - Get the ECDSA curve of a private key
2459
 * INPUTS
2460
 *   $pkey  : The private key to check
2461
 *   $decode: true: base64 decode the string before use, false to use as-is.
2462
 * RESULT
2463
 *   false if the private key is not ECDSA or the private key is not present.
2464
 *   Otherwise, the name of the ECDSA curve used for the private key.
2465
 ******/
2466

    
2467
function cert_get_pkey_curve($pkey, $decode = true) {
2468
	if ($decode) {
2469
		$pkey = base64_decode($pkey);
2470
	}
2471

    
2472
	/* Attempt to read the private key, and if successful, its details. */
2473
	$res_key = openssl_pkey_get_private($pkey);
2474
	if ($res_key) {
2475
		$key_details = openssl_pkey_get_details($res_key);
2476
		/* If this is an EC key, and the curve name is not empty, return
2477
		 * that curve name. */
2478
		if ($key_details['type'] ==  OPENSSL_KEYTYPE_EC) {
2479
			if (!empty($key_details['ec']['curve_name'])) {
2480
				return $key_details['ec']['curve_name'];
2481
			} else {
2482
				return $key_details['ec']['curve_oid'];
2483
			}
2484
		}
2485
	}
2486

    
2487
	/* Either the private key could not be read, or this is not an EC certificate. */
2488
	return false;
2489
}
2490

    
2491
/* Array containing ECDSA curve names allowed in certain contexts. For instance,
2492
 * HTTPS servers and web browsers only support specific curves in TLSv1.3. */
2493
global $cert_curve_compatible, $curve_compatible_list;
2494
$cert_curve_compatible = array(
2495
	/* HTTPS list per TLSv1.3 spec and Mozilla compatibility list */
2496
	'HTTPS' => array('prime256v1', 'secp384r1'),
2497
	/* IPsec/EAP/TLS list per strongSwan docs/issues */
2498
	'IPsec' => array('prime256v1', 'secp384r1', 'secp521r1'),
2499
	/* OpenVPN bug limits usable curves, see https://redmine.pfsense.org/issues/9744 */
2500
	'OpenVPN' => array('prime256v1', 'secp384r1', 'secp521r1'),
2501
);
2502
$curve_compatible_list = array_unique(call_user_func_array('array_merge', $cert_curve_compatible));
2503

    
2504
/****f* certs/cert_build_curve_list
2505
 * NAME
2506
 *   cert_build_curve_list - Build an option list of ECDSA curves with notations
2507
 *                           about known compatible uses.
2508
 * INPUTS
2509
 *   None
2510
 * RESULT
2511
 *   Returns an option list of OpenSSL EC names with added notes. This can be
2512
 *   used directly in form option selection lists.
2513
 ******/
2514

    
2515
function cert_build_curve_list() {
2516
	global $cert_curve_compatible;
2517
	/* Get the default list of curve names */
2518
	$openssl_ecnames = openssl_get_curve_names();
2519
	/* Turn this into a hashed array where key==value */
2520
	$curvelist = array_combine($openssl_ecnames, $openssl_ecnames);
2521
	/* Check all known compatible curves and note matches */
2522
	foreach ($cert_curve_compatible as $consumer => $validcurves) {
2523
		/* $consumer will be a name like HTTPS or IPsec
2524
		 * $validcurves will be an array of curves compatible with the consumer */
2525
		foreach ($validcurves as $vc) {
2526
			/* If the valid curve is present in the curve list, add
2527
			 * a note with the consumer name to the value visible to
2528
			 * the user. */
2529
			if (array_key_exists($vc, $curvelist)) {
2530
				$curvelist[$vc] .= " [{$consumer}]";
2531
			}
2532
		}
2533
	}
2534
	return $curvelist;
2535
}
2536

    
2537
/****f* certs/cert_check_pkey_compatibility
2538
 * NAME
2539
 *   cert_check_pkey_compatibility - Check a private key to see if it can be
2540
 *                                   used in a specific compatible context.
2541
 * INPUTS
2542
 *   $pkey    : The private key to check
2543
 *   $consumer: The consumer name used to validate the curve. See the contents
2544
 *                 of $cert_curve_compatible for details.
2545
 * RESULT
2546
 *   true if the private key may be used in requested area, or if there are no
2547
 *        restrictions.
2548
 *   false if the private key cannot be used.
2549
 ******/
2550

    
2551
function cert_check_pkey_compatibility($pkey, $consumer) {
2552
	global $cert_curve_compatible;
2553

    
2554
	/* Read the curve name from the key */
2555
	$curve = cert_get_pkey_curve($pkey);
2556
	/* Return true if any of the following conditions are met:
2557
	 *  * This is not an EC key
2558
	 *  * The private key cannot be read
2559
	 *  * There are no restrictions
2560
	 *  * The requested curve is compatible */
2561
	return (($curve === false) ||
2562
		!array_key_exists($consumer, $cert_curve_compatible) ||
2563
		in_array($curve, $cert_curve_compatible[$consumer]));
2564
}
2565

    
2566
/****f* certs/cert_build_list
2567
 * NAME
2568
 *   cert_build_list - Build an option list of cert or CA entries, checked
2569
 *                     against a specific consumer name.
2570
 * INPUTS
2571
 *   $type    : 'ca' for certificate authority entries, 'cert' for certificates.
2572
 *   $consumer: The consumer name used to filter certificates out of the result.
2573
 *                 See the contents of $cert_curve_compatible for details.
2574
 *   $selectsource: Then true, outputs in a format usable by select_source in
2575
 *                  packages.
2576
 *   $addnone: When true, a 'none' choice is added to the list.
2577
 * RESULT
2578
 *   Returns an option list of entries with incompatible entries removed. This
2579
 *   can be used directly in form option selection lists.
2580
 * NOTES
2581
 *   This can be expanded in the future to allow for other types of restrictions.
2582
 ******/
2583

    
2584
function cert_build_list($type = 'cert', $consumer = '', $selectsource = false, $addnone = false) {
2585
	global $config;
2586

    
2587
	/* Ensure that $type is valid */
2588
	if (!in_array($type, array('ca', 'cert'))) {
2589
		return array();
2590
	}
2591

    
2592
	/* Initialize arrays */
2593
	init_config_arr(array($type));
2594
	$list = array();
2595

    
2596
	if ($addnone) {
2597
		if ($selectsource) {
2598
			$list[] = array('refid' => 'none', 'descr' => 'None');
2599
		} else {
2600
			$list['none'] = "None";
2601
		}
2602
	}
2603

    
2604
	/* Create a hashed array with the certificate refid as the key and
2605
	 * descriptive name as the value. Exclude incompatible certificates. */
2606
	foreach ($config[$type] as $cert) {
2607
		if (empty($cert['prv']) && ($type == 'cert')) {
2608
			continue;
2609
		} else if (cert_check_pkey_compatibility($cert['prv'], $consumer)) {
2610
			if ($selectsource) {
2611
				$list[] = array('refid' => $cert['refid'],
2612
						'descr' => $cert['descr']);
2613
			} else {
2614
				$list[$cert['refid']] = $cert['descr'];
2615
			}
2616
		}
2617
	}
2618

    
2619
	return $list;
2620
}
2621

    
2622
?>
(7-7/61)