Project

General

Profile

Download (77.9 KB) Statistics
| Branch: | Tag: | Revision:
1
<?php
2
/*
3
 * certs.inc
4
 *
5
 * part of pfSense (https://www.pfsense.org)
6
 * Copyright (c) 2008-2013 BSD Perimeter
7
 * Copyright (c) 2013-2016 Electric Sheep Fencing
8
 * Copyright (c) 2014-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
global $crl_max_lifetime;
58
$crl_max_lifetime = 9999;
59

    
60
function & lookup_ca($refid) {
61
	global $config;
62

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

    
71
	return false;
72
}
73

    
74
function & lookup_ca_by_subject($subject) {
75
	global $config;
76

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

    
86
	return false;
87
}
88

    
89
function & lookup_cert($refid) {
90
	global $config;
91

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

    
100
	return false;
101
}
102

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

    
114
function & lookup_crl($refid) {
115
	global $config;
116

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

    
125
	return false;
126
}
127

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

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

    
164
function ca_import(& $ca, $str, $key = "", $serial = "") {
165
	global $config;
166

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

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

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

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

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

    
227
	// generate a new key pair
228
	$res_key = openssl_pkey_new($args);
229
	if (!$res_key) {
230
		return false;
231
	}
232

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

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

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

    
251
	// return our ca information
252
	$ca['crt'] = base64_encode($str_crt);
253
	$ca['prv'] = base64_encode($str_key);
254
	$ca['serial'] = 0;
255

    
256
	return true;
257
}
258

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

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

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

    
285
	// generate a new key pair
286
	$res_key = openssl_pkey_new($args);
287
	if (!$res_key) {
288
		return false;
289
	}
290

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

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

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

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

    
315
	return true;
316
}
317

    
318
function cert_import(& $cert, $crt_str, $key_str) {
319

    
320
	$cert['crt'] = base64_encode($crt_str);
321
	$cert['prv'] = base64_encode($key_str);
322

    
323
	$subject = cert_get_subject($crt_str, false);
324
	$issuer = cert_get_issuer($crt_str, false);
325

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

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

    
338
	$cert['type'] = $type;
339

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

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

    
355
		/* Get the next available CA serial number. */
356
		$ca_serial = ca_get_next_serial($ca);
357
	}
358

    
359
	$cert_type = cert_type_config_section($type);
360

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

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

    
381
	// generate a new key pair
382
	$res_key = openssl_pkey_new($args);
383
	if (!$res_key) {
384
		return false;
385
	}
386

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

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

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

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

    
415
	// return our certificate information
416
	$cert['crt'] = base64_encode($str_crt);
417
	$cert['prv'] = base64_encode($str_key);
418

    
419
	return true;
420
}
421

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

    
424
	$cert_type = cert_type_config_section($type);
425

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

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

    
447
	// generate a new key pair
448
	$res_key = openssl_pkey_new($args);
449
	if (!$res_key) {
450
		return false;
451
	}
452

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

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

    
465
	// return our request information
466
	$cert['csr'] = base64_encode($str_csr);
467
	$cert['prv'] = base64_encode($str_key);
468

    
469
	return true;
470
}
471

    
472
function csr_sign($csr, & $ca, $duration, $type, $altnames, $digest_alg = "sha256") {
473
	global $config;
474
	$old_err_level = error_reporting(0);
475

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

    
484
	/* Get the next available CA serial number. */
485
	$ca_serial = ca_get_next_serial($ca);
486

    
487
	$cert_type = cert_type_config_section($type);
488

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

    
494
	$args = array(
495
		"x509_extensions" => $cert_type,
496
		"digest_alg" => $digest_alg,
497
		"req_extensions" => "req_{$cert_type}"
498
	);
499

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

    
504
	return $n509;
505
}
506

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

    
514
function csr_get_subject($str_crt, $decode = true) {
515

    
516
	if ($decode) {
517
		$str_crt = base64_decode($str_crt);
518
	}
519

    
520
	$components = openssl_csr_get_subject($str_crt);
521

    
522
	if (empty($components) || !is_array($components)) {
523
		return "unknown";
524
	}
525

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

    
535
	return $subject;
536
}
537

    
538
function cert_get_subject($str_crt, $decode = true) {
539

    
540
	if ($decode) {
541
		$str_crt = base64_decode($str_crt);
542
	}
543

    
544
	$inf_crt = openssl_x509_parse($str_crt);
545
	$components = $inf_crt['subject'];
546

    
547
	if (empty($components) || !is_array($components)) {
548
		return "unknown";
549
	}
550

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

    
565
	return $subject;
566
}
567

    
568
function cert_get_subject_array($crt) {
569
	$str_crt = base64_decode($crt);
570
	$inf_crt = openssl_x509_parse($str_crt);
571
	$components = $inf_crt['subject'];
572

    
573
	if (!is_array($components)) {
574
		return;
575
	}
576

    
577
	$subject_array = array();
578

    
579
	foreach ($components as $a => $v) {
580
		$subject_array[] = array('a' => $a, 'v' => $v);
581
	}
582

    
583
	return $subject_array;
584
}
585

    
586
function cert_get_subject_hash($crt) {
587
	$str_crt = base64_decode($crt);
588
	$inf_crt = openssl_x509_parse($str_crt);
589
	return $inf_crt['subject'];
590
}
591

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

    
604
function cert_get_issuer($str_crt, $decode = true) {
605

    
606
	if ($decode) {
607
		$str_crt = base64_decode($str_crt);
608
	}
609

    
610
	$inf_crt = openssl_x509_parse($str_crt);
611
	$components = $inf_crt['issuer'];
612

    
613
	if (empty($components) || !is_array($components)) {
614
		return "unknown";
615
	}
616

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

    
631
	return $issuer;
632
}
633

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

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

    
693
	return $purpose;
694
}
695

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

    
708
function cert_format_date($validTS, $validTS_time_t, $outputstring = true) {
709
	$now = new DateTime("now");
710

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

    
731
function cert_get_dates($str_crt, $decode = true, $outputstring = true) {
732
	if ($decode) {
733
		$str_crt = base64_decode($str_crt);
734
	}
735
	$crt_details = openssl_x509_parse($str_crt);
736

    
737
	$start = cert_format_date($crt_details['validFrom'], $crt_details['validFrom_time_t'], $outputstring);
738
	$end   = cert_format_date($crt_details['validTo'], $crt_details['validTo_time_t'], $outputstring);
739

    
740
	return array($start, $end);
741
}
742

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

    
755
function cert_get_sigtype($str_crt, $decode = true) {
756
	if ($decode) {
757
		$str_crt = base64_decode($str_crt);
758
	}
759
	$crt_details = openssl_x509_parse($str_crt);
760

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

    
772
	return $signature;
773
}
774

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

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

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

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

    
828
function ca_in_use($caref) {
829
	return (is_openvpn_server_ca($caref) ||
830
		is_openvpn_client_ca($caref) ||
831
		is_ipsec_peer_ca($caref) ||
832
		is_ldap_peer_ca($caref));
833
}
834

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

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

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

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

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

    
900
function is_package_cert($certref) {
901
	$pluginparams = array();
902
	$pluginparams['type'] = 'certificates';
903
	$pluginparams['event'] = 'used_certificates';
904

    
905
	$certificates_used_by_packages = pkg_call_plugins('plugin_certificates', $pluginparams);
906

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

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

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

    
938
function cert_in_use($certref) {
939

    
940
	return (is_webgui_cert($certref) ||
941
		is_user_cert($certref) ||
942
		is_openvpn_server_cert($certref) ||
943
		is_openvpn_client_cert($certref) ||
944
		is_ipsec_cert($certref) ||
945
		is_captiveportal_cert($certref) ||
946
		is_unbound_cert($certref) ||
947
		is_package_cert($certref));
948
}
949

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

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

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

    
984
/* Detect a rollover at 2050 with UTCTime
985
 * See: https://redmine.pfsense.org/issues/9098 */
986
function crl_get_max_lifetime() {
987
	global $crl_max_lifetime;
988
	$max = $crl_max_lifetime;
989

    
990
	$now = new DateTime("now");
991
	$utctime_before_roll = DateTime::createFromFormat('Ymd', '20491231');
992
	if ($date !== false) {
993
		$interval = $now->diff($utctime_before_roll);
994
		$max_days = abs($interval->days);
995
		/* Reduce the max well below the rollover time */
996
		if ($max_days > 1000) {
997
			$max_days -= 1000;
998
		}
999
		return min($max_days, cert_get_max_lifetime());
1000
	}
1001

    
1002
	/* Cannot use date functions, so use a lower default max. */
1003
	return min(7000, cert_get_max_lifetime());
1004
}
1005

    
1006
function crl_create(& $crl, $caref, $name, $serial = 0, $lifetime = 3650) {
1007
	global $config;
1008
	$max_lifetime = crl_get_max_lifetime();
1009
	$ca =& lookup_ca($caref);
1010
	if (!$ca) {
1011
		return false;
1012
	}
1013
	$crl['descr'] = $name;
1014
	$crl['caref'] = $caref;
1015
	$crl['serial'] = $serial;
1016
	$crl['lifetime'] = ($lifetime > $max_lifetime) ? $max_lifetime : $lifetime;
1017
	$crl['cert'] = array();
1018
	$config['crl'][] = $crl;
1019
	return $crl;
1020
}
1021

    
1022
function crl_update(& $crl) {
1023
	require_once('ASN1.php');
1024
	require_once('ASN1_UTF8STRING.php');
1025
	require_once('ASN1_ASCIISTRING.php');
1026
	require_once('ASN1_BITSTRING.php');
1027
	require_once('ASN1_BOOL.php');
1028
	require_once('ASN1_GENERALTIME.php');
1029
	require_once('ASN1_INT.php');
1030
	require_once('ASN1_ENUM.php');
1031
	require_once('ASN1_NULL.php');
1032
	require_once('ASN1_OCTETSTRING.php');
1033
	require_once('ASN1_OID.php');
1034
	require_once('ASN1_SEQUENCE.php');
1035
	require_once('ASN1_SET.php');
1036
	require_once('ASN1_SIMPLE.php');
1037
	require_once('ASN1_TELETEXSTRING.php');
1038
	require_once('ASN1_UTCTIME.php');
1039
	require_once('OID.php');
1040
	require_once('X509.php');
1041
	require_once('X509_CERT.php');
1042
	require_once('X509_CRL.php');
1043

    
1044
	global $config;
1045
	$max_lifetime = crl_get_max_lifetime();
1046
	$ca =& lookup_ca($crl['caref']);
1047
	if (!$ca) {
1048
		return false;
1049
	}
1050
	// If we have text but no certs, it was imported and cannot be updated.
1051
	if (($crl["method"] != "internal") && (!empty($crl['text']) && empty($crl['cert']))) {
1052
		return false;
1053
	}
1054
	$crl['serial']++;
1055
	$ca_cert = \Ukrbublik\openssl_x509_crl\X509::pem2der(base64_decode($ca['crt']));
1056
	$ca_pkey = openssl_pkey_get_private(base64_decode($ca['prv']));
1057

    
1058
	$crlconf = array(
1059
		'no' => $crl['serial'],
1060
		'version' => 2,
1061
		'days' => ($crl['lifetime'] > $max_lifetime) ? $max_lifetime : $crl['lifetime'],
1062
		'alg' => OPENSSL_ALGO_SHA1,
1063
		'revoked' => array()
1064
	);
1065

    
1066
	if (is_array($crl['cert']) && (count($crl['cert']) > 0)) {
1067
		foreach ($crl['cert'] as $cert) {
1068
			/* Determine the serial number to revoke */
1069
			if (isset($cert['serial'])) {
1070
				$serial = $cert['serial'];
1071
			} elseif (isset($cert['crt'])) {
1072
				$serial = cert_get_serial($cert['crt'], true);
1073
			} else {
1074
				continue;
1075
			}
1076
			$crlconf['revoked'][] = array(
1077
				'serial' => $serial,
1078
				'rev_date' => $cert['revoke_time'],
1079
				'reason' => ($cert['reason'] == -1) ? null : (int) $cert['reason'],
1080
			);
1081
		}
1082
	}
1083

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

    
1087
	return $crl['text'];
1088
}
1089

    
1090
function cert_revoke($cert, & $crl, $reason = OCSP_REVOKED_STATUS_UNSPECIFIED) {
1091
	global $config;
1092
	if (is_cert_revoked($cert, $crl['refid'])) {
1093
		return true;
1094
	}
1095
	// If we have text but no certs, it was imported and cannot be updated.
1096
	if (!is_crl_internal($crl)) {
1097
		return false;
1098
	}
1099

    
1100
	if (!is_array($cert)) {
1101
		/* If passed a not an array but a serial string, set it up as an
1102
		 * array with the serial number defined */
1103
		$rcert = array();
1104
		$rcert['serial'] = $cert;
1105
	} else {
1106
		/* If passed a certificate entry, read out the serial and store
1107
		 * it separately. */
1108
		$rcert = $cert;
1109
		$rcert['serial'] = cert_get_serial($cert['crt']);
1110
	}
1111
	$rcert['reason'] = $reason;
1112
	$rcert['revoke_time'] = time();
1113
	$crl['cert'][] = $rcert;
1114
	crl_update($crl);
1115
	return true;
1116
}
1117

    
1118
function cert_unrevoke($cert, & $crl) {
1119
	global $config;
1120
	if (!is_crl_internal($crl)) {
1121
		return false;
1122
	}
1123

    
1124
	$serial = crl_get_entry_serial($cert);
1125

    
1126
	foreach ($crl['cert'] as $id => $rcert) {
1127
		/* Check for a match by refid, name, or serial number */
1128
		if (($rcert['refid'] == $cert['refid']) ||
1129
		    ($rcert['descr'] == $cert['descr']) ||
1130
		    (crl_get_entry_serial($rcert) == $serial)) {
1131
			unset($crl['cert'][$id]);
1132
			if (count($crl['cert']) == 0) {
1133
				// Protect against accidentally switching the type to imported, for older CRLs
1134
				if (!isset($crl['method'])) {
1135
					$crl['method'] = "internal";
1136
				}
1137
				crl_update($crl);
1138
			} else {
1139
				crl_update($crl);
1140
			}
1141
			return true;
1142
		}
1143
	}
1144
	return false;
1145
}
1146

    
1147
/* Compare two certificates to see if they match. */
1148
function cert_compare($cert1, $cert2) {
1149
	/* Ensure two certs are identical by first checking that their issuers match, then
1150
		subjects, then serial numbers, and finally the moduli. Anything less strict
1151
		could accidentally count two similar, but different, certificates as
1152
		being identical. */
1153
	$c1 = base64_decode($cert1['crt']);
1154
	$c2 = base64_decode($cert2['crt']);
1155
	if ((cert_get_issuer($c1, false) == cert_get_issuer($c2, false)) &&
1156
	    (cert_get_subject($c1, false) == cert_get_subject($c2, false)) &&
1157
	    (cert_get_serial($c1, false) == cert_get_serial($c2, false)) &&
1158
	    (cert_get_publickey($c1, false) == cert_get_publickey($c2, false))) {
1159
		return true;
1160
	}
1161
	return false;
1162
}
1163

    
1164
/****f* certs/crl_get_entry_serial
1165
 * NAME
1166
 *   crl_get_entry_serial - Take a CRL entry and determine the associated serial
1167
 * INPUTS
1168
 *   $entry: CRL certificate list entry to inspect, or serial string
1169
 * RESULT
1170
 *   The requested serial string, if present, or null if it cannot be determined.
1171
 ******/
1172

    
1173
function crl_get_entry_serial($entry) {
1174
	/* Check the passed entry several ways to determine the serial */
1175
	if (isset($entry['serial']) && (strlen($entry['serial']) > 0)) {
1176
		/* Entry is an array with a viable 'serial' element */
1177
		return $entry['serial'];
1178
	} elseif (isset($entry['crt'])) {
1179
		/* Entry is an array with certificate text which can be used to
1180
		 * determine the serial */
1181
		return cert_get_serial($entry['crt'], true);
1182
	} elseif (cert_validate_serial($entry, false, true) != null) {
1183
		/* Entry is a valid serial string */
1184
		return $entry;
1185
	}
1186
	/* Unable to find or determine a serial number */
1187
	return null;
1188
}
1189

    
1190
/****f* certs/cert_validate_serial
1191
 * NAME
1192
 *   cert_validate_serial - Validate a given string to test if it can be used as
1193
 *                          a certificate serial.
1194
 * INPUTS
1195
 *   $serial     : Serial number string to test
1196
 *   $returnvalue: Whether to return the parsed value or true/false
1197
 * RESULT
1198
 *   If $returnvalue is true, then the parsed ASN.1 integer value string for
1199
 *     $serial or null if invalid
1200
 *   If $returnvalue is false, then true/false based on whether or not $serial
1201
 *     is valid.
1202
 ******/
1203

    
1204
function cert_validate_serial($serial, $returnvalue = false, $allowlarge = false) {
1205
	require_once('ASN1.php');
1206
	require_once('ASN1_INT.php');
1207
	/* The ASN.1 parsing function will throw an exception if the value is
1208
	 * invalid, so take advantage of that to catch other error as well. */
1209
	try {
1210
		/* If the serial is not a string, then do not bother with
1211
		 * further tests. */
1212
		if (!is_string($serial)) {
1213
			throw new Exception('Not a string');
1214
		}
1215
		/* Process a hex string */
1216
		if ((substr($serial, 0, 2) == '0x')) {
1217
			/* If the string is hex, then it must contain only
1218
			 * valid hex digits */
1219
			if (!ctype_xdigit(substr($serial, 2))) {
1220
				throw new Exception('Not a valid hex string');
1221
			}
1222
			/* Convert to decimal */
1223
			$serial = base_convert($serial, 16, 10);
1224
		}
1225

    
1226
		/* Unfortunately, PHP openssl_csr_sign() limits serial numbers to a
1227
		 * PHP integer, so we cannot generate large numbers up to the maximum
1228
		 * allowed ASN.1 size (2^159). We are limited to PHP_INT_MAX --
1229
		 * As such, numbers larger than that limit should be rejected */
1230
		if ($serial > PHP_INT_MAX) {
1231
			throw new Exception('Serial too large for PHP OpenSSL');
1232
		}
1233

    
1234
		/* Attempt to create an ASN.1 integer, if it fails, an exception will be thrown */
1235
		$asn1serial = new \Ukrbublik\openssl_x509_crl\ASN1_INT( $serial );
1236
		return ($returnvalue) ? $asn1serial->content : true;
1237
	} catch (Exception $ex) {
1238
		/* No mattter what the error is, return null or false depending
1239
		 * on what was requested. */
1240
		return ($returnvalue) ? null : false;
1241
	}
1242
}
1243

    
1244
/****f* certs/cert_generate_serial
1245
 * NAME
1246
 *   cert_generate_serial - Generate a random positive integer usable as a
1247
 *                          certificate serial number
1248
 * INPUTS
1249
 *   None
1250
 * RESULT
1251
 *   Integer representing an ASN.1 compatible certificate serial number.
1252
 ******/
1253

    
1254
function cert_generate_serial() {
1255
	/* Use a separate function for this to make it easier to use a better
1256
	 * randomization function in the future. */
1257

    
1258
	/* Unfortunately, PHP openssl_csr_sign() limits serial numbers to a
1259
	 * PHP integer, so we cannot generate large numbers up to the maximum
1260
	 * allowed ASN.1 size (2^159). We are limited to PHP_INT_MAX */
1261
	return random_int(1, PHP_INT_MAX);
1262
}
1263

    
1264
/****f* certs/ca_has_serial
1265
 * NAME
1266
 *   ca_has_serial - Check if a serial number is used by any certificate in a given CA
1267
 * INPUTS
1268
 *   $ca    : Certificate Authority to check
1269
 *   $serial: Serial number to check
1270
 * RESULT
1271
 *   true if the serial number is in use by a certificate issued by this CA,
1272
 *   false otherwise.
1273
 ******/
1274

    
1275
function ca_has_serial($caref, $serial) {
1276
	global $config;
1277

    
1278
	/* Check certs first -- more likely to find a hit */
1279
	foreach ($config['cert'] as $cert) {
1280
		if (($cert['caref'] == $caref) &&
1281
		    (cert_get_serial($cert['crt'], true) == $serial)) {
1282
			/* If this certificate is issued by the CA in question
1283
			 * and has a matching serial number, stop processing
1284
			 * and return true. */
1285
			return true;
1286
		}
1287
	}
1288

    
1289
	/* Check the CA iteself */
1290
	$this_ca = lookup_ca($caref);
1291
	$this_serial = cert_get_serial($this_ca['crt'], true);
1292
	if ($serial == $this_serial) {
1293
		return true;
1294
	}
1295

    
1296
	/* Check other CAs for a match (intermediates signed by this CA) */
1297
	foreach ($config['ca'] as $ca) {
1298
		if (($ca['caref'] == $caref) &&
1299
		    (cert_get_serial($ca['crt'], true) == $serial)) {
1300
			/* If this CA is issued by the CA in question
1301
			 * and has a matching serial number, stop processing
1302
			 * and return true. */
1303
			return true;
1304
		}
1305
	}
1306

    
1307
	return false;
1308
}
1309

    
1310
/****f* certs/cert_get_random_serial
1311
 * NAME
1312
 *   cert_get_random_serial - Generate a random certificate serial unique in a CA
1313
 * INPUTS
1314
 *   $caref : Certificate Authority refid to test for serial uniqueness.
1315
 * RESULT
1316
 *   Random serial number which is not in use by any known certificate in a CA
1317
 ******/
1318

    
1319
function cert_get_random_serial($caref = '') {
1320
	/* Number of attempts to generate a usable serial. Multiple attempts
1321
	 *  are necessary to ensure that the number is usable and unique. */
1322
	$attempts = 10;
1323

    
1324
	/* Default value, -1 indicates an error */
1325
	$serial = -1;
1326

    
1327
	for ($i=0; $i < $attempts; $i++) {
1328
		/* Generate a random serial */
1329
		$serial = cert_generate_serial();
1330
		/* Check that the serial number is usable and unique:
1331
		 *  * Cannot be 0
1332
		 *  * Must be a valid ASN.1 serial number
1333
		 *  * Cannot be used by any other certificate on this CA */
1334
		if (($serial != 0) &&
1335
		    cert_validate_serial($serial) &&
1336
		    !ca_has_serial($caref, $serial)) {
1337
			/* If all conditions are met, we have a good serial, so stop. */
1338
			break;
1339
		}
1340
	}
1341
	return $serial;
1342
}
1343

    
1344
/****f* certs/ca_get_next_serial
1345
 * NAME
1346
 *   ca_get_next_serial - Get the next available serial number for a CA
1347
 * INPUTS
1348
 *   $ca: Reference to a CA entry
1349
 * RESULT
1350
 *   A randomized serial number (if enabled for a CA) or the next sequential value.
1351
 ******/
1352

    
1353
function ca_get_next_serial(& $ca) {
1354
	$ca_serial = null;
1355
	/* Get a randomized serial if enabled */
1356
	if ($ca['randomserial'] == 'enabled') {
1357
		$ca_serial = cert_get_random_serial($ca['refid']);
1358
	}
1359
	/* Initialize the sequential serial to be safe */
1360
	if (empty($ca['serial'])) {
1361
		$ca['serial'] = 0;
1362
	}
1363
	/* If not using a randomized serial, or randomizing the serial
1364
	 * failed, then fall back to sequential serials. */
1365
	return (empty($ca_serial) || ($ca_serial == -1)) ? ++$ca['serial'] : $ca_serial;
1366
}
1367

    
1368
/****f* certs/crl_contains_cert
1369
 * NAME
1370
 *   crl_contains_cert - Check if a certificate is present in a CRL
1371
 * INPUTS
1372
 *   $crl : CRL to check
1373
 *   $cert: Certificate to test
1374
 * RESULT
1375
 *   true if the CRL contains the certificate, false otherwise
1376
 ******/
1377

    
1378
function crl_contains_cert($crl, $cert) {
1379
	global $config;
1380
	if (!is_array($config['crl']) ||
1381
	    !is_array($crl['cert'])) {
1382
		return false;
1383
	}
1384

    
1385
	/* Find the issuer of this CRL */
1386
	$ca = lookup_ca($crl['caref']);
1387
	$crlissuer = is_array($cert) ? cert_get_subject($ca['crt']) : null;
1388
	$serial = crl_get_entry_serial($cert);
1389

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

    
1393
	/* If the requested certificate was not issued by the
1394
	 * same CA as the CRL, then do not bother checking this
1395
	 * CRL. */
1396
	if ($issuer != $crlissuer) {
1397
		return false;
1398
	}
1399

    
1400
	/* Check CRL entries to see if the certificate serial is revoked */
1401
	foreach ($crl['cert'] as $rcert) {
1402
		if (crl_get_entry_serial($rcert) == $serial) {
1403
			return true;
1404
		}
1405
	}
1406

    
1407
	/* Certificate was not found in the CRL */
1408
	return false;
1409
}
1410

    
1411
/****f* certs/is_cert_revoked
1412
 * NAME
1413
 *   is_cert_revoked - Test if a given certificate or serial is revoked
1414
 * INPUTS
1415
 *   $cert  : Certificate entry or serial number to test
1416
 *   $crlref: CRL to check for revoked entries, or empty to check all CRLs
1417
 * RESULT
1418
 *   true if the requested entry is revoked
1419
 *   false if the requested entry is not revoked
1420
 ******/
1421

    
1422
function is_cert_revoked($cert, $crlref = "") {
1423
	global $config;
1424
	if (!is_array($config['crl'])) {
1425
		return false;
1426
	}
1427

    
1428
	if (!empty($crlref)) {
1429
		$crl = lookup_crl($crlref);
1430
		return crl_contains_cert($crl, $cert);
1431
	} else {
1432
		if (!is_array($cert)) {
1433
			/* If passed a serial, then it cannot be definitively
1434
			 * matched in this way since we do not know the CA
1435
			 * associated with the bare serial. */
1436
			return null;
1437
		}
1438

    
1439
		/* Check every CRL in the configuration for a match */
1440
		foreach ($config['crl'] as $crl) {
1441
			if (!is_array($crl['cert'])) {
1442
				continue;
1443
			}
1444
			if (crl_contains_cert($crl, $cert)) {
1445
				return true;
1446
			}
1447
		}
1448
	}
1449
	return false;
1450
}
1451

    
1452
function is_openvpn_server_crl($crlref) {
1453
	global $config;
1454
	if (!is_array($config['openvpn']['openvpn-server'])) {
1455
		return;
1456
	}
1457
	foreach ($config['openvpn']['openvpn-server'] as $ovpns) {
1458
		if (!empty($ovpns['crlref']) && ($ovpns['crlref'] == $crlref)) {
1459
			return true;
1460
		}
1461
	}
1462
	return false;
1463
}
1464

    
1465
function is_package_crl($crlref) {
1466
	$pluginparams = array();
1467
	$pluginparams['type'] = 'certificates';
1468
	$pluginparams['event'] = 'used_crl';
1469

    
1470
	$certificates_used_by_packages = pkg_call_plugins('plugin_certificates', $pluginparams);
1471

    
1472
	/* Check if any package is using CRL */
1473
	foreach ($certificates_used_by_packages as $name => $package) {
1474
		if (is_array($package['certificatelist'][$crlref]) &&
1475
		    (count($package['certificatelist'][$crlref]) > 0)) {
1476
			return true;
1477
		}
1478
	}
1479
}
1480

    
1481
// Keep this general to allow for future expansion. See cert_in_use() above.
1482
function crl_in_use($crlref) {
1483
	return (is_openvpn_server_crl($crlref) ||
1484
		is_package_crl($crlref));
1485
}
1486

    
1487
function is_crl_internal($crl) {
1488
	return (!(!empty($crl['text']) && empty($crl['cert'])) || ($crl["method"] == "internal"));
1489
}
1490

    
1491
function cert_get_cn($crt, $isref = false) {
1492
	/* If this is a certref, not an actual cert, look up the cert first */
1493
	if ($isref) {
1494
		$cert = lookup_cert($crt);
1495
		/* If it's not a valid cert, bail. */
1496
		if (!(is_array($cert) && !empty($cert['crt']))) {
1497
			return "";
1498
		}
1499
		$cert = $cert['crt'];
1500
	} else {
1501
		$cert = $crt;
1502
	}
1503
	$sub = cert_get_subject_array($cert);
1504
	if (is_array($sub)) {
1505
		foreach ($sub as $s) {
1506
			if (strtoupper($s['a']) == "CN") {
1507
				return $s['v'];
1508
			}
1509
		}
1510
	}
1511
	return "";
1512
}
1513

    
1514
function cert_escape_x509_chars($str, $reverse = false) {
1515
	/* Characters which need escaped when present in x.509 fields.
1516
	 * See https://www.ietf.org/rfc/rfc4514.txt
1517
	 *
1518
	 * The backslash (\) must be listed first in these arrays!
1519
	 */
1520
	$cert_directory_string_special_chars = array('\\', '"', '#', '+', ',', ';', '<', '=', '>');
1521
	$cert_directory_string_special_chars_esc = array('\\\\', '\"', '\#', '\+', '\,', '\;', '\<', '\=', '\>');
1522
	if ($reverse) {
1523
		return str_replace($cert_directory_string_special_chars_esc, $cert_directory_string_special_chars, $str);
1524
	} else {
1525
		/* First unescape and then escape again, to prevent possible double escaping. */
1526
		return str_replace($cert_directory_string_special_chars, $cert_directory_string_special_chars_esc, cert_escape_x509_chars($str, true));
1527
	}
1528
}
1529

    
1530
function cert_add_altname_type($str) {
1531
	$type = "";
1532
	if (is_ipaddr($str)) {
1533
		$type = "IP";
1534
	} elseif (is_hostname($str, true)) {
1535
		$type = "DNS";
1536
	} elseif (is_URL($str)) {
1537
		$type = "URI";
1538
	} elseif (filter_var($str, FILTER_VALIDATE_EMAIL)) {
1539
		$type = "email";
1540
	}
1541
	if (!empty($type)) {
1542
		return "{$type}:" . cert_escape_x509_chars($str);
1543
	} else {
1544
		return null;
1545
	}
1546
}
1547

    
1548
function cert_type_config_section($type) {
1549
	switch ($type) {
1550
		case "ca":
1551
			$cert_type = "v3_ca";
1552
			break;
1553
		case "server":
1554
		case "self-signed":
1555
			$cert_type = "server";
1556
			break;
1557
		default:
1558
			$cert_type = "usr_cert";
1559
			break;
1560
	}
1561
	return $cert_type;
1562
}
1563

    
1564
/****f* certs/is_cert_locally_renewable
1565
 * NAME
1566
 *   is_cert_locally_renewable - Check to see if an existing certificate can be
1567
 *                               renewed by a local internal CA.
1568
 * INPUTS
1569
 *   $cert : The certificate to be tested
1570
 * RESULT
1571
 *   true if the certificate can be locally renewed, false otherwise.
1572
 ******/
1573

    
1574
function is_cert_locally_renewable($cert) {
1575
	/* If there is no certificate or private key string, this entry is either
1576
	 * invalid or cannot be renewed. */
1577
	if (empty($cert['crt']) || empty($cert['prv'])) {
1578
		return false;
1579
	}
1580

    
1581
	/* Get subject and issuer values to test for self-signed state */
1582
	$subj = cert_get_subject($cert['crt']);
1583
	$issuer = cert_get_issuer($cert['crt']);
1584

    
1585
	/* Lookup CA for this certificate */
1586
	$ca = array();
1587
	if (!empty($cert['caref'])) {
1588
		$ca = lookup_ca($cert['caref']);
1589
	}
1590

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

    
1596
/* Strict certificate requirements based on
1597
 * https://redmine.pfsense.org/issues/9825
1598
 */
1599
global $cert_strict_values;
1600
$cert_strict_values = array(
1601
	'max_server_cert_lifetime' => 398,
1602
	'digest_blacklist' => array('md4', 'RSA-MD4',  'md5', 'RSA-MD5', 'md5-sha1',
1603
					'mdc2', 'RSA-MDC2', 'sha1', 'RSA-SHA1',
1604
					'RSA-SHA1-2', 'sha224', 'RSA-SHA224'),
1605
	'min_private_key_bits' => 2048,
1606
	'ec_curve' => 'prime256v1',
1607
);
1608

    
1609
/****f* certs/cert_renew
1610
 * NAME
1611
 *   cert_renew - Renew an existing internal CA or certificate
1612
 * INPUTS
1613
 *   $cert : The entry to be renewed (used as a reference so it can be altered directly)
1614
 *   $reusekey : Whether or not to reuse the existing key for the certificate
1615
 *      true: Reuse the existing key (Default)
1616
 *      false: Generate a new key based on current (or enforced minimum) parameters
1617
 *   $strictsecurity : Whether or not to enforce stricter security for specific attributes
1618
 *      true: Enforce maximum lifetime for server certs, minimum digest type, and
1619
 *            minimum private key size. See https://redmine.pfsense.org/issues/9825
1620
 *      false: Use existing values as-is (Default).
1621
 * RESULT
1622
 *   true if successful, false if failure.
1623
 * NOTES
1624
 *   See https://redmine.pfsense.org/issues/9842 for more information on behavior.
1625
 *   Does NOT run write_config(), that must be performed by the caller.
1626
 ******/
1627

    
1628
function cert_renew(& $cert, $reusekey = true, $strictsecurity = false, $reuseserial = false) {
1629
	global $cert_strict_values, $cert_curve_compatible, $curve_compatible_list;
1630

    
1631
	/* If there is no certificate or private key string, this entry is either
1632
	 *  invalid or cannot be renewed by this function. */
1633
	if (empty($cert['crt']) || empty($cert['prv'])) {
1634
		return false;
1635
	}
1636

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

    
1640
	/* No details, must not be valid in some way */
1641
	if (!array($cert_details) || empty($cert_details)) {
1642
		return false;
1643
	}
1644

    
1645
	$subj = cert_get_subject($cert['crt']);
1646
	$issuer = cert_get_issuer($cert['crt']);
1647
	$purpose = cert_get_purpose($cert['crt']);
1648

    
1649
	$res_key = openssl_pkey_get_private(base64_decode($cert['prv']));
1650
	$key_details = openssl_pkey_get_details($res_key);
1651

    
1652
	/* Form a new Distinguished Name from the existing values.
1653
	 * Note: Deprecated/unsupported DN fields may not be carried forward, but
1654
	 *       may be preserved to avoid altering a subject.
1655
	 */
1656
	$subject_map = array(
1657
		'CN' => 'commonName',
1658
		'C' => 'countryName',
1659
		'ST' => 'stateOrProvinceName',
1660
		'L' => 'localityName',
1661
		'O' => 'organizationName',
1662
		'OU' => 'organizationalUnitName',
1663
		'emailAddress' => 'emailAddress', /* deprecated, but commonly found in older entries */
1664
	);
1665
	$dn = array();
1666
	/* This is necessary to ensure the order of subject components is
1667
	 * identical on the old and new certificate. */
1668
	foreach ($cert_details['subject'] as $p => $v) {
1669
		if (array_key_exists($p, $subject_map)) {
1670
			$dn[$subject_map[$p]] = $v;
1671
		}
1672
	}
1673

    
1674
	/* Test for self-signed or signed by a CA */
1675
	$selfsigned = ($subj == $issuer);
1676

    
1677
	/* Determine the type if it is not specified directly */
1678
	if (array_key_exists('serial', $cert)) {
1679
		/* If a serial value is present, this must be a CA */
1680
		$cert['type'] = 'ca';
1681
	} elseif (empty($cert['type'])) {
1682
		/* Automatically determine certificate type if unset based on purpose value */
1683
		$cert['type'] = ($purpose['server'] == 'Yes') ? 'server' : 'user';
1684
	}
1685

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

    
1689
	/* Reuse lifetime (convert seconds to days) */
1690
	$lifetime = (int) round(($cert_details['validTo_time_t'] - $cert_details['validFrom_time_t']) / 86400);
1691

    
1692
	/* If we are enforcing strict security, then cap the lifetime for server certificates */
1693
	if (($cert_type == 'server') && $strictsecurity &&
1694
	    ($lifetime > $cert_strict_values['max_server_cert_lifetime'])) {
1695
		$lifetime = $cert_strict_values['max_server_cert_lifetime'];
1696
	}
1697

    
1698
	/* Reuse SAN list, or, if empty, add CN as SAN. */
1699
	$sans = str_replace("IP Address", "IP", $cert_details['extensions']['subjectAltName']);
1700
	if (empty($sans)) {
1701
		$sans = cert_add_altname_type($dn['commonName']);
1702
	}
1703

    
1704
	/* Do not setup SANs if the SAN list is empty (e.g. no SAN list and/or
1705
	 * CN cannot be mapped to a valid SAN type) */
1706
	if (!empty($sans)) {
1707
		if ($cert['type'] != 'ca') {
1708
			$cert_type .= '_san';
1709
		}
1710
		/* subjectAltName can be set _only_ via configuration file, so put the
1711
		 * value into the environment where it will be read from the configuration */
1712
		putenv("SAN={$sans}");
1713
	}
1714

    
1715
	/* Determine current digest algorithm. */
1716
	$digest_alg = strtolower($cert_details['signatureTypeSN']);
1717

    
1718
	/* Check for and remove unnecessary ECDSA digest prefix
1719
	 * See https://redmine.pfsense.org/issues/13437 */
1720
	$ecdsa_prefix = 'ecdsa-with-';
1721
	if (substr($digest_alg, 0, strlen($ecdsa_prefix)) == $ecdsa_prefix) {
1722
		$digest_alg = substr($digest_alg, strlen($ecdsa_prefix));
1723
	}
1724

    
1725
	/* If we are enforcing strict security, then check the digest against a
1726
	 * blacklist of insecure digest methods. */
1727
	if ($strictsecurity &&
1728
	    (in_array($digest_alg, $cert_strict_values['digest_blacklist']))) {
1729
		$digest_alg = 'sha256';
1730
	}
1731

    
1732
	/* Validate key type, assume RSA if it cannot be read. */
1733
	if (is_array($key_details) && array_key_exists('type', $key_details)) {
1734
		$private_key_type = $key_details['type'];
1735
	} else {
1736
		$private_key_type = OPENSSL_KEYTYPE_RSA;
1737
	}
1738

    
1739
	/* Setup certificate and key arguments */
1740
	$args = array(
1741
		"x509_extensions" => $cert_type,
1742
		"digest_alg" => $digest_alg,
1743
		"private_key_type" => $private_key_type,
1744
		"encrypt_key" => false);
1745

    
1746
	/* If we are enforcing strict security, then ensure the private key size
1747
	 * is at least 2048 bits or NIST P-256 elliptic curve*/
1748
	$private_key_bits = $key_details['bits'];
1749
	if ($strictsecurity) {
1750
		if (($key_details['type'] == OPENSSL_KEYTYPE_RSA) &&
1751
		    ($private_key_bits < $cert_strict_values['min_private_key_bits'])) {
1752
			$private_key_bits = $cert_strict_values['min_private_key_bits'];
1753
			$reusekey = false;
1754
		} else if (!in_array($key_details['ec']['curve_name'], $curve_compatible_list)) {
1755
			$ec_curve = $cert_strict_values['ec_curve'];
1756
			$reusekey = false;
1757
		}
1758
	}
1759

    
1760
	/* Set key parameters. */
1761
	if ($key_details['type'] ==  OPENSSL_KEYTYPE_RSA) {
1762
		$args['private_key_bits'] = (int)$private_key_bits;
1763
	} else if ($ec_curve) {
1764
		$args['curve_name'] = $ec_curve;
1765
	} else {
1766
		$args['curve_name'] = $key_details['ec']['curve_name'];
1767
	}
1768

    
1769
	/* Make a new key if necessary */
1770
	if (!$res_key || !$reusekey) {
1771
		$res_key = openssl_pkey_new($args);
1772
		if (!$res_key) {
1773
			return false;
1774
		}
1775
	}
1776

    
1777
	/* Create a new CSR from derived parameters and key */
1778
	$res_csr = openssl_csr_new($dn, $res_key, $args);
1779
	/* If the CSR could not be created, bail */
1780
	if (!$res_csr) {
1781
		return false;
1782
	}
1783

    
1784
	if (!empty($cert['caref'])) {
1785
		/* The certificate was signed by a CA, so read the CA details. */
1786
		$ca = & lookup_ca($cert['caref']);
1787
		/* If the referenced CA cannot be found, bail. */
1788
		if (!$ca) {
1789
			return false;
1790
		}
1791
		$ca_str_crt = base64_decode($ca['crt']);
1792
		$ca_str_key = base64_decode($ca['prv']);
1793
		$ca_res_crt = openssl_x509_read($ca_str_crt);
1794
		$ca_res_key = openssl_pkey_get_private(array(0 => $ca_str_key, 1 => ""));
1795
		if (!$ca_res_key) {
1796
			/* If the CA key cannot be read, bail. */
1797
			return false;
1798
		}
1799
		/* If the CA does not have a serial number, assume 0. */
1800
		if (empty($ca['serial'])) {
1801
			$ca['serial'] = 0;
1802
		}
1803
		/* Get the next available CA serial number. */
1804
		$ca_serial = ca_get_next_serial($ca);
1805
	} elseif ($selfsigned) {
1806
		/* For self-signed CAs & certificates, set the CA details to self and
1807
		 * use the key for this entry to sign itself.
1808
		 */
1809
		$ca_res_crt   = null;
1810
		$ca_res_key   = $res_key;
1811
		/* Use random serial from this CA/Self-Signed Cert */
1812
		$ca_serial    = cert_get_random_serial($cert['refid']);
1813
	}
1814

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

    
1818
	/* Sign the CSR */
1819
	$res_crt = openssl_csr_sign($res_csr, $ca_res_crt, $ca_res_key, $lifetime,
1820
				 $args, $ca_serial);
1821
	/* If the CSR could not be signed, bail */
1822
	if (!$res_crt) {
1823
		return false;
1824
	}
1825

    
1826
	/* Attempt to read the key and certificate and if that fails, bail */
1827
	if (!openssl_pkey_export($res_key, $str_key) ||
1828
	    !openssl_x509_export($res_crt, $str_crt)) {
1829
		return false;
1830
	}
1831

    
1832
	/* Load the new certificate string and key into the configuration */
1833
	$cert['crt'] = base64_encode($str_crt);
1834
	$cert['prv'] = base64_encode($str_key);
1835

    
1836
	return true;
1837
}
1838

    
1839
/****f* certs/cert_get_all_services
1840
 * NAME
1841
 *   cert_get_all_services - Locate services using a given certificate
1842
 * INPUTS
1843
 *   $refid: The refid of a certificate to check
1844
 * RESULT
1845
 *   array containing the services which use this certificate, including:
1846
 *     webgui: Present and true if the WebGUI uses this certificate. Unset otherwise.
1847
 *     services: Array of service definitions using this certificate, with:
1848
 *       name: Name of the service
1849
 *       extras: Extra information needed by some services, such as OpenVPN or Captive Portal.
1850
 *     packages: Array containing package names using this certificate.
1851
 ******/
1852

    
1853
function cert_get_all_services($refid) {
1854
	global $config;
1855
	$services = array();
1856
	$services['services'] = array();
1857
	$services['packages'] = array();
1858

    
1859
	/* Only set if true, otherwise leave unset. */
1860
	if (is_webgui_cert($refid)) {
1861
		$services['webgui'] = true;
1862
	}
1863

    
1864
	init_config_arr(array('openvpn', 'openvpn-server'));
1865
	init_config_arr(array('openvpn', 'openvpn-client'));
1866
	/* Find all OpenVPN clients and servers which use this certificate */
1867
	foreach(array('server', 'client') as $mode) {
1868
		foreach ($config['openvpn']["openvpn-{$mode}"] as $ovpn) {
1869
			if ($ovpn['certref'] == $refid) {
1870
				/* OpenVPN instances are restarted individually,
1871
				 * so we need to note the mode and ID. */
1872
				$services['services'][] = array(
1873
					'name' => 'openvpn',
1874
					'extras' => array(
1875
						'vpnmode' => $mode,
1876
						'id' => $ovpn['vpnid']
1877
					)
1878
				);
1879
			}
1880
		}
1881
	}
1882

    
1883
	/* If any one IPsec tunnel uses this certificate then the whole service
1884
	 * needs a bump. */
1885
	init_config_arr(array('ipsec', 'phase1'));
1886
	foreach ($config['ipsec']['phase1'] as $ipsec) {
1887
		if (($ipsec['authentication_method'] == 'cert') &&
1888
		    ($ipsec['certref'] == $refid)) {
1889
			$services['services'][] = array('name' => 'ipsec');
1890
			/* Stop after finding one, no need to search for more. */
1891
			break;
1892
		}
1893
	}
1894

    
1895
	/* Check to see if any HTTPS-enabled Captive Portal zones use this
1896
	 * certificate. */
1897
	init_config_arr(array('captiveportal'));
1898
	foreach ($config['captiveportal'] as $zone => $portal) {
1899
		if (isset($portal['enable']) && isset($portal['httpslogin']) &&
1900
		    ($portal['certref'] == $refid)) {
1901
			/* Captive Portal zones are restarted individually, so
1902
			 * we need to note the zone name. */
1903
			$services['services'][] = array(
1904
				'name' => 'captiveportal',
1905
				'extras' => array(
1906
					'zone' => $zone,
1907
				)
1908
			);
1909
		}
1910
	}
1911

    
1912
	/* Locate any packages using this certificate */
1913
	$pkgcerts = pkg_call_plugins('plugin_certificates', array('type' => 'certificates', 'event' => 'used_certificates'));
1914
	foreach ($pkgcerts as $name => $package) {
1915
		if (is_array($package['certificatelist'][$refid]) &&
1916
		    isset($package['certificatelist'][$refid]) > 0) {
1917
			$services['packages'][] = $name;
1918
		}
1919
	}
1920

    
1921
	return $services;
1922
}
1923

    
1924
/****f* certs/ca_get_all_services
1925
 * NAME
1926
 *   ca_get_all_services - Locate services using a given certificate authority or its decendents
1927
 * INPUTS
1928
 *   $refid: The refid of a certificate authority to check
1929
 * RESULT
1930
 *   array containing the services which use this certificate authority, including:
1931
 *     webgui: Present and true if the WebGUI uses this certificate. Unset otherwise.
1932
 *     services: Array of service definitions using this certificate, with:
1933
 *       name: Name of the service
1934
 *       extras: Extra information needed by some services, such as OpenVPN or Captive Portal.
1935
 *     packages: Array containing package names using this certificate.
1936
 * NOTES
1937
 *   This searches recursively to find entries using this CA as well as intermediate
1938
 *   CAs and certificates signed by this CA, and returns a single set of all services.
1939
 *   This avoids restarting affected services multiple times when there is overlapping
1940
 *   usage.
1941
 ******/
1942
function ca_get_all_services($refid) {
1943
	global $config;
1944
	$services = array();
1945
	$services['services'] = array();
1946

    
1947
	init_config_arr(array('openvpn', 'openvpn-server'));
1948
	init_config_arr(array('openvpn', 'openvpn-client'));
1949
	foreach(array('server', 'client') as $mode) {
1950
		foreach ($config['openvpn']["openvpn-{$mode}"] as $ovpn) {
1951
			if ($ovpn['caref'] == $refid) {
1952
				$services['services'][] = array(
1953
					'name' => 'openvpn',
1954
					'extras' => array(
1955
						'vpnmode' => $mode,
1956
						'id' => $ovpn['vpnid']
1957
					)
1958
				);
1959
			}
1960
		}
1961
	}
1962
	init_config_arr(array('ipsec', 'phase1'));
1963
	foreach ($config['ipsec']['phase1'] as $ipsec) {
1964
		if ($ipsec['certref'] == $refid) {
1965
			break;
1966
		}
1967
	}
1968
	foreach ($config['ipsec']['phase1'] as $ipsec) {
1969
		if (($ipsec['authentication_method'] == 'cert') &&
1970
		    ($ipsec['caref'] == $refid)) {
1971
			$services['services'][] = array('name' => 'ipsec');
1972
			break;
1973
		}
1974
	}
1975

    
1976
	/* Loop through all certs and get their services as well */
1977
	init_config_arr(array('cert'));
1978
	foreach ($config['cert'] as $cert) {
1979
		if ($cert['caref'] == $refid) {
1980
			$services = array_merge_recursive_unique($services, cert_get_all_services($cert['refid']));
1981
		}
1982
	}
1983

    
1984
	/* Look for intermediate certs and services */
1985
	init_config_arr(array('ca'));
1986
	foreach ($config['ca'] as $cert) {
1987
		if ($cert['caref'] == $refid) {
1988
			$services = array_merge_recursive_unique($services, ca_get_all_services($cert['refid']));
1989
		}
1990
	}
1991

    
1992
	return $services;
1993
}
1994

    
1995
/****f* certs/cert_restart_services
1996
 * NAME
1997
 *   cert_restart_services - Restarts services specific to CA/Certificate usage
1998
 * INPUTS
1999
 *   $services: An array of services returned by cert_get_all_services or ca_get_all_services
2000
 * RESULT
2001
 *   Services in the given array are restarted
2002
 *   returns false if the input is invalid
2003
 *   returns true at the end of execution
2004
 ******/
2005

    
2006
function cert_restart_services($services) {
2007
	require_once("service-utils.inc");
2008
	/* If the input is not an array, it is invalid. */
2009
	if (!is_array($services)) {
2010
		return false;
2011
	}
2012

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

    
2016
	/* Restart GUI: */
2017
	if ($services['webgui']) {
2018
		ob_flush();
2019
		flush();
2020
		log_error(sprintf($restart_string, gettext('service'), 'WebGUI'));
2021
		send_event("service restart webgui");
2022
	}
2023

    
2024
	/* Restart other base services: */
2025
	if (is_array($services['services'])) {
2026
		foreach ($services['services'] as $service) {
2027
			switch ($service['name']) {
2028
				case 'openvpn':
2029
					$service_name = "{$service['name']} {$service['extras']['vpnmode']} {$service['extras']['id']}";
2030
					break;
2031
				case 'captiveportal':
2032
					$service_name = "{$service['name']} zone {$service['extras']['zone']}";
2033
					break;
2034
				default:
2035
					$service_name = $service['name'];
2036
			}
2037
			log_error(sprintf($restart_string, gettext('service'), $service_name));
2038
			service_control_restart($service['name'], $service['extras']);
2039
		}
2040
	}
2041

    
2042
	/* Restart Packages: */
2043
	if (is_array($services['packages'])) {
2044
		foreach ($services['packages'] as $service) {
2045
			log_error(sprintf($restart_string, gettext('package'), $service));
2046
			restart_service($service);
2047
		}
2048
	}
2049
	return true;
2050
}
2051

    
2052
/****f* certs/cert_get_lifetime
2053
 * NAME
2054
 *   cert_get_lifetime - Returns the number of days the certificate is valid
2055
 * INPUTS
2056
 *   $untilexpire: Boolean
2057
 *     true: The number of days returned is from now until the certificate expiration.
2058
 *     false (default): The number of days returned is the total lifetime of the certificate.
2059
 * RESULT
2060
 *   Integer number of days in the certificate total or remaining lifetime
2061
 ******/
2062

    
2063
function cert_get_lifetime($cert, $untilexpire = false) {
2064
	/* If the certificate is not valid, bail. */
2065
	if (!is_array($cert) || empty($cert['crt'])) {
2066
		return null;
2067
	}
2068
	/* Read certificate details */
2069
	list($startdate, $enddate) = cert_get_dates($cert['crt'], true, false);
2070

    
2071
	/* If either of the dates are invalid, there is nothing we can do here. */
2072
	if (($startdate === false) || ($enddate === false)) {
2073
		return false;
2074
	}
2075

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

    
2079
	/* Calculate the requested intervals */
2080
	$interval = $startdate->diff($enddate);
2081

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

    
2086
/****f* certs/cert_analyze_lifetime
2087
 * NAME
2088
 *   cert_analyze_lifetime - Analyze a certificate lifetime for expiration notices
2089
 * INPUTS
2090
 *   $expiredays: Number of days until the certificate expires (See cert_get_lifetime())
2091
 * RESULT
2092
 *   An array of two entries:
2093
 *   0/$lrclass: A bootstrap name for use with classes like text-<x>
2094
 *   1/$expstring: A text analysis describing the expiration timeframe.
2095
 ******/
2096

    
2097
function cert_analyze_lifetime($expiredays) {
2098
	global $config, $g;
2099
	/* Number of days at which to warn of expiration. */
2100
	init_config_arr(array('notifications', 'certexpire'));
2101
	if (!isset($config['notifications']['certexpire']['expiredays']) ||
2102
	    empty($config['notifications']['certexpire']['expiredays'])) {
2103
		$warning_days = $g['default_cert_expiredays'];
2104
	} else {
2105
		$warning_days = $config['notifications']['certexpire']['expiredays'];
2106
	}
2107

    
2108
	if ($expiredays > $warning_days) {
2109
		/* Not expiring soon */
2110
		$lrclass = 'normal';
2111
		$expstring = gettext("%d %s until expiration");
2112
	} elseif ($expiredays >= 0) {
2113
		/* Still valid but expiring soon */
2114
		$lrclass = 'warning';
2115
		$expstring = gettext("Expiring soon, in %d %s");
2116
	} else {
2117
		/* Certificate has expired */
2118
		$lrclass = 'danger';
2119
		$expstring = gettext("Expired %d %s ago");
2120
	}
2121
	$days = (abs($expiredays) == 1) ? gettext('day') : gettext('days');
2122
	$expstring = sprintf($expstring, abs($expiredays), $days);
2123
	return array($lrclass, $expstring);
2124
}
2125

    
2126
/****f* certs/cert_print_dates
2127
 * NAME
2128
 *   cert_print_dates - Print the start and end timestamps for the given certificate
2129
 * INPUTS
2130
 *   $cert: CA or Cert entry for which the dates will be printed
2131
 * RESULT
2132
 *   Returns null if the passed entry is invalid
2133
 *   Otherwise, outputs the dates to the user with formatting.
2134
 ******/
2135

    
2136
function cert_print_dates($cert) {
2137
	/* If the certificate is not valid, bail. */
2138
	if (!is_array($cert) || empty($cert['crt'])) {
2139
		return null;
2140
	}
2141
	/* Attempt to extract the dates from the certificate */
2142
	list($startdate, $enddate) = cert_get_dates($cert['crt']);
2143
	/* If either of the timestamps are empty, then do not print anything.
2144
	 * The entry may not be valid or it may just be missing date information */
2145
	if (empty($startdate) || empty($enddate)) {
2146
		return null;
2147
	}
2148
	/* Get the expiration days */
2149
	$expiredays = cert_get_lifetime($cert, true);
2150
	/* Analyze the lifetime value */
2151
	list($lrclass, $expstring) = cert_analyze_lifetime($expiredays);
2152
	/* Output the dates, with a tooltip showing days until expiration, and
2153
	 * a visual indication of warning/expired status. */
2154
	?>
2155
	<br />
2156
	<small>
2157
	<?=gettext("Valid From")?>: <b><?=$startdate ?></b><br />
2158
	<?=gettext("Valid Until")?>:
2159
	<span class="text-<?=$lrclass?>" data-toggle="tooltip" data-placement="bottom" title="<?= $expstring ?>">
2160
	<b><?=$enddate ?></b>
2161
	</span>
2162
	</small>
2163
	<?php
2164
}
2165

    
2166
/****f* certs/cert_print_infoblock
2167
 * NAME
2168
 *   cert_print_infoblock - Print an information block containing certificate details
2169
 * INPUTS
2170
 *   $cert: CA or Cert entry for which the information will be printed
2171
 * RESULT
2172
 *   Returns null if the passed entry is invalid
2173
 *   Otherwise, outputs information to the user with formatting.
2174
 ******/
2175

    
2176
function cert_print_infoblock($cert) {
2177
	/* If the certificate is not valid, bail. */
2178
	if (!is_array($cert) || empty($cert['crt'])) {
2179
		return null;
2180
	}
2181
	/* Variable to hold the formatted information */
2182
	$certextinfo = "";
2183

    
2184
	/* Serial number */
2185
	$cert_details = openssl_x509_parse(base64_decode($cert['crt']));
2186
	if (isset($cert_details['serialNumber']) && (strlen($cert_details['serialNumber']) > 0)) {
2187
		$certextinfo .= '<b>' . gettext("Serial: ") . '</b> ';
2188
		$certextinfo .= htmlspecialchars(cert_escape_x509_chars($cert_details['serialNumber'], true));
2189
		$certextinfo .= '<br/>';
2190
	}
2191

    
2192
	/* Digest type */
2193
	$certsig = cert_get_sigtype($cert['crt']);
2194
	if (is_array($certsig) && !empty($certsig) && !empty($certsig['shortname'])) {
2195
		$certextinfo .= '<b>' . gettext("Signature Digest: ") . '</b> ';
2196
		$certextinfo .= htmlspecialchars(cert_escape_x509_chars($certsig['shortname'], true));
2197
		$certextinfo .= '<br/>';
2198
	}
2199

    
2200
	/* Subject Alternative Name (SAN) list */
2201
	$sans = cert_get_sans($cert['crt']);
2202
	if (is_array($sans) && !empty($sans)) {
2203
		$certextinfo .= '<b>' . gettext("SAN: ") . '</b> ';
2204
		$certextinfo .= htmlspecialchars(implode(', ', cert_escape_x509_chars($sans, true)));
2205
		$certextinfo .= '<br/>';
2206
	}
2207

    
2208
	/* Key usage */
2209
	$purpose = cert_get_purpose($cert['crt']);
2210
	if (is_array($purpose) && !empty($purpose['ku'])) {
2211
		$certextinfo .= '<b>' . gettext("KU: ") . '</b> ';
2212
		$certextinfo .= htmlspecialchars(implode(', ', $purpose['ku']));
2213
		$certextinfo .= '<br/>';
2214
	}
2215

    
2216
	/* Extended key usage */
2217
	if (is_array($purpose) && !empty($purpose['eku'])) {
2218
		$certextinfo .= '<b>' . gettext("EKU: ") . '</b> ';
2219
		$certextinfo .= htmlspecialchars(implode(', ', $purpose['eku']));
2220
		$certextinfo .= '<br/>';
2221
	}
2222

    
2223
	/* OCSP / Must Staple */
2224
	if (cert_get_ocspstaple($cert['crt'])) {
2225
		$certextinfo .= '<b>' . gettext("OCSP: ") . '</b> ';
2226
		$certextinfo .= gettext("Must Staple");
2227
		$certextinfo .= '<br/>';
2228
	}
2229

    
2230
	/* Private key information */
2231
	if (!empty($cert['prv'])) {
2232
		$res_key = openssl_pkey_get_private(base64_decode($cert['prv']));
2233
		$certextinfo .= '<b>' . gettext("Key Type: ") . '</b> ';
2234
		if ($res_key) {
2235
			$key_details = openssl_pkey_get_details($res_key);
2236
			/* Key type (RSA or EC) */
2237
			if ($key_details['type'] == OPENSSL_KEYTYPE_RSA) {
2238
				/* RSA Key size */
2239
				$certextinfo .= 'RSA<br/>';
2240
				$certextinfo .= '<b>' . gettext("Key Size: ") . '</b> ';
2241
				$certextinfo .= $key_details['bits'] . '<br/>';
2242
			} else {
2243
				/* Elliptic curve (EC) key curve name */
2244
				$certextinfo .= 'ECDSA<br/>';
2245
				$curve = cert_get_pkey_curve($cert['prv']);
2246
				if (!empty($curve)) {
2247
					$certextinfo .= '<b>' . gettext("Elliptic curve name:") . ' </b>';
2248
					$certextinfo .= $curve . '<br/>';
2249
				}
2250
			}
2251
		} else {
2252
			$certextinfo .= '<i>' . gettext("Unknown (Key could not be parsed)") . '</i><br/>';
2253
		}
2254
	}
2255

    
2256
	/* Distinguished name (DN) */
2257
	if (!empty($cert_details['name'])) {
2258
		$certextinfo .= '<b>' . gettext("DN: ") . '</b> ';
2259
		/* UTF8 DN support, see https://redmine.pfsense.org/issues/12041 */
2260
		$certdnstring = preg_replace_callback('/\\\\x([0-9A-F]{2})/', function ($a) { return pack('H*', $a[1]); }, $cert_details['name']);
2261
		$certextinfo .= htmlspecialchars(cert_escape_x509_chars($certdnstring, true));
2262
		$certextinfo .= '<br/>';
2263
	}
2264

    
2265
	/* Hash value */
2266
	if (!empty($cert_details['hash'])) {
2267
		$certextinfo .= '<b>' . gettext("Hash: ") . '</b> ';
2268
		$certextinfo .= htmlspecialchars(cert_escape_x509_chars($cert_details['hash'], true));
2269
		$certextinfo .= '<br/>';
2270
	}
2271

    
2272
	/* Subject Key Identifier (SKID) */
2273
	if (is_array($cert_details["extensions"]) && !empty($cert_details["extensions"]["subjectKeyIdentifier"])) {
2274
		$certextinfo .= '<b>' . gettext("Subject Key ID: ") . '</b> ';
2275
		$certextinfo .= htmlspecialchars(cert_escape_x509_chars($cert_details["extensions"]["subjectKeyIdentifier"], true));
2276
		$certextinfo .= '<br/>';
2277
	}
2278

    
2279
	/* Authority Key Identifier (AKID) */
2280
	if (is_array($cert_details["extensions"]) && !empty($cert_details["extensions"]["authorityKeyIdentifier"])) {
2281
		$certextinfo .= '<b>' . gettext("Authority Key ID: ") . '</b> ';
2282
		$certextinfo .= str_replace("\n", '<br/>', htmlspecialchars(cert_escape_x509_chars($cert_details["extensions"]["authorityKeyIdentifier"], true)));
2283
		$certextinfo .= '<br/>';
2284
	}
2285

    
2286
	/* Total Lifetime (days from cert start to end) */
2287
	$lifetime = cert_get_lifetime($cert);
2288
	if ($lifetime !== false) {
2289
		$certextinfo .= '<b>' . gettext("Total Lifetime: ") . '</b> ';
2290
		$certextinfo .= sprintf("%d %s", $lifetime, (abs($lifetime) == 1) ? gettext('day') : gettext('days'));
2291
		$certextinfo .= '<br/>';
2292

    
2293
		/* Lifetime before certificate expires (days from now to end) */
2294
		$expiredays = cert_get_lifetime($cert, true);
2295
		list($lrclass, $expstring) = cert_analyze_lifetime($expiredays);
2296
		$certextinfo .= '<b>' . gettext("Lifetime Remaining: ") . '</b> ';
2297
		$certextinfo .= "<span class=\"text-{$lrclass}\">{$expstring}</span>";
2298
		$certextinfo .= '<br/>';
2299
	}
2300

    
2301
	if ($purpose['ca'] == 'Yes') {
2302
		/* CA Trust store presence */
2303
		$certextinfo .= '<b>' . gettext("Trust Store: ") . '</b> ';
2304
		$certextinfo .= (isset($cert['trust']) && ($cert['trust'] == "enabled")) ? gettext('Included') : gettext('Excluded');
2305
		$certextinfo .= '<br/>';
2306

    
2307
		if (!empty($cert['prv'])) {
2308
			/* CA Next/Randomize Serial */
2309
			$certextinfo .= '<b>' . gettext("Next Serial: ") . '</b> ';
2310
			$certextinfo .= (isset($cert['randomserial']) && ($cert['randomserial'] == "enabled")) ? gettext('Randomized') : $cert['serial'];
2311
			$certextinfo .= '<br/>';
2312
		}
2313
	}
2314

    
2315
	/* Output the infoblock */
2316
	if (!empty($certextinfo)) { ?>
2317
		<div class="infoblock">
2318
		<? print_info_box($certextinfo, 'info', false); ?>
2319
		</div>
2320
	<?php
2321
	}
2322
}
2323

    
2324
/****f* certs/cert_notify_expiring
2325
 * NAME
2326
 *   cert_notify_expiring - Notify admin about expiring certificates
2327
 * INPUTS
2328
 *   None
2329
 * RESULT
2330
 *   File a notice containing expiring certificate information, which is then
2331
 *   logged, displayed in the GUI, and sent via e-mail (if enabled).
2332
 ******/
2333

    
2334
function cert_notify_expiring() {
2335
	global $config;
2336

    
2337
	/* If certificate expiration notifications are disabled, there is nothing to do. */
2338
	init_config_arr(array('notifications', 'certexpire'));
2339
	if ($config['notifications']['certexpire']['enable'] == "disabled") {
2340
		return;
2341
	}
2342

    
2343
	$notifications = array();
2344

    
2345
	/* Check all CA and Cert entries at once */
2346
	init_config_arr(array('ca'));
2347
	init_config_arr(array('cert'));
2348
	$all_certs = array_merge_recursive($config['ca'], $config['cert']);
2349

    
2350
	foreach ($all_certs as $cert) {
2351
		if (empty($cert)) {
2352
			continue;
2353
		}
2354
		/* Proceed only for not revoked certificate if ignore setting enabled */
2355
		if (($config['notifications']['certexpire']['ignore_revoked'] == "enabled") && is_cert_revoked($cert)) {
2356
			continue;
2357
		}
2358
		/* Fetch and analyze expiration */
2359
		$expiredays = cert_get_lifetime($cert, true);
2360
		/* If the result is null, then the lifetime data is missing, so skip the invalid entry. */
2361
		if ($expiredays === null) {
2362
			continue;
2363
		}
2364
		list($lrclass, $expstring) = cert_analyze_lifetime($expiredays);
2365
		/* Only notify if the certificate is expiring soon, or has
2366
		 * already expired */
2367
		if ($lrclass != 'normal') {
2368
			$notify_string = (array_key_exists('serial', $cert)) ? gettext('Certificate Authority') : gettext('Certificate');
2369
			$notify_string .= ": {$cert['descr']} ({$cert['refid']}): {$expstring}";
2370
			$notifications[] = $notify_string;
2371
		}
2372
	}
2373
	if (!empty($notifications)) {
2374
		$message = gettext("The following CA/Certificate entries are expiring:") . "\n" .
2375
			implode("\n", $notifications);
2376
		file_notice("Certificate Expiration", $message, "Certificate Manager");
2377
	}
2378
}
2379

    
2380
/****f* certs/ca_setup_trust_store
2381
 * NAME
2382
 *   ca_setup_trust_store - Setup local CA trust store so that CA entries in the
2383
 *                          configuration may be trusted by the operating system.
2384
 * INPUTS
2385
 *   None
2386
 * RESULT
2387
 *   CAs marked as trusted in the configuration will be setup in the OS trust store.
2388
 ******/
2389

    
2390
function ca_setup_trust_store() {
2391
	global $config;
2392

    
2393
	/* This directory is trusted by OpenSSL on FreeBSD by default */
2394
	$trust_store_directory = '/etc/ssl/certs';
2395

    
2396
	/* Create the directory if it does not already exist, and clean it up if it does. */
2397
	safe_mkdir($trust_store_directory);
2398
	unlink_if_exists("{$trust_store_directory}/*.0");
2399

    
2400
	init_config_arr(array('ca'));
2401
	foreach ($config['ca'] as $ca) {
2402
		/* If the entry is invalid or is not trusted, skip it. */
2403
		if (!is_array($ca) ||
2404
		    empty($ca['crt']) ||
2405
		    !isset($ca['trust']) ||
2406
		    ($ca['trust'] != "enabled")) {
2407
			continue;
2408
		}
2409

    
2410
		ca_setup_capath($ca, $trust_store_directory);
2411
	}
2412
}
2413

    
2414
/****f* certs/ca_setup_capath
2415
 * NAME
2416
 *   ca_setup_capath - Setup CApath structure so that CA chains and related CRLs
2417
 *                     may be written and validated by the -CApath option in
2418
 *                     OpenSSL and other compatible validators.
2419
 * INPUTS
2420
 *   $ca     : A CA (not a refid) to write
2421
 *   $basedir: The directory which will contain the CA structure.
2422
 *   $crl    : A CRL (not a refid) associated with the CA to write.
2423
 *   $refresh: Refresh CRLs -- When true, perform no cleanup and increment suffix
2424
 * RESULT
2425
 *   $basedir is populated with CA and CRL files in a format usable by OpenSSL
2426
 *   CApath. This has the filenames as the CA hash with the CA named <hash>.0
2427
 *   and CRLs named <hash>.r0
2428
 ******/
2429

    
2430
function ca_setup_capath($ca, $basedir, $crl = "", $refresh = false) {
2431
	global $config;
2432
	/* Check for an invalid CA */
2433
	if (!$ca || !is_array($ca)) {
2434
		return false;
2435
	}
2436
	/* Check for an invalid CRL, but do not consider it fatal if it's wrong */
2437
	if (!$crl || !is_array($crl) || ($crl['caref'] != $ca['refid'])) {
2438
		unset($crl);
2439
	}
2440

    
2441
	/* Check for an empty base directory, which is invalid */
2442
	if (empty($basedir)) {
2443
		return false;
2444
	}
2445

    
2446
	/* Ensure that $basedir exists and is a directory */
2447
	if (!is_dir($basedir)) {
2448
		/* If it's a file, remove it, otherwise the directory cannot
2449
		 * be created. */
2450
		if (file_exists($basedir)) {
2451
			@unlink_if_exists($basedir);
2452
		}
2453
		@safe_mkdir($basedir);
2454
	}
2455
	/* Decode the certificate contents */
2456
	$cert_contents = base64_decode($ca['crt']);
2457
	/* Get hash value to use for filename */
2458
	$cert_details = openssl_x509_parse($cert_contents);
2459
	$fprefix = "{$basedir}/{$cert_details['hash']}";
2460

    
2461

    
2462
	$ca_filename = "{$fprefix}.0";
2463
	/* Cleanup old CA/CRL files for this hash */
2464
	@unlink_if_exists($ca_filename);
2465
	/* Write CA to base dir and ensure it has correct permissions. */
2466
	file_put_contents($ca_filename, $cert_contents);
2467
	chmod($ca_filename, 0644);
2468
	chown($ca_filename, 'root');
2469
	chgrp($ca_filename, 'wheel');
2470

    
2471
	/* If there is a CRL, process it. */
2472
	if ($crl) {
2473
		$crl_filename = "{$fprefix}.r";
2474
		if (!$refresh) {
2475
			/* Cleanup old CA/CRL files for this hash */
2476
			@unlink_if_exists("{$crl_filename}*");
2477
		}
2478
		/* Find next suffix based on how many existing files there are (start=0) */
2479
		$crl_filename .= count(glob("{$crl_filename}*"));
2480
		/* Write CRL to base dir and ensure it has correct permissions. */
2481
		file_put_contents($crl_filename, base64_decode($crl['text']));
2482
		chmod($crl_filename, 0644);
2483
		chown($crl_filename, 'root');
2484
		chgrp($crl_filename, 'wheel');
2485
	}
2486

    
2487
	return true;
2488
}
2489

    
2490
/****f* certs/cert_get_pkey_curve
2491
 * NAME
2492
 *   cert_get_pkey_curve - Get the ECDSA curve of a private key
2493
 * INPUTS
2494
 *   $pkey  : The private key to check
2495
 *   $decode: true: base64 decode the string before use, false to use as-is.
2496
 * RESULT
2497
 *   false if the private key is not ECDSA or the private key is not present.
2498
 *   Otherwise, the name of the ECDSA curve used for the private key.
2499
 ******/
2500

    
2501
function cert_get_pkey_curve($pkey, $decode = true) {
2502
	if ($decode) {
2503
		$pkey = base64_decode($pkey);
2504
	}
2505

    
2506
	/* Attempt to read the private key, and if successful, its details. */
2507
	$res_key = openssl_pkey_get_private($pkey);
2508
	if ($res_key) {
2509
		$key_details = openssl_pkey_get_details($res_key);
2510
		/* If this is an EC key, and the curve name is not empty, return
2511
		 * that curve name. */
2512
		if ($key_details['type'] ==  OPENSSL_KEYTYPE_EC) {
2513
			if (!empty($key_details['ec']['curve_name'])) {
2514
				return $key_details['ec']['curve_name'];
2515
			} else {
2516
				return $key_details['ec']['curve_oid'];
2517
			}
2518
		}
2519
	}
2520

    
2521
	/* Either the private key could not be read, or this is not an EC certificate. */
2522
	return false;
2523
}
2524

    
2525
/* Array containing ECDSA curve names allowed in certain contexts. For instance,
2526
 * HTTPS servers and web browsers only support specific curves in TLSv1.3. */
2527
global $cert_curve_compatible, $curve_compatible_list;
2528
$cert_curve_compatible = array(
2529
	/* HTTPS list per TLSv1.3 spec and Mozilla compatibility list */
2530
	'HTTPS' => array('prime256v1', 'secp384r1'),
2531
	/* IPsec/EAP/TLS list per strongSwan docs/issues */
2532
	'IPsec' => array('prime256v1', 'secp384r1', 'secp521r1'),
2533
	/* OpenVPN bug limits usable curves, see https://redmine.pfsense.org/issues/9744 */
2534
	'OpenVPN' => array('prime256v1', 'secp384r1', 'secp521r1'),
2535
);
2536
$curve_compatible_list = array_unique(call_user_func_array('array_merge', array_values($cert_curve_compatible)));
2537

    
2538
/****f* certs/cert_build_curve_list
2539
 * NAME
2540
 *   cert_build_curve_list - Build an option list of ECDSA curves with notations
2541
 *                           about known compatible uses.
2542
 * INPUTS
2543
 *   None
2544
 * RESULT
2545
 *   Returns an option list of OpenSSL EC names with added notes. This can be
2546
 *   used directly in form option selection lists.
2547
 ******/
2548

    
2549
function cert_build_curve_list() {
2550
	global $cert_curve_compatible;
2551
	/* Get the default list of curve names */
2552
	$openssl_ecnames = openssl_get_curve_names();
2553
	/* Turn this into a hashed array where key==value */
2554
	$curvelist = array_combine($openssl_ecnames, $openssl_ecnames);
2555
	/* Check all known compatible curves and note matches */
2556
	foreach ($cert_curve_compatible as $consumer => $validcurves) {
2557
		/* $consumer will be a name like HTTPS or IPsec
2558
		 * $validcurves will be an array of curves compatible with the consumer */
2559
		foreach ($validcurves as $vc) {
2560
			/* If the valid curve is present in the curve list, add
2561
			 * a note with the consumer name to the value visible to
2562
			 * the user. */
2563
			if (array_key_exists($vc, $curvelist)) {
2564
				$curvelist[$vc] .= " [{$consumer}]";
2565
			}
2566
		}
2567
	}
2568
	return $curvelist;
2569
}
2570

    
2571
/****f* certs/cert_check_pkey_compatibility
2572
 * NAME
2573
 *   cert_check_pkey_compatibility - Check a private key to see if it can be
2574
 *                                   used in a specific compatible context.
2575
 * INPUTS
2576
 *   $pkey    : The private key to check
2577
 *   $consumer: The consumer name used to validate the curve. See the contents
2578
 *                 of $cert_curve_compatible for details.
2579
 * RESULT
2580
 *   true if the private key may be used in requested area, or if there are no
2581
 *        restrictions.
2582
 *   false if the private key cannot be used.
2583
 ******/
2584

    
2585
function cert_check_pkey_compatibility($pkey, $consumer) {
2586
	global $cert_curve_compatible;
2587

    
2588
	/* Read the curve name from the key */
2589
	$curve = cert_get_pkey_curve($pkey);
2590
	/* Return true if any of the following conditions are met:
2591
	 *  * This is not an EC key
2592
	 *  * The private key cannot be read
2593
	 *  * There are no restrictions
2594
	 *  * The requested curve is compatible */
2595
	return (($curve === false) ||
2596
		!array_key_exists($consumer, $cert_curve_compatible) ||
2597
		in_array($curve, $cert_curve_compatible[$consumer]));
2598
}
2599

    
2600
/****f* certs/cert_build_list
2601
 * NAME
2602
 *   cert_build_list - Build an option list of cert or CA entries, checked
2603
 *                     against a specific consumer name.
2604
 * INPUTS
2605
 *   $type    : 'ca' for certificate authority entries, 'cert' for certificates.
2606
 *   $consumer: The consumer name used to filter certificates out of the result.
2607
 *                 See the contents of $cert_curve_compatible for details.
2608
 *   $selectsource: Then true, outputs in a format usable by select_source in
2609
 *                  packages.
2610
 *   $addnone: When true, a 'none' choice is added to the list.
2611
 * RESULT
2612
 *   Returns an option list of entries with incompatible entries removed. This
2613
 *   can be used directly in form option selection lists.
2614
 * NOTES
2615
 *   This can be expanded in the future to allow for other types of restrictions.
2616
 ******/
2617

    
2618
function cert_build_list($type = 'cert', $consumer = '', $selectsource = false, $addnone = false) {
2619
	global $config;
2620

    
2621
	/* Ensure that $type is valid */
2622
	if (!in_array($type, array('ca', 'cert'))) {
2623
		return array();
2624
	}
2625

    
2626
	/* Initialize arrays */
2627
	init_config_arr(array($type));
2628
	$list = array();
2629

    
2630
	if ($addnone) {
2631
		if ($selectsource) {
2632
			$list[] = array('refid' => 'none', 'descr' => 'None');
2633
		} else {
2634
			$list['none'] = "None";
2635
		}
2636
	}
2637

    
2638
	/* Create a hashed array with the certificate refid as the key and
2639
	 * descriptive name as the value. Exclude incompatible certificates. */
2640
	foreach ($config[$type] as $cert) {
2641
		if (empty($cert['prv']) && ($type == 'cert')) {
2642
			continue;
2643
		} else if (cert_check_pkey_compatibility($cert['prv'], $consumer)) {
2644
			if ($selectsource) {
2645
				$list[] = array('refid' => $cert['refid'],
2646
						'descr' => $cert['descr']);
2647
			} else {
2648
				$list[$cert['refid']] = $cert['descr'];
2649
			}
2650
		}
2651
	}
2652

    
2653
	return $list;
2654
}
2655

    
2656
?>
(8-8/62)