Project

General

Profile

Download (88.4 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-2024 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 $p12_encryption_levels;
55
$p12_encryption_levels = array(
56
	'high'   => gettext('High: AES-256 + SHA256 (pfSense Software, FreeBSD, Linux, Windows 10)'),
57
	'low'    => gettext('Low: 3DES + SHA1 (macOS, older Windows versions)'),
58
	'legacy' => gettext('Legacy: RC2-40 + SHA1 (legacy OS versions)'),
59
);
60

    
61
global $cert_max_lifetime;
62
$cert_max_lifetime = 12000;
63

    
64
global $crl_max_lifetime;
65
$crl_max_lifetime = 9999;
66

    
67
/**
68
 * @return array Contains the CA config index and item.
69
 */
70
function lookup_ca($refid) {
71
	$ca_item = ['idx' => null, 'item' => null];
72
	foreach (config_get_path('ca', []) as $idx => $ca) {
73
		if (empty($ca)) {
74
			continue;
75
		}
76
		if ($ca['refid'] == $refid) {
77
			$ca_item = ['idx' => $idx, 'item' => $ca];
78
			break;
79
		}
80
	}
81

    
82
	return $ca_item;
83
}
84

    
85
/**
86
 * @return array Contains the CA config index and item.
87
 */
88
function lookup_ca_by_subject($subject) {
89
	$ca_item = ['idx' => null, 'item' => null];
90
	foreach (config_get_path('ca', []) as $idx => $ca) {
91
		if (empty($ca)) {
92
			continue;
93
		}
94
		$ca_subject = cert_get_subject($ca['crt']);
95
		if ($ca_subject == $subject) {
96
			$ca_item = ['idx' => $idx, 'item' => $ca];
97
			break;
98
		}
99
	}
100

    
101
	return $ca_item;
102
}
103

    
104
/**
105
 * @return array Contains the cert config index and item.
106
 */
107
function lookup_cert($refid) {
108
	$cert_item = ['idx' => null, 'item' => null];
109
	foreach (config_get_path('cert', []) as $idx => $cert) {
110
		if (empty($cert)) {
111
			continue;
112
		}
113
		if ($cert['refid'] == $refid) {
114
			$cert_item = ['idx' => $idx, 'item' => $cert];
115
			break;
116
		}
117
	}
118

    
119
	return $cert_item;
120
}
121

    
122
/**
123
 * @return array Contains the cert config index and item.
124
 */
125
function lookup_cert_by_name($name) {
126
	$cert_item = ['idx' => null, 'item' => null];
127
	foreach (config_get_path('cert', []) as $idx => $cert) {
128
		if (empty($cert)) {
129
			continue;
130
		}
131
		if ($cert['descr'] == $name) {
132
			$cert_item = ['idx' => $idx, 'item' => $cert];
133
			break;
134
		}
135
	}
136

    
137
	return $cert_item;
138
}
139

    
140
/**
141
 * @return array Contains the CRL config index and item.
142
 */
143
function lookup_crl($refid) {
144
	$crl_item = ['idx' => null, 'item' => null];
145
	foreach (config_get_path('crl', []) as $idx => $crl) {
146
		if (empty($crl)) {
147
			continue;
148
		}
149
		if ($crl['refid'] == $refid) {
150
			$crl_item = ['idx' => $idx, 'item' => $crl];
151
			break;
152
		}
153
	}
154

    
155
	return $crl_item;
156
}
157

    
158
function ca_chain_array($cert) {
159
	if ($cert['caref']) {
160
		$chain = array();
161
		$crt = lookup_ca($cert['caref']);
162
		$crt = $crt['item'];
163
		$chain[] = $crt;
164
		while ($crt) {
165
			$caref = $crt['caref'];
166
			if ($caref) {
167
				$crt = lookup_ca($caref);
168
				$crt = $crt['item'];
169
			} else {
170
				$crt = false;
171
			}
172
			if ($crt) {
173
				$chain[] = $crt;
174
			}
175
		}
176
		return $chain;
177
	}
178
	return false;
179
}
180

    
181
function ca_chain($cert) {
182
	if ($cert['caref']) {
183
		$ca = "";
184
		$cas = ca_chain_array($cert);
185
		if (is_array($cas)) {
186
			foreach ($cas as $ca_cert) {
187
				$ca .= base64_decode($ca_cert['crt']);
188
				$ca .= "\n";
189
			}
190
		}
191
		return $ca;
192
	}
193
	return "";
194
}
195

    
196
/**
197
 * Writes the given CA to config and updates certs that reference it.
198
 * 
199
 * @param array  &$ca    Directly modified; written to config if it does not exist.
200
 * @param string $str    CA cert - not encoded.
201
 * @param string $key    Optional. CA key - not encoded.
202
 * @param string $serial Optional. CA serial; defaults to 0.
203
 * 
204
 * @return bool False if CA already exists; True otherwise.
205
 */
206
function ca_import(& $ca, $str, $key = "", $serial = "") {
207
	$ca['crt'] = base64_encode($str);
208
	if (!empty($key)) {
209
		$ca['prv'] = base64_encode($key);
210
	}
211
	if (empty($serial)) {
212
		$ca['serial'] = 0;
213
	} else {
214
		$ca['serial'] = $serial;
215
	}
216
	$subject = cert_get_subject($str, false);
217
	$issuer = cert_get_issuer($str, false);
218
	$serialNumber = cert_get_serial($str, false);
219

    
220
	// Find my issuer unless self-signed
221
	if ($issuer <> $subject) {
222
		$issuer_crt = lookup_ca_by_subject($issuer);
223
		$issuer_crt = $issuer_crt['item'];
224
		if ($issuer_crt) {
225
			$ca['caref'] = $issuer_crt['refid'];
226
		}
227
	}
228

    
229
	/* Correct if child certificate was loaded first */
230
	foreach (config_get_path('ca', []) as $idx => $oca) {
231
		// check by serial number if CA already exists
232
		$osn = cert_get_serial($oca['crt']);
233
		if (($ca['refid'] <> $oca['refid']) && ($serialNumber == $osn)) {
234
			return false;
235
		}
236
		$issuer = cert_get_issuer($oca['crt']);
237
		if (($ca['refid'] <> $oca['refid']) && ($issuer == $subject)) {
238
			config_set_path("ca/{$idx}/caref", $ca['refid']);
239
		}
240
	}
241
	$cert_config = config_get_path('cert');
242
	if (is_array($cert_config)) {
243
		foreach ($cert_config as & $cert) {
244
			$issuer = cert_get_issuer($cert['crt']);
245
			if ($issuer == $subject) {
246
				$cert['caref'] = $ca['refid'];
247
			}
248
		}
249
		config_set_path('cert', $cert_config);
250
	}
251
	return true;
252
}
253

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

    
256
	$args = array(
257
		"x509_extensions" => "v3_ca",
258
		"digest_alg" => $digest_alg,
259
		"encrypt_key" => false);
260
	if ($keytype == 'ECDSA') {
261
		$args["curve_name"] = $ecname;
262
		$args["private_key_type"] = OPENSSL_KEYTYPE_EC;
263
	} else {
264
		$args["private_key_bits"] = (int)$keylen;
265
		$args["private_key_type"] = OPENSSL_KEYTYPE_RSA;
266
	}
267

    
268
	// generate a new key pair
269
	$res_key = openssl_pkey_new($args);
270
	if (!$res_key) {
271
		return false;
272
	}
273

    
274
	// generate a certificate signing request
275
	$res_csr = openssl_csr_new($dn, $res_key, $args);
276
	if (!$res_csr) {
277
		return false;
278
	}
279

    
280
	// self sign the certificate
281
	$res_crt = openssl_csr_sign($res_csr, null, $res_key, $lifetime, $args, cert_get_random_serial());
282
	if (!$res_crt) {
283
		return false;
284
	}
285

    
286
	// export our certificate data
287
	if (!openssl_pkey_export($res_key, $str_key) ||
288
	    !openssl_x509_export($res_crt, $str_crt)) {
289
		return false;
290
	}
291

    
292
	// return our ca information
293
	$ca['crt'] = base64_encode($str_crt);
294
	$ca['prv'] = base64_encode($str_key);
295
	$ca['serial'] = 1;
296

    
297
	return true;
298
}
299

    
300
/**
301
 * Creates intermediate Certificate Authority; increments the CA's serial and writes it to config.
302
 */
303
function ca_inter_create(& $ca, $keylen, $lifetime, $dn, $caref, $digest_alg = "sha256", $keytype = "RSA", $ecname = "prime256v1") {
304
	// Create Intermediate Certificate Authority
305
	$ca_item_config = lookup_ca($caref);
306
	$signing_ca = &$ca_item_config['item'];
307
	if (!$signing_ca) {
308
		return false;
309
	}
310

    
311
	$signing_ca_res_crt = openssl_x509_read(base64_decode($signing_ca['crt']));
312
	$signing_ca_res_key = openssl_pkey_get_private(array(0 => base64_decode($signing_ca['prv']) , 1 => ""));
313
	if (!$signing_ca_res_crt || !$signing_ca_res_key) {
314
		return false;
315
	}
316
	$signing_ca_serial = config_set_path("ca/{$ca_item_config['idx']}", ++$signing_ca['serial']);
317

    
318
	$args = array(
319
		"x509_extensions" => "v3_ca",
320
		"digest_alg" => $digest_alg,
321
		"encrypt_key" => false);
322
	if ($keytype == 'ECDSA') {
323
		$args["curve_name"] = $ecname;
324
		$args["private_key_type"] = OPENSSL_KEYTYPE_EC;
325
	} else {
326
		$args["private_key_bits"] = (int)$keylen;
327
		$args["private_key_type"] = OPENSSL_KEYTYPE_RSA;
328
	}
329

    
330
	// generate a new key pair
331
	$res_key = openssl_pkey_new($args);
332
	if (!$res_key) {
333
		return false;
334
	}
335

    
336
	// generate a certificate signing request
337
	$res_csr = openssl_csr_new($dn, $res_key, $args);
338
	if (!$res_csr) {
339
		return false;
340
	}
341

    
342
	// Sign the certificate
343
	$res_crt = openssl_csr_sign($res_csr, $signing_ca_res_crt, $signing_ca_res_key, $lifetime, $args, $signing_ca_serial);
344
	if (!$res_crt) {
345
		return false;
346
	}
347

    
348
	// export our certificate data
349
	if (!openssl_pkey_export($res_key, $str_key) ||
350
	    !openssl_x509_export($res_crt, $str_crt)) {
351
		return false;
352
	}
353

    
354
	// return our ca information
355
	$ca['crt'] = base64_encode($str_crt);
356
	$ca['prv'] = base64_encode($str_key);
357
	$ca['serial'] = 0;
358
	$ca['caref'] = $caref;
359

    
360
	return true;
361
}
362

    
363
function cert_import(& $cert, $crt_str, $key_str) {
364

    
365
	$cert['crt'] = base64_encode($crt_str);
366
	$cert['prv'] = base64_encode($key_str);
367

    
368
	$subject = cert_get_subject($crt_str, false);
369
	$issuer = cert_get_issuer($crt_str, false);
370

    
371
	// Find my issuer unless self-signed
372
	if ($issuer <> $subject) {
373
		$issuer_crt = lookup_ca_by_subject($issuer);
374
		$issuer_crt = $issuer_crt['item'];
375
		if ($issuer_crt) {
376
			$cert['caref'] = $issuer_crt['refid'];
377
		}
378
	}
379
	return true;
380
}
381

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

    
384
	$cert['type'] = $type;
385

    
386
	if ($type != "self-signed") {
387
		$cert['caref'] = $caref;
388
		$ca_item_config = lookup_ca($caref);
389
		$ca = &$ca_item_config['item'];
390
		if (!$ca) {
391
			return false;
392
		}
393

    
394
		$ca_str_crt = base64_decode($ca['crt']);
395
		$ca_str_key = base64_decode($ca['prv']);
396
		$ca_res_crt = openssl_x509_read($ca_str_crt);
397
		$ca_res_key = openssl_pkey_get_private(array(0 => $ca_str_key, 1 => ""));
398
		if (!$ca_res_key) {
399
			return false;
400
		}
401

    
402
		/* Get the next available CA serial number. */
403
		$ca_serial = ca_get_next_serial($ca);
404
		config_set_path("ca/{$ca_item_config['idx']}", $ca);
405
	}
406

    
407
	$cert_type = cert_type_config_section($type);
408

    
409
	// in case of using Subject Alternative Names use other sections (with postfix '_san')
410
	// pass subjectAltName over environment variable 'SAN'
411
	if ($dn['subjectAltName']) {
412
		putenv("SAN={$dn['subjectAltName']}"); // subjectAltName can be set _only_ via configuration file
413
		$cert_type .= '_san';
414
		unset($dn['subjectAltName']);
415
	}
416

    
417
	$args = array(
418
		"x509_extensions" => $cert_type,
419
		"digest_alg" => $digest_alg,
420
		"encrypt_key" => false);
421
	if ($keytype == 'ECDSA') {
422
		$args["curve_name"] = $ecname;
423
		$args["private_key_type"] = OPENSSL_KEYTYPE_EC;
424
	} else {
425
		$args["private_key_bits"] = (int)$keylen;
426
		$args["private_key_type"] = OPENSSL_KEYTYPE_RSA;
427
	}
428

    
429
	// generate a new key pair
430
	$res_key = openssl_pkey_new($args);
431
	if (!$res_key) {
432
		return false;
433
	}
434

    
435
	// If this is a self-signed cert, blank out the CA and sign with the cert's key
436
	if ($type == "self-signed") {
437
		config_del_path("ca/{$ca_item_config['idx']}");
438
		$ca           = null;
439
		$ca_res_crt   = null;
440
		$ca_res_key   = $res_key;
441
		$ca_serial    = cert_get_random_serial();
442
		$cert['type'] = "server";
443
	}
444

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

    
451
	// sign the certificate using an internal CA
452
	$res_crt = openssl_csr_sign($res_csr, $ca_res_crt, $ca_res_key, $lifetime,
453
				 $args, $ca_serial);
454
	if (!$res_crt) {
455
		return false;
456
	}
457

    
458
	// export our certificate data
459
	if (!openssl_pkey_export($res_key, $str_key) ||
460
	    !openssl_x509_export($res_crt, $str_crt)) {
461
		return false;
462
	}
463

    
464
	// return our certificate information
465
	$cert['crt'] = base64_encode($str_crt);
466
	$cert['prv'] = base64_encode($str_key);
467

    
468
	return true;
469
}
470

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

    
473
	$cert_type = cert_type_config_section($type);
474

    
475
	// in case of using Subject Alternative Names use other sections (with postfix '_san')
476
	// pass subjectAltName over environment variable 'SAN'
477
	if ($dn['subjectAltName']) {
478
		putenv("SAN={$dn['subjectAltName']}"); // subjectAltName can be set _only_ via configuration file
479
		$cert_type .= '_san';
480
		unset($dn['subjectAltName']);
481
	}
482

    
483
	$args = array(
484
		"x509_extensions" => $cert_type,
485
		"req_extensions" => "req_{$cert_type}",
486
		"digest_alg" => $digest_alg,
487
		"encrypt_key" => false);
488
	if ($keytype == 'ECDSA') {
489
		$args["curve_name"] = $ecname;
490
		$args["private_key_type"] = OPENSSL_KEYTYPE_EC;
491
	} else {
492
		$args["private_key_bits"] = (int)$keylen;
493
		$args["private_key_type"] = OPENSSL_KEYTYPE_RSA;
494
	}
495

    
496
	// generate a new key pair
497
	$res_key = openssl_pkey_new($args);
498
	if (!$res_key) {
499
		return false;
500
	}
501

    
502
	// generate a certificate signing request
503
	$res_csr = openssl_csr_new($dn, $res_key, $args);
504
	if (!$res_csr) {
505
		return false;
506
	}
507

    
508
	// export our request data
509
	if (!openssl_pkey_export($res_key, $str_key) ||
510
	    !openssl_csr_export($res_csr, $str_csr)) {
511
		return false;
512
	}
513

    
514
	// return our request information
515
	$cert['csr'] = base64_encode($str_csr);
516
	$cert['prv'] = base64_encode($str_key);
517

    
518
	return true;
519
}
520

    
521
function csr_sign($csr, & $ca, $duration, $type, $altnames, $digest_alg = "sha256") {
522
	$old_err_level = error_reporting(0);
523

    
524
	// Gather the information required for signed cert
525
	$ca_str_crt = base64_decode($ca['crt']);
526
	$ca_str_key = base64_decode($ca['prv']);
527
	$ca_res_key = openssl_pkey_get_private(array(0 => $ca_str_key, 1 => ""));
528
	if (!$ca_res_key) {
529
		return false;
530
	}
531

    
532
	/* Get the next available CA serial number. */
533
	$ca_serial = ca_get_next_serial($ca);
534

    
535
	$cert_type = cert_type_config_section($type);
536

    
537
	if (!empty($altnames)) {
538
		putenv("SAN={$altnames}"); // subjectAltName can be set _only_ via configuration file
539
		$cert_type .= '_san';
540
	}
541

    
542
	$args = array(
543
		"x509_extensions" => $cert_type,
544
		"digest_alg" => $digest_alg,
545
		"req_extensions" => "req_{$cert_type}"
546
	);
547

    
548
	// Sign the new cert and export it in x509 format
549
	openssl_x509_export(openssl_csr_sign($csr, $ca_str_crt, $ca_str_key, $duration, $args, $ca_serial), $n509);
550
	error_reporting($old_err_level);
551

    
552
	return $n509;
553
}
554

    
555
function csr_complete(& $cert, $str_crt) {
556
	$str_key = base64_decode($cert['prv']);
557
	cert_import($cert, $str_crt, $str_key);
558
	unset($cert['csr']);
559
	return true;
560
}
561

    
562
function csr_get_subject($str_crt, $decode = true) {
563

    
564
	if ($decode) {
565
		$str_crt = base64_decode($str_crt);
566
	}
567

    
568
	$components = openssl_csr_get_subject($str_crt);
569

    
570
	if (empty($components) || !is_array($components)) {
571
		return "unknown";
572
	}
573

    
574
	ksort($components);
575
	foreach ($components as $a => $v) {
576
		if (!strlen($subject)) {
577
			$subject = "{$a}={$v}";
578
		} else {
579
			$subject = "{$a}={$v}, {$subject}";
580
		}
581
	}
582

    
583
	return $subject;
584
}
585

    
586
function cert_get_subject($str_crt, $decode = true) {
587

    
588
	if ($decode) {
589
		$str_crt = base64_decode($str_crt);
590
	}
591

    
592
	$inf_crt = openssl_x509_parse($str_crt);
593
	$components = $inf_crt['subject'];
594

    
595
	if (empty($components) || !is_array($components)) {
596
		return "unknown";
597
	}
598

    
599
	ksort($components);
600
	foreach ($components as $a => $v) {
601
		if (is_array($v)) {
602
			ksort($v);
603
			foreach ($v as $w) {
604
				$asubject = "{$a}={$w}";
605
				$subject = (strlen($subject)) ? "{$asubject}, {$subject}" : $asubject;
606
			}
607
		} else {
608
			$asubject = "{$a}={$v}";
609
			$subject = (strlen($subject)) ? "{$asubject}, {$subject}" : $asubject;
610
		}
611
	}
612

    
613
	return $subject;
614
}
615

    
616
function cert_get_subject_array($crt) {
617
	$str_crt = base64_decode($crt);
618
	$inf_crt = openssl_x509_parse($str_crt);
619
	$components = $inf_crt['subject'];
620

    
621
	if (!is_array($components)) {
622
		return;
623
	}
624

    
625
	$subject_array = array();
626

    
627
	foreach ($components as $a => $v) {
628
		$subject_array[] = array('a' => $a, 'v' => $v);
629
	}
630

    
631
	return $subject_array;
632
}
633

    
634
function cert_get_subject_hash($crt) {
635
	$str_crt = base64_decode($crt);
636
	$inf_crt = openssl_x509_parse($str_crt);
637
	return $inf_crt['subject'];
638
}
639

    
640
function cert_get_sans($str_crt, $decode = true) {
641
	if ($decode) {
642
		$str_crt = base64_decode($str_crt);
643
	}
644
	$sans = array();
645
	$crt_details = openssl_x509_parse($str_crt);
646
	if (!empty($crt_details['extensions']['subjectAltName'])) {
647
		$sans = explode(',', $crt_details['extensions']['subjectAltName']);
648
	}
649
	return $sans;
650
}
651

    
652
function cert_get_issuer($str_crt, $decode = true) {
653

    
654
	if ($decode) {
655
		$str_crt = base64_decode($str_crt);
656
	}
657

    
658
	$inf_crt = openssl_x509_parse($str_crt);
659
	$components = $inf_crt['issuer'];
660

    
661
	if (empty($components) || !is_array($components)) {
662
		return "unknown";
663
	}
664

    
665
	ksort($components);
666
	foreach ($components as $a => $v) {
667
		if (is_array($v)) {
668
			ksort($v);
669
			foreach ($v as $w) {
670
				$aissuer = "{$a}={$w}";
671
				$issuer = (strlen($issuer)) ? "{$aissuer}, {$issuer}" : $aissuer;
672
			}
673
		} else {
674
			$aissuer = "{$a}={$v}";
675
			$issuer = (strlen($issuer)) ? "{$aissuer}, {$issuer}" : $aissuer;
676
		}
677
	}
678

    
679
	return $issuer;
680
}
681

    
682
/* Works for both RSA and ECC (crt) and key (prv) */
683
function cert_get_publickey($str_crt, $decode = true, $type = "crt") {
684
	if ($decode) {
685
		$str_crt = base64_decode($str_crt);
686
	}
687
	$certfn = tempnam('/tmp', 'CGPK');
688
	file_put_contents($certfn, $str_crt);
689
	switch ($type) {
690
		case 'prv':
691
			exec("/usr/bin/openssl pkey -in {$certfn} -pubout", $out);
692
			break;
693
		case 'crt':
694
			exec("/usr/bin/openssl x509 -in {$certfn} -inform pem -noout -pubkey", $out);
695
			break;
696
		case 'csr':
697
			exec("/usr/bin/openssl req -in {$certfn} -inform pem -noout -pubkey", $out);
698
			break;
699
		default:
700
			$out = array();
701
			break;
702
	}
703
	unlink($certfn);
704
	return implode("\n", $out);
705
}
706

    
707
function cert_get_purpose($str_crt, $decode = true) {
708
	$extended_oids = array(
709
		"1.3.6.1.5.5.8.2.2" => "IP Security IKE Intermediate",
710
	);
711
	if ($decode) {
712
		$str_crt = base64_decode($str_crt);
713
	}
714
	$crt_details = openssl_x509_parse($str_crt);
715
	$purpose = array();
716
	if (!empty($crt_details['extensions']['keyUsage'])) {
717
		$purpose['ku'] = explode(',', $crt_details['extensions']['keyUsage']);
718
		foreach ($purpose['ku'] as & $ku) {
719
			$ku = trim($ku);
720
			if (array_key_exists($ku, $extended_oids)) {
721
				$ku = $extended_oids[$ku];
722
			}
723
		}
724
	} else {
725
		$purpose['ku'] = array();
726
	}
727
	if (!empty($crt_details['extensions']['extendedKeyUsage'])) {
728
		$purpose['eku'] = explode(',', $crt_details['extensions']['extendedKeyUsage']);
729
		foreach ($purpose['eku'] as & $eku) {
730
			$eku = trim($eku);
731
			if (array_key_exists($eku, $extended_oids)) {
732
				$eku = $extended_oids[$eku];
733
			}
734
		}
735
	} else {
736
		$purpose['eku'] = array();
737
	}
738
	$purpose['ca'] = (stristr($crt_details['extensions']['basicConstraints'], 'CA:TRUE') === false) ? 'No': 'Yes';
739
	$purpose['server'] = (in_array('TLS Web Server Authentication', $purpose['eku'])) ? 'Yes': 'No';
740

    
741
	return $purpose;
742
}
743

    
744
function cert_get_ocspstaple($str_crt, $decode = true) {
745
	if ($decode) {
746
		$str_crt = base64_decode($str_crt);
747
	}
748
	$crt_details = openssl_x509_parse($str_crt);
749
	if (($crt_details['extensions']['tlsfeature'] == "status_request") ||
750
	    !empty($crt_details['extensions']['1.3.6.1.5.5.7.1.24'])) {
751
		return true;
752
	}
753
	return false;
754
}
755

    
756
function cert_format_date($validTS, $validTS_time_t, $outputstring = true) {
757
	$now = new DateTime("now");
758

    
759
	/* Try to create a DateTime object from the full time string */
760
	$date = DateTime::createFromFormat('ymdHis', rtrim($validTS, 'Z'), new DateTimeZone('Z'));
761
	/* If that failed, try using a four digit year */
762
	if ($date === false) {
763
		$date = DateTime::createFromFormat('YmdHis', rtrim($validTS, 'Z'), new DateTimeZone('Z'));
764
	}
765
	/* If that failed, try to create it from the UNIX timestamp */
766
	if (($date === false) && (!empty($validTS_time_t))) {
767
		$date = new DateTime('@' . $validTS_time_t, new DateTimeZone('Z'));
768
	}
769
	/* If we have a valid DateTime object, format it in a nice way */
770
	if ($date !== false) {
771
		$date->setTimezone($now->getTimeZone());
772
		if ($outputstring) {
773
			$date = $date->format(DateTimeInterface::RFC2822);
774
		}
775
	}
776
	return $date;
777
}
778

    
779
function cert_get_dates($str_crt, $decode = true, $outputstring = true) {
780
	if ($decode) {
781
		$str_crt = base64_decode($str_crt);
782
	}
783
	$crt_details = openssl_x509_parse($str_crt);
784

    
785
	$start = cert_format_date($crt_details['validFrom'], $crt_details['validFrom_time_t'], $outputstring);
786
	$end   = cert_format_date($crt_details['validTo'], $crt_details['validTo_time_t'], $outputstring);
787

    
788
	return array($start, $end);
789
}
790

    
791
function cert_get_serial($str_crt, $decode = true) {
792
	if ($decode) {
793
		$str_crt = base64_decode($str_crt);
794
	}
795
	$crt_details = openssl_x509_parse($str_crt);
796
	if (isset($crt_details['serialNumber'])) {
797
		return $crt_details['serialNumber'];
798
	} else {
799
		return NULL;
800
	}
801
}
802

    
803
function cert_get_sigtype($str_crt, $decode = true) {
804
	if ($decode) {
805
		$str_crt = base64_decode($str_crt);
806
	}
807
	$crt_details = openssl_x509_parse($str_crt);
808

    
809
	$signature = array();
810
	if (isset($crt_details['signatureTypeSN']) && !empty($crt_details['signatureTypeSN'])) {
811
		$signature['shortname'] = $crt_details['signatureTypeSN'];
812
	}
813
	if (isset($crt_details['signatureTypeLN']) && !empty($crt_details['signatureTypeLN'])) {
814
		$signature['longname'] = $crt_details['signatureTypeLN'];
815
	}
816
	if (isset($crt_details['signatureTypeNID']) && !empty($crt_details['signatureTypeNID'])) {
817
		$signature['nid'] = $crt_details['signatureTypeNID'];
818
	}
819

    
820
	return $signature;
821
}
822

    
823
function is_openvpn_server_ca($caref) {
824
	foreach(config_get_path('openvpn/openvpn-server', []) as $ovpns) {
825
		if ($ovpns['caref'] == $caref) {
826
			return true;
827
		}
828
	}
829
	return false;
830
}
831

    
832
function is_openvpn_client_ca($caref) {
833
	foreach(config_get_path('openvpn/openvpn-client', []) as $ovpnc) {
834
		if ($ovpnc['caref'] == $caref) {
835
			return true;
836
		}
837
	}
838
	return false;
839
}
840

    
841
function is_ipsec_peer_ca($caref) {
842
	foreach (config_get_path('ipsec/phase1', []) as $ipsec) {
843
		if ($ipsec['caref'] == $caref) {
844
			return true;
845
		}
846
	}
847
	return false;
848
}
849

    
850
function is_ldap_peer_ca($caref) {
851
	foreach (config_get_path('system/authserver', []) as $authserver) {
852
		if (($authserver['ldap_caref'] == $caref) &&
853
		    ($authserver['ldap_urltype'] != 'Standard TCP')) {
854
			return true;
855
		}
856
	}
857
	return false;
858
}
859

    
860
function ca_in_use($caref) {
861
	return (is_openvpn_server_ca($caref) ||
862
		is_openvpn_client_ca($caref) ||
863
		is_ipsec_peer_ca($caref) ||
864
		is_ldap_peer_ca($caref));
865
}
866

    
867
function is_kea_cert(string $certref): bool {
868
	if (config_path_enabled('kea/ha', 'tls')) {
869
		if (config_get_path('kea/ha/scertref') === $certref) {
870
			return true;
871
		}
872
		if (config_path_enabled('kea/ha', 'mutualtls') &&
873
		    (config_get_path('kea/ha/ccertref') === $certref)) {
874
			return true;
875
		}
876
	}
877

    
878
	return false;
879
}
880

    
881
function is_user_cert($certref) {
882
	foreach (config_get_path('system/user', []) as $user) {
883
		if (!is_array($user['cert'])) {
884
			continue;
885
		}
886
		foreach ($user['cert'] as $cert) {
887
			if ($certref == $cert) {
888
				return true;
889
			}
890
		}
891
	}
892
	return false;
893
}
894

    
895
function is_openvpn_server_cert($certref) {
896
	foreach (config_get_path('openvpn/openvpn-server', []) as $ovpns) {
897
		if ($ovpns['certref'] == $certref) {
898
			return true;
899
		}
900
	}
901
	return false;
902
}
903

    
904
function is_openvpn_client_cert($certref) {
905
	foreach (config_get_path('openvpn/openvpn-client', []) as $ovpnc) {
906
		if ($ovpnc['certref'] == $certref) {
907
			return true;
908
		}
909
	}
910
	return false;
911
}
912

    
913
function is_ipsec_cert($certref) {
914
	foreach(config_get_path('ipsec/phase1', []) as $ipsec) {
915
		if ($ipsec['certref'] == $certref) {
916
			return true;
917
		}
918
	}
919
	return false;
920
}
921

    
922
function is_webgui_cert($certref) {
923
	if ((config_get_path('system/webgui/ssl-certref') == $certref) &&
924
	    (config_get_path('system/webgui/protocol') != "http")) {
925
		return true;
926
	}
927
}
928

    
929
function is_package_cert($certref) {
930
	$pluginparams = array();
931
	$pluginparams['type'] = 'certificates';
932
	$pluginparams['event'] = 'used_certificates';
933

    
934
	$certificates_used_by_packages = pkg_call_plugins('plugin_certificates', $pluginparams);
935

    
936
	/* Check if any package is using certificate */
937
	foreach ($certificates_used_by_packages as $name => $package) {
938
		if (is_array($package['certificatelist'][$certref]) &&
939
		    isset($package['certificatelist'][$certref]) > 0) {
940
			return true;
941
		}
942
	}
943
}
944

    
945
function is_captiveportal_cert($certref) {
946
	foreach (config_get_path('captiveportal', []) as $portal) {
947
		if (isset($portal['enable']) && isset($portal['httpslogin']) && ($portal['certref'] == $certref)) {
948
			return true;
949
		}
950
	}
951
	return false;
952
}
953

    
954
function is_unbound_cert($certref) {
955
	if (config_path_enabled('unbound') &&
956
	    config_path_enabled('unbound','enablessl') &&
957
	    (config_get_path('unbound/sslcertref') == $certref)) {
958
		return true;
959
	}
960
}
961

    
962
function cert_in_use($certref) {
963
	return (is_kea_cert($certref) ||
964
		is_webgui_cert($certref) ||
965
		is_user_cert($certref) ||
966
		is_openvpn_server_cert($certref) ||
967
		is_openvpn_client_cert($certref) ||
968
		is_ipsec_cert($certref) ||
969
		is_captiveportal_cert($certref) ||
970
		is_unbound_cert($certref) ||
971
		is_package_cert($certref));
972
}
973

    
974
function cert_usedby_description($refid, $certificates_used_by_packages) {
975
	$result = "";
976
	if (is_array($certificates_used_by_packages)) {
977
		foreach ($certificates_used_by_packages as $name => $package) {
978
			if (isset($package['certificatelist'][$refid])) {
979
				$hint = "" ;
980
				if (is_array($package['certificatelist'][$refid])) {
981
					foreach ($package['certificatelist'][$refid] as $cert_used) {
982
						$hint = $hint . $cert_used['usedby']."\n";
983
					}
984
				}
985
				$count = count($package['certificatelist'][$refid]);
986
				$result .= "<div title='".htmlspecialchars($hint)."'>";
987
				$result .= htmlspecialchars($package['pkgname'])." ($count)<br />";
988
				$result .= "</div>";
989
			}
990
		}
991
	}
992
	return $result;
993
}
994

    
995
/* Detect a rollover at 2038 on some platforms (e.g. ARM)
996
 * See: https://redmine.pfsense.org/issues/9098 */
997
function cert_get_max_lifetime() {
998
	global $cert_max_lifetime;
999
	$max = $cert_max_lifetime;
1000

    
1001
	$current_time = time();
1002
	while ((int)($current_time + ($max * 24 * 60 * 60)) < 0) {
1003
		$max--;
1004
	}
1005
	return min($max, $cert_max_lifetime);
1006
}
1007

    
1008
/* Detect a rollover at 2050 with UTCTime
1009
 * See: https://redmine.pfsense.org/issues/9098 */
1010
function crl_get_max_lifetime() {
1011
	$now = new DateTime("now");
1012
	$utctime_before_roll = DateTime::createFromFormat('Ymd', '20491231');
1013
	if ($now !== false) {
1014
		$interval = $now->diff($utctime_before_roll);
1015
		$max_days = abs($interval->days);
1016
		/* Reduce the max well below the rollover time */
1017
		if ($max_days > 1000) {
1018
			$max_days -= 1000;
1019
		}
1020
		return min($max_days, cert_get_max_lifetime());
1021
	}
1022

    
1023
	/* Cannot use date functions, so use a lower default max. */
1024
	return min(7000, cert_get_max_lifetime());
1025
}
1026

    
1027
/**
1028
 * Directly modifes the Certificate Revocation List and adds it to config.
1029
 */
1030
function crl_create(& $crl, $caref, $name, $serial = 0, $lifetime = 3650) {
1031
	$max_lifetime = crl_get_max_lifetime();
1032
	$ca = lookup_ca($caref);
1033
	$ca = $ca['item'];
1034
	if (!$ca) {
1035
		return false;
1036
	}
1037
	$crl['descr'] = $name;
1038
	$crl['caref'] = $caref;
1039
	$crl['serial'] = $serial;
1040
	$crl['lifetime'] = ($lifetime > $max_lifetime) ? $max_lifetime : $lifetime;
1041
	$crl['cert'] = array();
1042

    
1043
	config_set_path('crl/', $crl);
1044
	return $crl;
1045
}
1046

    
1047
/**
1048
 * @param array &$crl_config Must contain the CRL config index and item.
1049
 */
1050
function crl_update(& $crl_config) {
1051
	$crl = &$crl_config['item'];
1052
	require_once('ASN1.php');
1053
	require_once('ASN1_UTF8STRING.php');
1054
	require_once('ASN1_ASCIISTRING.php');
1055
	require_once('ASN1_BITSTRING.php');
1056
	require_once('ASN1_BOOL.php');
1057
	require_once('ASN1_GENERALTIME.php');
1058
	require_once('ASN1_INT.php');
1059
	require_once('ASN1_ENUM.php');
1060
	require_once('ASN1_NULL.php');
1061
	require_once('ASN1_OCTETSTRING.php');
1062
	require_once('ASN1_OID.php');
1063
	require_once('ASN1_SEQUENCE.php');
1064
	require_once('ASN1_SET.php');
1065
	require_once('ASN1_SIMPLE.php');
1066
	require_once('ASN1_TELETEXSTRING.php');
1067
	require_once('ASN1_UTCTIME.php');
1068
	require_once('OID.php');
1069
	require_once('X509.php');
1070
	require_once('X509_CERT.php');
1071
	require_once('X509_CRL.php');
1072

    
1073
	$max_lifetime = crl_get_max_lifetime();
1074
	$ca = lookup_ca($crl['caref']);
1075
	$ca = $ca['item'];
1076
	if (!$ca) {
1077
		return false;
1078
	}
1079
	// If we have text but no certs, it was imported and cannot be updated.
1080
	if (($crl["method"] != "internal") && (!empty($crl['text']) && empty($crl['cert']))) {
1081
		return false;
1082
	}
1083
	$crl['serial']++;
1084
	config_set_path("crl/{$crl_config['idx']}", $crl);
1085
	$ca_cert = \Ukrbublik\openssl_x509_crl\X509::pem2der(base64_decode($ca['crt']));
1086
	$ca_pkey = openssl_pkey_get_private(base64_decode($ca['prv']));
1087

    
1088
	$crlconf = array(
1089
		'no' => $crl['serial'],
1090
		'version' => 2,
1091
		'days' => ($crl['lifetime'] > $max_lifetime) ? $max_lifetime : $crl['lifetime'],
1092
		'alg' => OPENSSL_ALGO_SHA1,
1093
		'revoked' => array()
1094
	);
1095

    
1096
	if (is_array($crl['cert']) && (count($crl['cert']) > 0)) {
1097
		foreach ($crl['cert'] as $cert) {
1098
			/* Determine the serial number to revoke */
1099
			if (isset($cert['serial'])) {
1100
				$serial = $cert['serial'];
1101
			} elseif (isset($cert['crt'])) {
1102
				$serial = cert_get_serial($cert['crt'], true);
1103
			} else {
1104
				continue;
1105
			}
1106
			$crlconf['revoked'][] = array(
1107
				'serial' => $serial,
1108
				'rev_date' => $cert['revoke_time'],
1109
				'reason' => ($cert['reason'] == -1) ? null : (int) $cert['reason'],
1110
			);
1111
		}
1112
	}
1113

    
1114
	$crl_data = \Ukrbublik\openssl_x509_crl\X509_CRL::create($crlconf, $ca_pkey, $ca_cert);
1115
	$crl['text'] = base64_encode(\Ukrbublik\openssl_x509_crl\X509::der2pem4crl($crl_data));
1116
	config_set_path("crl/{$crl_config['idx']}", $crl);
1117

    
1118
	return $crl['text'];
1119
}
1120

    
1121
/**
1122
 * @param array|string $cert        The cert item or cert serial to revoke.
1123
 * @param array        &$crl_config Must contain the CRL config index and item.
1124
 * @param int          $reason      Revocation reason; defined by RFC5280.
1125
 */
1126
function cert_revoke($cert, &$crl_config, $reason = -1) {
1127
	$crl = &$crl_config['item'];
1128
	if (is_cert_revoked($cert, $crl['refid'])) {
1129
		return true;
1130
	}
1131
	// If we have text but no certs, it was imported and cannot be updated.
1132
	if (!is_crl_internal($crl)) {
1133
		return false;
1134
	}
1135

    
1136
	if (!is_array($cert)) {
1137
		/* If passed a not an array but a serial string, set it up as an
1138
		 * array with the serial number defined */
1139
		$rcert = array();
1140
		$rcert['serial'] = $cert;
1141
	} else {
1142
		/* If passed a certificate entry, read out the serial and store
1143
		 * it separately. */
1144
		$rcert = $cert;
1145
		$rcert['serial'] = cert_get_serial($cert['crt']);
1146
	}
1147
	$rcert['reason'] = $reason;
1148
	$rcert['revoke_time'] = time();
1149
	$crl['cert'][] = $rcert;
1150
	crl_update($crl_config);
1151
	return true;
1152
}
1153

    
1154
/**
1155
 * @param array|string $cert        The cert item or cert serial to unrevoke.
1156
 * @param array        &$crl_config Must contain the CRL config index and item.
1157
 */
1158
function cert_unrevoke($cert, &$crl_config) {
1159
	$crl = &$crl_config['item'];
1160
	if (!is_crl_internal($crl)) {
1161
		return false;
1162
	}
1163

    
1164
	$serial = crl_get_entry_serial($cert);
1165

    
1166
	foreach ($crl['cert'] as $id => $rcert) {
1167
		/* Check for a match by refid, name, or serial number */
1168
		if (($rcert['refid'] == $cert['refid']) ||
1169
		    ($rcert['descr'] == $cert['descr']) ||
1170
		    (crl_get_entry_serial($rcert) == $serial)) {
1171
			unset($crl['cert'][$id]);
1172
			if (count($crl['cert']) == 0) {
1173
				// Protect against accidentally switching the type to imported, for older CRLs
1174
				if (!isset($crl['method'])) {
1175
					$crl['method'] = "internal";
1176
				}
1177
				crl_update($crl_config);
1178
			} else {
1179
				crl_update($crl_config);
1180
			}
1181
			return true;
1182
		}
1183
	}
1184
	return false;
1185
}
1186

    
1187
/* Compare two certificates to see if they match. */
1188
function cert_compare($cert1, $cert2) {
1189
	/* Ensure two certs are identical by first checking that their issuers match, then
1190
		subjects, then serial numbers, and finally the moduli. Anything less strict
1191
		could accidentally count two similar, but different, certificates as
1192
		being identical. */
1193
	$c1 = base64_decode($cert1['crt']);
1194
	$c2 = base64_decode($cert2['crt']);
1195
	if ((cert_get_issuer($c1, false) == cert_get_issuer($c2, false)) &&
1196
	    (cert_get_subject($c1, false) == cert_get_subject($c2, false)) &&
1197
	    (cert_get_serial($c1, false) == cert_get_serial($c2, false)) &&
1198
	    (cert_get_publickey($c1, false) == cert_get_publickey($c2, false))) {
1199
		return true;
1200
	}
1201
	return false;
1202
}
1203

    
1204
/****f* certs/crl_get_entry_serial
1205
 * NAME
1206
 *   crl_get_entry_serial - Take a CRL entry and determine the associated serial
1207
 * INPUTS
1208
 *   $entry: CRL certificate list entry to inspect, or serial string
1209
 * RESULT
1210
 *   The requested serial string, if present, or null if it cannot be determined.
1211
 ******/
1212

    
1213
function crl_get_entry_serial($entry) {
1214
	/* Check the passed entry several ways to determine the serial */
1215
	if (isset($entry['serial']) && (strlen($entry['serial']) > 0)) {
1216
		/* Entry is an array with a viable 'serial' element */
1217
		return $entry['serial'];
1218
	} elseif (isset($entry['crt'])) {
1219
		/* Entry is an array with certificate text which can be used to
1220
		 * determine the serial */
1221
		return cert_get_serial($entry['crt'], true);
1222
	} elseif (cert_validate_serial($entry, false, true) != null) {
1223
		/* Entry is a valid serial string */
1224
		return $entry;
1225
	}
1226
	/* Unable to find or determine a serial number */
1227
	return null;
1228
}
1229

    
1230
/****f* certs/cert_validate_serial
1231
 * NAME
1232
 *   cert_validate_serial - Validate a given string to test if it can be used as
1233
 *                          a certificate serial.
1234
 * INPUTS
1235
 *   $serial     : Serial number string to test
1236
 *   $returnvalue: Whether to return the parsed value or true/false
1237
 * RESULT
1238
 *   If $returnvalue is true, then the parsed ASN.1 integer value string for
1239
 *     $serial or null if invalid
1240
 *   If $returnvalue is false, then true/false based on whether or not $serial
1241
 *     is valid.
1242
 ******/
1243

    
1244
function cert_validate_serial($serial, $returnvalue = false, $allowlarge = false) {
1245
	require_once('ASN1.php');
1246
	require_once('ASN1_INT.php');
1247
	/* The ASN.1 parsing function will throw an exception if the value is
1248
	 * invalid, so take advantage of that to catch other error as well. */
1249
	try {
1250
		/* If the serial is not a string, then do not bother with
1251
		 * further tests. */
1252
		if (!is_string($serial)) {
1253
			throw new Exception('Not a string');
1254
		}
1255
		/* Process a hex string */
1256
		if ((substr($serial, 0, 2) == '0x')) {
1257
			/* If the string is hex, then it must contain only
1258
			 * valid hex digits */
1259
			if (!ctype_xdigit(substr($serial, 2))) {
1260
				throw new Exception('Not a valid hex string');
1261
			}
1262
			/* Convert to decimal */
1263
			$serial = base_convert($serial, 16, 10);
1264
		}
1265

    
1266
		/* Unfortunately, PHP openssl_csr_sign() limits serial numbers to a
1267
		 * PHP integer, so we cannot generate large numbers up to the maximum
1268
		 * allowed ASN.1 size (2^159). We are limited to PHP_INT_MAX --
1269
		 * As such, numbers larger than that limit should be rejected */
1270
		if ($serial > PHP_INT_MAX) {
1271
			throw new Exception('Serial too large for PHP OpenSSL');
1272
		}
1273

    
1274
		/* Attempt to create an ASN.1 integer, if it fails, an exception will be thrown */
1275
		$asn1serial = new \Ukrbublik\openssl_x509_crl\ASN1_INT( $serial );
1276
		return ($returnvalue) ? $asn1serial->content : true;
1277
	} catch (Exception $ex) {
1278
		/* No matter what the error is, return null or false depending
1279
		 * on what was requested. */
1280
		return ($returnvalue) ? null : false;
1281
	}
1282
}
1283

    
1284
/****f* certs/cert_generate_serial
1285
 * NAME
1286
 *   cert_generate_serial - Generate a random positive integer usable as a
1287
 *                          certificate serial number
1288
 * INPUTS
1289
 *   None
1290
 * RESULT
1291
 *   Integer representing an ASN.1 compatible certificate serial number.
1292
 ******/
1293

    
1294
function cert_generate_serial() {
1295
	/* Use a separate function for this to make it easier to use a better
1296
	 * randomization function in the future. */
1297

    
1298
	/* Unfortunately, PHP openssl_csr_sign() limits serial numbers to a
1299
	 * PHP integer, so we cannot generate large numbers up to the maximum
1300
	 * allowed ASN.1 size (2^159). We are limited to PHP_INT_MAX */
1301
	return random_int(1, PHP_INT_MAX);
1302
}
1303

    
1304
/****f* certs/ca_has_serial
1305
 * NAME
1306
 *   ca_has_serial - Check if a serial number is used by any certificate in a given CA
1307
 * INPUTS
1308
 *   $ca    : Certificate Authority to check
1309
 *   $serial: Serial number to check
1310
 * RESULT
1311
 *   true if the serial number is in use by a certificate issued by this CA,
1312
 *   false otherwise.
1313
 ******/
1314

    
1315
function ca_has_serial($caref, $serial) {
1316
	/* Check certs first -- more likely to find a hit */
1317
	foreach (config_get_path('cert', []) as $cert) {
1318
		if (($cert['caref'] == $caref) &&
1319
		    (cert_get_serial($cert['crt'], true) == $serial)) {
1320
			/* If this certificate is issued by the CA in question
1321
			 * and has a matching serial number, stop processing
1322
			 * and return true. */
1323
			return true;
1324
		}
1325
	}
1326

    
1327
	/* Check the CA itself */
1328
	$this_ca = lookup_ca($caref);
1329
	$this_ca = $this_ca['item'];
1330
	$this_serial = cert_get_serial($this_ca['crt'], true);
1331
	if ($serial == $this_serial) {
1332
		return true;
1333
	}
1334

    
1335
	/* Check other CAs for a match (intermediates signed by this CA) */
1336
	foreach (config_get_path('ca', []) as $ca) {
1337
		if (($ca['caref'] == $caref) &&
1338
		    (cert_get_serial($ca['crt'], true) == $serial)) {
1339
			/* If this CA is issued by the CA in question
1340
			 * and has a matching serial number, stop processing
1341
			 * and return true. */
1342
			return true;
1343
		}
1344
	}
1345

    
1346
	return false;
1347
}
1348

    
1349
/****f* certs/cert_get_random_serial
1350
 * NAME
1351
 *   cert_get_random_serial - Generate a random certificate serial unique in a CA
1352
 * INPUTS
1353
 *   $caref : Certificate Authority refid to test for serial uniqueness.
1354
 * RESULT
1355
 *   Random serial number which is not in use by any known certificate in a CA
1356
 ******/
1357

    
1358
function cert_get_random_serial($caref = '') {
1359
	/* Number of attempts to generate a usable serial. Multiple attempts
1360
	 *  are necessary to ensure that the number is usable and unique. */
1361
	$attempts = 10;
1362

    
1363
	/* Default value, -1 indicates an error */
1364
	$serial = -1;
1365

    
1366
	for ($i=0; $i < $attempts; $i++) {
1367
		/* Generate a random serial */
1368
		$serial = cert_generate_serial();
1369
		/* Check that the serial number is usable and unique:
1370
		 *  * Cannot be 0
1371
		 *  * Must be a valid ASN.1 serial number
1372
		 *  * Cannot be used by any other certificate on this CA */
1373
		if (($serial != 0) &&
1374
		    cert_validate_serial($serial) &&
1375
		    !ca_has_serial($caref, $serial)) {
1376
			/* If all conditions are met, we have a good serial, so stop. */
1377
			break;
1378
		}
1379
	}
1380
	return $serial;
1381
}
1382

    
1383
/****f* certs/ca_get_next_serial
1384
 * NAME
1385
 *   ca_get_next_serial - Get the next available serial number for a CA
1386
 * INPUTS
1387
 *   $ca: Reference to a CA entry
1388
 * RESULT
1389
 *   A randomized serial number (if enabled for a CA) or the next sequential value.
1390
 ******/
1391

    
1392
function ca_get_next_serial(& $ca) {
1393
	$ca_serial = null;
1394
	/* Get a randomized serial if enabled */
1395
	if ($ca['randomserial'] == 'enabled') {
1396
		$ca_serial = cert_get_random_serial($ca['refid']);
1397
	}
1398
	/* Initialize the sequential serial to be safe */
1399
	if (empty($ca['serial'])) {
1400
		$ca['serial'] = 0;
1401
	}
1402
	/* If not using a randomized serial, or randomizing the serial
1403
	 * failed, then fall back to sequential serials. */
1404
	return (empty($ca_serial) || ($ca_serial == -1)) ? ++$ca['serial'] : $ca_serial;
1405
}
1406

    
1407
/****f* certs/crl_contains_cert
1408
 * NAME
1409
 *   crl_contains_cert - Check if a certificate is present in a CRL
1410
 * INPUTS
1411
 *   $crl : CRL to check
1412
 *   $cert: Certificate to test
1413
 * RESULT
1414
 *   true if the CRL contains the certificate, false otherwise
1415
 ******/
1416

    
1417
function crl_contains_cert($crl, $cert) {
1418
	if (!is_array(config_get_path('crl')) ||
1419
	    !is_array($crl['cert'])) {
1420
		return false;
1421
	}
1422

    
1423
	/* Find the issuer of this CRL */
1424
	$ca = lookup_ca($crl['caref']);
1425
	$ca = $ca['item'];
1426
	$crlissuer = is_array($cert) ? cert_get_subject($ca['crt']) : null;
1427
	$serial = crl_get_entry_serial($cert);
1428

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

    
1432
	/* If the requested certificate was not issued by the
1433
	 * same CA as the CRL, then do not bother checking this
1434
	 * CRL. */
1435
	if ($issuer != $crlissuer) {
1436
		return false;
1437
	}
1438

    
1439
	/* Check CRL entries to see if the certificate serial is revoked */
1440
	foreach ($crl['cert'] as $rcert) {
1441
		if (crl_get_entry_serial($rcert) == $serial) {
1442
			return true;
1443
		}
1444
	}
1445

    
1446
	/* Certificate was not found in the CRL */
1447
	return false;
1448
}
1449

    
1450
/****f* certs/is_cert_revoked
1451
 * NAME
1452
 *   is_cert_revoked - Test if a given certificate or serial is revoked
1453
 * INPUTS
1454
 *   $cert  : Certificate entry or serial number to test
1455
 *   $crlref: CRL to check for revoked entries, or empty to check all CRLs
1456
 * RESULT
1457
 *   true if the requested entry is revoked
1458
 *   false if the requested entry is not revoked
1459
 ******/
1460

    
1461
function is_cert_revoked($cert, $crlref = "") {
1462
	if (!is_array(config_get_path('crl'))) {
1463
		return false;
1464
	}
1465

    
1466
	if (!empty($crlref)) {
1467
		$crl = lookup_crl($crlref);
1468
		$crl = $crl['item'];
1469
		return crl_contains_cert($crl, $cert);
1470
	} else {
1471
		if (!is_array($cert)) {
1472
			/* If passed a serial, then it cannot be definitively
1473
			 * matched in this way since we do not know the CA
1474
			 * associated with the bare serial. */
1475
			return null;
1476
		}
1477

    
1478
		/* Check every CRL in the configuration for a match */
1479
		foreach (config_get_path('crl', []) as $crl) {
1480
			if (!is_array($crl['cert'])) {
1481
				continue;
1482
			}
1483
			if (crl_contains_cert($crl, $cert)) {
1484
				return true;
1485
			}
1486
		}
1487
	}
1488
	return false;
1489
}
1490

    
1491
function is_openvpn_server_crl($crlref) {
1492
	foreach (config_get_path('openvpn/openvpn-server', []) as $ovpns) {
1493
		if (!empty($ovpns['crlref']) && ($ovpns['crlref'] == $crlref)) {
1494
			return true;
1495
		}
1496
	}
1497
	return false;
1498
}
1499

    
1500
function is_package_crl($crlref) {
1501
	$pluginparams = array();
1502
	$pluginparams['type'] = 'certificates';
1503
	$pluginparams['event'] = 'used_crl';
1504

    
1505
	$certificates_used_by_packages = pkg_call_plugins('plugin_certificates', $pluginparams);
1506

    
1507
	/* Check if any package is using CRL */
1508
	foreach ($certificates_used_by_packages as $name => $package) {
1509
		if (is_array($package['certificatelist'][$crlref]) &&
1510
		    (count($package['certificatelist'][$crlref]) > 0)) {
1511
			return true;
1512
		}
1513
	}
1514
}
1515

    
1516
// Keep this general to allow for future expansion. See cert_in_use() above.
1517
function crl_in_use($crlref) {
1518
	return (is_openvpn_server_crl($crlref) ||
1519
		is_package_crl($crlref));
1520
}
1521

    
1522
function is_crl_internal($crl) {
1523
	return (!(!empty($crl['text']) && empty($crl['cert'])) || ($crl["method"] == "internal"));
1524
}
1525

    
1526
function cert_get_cn($crt, $isref = false) {
1527
	/* If this is a certref, not an actual cert, look up the cert first */
1528
	if ($isref) {
1529
		$cert = lookup_cert($crt);
1530
		$cert = $cert['item'];
1531
		/* If it's not a valid cert, bail. */
1532
		if (!(is_array($cert) && !empty($cert['crt']))) {
1533
			return "";
1534
		}
1535
		$cert = $cert['crt'];
1536
	} else {
1537
		$cert = $crt;
1538
	}
1539
	$sub = cert_get_subject_array($cert);
1540
	if (is_array($sub)) {
1541
		foreach ($sub as $s) {
1542
			if (strtoupper($s['a']) == "CN") {
1543
				// Multiple CNs are not supported so use the first available; see RFC9525, RFC5280
1544
				return is_array($s['v']) ? $s['v'][array_key_first($s['v'])] : $s['v'];
1545
			}
1546
		}
1547
	}
1548
	return "";
1549
}
1550

    
1551
function cert_escape_x509_chars($str, $reverse = false) {
1552
	/* Characters which need escaped when present in x.509 fields.
1553
	 * See https://www.ietf.org/rfc/rfc4514.txt
1554
	 *
1555
	 * The backslash (\) must be listed first in these arrays!
1556
	 */
1557
	$cert_directory_string_special_chars = array('\\', '"', '#', '+', ',', ';', '<', '=', '>');
1558
	$cert_directory_string_special_chars_esc = array('\\\\', '\"', '\#', '\+', '\,', '\;', '\<', '\=', '\>');
1559
	if ($reverse) {
1560
		return str_replace($cert_directory_string_special_chars_esc, $cert_directory_string_special_chars, $str);
1561
	} else {
1562
		/* First unescape and then escape again, to prevent possible double escaping. */
1563
		return str_replace($cert_directory_string_special_chars, $cert_directory_string_special_chars_esc, cert_escape_x509_chars($str, true));
1564
	}
1565
}
1566

    
1567
function cert_add_altname_type($str) {
1568
	$type = "";
1569
	if (is_ipaddr($str)) {
1570
		$type = "IP";
1571
	} elseif (is_hostname($str, true)) {
1572
		$type = "DNS";
1573
	} elseif (is_URL($str)) {
1574
		$type = "URI";
1575
	} elseif (filter_var($str, FILTER_VALIDATE_EMAIL)) {
1576
		$type = "email";
1577
	}
1578
	if (!empty($type)) {
1579
		return "{$type}:" . cert_escape_x509_chars($str);
1580
	} else {
1581
		return null;
1582
	}
1583
}
1584

    
1585
function cert_type_config_section($type) {
1586
	switch ($type) {
1587
		case "ca":
1588
			$cert_type = "v3_ca";
1589
			break;
1590
		case "server":
1591
		case "self-signed":
1592
			$cert_type = "server";
1593
			break;
1594
		default:
1595
			$cert_type = "usr_cert";
1596
			break;
1597
	}
1598
	return $cert_type;
1599
}
1600

    
1601
/****f* certs/is_cert_locally_renewable
1602
 * NAME
1603
 *   is_cert_locally_renewable - Check to see if an existing certificate can be
1604
 *                               renewed by a local internal CA.
1605
 * INPUTS
1606
 *   $cert : The certificate to be tested
1607
 * RESULT
1608
 *   true if the certificate can be locally renewed, false otherwise.
1609
 ******/
1610

    
1611
function is_cert_locally_renewable($cert) {
1612
	/* If there is no certificate or private key string, this entry is either
1613
	 * invalid or cannot be renewed. */
1614
	if (empty($cert['crt']) || empty($cert['prv'])) {
1615
		return false;
1616
	}
1617

    
1618
	/* Get subject and issuer values to test for self-signed state */
1619
	$subj = cert_get_subject($cert['crt']);
1620
	$issuer = cert_get_issuer($cert['crt']);
1621

    
1622
	/* Lookup CA for this certificate */
1623
	$ca = array();
1624
	if (!empty($cert['caref'])) {
1625
		$ca = lookup_ca($cert['caref']);
1626
		$ca = $ca['item'];
1627
	}
1628

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

    
1634
/* Strict certificate requirements based on
1635
 * https://redmine.pfsense.org/issues/9825
1636
 */
1637
global $cert_strict_values;
1638
$cert_strict_values = array(
1639
	'max_server_cert_lifetime' => 398,
1640
	'digest_blacklist' => array('md4', 'RSA-MD4',  'md5', 'RSA-MD5', 'md5-sha1',
1641
					'mdc2', 'RSA-MDC2', 'sha1', 'RSA-SHA1',
1642
					'RSA-SHA1-2', 'sha224', 'RSA-SHA224'),
1643
	'min_private_key_bits' => 2048,
1644
	'ec_curve' => 'prime256v1',
1645
);
1646

    
1647
/****f* certs/cert_renew
1648
 * NAME
1649
 *   cert_renew - Renew an existing internal CA or certificate
1650
 * INPUTS
1651
 *   $cert_config : An array containing the config array path and the ca or
1652
 *                  cert item; it is modified directly and written to config.
1653
 *   $reusekey : Whether or not to reuse the existing key for the certificate
1654
 *      true: Reuse the existing key (Default)
1655
 *      false: Generate a new key based on current (or enforced minimum) parameters
1656
 *   $strictsecurity : Whether or not to enforce stricter security for specific attributes
1657
 *      true: Enforce maximum lifetime for server certs, minimum digest type, and
1658
 *            minimum private key size. See https://redmine.pfsense.org/issues/9825
1659
 *      false: Use existing values as-is (Default).
1660
 * RESULT
1661
 *   true if successful, false if failure.
1662
 * NOTES
1663
 *   See https://redmine.pfsense.org/issues/9842 for more information on behavior.
1664
 *   Does NOT run write_config(), that must be performed by the caller.
1665
 ******/
1666

    
1667
function cert_renew(& $cert_config, $reusekey = true, $strictsecurity = false, $reuseserial = false) {
1668
	global $cert_strict_values, $cert_curve_compatible, $curve_compatible_list;
1669

    
1670
	$cert = &$cert_config['item'];
1671
	/* If there is no certificate or private key string, this entry is either
1672
	 *  invalid or cannot be renewed by this function. */
1673
	if (empty($cert['crt']) || empty($cert['prv'])) {
1674
		return false;
1675
	}
1676

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

    
1680
	/* No details, must not be valid in some way */
1681
	if (!array($cert_details) || empty($cert_details)) {
1682
		return false;
1683
	}
1684

    
1685
	$subj = cert_get_subject($cert['crt']);
1686
	$issuer = cert_get_issuer($cert['crt']);
1687
	$purpose = cert_get_purpose($cert['crt']);
1688

    
1689
	$res_key = openssl_pkey_get_private(base64_decode($cert['prv']));
1690
	$key_details = openssl_pkey_get_details($res_key);
1691

    
1692
	/* Form a new Distinguished Name from the existing values.
1693
	 * Note: Deprecated/unsupported DN fields may not be carried forward, but
1694
	 *       may be preserved to avoid altering a subject.
1695
	 */
1696
	$subject_map = array(
1697
		'CN' => 'commonName',
1698
		'C' => 'countryName',
1699
		'ST' => 'stateOrProvinceName',
1700
		'L' => 'localityName',
1701
		'O' => 'organizationName',
1702
		'OU' => 'organizationalUnitName',
1703
		'emailAddress' => 'emailAddress', /* deprecated, but commonly found in older entries */
1704
	);
1705
	$dn = array();
1706
	/* This is necessary to ensure the order of subject components is
1707
	 * identical on the old and new certificate. */
1708
	foreach ($cert_details['subject'] as $p => $v) {
1709
		if (array_key_exists($p, $subject_map)) {
1710
			$dn[$subject_map[$p]] = $v;
1711
		}
1712
	}
1713

    
1714
	/* Test for self-signed or signed by a CA */
1715
	$selfsigned = ($subj == $issuer);
1716

    
1717
	/* Determine the type if it is not specified directly */
1718
	if (array_key_exists('serial', $cert)) {
1719
		/* If a serial value is present, this must be a CA */
1720
		$cert['type'] = 'ca';
1721
	} elseif (empty($cert['type'])) {
1722
		/* Automatically determine certificate type if unset based on purpose value */
1723
		$cert['type'] = ($purpose['server'] == 'Yes') ? 'server' : 'user';
1724
	}
1725
	config_set_path("{$cert_config['path']}", $cert);
1726

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

    
1730
	/* Reuse lifetime (convert seconds to days) */
1731
	$lifetime = (int) round(($cert_details['validTo_time_t'] - $cert_details['validFrom_time_t']) / 86400);
1732

    
1733
	/* If we are enforcing strict security, then cap the lifetime for server certificates */
1734
	if (($cert_type == 'server') && $strictsecurity &&
1735
	    ($lifetime > $cert_strict_values['max_server_cert_lifetime'])) {
1736
		$lifetime = $cert_strict_values['max_server_cert_lifetime'];
1737
	}
1738

    
1739
	/* Reuse SAN list, or, if empty, add CN as SAN. */
1740
	$sans = str_replace("IP Address", "IP", $cert_details['extensions']['subjectAltName']);
1741
	if (empty($sans)) {
1742
		$sans = cert_add_altname_type($dn['commonName']);
1743
	}
1744

    
1745
	/* Do not setup SANs if the SAN list is empty (e.g. no SAN list and/or
1746
	 * CN cannot be mapped to a valid SAN type) */
1747
	if (!empty($sans)) {
1748
		if ($cert['type'] != 'ca') {
1749
			$cert_type .= '_san';
1750
		}
1751
		/* subjectAltName can be set _only_ via configuration file, so put the
1752
		 * value into the environment where it will be read from the configuration */
1753
		putenv("SAN={$sans}");
1754
	}
1755

    
1756
	/* Determine current digest algorithm. */
1757
	$digest_alg = strtolower($cert_details['signatureTypeSN']);
1758

    
1759
	/* Check for and remove unnecessary ECDSA digest prefix
1760
	 * See https://redmine.pfsense.org/issues/13437 */
1761
	$ecdsa_prefix = 'ecdsa-with-';
1762
	if (substr($digest_alg, 0, strlen($ecdsa_prefix)) == $ecdsa_prefix) {
1763
		$digest_alg = substr($digest_alg, strlen($ecdsa_prefix));
1764
	}
1765

    
1766
	/* If we are enforcing strict security, then check the digest against a
1767
	 * blacklist of insecure digest methods. */
1768
	if ($strictsecurity &&
1769
	    is_weak_digest($cert_details['signatureTypeSN'])) {
1770
		$digest_alg = 'sha256';
1771
	}
1772

    
1773
	/* Validate key type, assume RSA if it cannot be read. */
1774
	if (is_array($key_details) && array_key_exists('type', $key_details)) {
1775
		$private_key_type = $key_details['type'];
1776
	} else {
1777
		$private_key_type = OPENSSL_KEYTYPE_RSA;
1778
	}
1779

    
1780
	/* Setup certificate and key arguments */
1781
	$args = array(
1782
		"x509_extensions" => $cert_type,
1783
		"digest_alg" => $digest_alg,
1784
		"private_key_type" => $private_key_type,
1785
		"encrypt_key" => false);
1786

    
1787
	/* If we are enforcing strict security, then ensure the private key size
1788
	 * is at least 2048 bits or NIST P-256 elliptic curve*/
1789
	$private_key_bits = $key_details['bits'];
1790
	if ($strictsecurity) {
1791
		if (($key_details['type'] == OPENSSL_KEYTYPE_RSA) &&
1792
		    ($private_key_bits < $cert_strict_values['min_private_key_bits'])) {
1793
			$private_key_bits = $cert_strict_values['min_private_key_bits'];
1794
			$reusekey = false;
1795
		} else if (!in_array($key_details['ec']['curve_name'], $curve_compatible_list)) {
1796
			$ec_curve = $cert_strict_values['ec_curve'];
1797
			$reusekey = false;
1798
		}
1799
	}
1800

    
1801
	/* Set key parameters. */
1802
	if ($key_details['type'] ==  OPENSSL_KEYTYPE_RSA) {
1803
		$args['private_key_bits'] = (int)$private_key_bits;
1804
	} else if ($ec_curve) {
1805
		$args['curve_name'] = $ec_curve;
1806
	} else {
1807
		$args['curve_name'] = $key_details['ec']['curve_name'];
1808
	}
1809

    
1810
	/* Make a new key if necessary */
1811
	if (!$res_key || !$reusekey) {
1812
		$res_key = openssl_pkey_new($args);
1813
		if (!$res_key) {
1814
			return false;
1815
		}
1816
	}
1817

    
1818
	/* Create a new CSR from derived parameters and key */
1819
	$res_csr = openssl_csr_new($dn, $res_key, $args);
1820
	/* If the CSR could not be created, bail */
1821
	if (!$res_csr) {
1822
		return false;
1823
	}
1824

    
1825
	if (!empty($cert['caref'])) {
1826
		/* The certificate was signed by a CA, so read the CA details. */
1827
		$ca_item_config = lookup_ca($cert['caref']);
1828
		$ca = &$ca_item_config['item'];
1829
		/* If the referenced CA cannot be found, bail. */
1830
		if (!$ca) {
1831
			return false;
1832
		}
1833
		$ca_str_crt = base64_decode($ca['crt']);
1834
		$ca_str_key = base64_decode($ca['prv']);
1835
		$ca_res_crt = openssl_x509_read($ca_str_crt);
1836
		$ca_res_key = openssl_pkey_get_private(array(0 => $ca_str_key, 1 => ""));
1837
		if (!$ca_res_key) {
1838
			/* If the CA key cannot be read, bail. */
1839
			return false;
1840
		}
1841
		/* If the CA does not have a serial number, assume 0. */
1842
		if (empty($ca['serial'])) {
1843
			$ca['serial'] = 0;
1844
		}
1845
		/* Get the next available CA serial number. */
1846
		$ca_serial = ca_get_next_serial($ca);
1847
		config_set_path("ca/{$ca_item_config['idx']}", $ca);
1848
		if ($cert['type'] == 'ca') {
1849
			$cert = $ca;
1850
		}
1851
	} elseif ($selfsigned) {
1852
		/* For self-signed CAs & certificates, set the CA details to self and
1853
		 * use the key for this entry to sign itself.
1854
		 */
1855
		$ca_res_crt   = null;
1856
		$ca_res_key   = $res_key;
1857
		/* Use random serial from this CA/Self-Signed Cert */
1858
		$ca_serial    = cert_get_random_serial($cert['refid']);
1859
	}
1860

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

    
1864
	/* Sign the CSR */
1865
	$res_crt = openssl_csr_sign($res_csr, $ca_res_crt, $ca_res_key, $lifetime,
1866
				 $args, $ca_serial);
1867
	/* If the CSR could not be signed, bail */
1868
	if (!$res_crt) {
1869
		return false;
1870
	}
1871

    
1872
	/* Attempt to read the key and certificate and if that fails, bail */
1873
	if (!openssl_pkey_export($res_key, $str_key) ||
1874
	    !openssl_x509_export($res_crt, $str_crt)) {
1875
		return false;
1876
	}
1877

    
1878
	/* Load the new certificate string and key into the configuration */
1879
	$cert['crt'] = base64_encode($str_crt);
1880
	$cert['prv'] = base64_encode($str_key);
1881
	config_set_path("{$cert_config['path']}", $cert);
1882

    
1883
	return true;
1884
}
1885

    
1886
/****f* certs/cert_get_all_services
1887
 * NAME
1888
 *   cert_get_all_services - Locate services using a given certificate
1889
 * INPUTS
1890
 *   $refid: The refid of a certificate to check
1891
 * RESULT
1892
 *   array containing the services which use this certificate, including:
1893
 *     webgui: Present and true if the WebGUI uses this certificate. Unset otherwise.
1894
 *     services: Array of service definitions using this certificate, with:
1895
 *       name: Name of the service
1896
 *       extras: Extra information needed by some services, such as OpenVPN or Captive Portal.
1897
 *     packages: Array containing package names using this certificate.
1898
 ******/
1899

    
1900
function cert_get_all_services($refid) {
1901
	$services = array();
1902
	$services['services'] = array();
1903
	$services['packages'] = array();
1904

    
1905
	/* Only set if true, otherwise leave unset. */
1906
	if (is_webgui_cert($refid)) {
1907
		$services['webgui'] = true;
1908
	}
1909

    
1910
	/* Find all OpenVPN clients and servers which use this certificate */
1911
	foreach (array('server', 'client') as $mode) {
1912
		foreach (config_get_path("openvpn/openvpn-{$mode}", []) as $ovpn) {
1913
			if ($ovpn['certref'] == $refid) {
1914
				/* OpenVPN instances are restarted individually,
1915
				 * so we need to note the mode and ID. */
1916
				$services['services'][] = array(
1917
					'name' => 'openvpn',
1918
					'extras' => array(
1919
						'vpnmode' => $mode,
1920
						'id' => $ovpn['vpnid']
1921
					)
1922
				);
1923
			}
1924
		}
1925
	}
1926

    
1927
	/* If any one IPsec tunnel uses this certificate then the whole service
1928
	 * needs a bump. */
1929

    
1930
	foreach (config_get_path('ipsec/phase1', []) as $ipsec) {
1931
		if (($ipsec['authentication_method'] == 'cert') &&
1932
		    ($ipsec['certref'] == $refid)) {
1933
			$services['services'][] = array('name' => 'ipsec');
1934
			/* Stop after finding one, no need to search for more. */
1935
			break;
1936
		}
1937
	}
1938

    
1939
	/* Check to see if any HTTPS-enabled Captive Portal zones use this
1940
	 * certificate. */
1941
	foreach (config_get_path('captiveportal', []) as $zone => $portal) {
1942
		if (isset($portal['enable']) && isset($portal['httpslogin']) &&
1943
		    ($portal['certref'] == $refid)) {
1944
			/* Captive Portal zones are restarted individually, so
1945
			 * we need to note the zone name. */
1946
			$services['services'][] = array(
1947
				'name' => 'captiveportal',
1948
				'extras' => array(
1949
					'zone' => $zone,
1950
				)
1951
			);
1952
		}
1953
	}
1954

    
1955
	/* Locate any packages using this certificate */
1956
	$pkgcerts = pkg_call_plugins('plugin_certificates', array('type' => 'certificates', 'event' => 'used_certificates'));
1957
	foreach ($pkgcerts as $name => $package) {
1958
		if (is_array($package['certificatelist'][$refid]) &&
1959
		    isset($package['certificatelist'][$refid]) > 0) {
1960
			$services['packages'][] = $name;
1961
		}
1962
	}
1963

    
1964
	return $services;
1965
}
1966

    
1967
/****f* certs/ca_get_all_services
1968
 * NAME
1969
 *   ca_get_all_services - Locate services using a given certificate authority or its decendents
1970
 * INPUTS
1971
 *   $refid: The refid of a certificate authority to check
1972
 * RESULT
1973
 *   array containing the services which use this certificate authority, including:
1974
 *     webgui: Present and true if the WebGUI uses this certificate. Unset otherwise.
1975
 *     services: Array of service definitions using this certificate, with:
1976
 *       name: Name of the service
1977
 *       extras: Extra information needed by some services, such as OpenVPN or Captive Portal.
1978
 *     packages: Array containing package names using this certificate.
1979
 * NOTES
1980
 *   This searches recursively to find entries using this CA as well as intermediate
1981
 *   CAs and certificates signed by this CA, and returns a single set of all services.
1982
 *   This avoids restarting affected services multiple times when there is overlapping
1983
 *   usage.
1984
 ******/
1985
function ca_get_all_services($refid) {
1986
	$services = array();
1987
	$services['services'] = array();
1988

    
1989
	foreach (array('server', 'client') as $mode) {
1990
		foreach (config_get_path("openvpn/openvpn-{$mode}", []) as $ovpn) {
1991
			if ($ovpn['caref'] == $refid) {
1992
				$services['services'][] = array(
1993
					'name' => 'openvpn',
1994
					'extras' => array(
1995
						'vpnmode' => $mode,
1996
						'id' => $ovpn['vpnid']
1997
					)
1998
				);
1999
			}
2000
		}
2001
	}
2002

    
2003
	foreach (config_get_path('ipsec/phase1', []) as $ipsec) {
2004
		if ($ipsec['certref'] == $refid) {
2005
			break;
2006
		}
2007
	}
2008

    
2009
	foreach (config_get_path('ipsec/phase1', []) as $ipsec) {
2010
		if (($ipsec['authentication_method'] == 'cert') &&
2011
		    ($ipsec['caref'] == $refid)) {
2012
			$services['services'][] = array('name' => 'ipsec');
2013
			break;
2014
		}
2015
	}
2016

    
2017
	/* Loop through all certs and get their services as well */
2018
	foreach (config_get_path('cert', []) as $cert) {
2019
		if ($cert['caref'] == $refid) {
2020
			$services = array_merge_recursive_unique($services, cert_get_all_services($cert['refid']));
2021
		}
2022
	}
2023

    
2024
	/* Look for intermediate certs and services */
2025
	foreach (config_get_path('ca', []) as $cert) {
2026
		if ($cert['caref'] == $refid) {
2027
			$services = array_merge_recursive_unique($services, ca_get_all_services($cert['refid']));
2028
		}
2029
	}
2030

    
2031
	return $services;
2032
}
2033

    
2034
/****f* certs/cert_restart_services
2035
 * NAME
2036
 *   cert_restart_services - Restarts services specific to CA/Certificate usage
2037
 * INPUTS
2038
 *   $services: An array of services returned by cert_get_all_services or ca_get_all_services
2039
 * RESULT
2040
 *   Services in the given array are restarted
2041
 *   returns false if the input is invalid
2042
 *   returns true at the end of execution
2043
 ******/
2044

    
2045
function cert_restart_services($services) {
2046
	require_once("service-utils.inc");
2047
	/* If the input is not an array, it is invalid. */
2048
	if (!is_array($services)) {
2049
		return false;
2050
	}
2051

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

    
2055
	/* Restart GUI: */
2056
	if ($services['webgui']) {
2057
		ob_flush();
2058
		flush();
2059
		log_error(sprintf($restart_string, gettext('service'), 'WebGUI'));
2060
		send_event("service restart webgui");
2061
	}
2062

    
2063
	/* Restart other base services: */
2064
	if (is_array($services['services'])) {
2065
		foreach ($services['services'] as $service) {
2066
			switch ($service['name']) {
2067
				case 'openvpn':
2068
					$service_name = "{$service['name']} {$service['extras']['vpnmode']} {$service['extras']['id']}";
2069
					break;
2070
				case 'captiveportal':
2071
					$service_name = "{$service['name']} zone {$service['extras']['zone']}";
2072
					break;
2073
				default:
2074
					$service_name = $service['name'];
2075
			}
2076
			log_error(sprintf($restart_string, gettext('service'), $service_name));
2077
			service_control_restart($service['name'], $service['extras']);
2078
		}
2079
	}
2080

    
2081
	/* Restart Packages: */
2082
	if (is_array($services['packages'])) {
2083
		foreach ($services['packages'] as $service) {
2084
			log_error(sprintf($restart_string, gettext('package'), $service));
2085
			restart_service($service);
2086
		}
2087
	}
2088
	return true;
2089
}
2090

    
2091
/****f* certs/cert_get_lifetime
2092
 * NAME
2093
 *   cert_get_lifetime - Returns the number of days the certificate is valid
2094
 * INPUTS
2095
 *   $untilexpire: Boolean
2096
 *     true: The number of days returned is from now until the certificate expiration.
2097
 *     false (default): The number of days returned is the total lifetime of the certificate.
2098
 * RESULT
2099
 *   Integer number of days in the certificate total or remaining lifetime
2100
 ******/
2101

    
2102
function cert_get_lifetime($cert, $untilexpire = false) {
2103
	/* If the certificate is not valid, bail. */
2104
	if (!is_array($cert) || empty($cert['crt'])) {
2105
		return null;
2106
	}
2107
	/* Read certificate details */
2108
	list($startdate, $enddate) = cert_get_dates($cert['crt'], true, false);
2109

    
2110
	/* If either of the dates are invalid, there is nothing we can do here. */
2111
	if (($startdate === false) || ($enddate === false)) {
2112
		return false;
2113
	}
2114

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

    
2118
	/* Calculate the requested intervals */
2119
	$interval = $startdate->diff($enddate);
2120

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

    
2125
/****f* certs/cert_analyze_lifetime
2126
 * NAME
2127
 *   cert_analyze_lifetime - Analyze a certificate lifetime for expiration notices
2128
 * INPUTS
2129
 *   $expiredays: Number of days until the certificate expires (See cert_get_lifetime())
2130
 * RESULT
2131
 *   An array of two entries:
2132
 *   0/$lrclass: A bootstrap name for use with classes like text-<x>
2133
 *   1/$expstring: A text analysis describing the expiration timeframe.
2134
 ******/
2135

    
2136
function cert_analyze_lifetime($expiredays) {
2137
	global $g;
2138
	/* Number of days at which to warn of expiration. */
2139
	$warning_days = config_get_path('notifications/certexpire/expiredays', g_get('default_cert_expiredays'));
2140

    
2141
	if ($expiredays > $warning_days) {
2142
		/* Not expiring soon */
2143
		$lrclass = 'normal';
2144
		$expstring = gettext("%d %s until expiration");
2145
	} elseif ($expiredays >= 0) {
2146
		/* Still valid but expiring soon */
2147
		$lrclass = 'warning';
2148
		$expstring = gettext("Expiring soon, in %d %s");
2149
	} else {
2150
		/* Certificate has expired */
2151
		$lrclass = 'danger';
2152
		$expstring = gettext("Expired %d %s ago");
2153
	}
2154
	$days = (abs($expiredays) == 1) ? gettext('day') : gettext('days');
2155
	$expstring = sprintf($expstring, abs($expiredays), $days);
2156
	return array($lrclass, $expstring);
2157
}
2158

    
2159
/****f* certs/cert_print_dates
2160
 * NAME
2161
 *   cert_print_dates - Print the start and end timestamps for the given certificate
2162
 * INPUTS
2163
 *   $cert: CA or Cert entry for which the dates will be printed
2164
 * RESULT
2165
 *   Returns null if the passed entry is invalid
2166
 *   Otherwise, outputs the dates to the user with formatting.
2167
 ******/
2168

    
2169
function cert_print_dates($cert) {
2170
	/* If the certificate is not valid, bail. */
2171
	if (!is_array($cert) || empty($cert['crt'])) {
2172
		return null;
2173
	}
2174
	/* Attempt to extract the dates from the certificate */
2175
	list($startdate, $enddate) = cert_get_dates($cert['crt']);
2176
	/* If either of the timestamps are empty, then do not print anything.
2177
	 * The entry may not be valid or it may just be missing date information */
2178
	if (empty($startdate) || empty($enddate)) {
2179
		return null;
2180
	}
2181
	/* Get the expiration days */
2182
	$expiredays = cert_get_lifetime($cert, true);
2183
	/* Analyze the lifetime value */
2184
	list($lrclass, $expstring) = cert_analyze_lifetime($expiredays);
2185
	/* Output the dates, with a tooltip showing days until expiration, and
2186
	 * a visual indication of warning/expired status. */
2187
	?>
2188
	<br />
2189
	<small>
2190
	<?=gettext("Valid From")?>: <b><?=$startdate ?></b><br />
2191
	<?=gettext("Valid Until")?>:
2192
	<span class="text-<?=$lrclass?>" data-toggle="tooltip" data-placement="bottom" title="<?= $expstring ?>">
2193
	<b><?=$enddate ?></b>
2194
	</span>
2195
	</small>
2196
<?php
2197
}
2198

    
2199
/****f* certs/cert_print_infoblock
2200
 * NAME
2201
 *   cert_print_infoblock - Print an information block containing certificate details
2202
 * INPUTS
2203
 *   $cert: CA or Cert entry for which the information will be printed
2204
 * RESULT
2205
 *   Returns null if the passed entry is invalid
2206
 *   Otherwise, outputs information to the user with formatting.
2207
 ******/
2208

    
2209
function cert_print_infoblock($cert) {
2210
	/* If the certificate is not valid, bail. */
2211
	if (!is_array($cert) || empty($cert['crt'])) {
2212
		return null;
2213
	}
2214
	/* Variable to hold the formatted information */
2215
	$certextinfo = "";
2216

    
2217
	/* Serial number */
2218
	$cert_details = openssl_x509_parse(base64_decode($cert['crt']));
2219
	if (isset($cert_details['serialNumber']) && (strlen($cert_details['serialNumber']) > 0)) {
2220
		$certextinfo .= '<b>' . gettext("Serial: ") . '</b> ';
2221
		$certextinfo .= htmlspecialchars(cert_escape_x509_chars($cert_details['serialNumber'], true));
2222
		$certextinfo .= '<br/>';
2223
	}
2224

    
2225
	/* Digest type */
2226
	$certsig = cert_get_sigtype($cert['crt']);
2227
	if (is_array($certsig) && !empty($certsig) && !empty($certsig['shortname'])) {
2228
		$certextinfo .= '<b>' . gettext("Signature Digest: ") . '</b> ';
2229
		$is_weak = is_weak_digest($certsig['shortname']);
2230
		$csgclass = ($is_weak) ? 'warning' : 'normal';
2231
		$certextinfo .= '<span class="text-' . $csgclass . '" data-placement="bottom">';
2232
		$certextinfo .= htmlspecialchars(cert_escape_x509_chars($certsig['shortname'], true));
2233
		if ($is_weak) {
2234
			$certextinfo .= ' (' . gettext("Weak Digest") . ')';
2235
		}
2236
		$certextinfo .= '</span>';
2237
		$certextinfo .= '<br/>';
2238
	}
2239

    
2240
	/* Subject Alternative Name (SAN) list */
2241
	$sans = cert_get_sans($cert['crt']);
2242
	if (is_array($sans) && !empty($sans)) {
2243
		$certextinfo .= '<b>' . gettext("SAN: ") . '</b> ';
2244
		$certextinfo .= htmlspecialchars(implode(', ', cert_escape_x509_chars($sans, true)));
2245
		$certextinfo .= '<br/>';
2246
	}
2247

    
2248
	/* Key usage */
2249
	$purpose = cert_get_purpose($cert['crt']);
2250
	if (is_array($purpose) && !empty($purpose['ku'])) {
2251
		$certextinfo .= '<b>' . gettext("KU: ") . '</b> ';
2252
		$certextinfo .= htmlspecialchars(implode(', ', $purpose['ku']));
2253
		$certextinfo .= '<br/>';
2254
	}
2255

    
2256
	/* Extended key usage */
2257
	if (is_array($purpose) && !empty($purpose['eku'])) {
2258
		$certextinfo .= '<b>' . gettext("EKU: ") . '</b> ';
2259
		$certextinfo .= htmlspecialchars(implode(', ', $purpose['eku']));
2260
		$certextinfo .= '<br/>';
2261
	}
2262

    
2263
	/* OCSP / Must Staple */
2264
	if (cert_get_ocspstaple($cert['crt'])) {
2265
		$certextinfo .= '<b>' . gettext("OCSP: ") . '</b> ';
2266
		$certextinfo .= gettext("Must Staple");
2267
		$certextinfo .= '<br/>';
2268
	}
2269

    
2270
	/* Private key information */
2271
	if (!empty($cert['prv'])) {
2272
		$res_key = openssl_pkey_get_private(base64_decode($cert['prv']));
2273
		$certextinfo .= '<b>' . gettext("Key Type: ") . '</b> ';
2274
		if ($res_key) {
2275
			$key_details = openssl_pkey_get_details($res_key);
2276
			/* Key type (RSA or EC) */
2277
			if ($key_details['type'] == OPENSSL_KEYTYPE_RSA) {
2278
				/* RSA Key size */
2279
				$certextinfo .= 'RSA<br/>';
2280
				$certextinfo .= '<b>' . gettext("Key Size: ") . '</b> ';
2281
				$certextinfo .= $key_details['bits'] . '<br/>';
2282
			} else {
2283
				/* Elliptic curve (EC) key curve name */
2284
				$certextinfo .= 'ECDSA<br/>';
2285
				$curve = cert_get_pkey_curve($cert['prv']);
2286
				if (!empty($curve)) {
2287
					$certextinfo .= '<b>' . gettext("Elliptic curve name:") . ' </b>';
2288
					$certextinfo .= $curve . '<br/>';
2289
				}
2290
			}
2291
		} else {
2292
			$certextinfo .= '<i>' . gettext("Unknown (Key could not be parsed)") . '</i><br/>';
2293
		}
2294
	}
2295

    
2296
	/* Distinguished name (DN) */
2297
	if (!empty($cert_details['name'])) {
2298
		$certextinfo .= '<b>' . gettext("DN: ") . '</b> ';
2299
		/* UTF8 DN support, see https://redmine.pfsense.org/issues/12041 */
2300
		$certdnstring = preg_replace_callback('/\\\\x([0-9A-F]{2})/', function ($a) { return pack('H*', $a[1]); }, $cert_details['name']);
2301
		$certextinfo .= htmlspecialchars(cert_escape_x509_chars($certdnstring, true));
2302
		$certextinfo .= '<br/>';
2303
	}
2304

    
2305
	/* Hash value */
2306
	if (!empty($cert_details['hash'])) {
2307
		$certextinfo .= '<b>' . gettext("Hash: ") . '</b> ';
2308
		$certextinfo .= htmlspecialchars(cert_escape_x509_chars($cert_details['hash'], true));
2309
		$certextinfo .= '<br/>';
2310
	}
2311

    
2312
	/* Subject Key Identifier (SKID) */
2313
	if (is_array($cert_details["extensions"]) && !empty($cert_details["extensions"]["subjectKeyIdentifier"])) {
2314
		$certextinfo .= '<b>' . gettext("Subject Key ID: ") . '</b> ';
2315
		$certextinfo .= htmlspecialchars(cert_escape_x509_chars($cert_details["extensions"]["subjectKeyIdentifier"], true));
2316
		$certextinfo .= '<br/>';
2317
	}
2318

    
2319
	/* Authority Key Identifier (AKID) */
2320
	if (is_array($cert_details["extensions"]) && !empty($cert_details["extensions"]["authorityKeyIdentifier"])) {
2321
		$certextinfo .= '<b>' . gettext("Authority Key ID: ") . '</b> ';
2322
		$certextinfo .= str_replace("\n", '<br/>', htmlspecialchars(cert_escape_x509_chars($cert_details["extensions"]["authorityKeyIdentifier"], true)));
2323
		$certextinfo .= '<br/>';
2324
	}
2325

    
2326
	/* Total Lifetime (days from cert start to end) */
2327
	$lifetime = cert_get_lifetime($cert);
2328
	if ($lifetime !== false) {
2329
		$certextinfo .= '<b>' . gettext("Total Lifetime: ") . '</b> ';
2330
		$certextinfo .= sprintf("%d %s", $lifetime, (abs($lifetime) == 1) ? gettext('day') : gettext('days'));
2331
		$certextinfo .= '<br/>';
2332

    
2333
		/* Lifetime before certificate expires (days from now to end) */
2334
		$expiredays = cert_get_lifetime($cert, true);
2335
		list($lrclass, $expstring) = cert_analyze_lifetime($expiredays);
2336
		$certextinfo .= '<b>' . gettext("Lifetime Remaining: ") . '</b> ';
2337
		$certextinfo .= "<span class=\"text-{$lrclass}\">{$expstring}</span>";
2338
		$certextinfo .= '<br/>';
2339
	}
2340

    
2341
	if ($purpose['ca'] == 'Yes') {
2342
		/* CA Trust store presence */
2343
		$certextinfo .= '<b>' . gettext("Trust Store: ") . '</b> ';
2344
		$certextinfo .= (isset($cert['trust']) && ($cert['trust'] == "enabled")) ? gettext('Included') : gettext('Excluded');
2345
		$certextinfo .= '<br/>';
2346

    
2347
		if (!empty($cert['prv'])) {
2348
			/* CA Next/Randomize Serial */
2349
			$certextinfo .= '<b>' . gettext("Next Serial: ") . '</b> ';
2350
			$certextinfo .= (isset($cert['randomserial']) && ($cert['randomserial'] == "enabled")) ? gettext('Randomized') : $cert['serial'];
2351
			$certextinfo .= '<br/>';
2352
		}
2353
	}
2354

    
2355
	/* Output the infoblock */
2356
	if (!empty($certextinfo)) { ?>
2357
		<div class="infoblock">
2358
		<? print_info_box($certextinfo, 'info', false); ?>
2359
		</div>
2360
	<?php
2361
	}
2362
}
2363

    
2364
/****f* certs/cert_notify_expiring
2365
 * NAME
2366
 *   cert_notify_expiring - Notify admin about expiring certificates
2367
 * INPUTS
2368
 *   None
2369
 * RESULT
2370
 *   File a notice containing expiring certificate information, which is then
2371
 *   logged, displayed in the GUI, and sent via e-mail (if enabled).
2372
 ******/
2373

    
2374
function cert_notify_expiring() {
2375
	/* If certificate expiration notifications are disabled, there is nothing to do. */
2376
	if (config_get_path('notifications/certexpire/enable') == "disabled") {
2377
		return;
2378
	}
2379

    
2380
	$notifications = array();
2381

    
2382
	/* Check all CA and Cert entries at once */
2383
	config_init_path('ca');
2384
	config_init_path('cert');
2385
	$all_certs = array_merge_recursive(config_get_path('ca'), config_get_path('cert'));
2386

    
2387
	foreach ($all_certs as $cert) {
2388
		if (empty($cert)) {
2389
			continue;
2390
		}
2391
		/* Proceed only for not revoked certificate if ignore setting enabled */
2392
		if ((config_get_path('notifications/certexpire/ignore_revoked') == "enabled") &&
2393
		    is_cert_revoked($cert)) {
2394
			continue;
2395
		}
2396
		/* Fetch and analyze expiration */
2397
		$expiredays = cert_get_lifetime($cert, true);
2398
		/* If the result is null, then the lifetime data is missing, so skip the invalid entry. */
2399
		if ($expiredays === null) {
2400
			continue;
2401
		}
2402
		list($lrclass, $expstring) = cert_analyze_lifetime($expiredays);
2403
		/* Only notify if the certificate is expiring soon, or has
2404
		 * already expired */
2405
		if ($lrclass != 'normal') {
2406
			$notify_string = (array_key_exists('serial', $cert)) ? gettext('Certificate Authority') : gettext('Certificate');
2407
			$notify_string .= ": {$cert['descr']} ({$cert['refid']}): {$expstring}";
2408
			$notifications[] = $notify_string;
2409
		}
2410
	}
2411
	if (!empty($notifications)) {
2412
		$message = gettext("The following CA/Certificate entries are expiring:") . "\n" .
2413
			implode("\n", $notifications);
2414
		file_notice("Certificate Expiration", $message, "Certificate Manager");
2415
	}
2416
}
2417

    
2418
/****f* certs/ca_setup_trust_store
2419
 * NAME
2420
 *   ca_setup_trust_store - Setup local CA trust store so that CA entries in the
2421
 *                          configuration may be trusted by the operating system.
2422
 * INPUTS
2423
 *   None
2424
 * RESULT
2425
 *   CAs marked as trusted in the configuration will be setup in the OS trust store.
2426
 ******/
2427

    
2428
function ca_setup_trust_store() {
2429
	/* This directory is trusted by OpenSSL on FreeBSD by default */
2430
	$trust_store_directory = '/usr/local/etc/ssl/certs';
2431

    
2432
	/* Create the directory if it does not already exist, and clean it up if it does. */
2433
	safe_mkdir($trust_store_directory);
2434
	unlink_if_exists("{$trust_store_directory}/*.crt");
2435
	unlink_if_exists("{$trust_store_directory}/*.crl");
2436

    
2437
	foreach (config_get_path('ca', []) as $ca) {
2438
		/* If the entry is invalid or is not trusted, skip it. */
2439
		if (!is_array($ca) ||
2440
		    empty($ca['crt']) ||
2441
		    !isset($ca['trust']) ||
2442
		    ($ca['trust'] != "enabled")) {
2443
			continue;
2444
		}
2445

    
2446
		ca_setup_capath($ca, $trust_store_directory, '', false, 'crt', 'crl');
2447
	}
2448

    
2449
	mwexec_bg('/usr/sbin/certctl rehash');
2450
}
2451

    
2452
/****f* certs/ca_setup_capath
2453
 * NAME
2454
 *   ca_setup_capath - Setup CApath structure so that CA chains and related CRLs
2455
 *                     may be written and validated by the -CApath option in
2456
 *                     OpenSSL and other compatible validators.
2457
 * INPUTS
2458
 *   $ca     : A CA (not a refid) to write
2459
 *   $basedir: The directory which will contain the CA structure.
2460
 *   $crl    : A CRL (not a refid) associated with the CA to write.
2461
 *   $refresh: Refresh CRLs -- When true, perform no cleanup and increment suffix
2462
 *   $crtext : Certificate file extension
2463
 *   $crlext : CRL file extension
2464
 * RESULT
2465
 *   $basedir is populated with CA and CRL files in a format usable by OpenSSL
2466
 *   CApath. This has the filenames as the CA hash with the CA named <hash>.0
2467
 *   and CRLs named <hash>.r0
2468
 ******/
2469

    
2470
function ca_setup_capath($ca, $basedir, $crl = "", $refresh = false, $crtext = '0', $crlext = 'r') {
2471
	/* Check for an invalid CA */
2472
	if (!$ca || !is_array($ca)) {
2473
		return false;
2474
	}
2475
	/* Check for an invalid CRL, but do not consider it fatal if it's wrong */
2476
	if (!$crl || !is_array($crl) || ($crl['caref'] != $ca['refid'])) {
2477
		unset($crl);
2478
	}
2479

    
2480
	/* Check for an empty base directory, which is invalid */
2481
	if (empty($basedir)) {
2482
		return false;
2483
	}
2484

    
2485
	/* Ensure that $basedir exists and is a directory */
2486
	if (!is_dir($basedir)) {
2487
		/* If it's a file, remove it, otherwise the directory cannot
2488
		 * be created. */
2489
		if (file_exists($basedir)) {
2490
			@unlink_if_exists($basedir);
2491
		}
2492
		@safe_mkdir($basedir);
2493
	}
2494
	/* Decode the certificate contents */
2495
	$cert_contents = base64_decode($ca['crt']);
2496
	/* Get hash value to use for filename */
2497
	$cert_details = openssl_x509_parse($cert_contents);
2498
	$fprefix = "{$basedir}/{$cert_details['hash']}";
2499

    
2500

    
2501
	$ca_filename = "{$fprefix}.{$crtext}";
2502
	/* Cleanup old CA/CRL files for this hash */
2503
	@unlink_if_exists($ca_filename);
2504
	/* Write CA to base dir and ensure it has correct permissions. */
2505
	file_put_contents($ca_filename, $cert_contents);
2506
	chmod($ca_filename, 0644);
2507
	chown($ca_filename, 'root');
2508
	chgrp($ca_filename, 'wheel');
2509

    
2510
	/* If there is a CRL, process it. */
2511
	if ($crl) {
2512
		$crl_filename = "{$fprefix}.{$crlext}";
2513
		if (!$refresh) {
2514
			/* Cleanup old CA/CRL files for this hash */
2515
			@unlink_if_exists("{$crl_filename}*");
2516
		}
2517

    
2518
		if ($crlext == 'r') {
2519
			/* Find next suffix based on how many existing files there are (start=0) */
2520
			$crl_filename .= count(glob("{$crl_filename}*"));
2521
		}
2522

    
2523
		/* Write CRL to base dir and ensure it has correct permissions. */
2524
		file_put_contents($crl_filename, base64_decode($crl['text']));
2525
		chmod($crl_filename, 0644);
2526
		chown($crl_filename, 'root');
2527
		chgrp($crl_filename, 'wheel');
2528
	}
2529

    
2530
	return true;
2531
}
2532

    
2533
/****f* certs/cert_get_pkey_curve
2534
 * NAME
2535
 *   cert_get_pkey_curve - Get the ECDSA curve of a private key
2536
 * INPUTS
2537
 *   $pkey  : The private key to check
2538
 *   $decode: true: base64 decode the string before use, false to use as-is.
2539
 * RESULT
2540
 *   false if the private key is not ECDSA or the private key is not present.
2541
 *   Otherwise, the name of the ECDSA curve used for the private key.
2542
 ******/
2543

    
2544
function cert_get_pkey_curve($pkey, $decode = true) {
2545
	if ($decode) {
2546
		$pkey = base64_decode($pkey);
2547
	}
2548

    
2549
	/* Attempt to read the private key, and if successful, its details. */
2550
	$res_key = openssl_pkey_get_private($pkey);
2551
	if ($res_key) {
2552
		$key_details = openssl_pkey_get_details($res_key);
2553
		/* If this is an EC key, and the curve name is not empty, return
2554
		 * that curve name. */
2555
		if ($key_details['type'] ==  OPENSSL_KEYTYPE_EC) {
2556
			if (!empty($key_details['ec']['curve_name'])) {
2557
				return $key_details['ec']['curve_name'];
2558
			} else {
2559
				return $key_details['ec']['curve_oid'];
2560
			}
2561
		}
2562
	}
2563

    
2564
	/* Either the private key could not be read, or this is not an EC certificate. */
2565
	return false;
2566
}
2567

    
2568
/* Array containing ECDSA curve names allowed in certain contexts. For instance,
2569
 * HTTPS servers and web browsers only support specific curves in TLSv1.3. */
2570
global $cert_curve_compatible, $curve_compatible_list;
2571
$cert_curve_compatible = array(
2572
	/* HTTPS list per TLSv1.3 spec and Mozilla compatibility list */
2573
	'HTTPS' => array('prime256v1', 'secp384r1'),
2574
	/* IPsec/EAP/TLS list per strongSwan docs/issues */
2575
	'IPsec' => array('prime256v1', 'secp384r1', 'secp521r1'),
2576
	/* OpenVPN bug limits usable curves, see https://redmine.pfsense.org/issues/9744 */
2577
	'OpenVPN' => array('prime256v1', 'secp384r1', 'secp521r1'),
2578
);
2579
$curve_compatible_list = array_unique(call_user_func_array('array_merge', array_values($cert_curve_compatible)));
2580

    
2581
/****f* certs/cert_build_curve_list
2582
 * NAME
2583
 *   cert_build_curve_list - Build an option list of ECDSA curves with notations
2584
 *                           about known compatible uses.
2585
 * INPUTS
2586
 *   None
2587
 * RESULT
2588
 *   Returns an option list of OpenSSL EC names with added notes. This can be
2589
 *   used directly in form option selection lists.
2590
 ******/
2591

    
2592
function cert_build_curve_list() {
2593
	global $cert_curve_compatible;
2594
	/* Get the default list of curve names */
2595
	$openssl_ecnames = openssl_get_curve_names();
2596
	/* Turn this into a hashed array where key==value */
2597
	$curvelist = array_combine($openssl_ecnames, $openssl_ecnames);
2598
	/* Check all known compatible curves and note matches */
2599
	foreach ($cert_curve_compatible as $consumer => $validcurves) {
2600
		/* $consumer will be a name like HTTPS or IPsec
2601
		 * $validcurves will be an array of curves compatible with the consumer */
2602
		foreach ($validcurves as $vc) {
2603
			/* If the valid curve is present in the curve list, add
2604
			 * a note with the consumer name to the value visible to
2605
			 * the user. */
2606
			if (array_key_exists($vc, $curvelist)) {
2607
				$curvelist[$vc] .= " [{$consumer}]";
2608
			}
2609
		}
2610
	}
2611
	return $curvelist;
2612
}
2613

    
2614
/****f* certs/cert_check_pkey_compatibility
2615
 * NAME
2616
 *   cert_check_pkey_compatibility - Check a private key to see if it can be
2617
 *                                   used in a specific compatible context.
2618
 * INPUTS
2619
 *   $pkey    : The private key to check
2620
 *   $consumer: The consumer name used to validate the curve. See the contents
2621
 *                 of $cert_curve_compatible for details.
2622
 * RESULT
2623
 *   true if the private key may be used in requested area, or if there are no
2624
 *        restrictions.
2625
 *   false if the private key cannot be used.
2626
 ******/
2627

    
2628
function cert_check_pkey_compatibility($pkey, $consumer) {
2629
	global $cert_curve_compatible;
2630

    
2631
	/* Read the curve name from the key */
2632
	$curve = cert_get_pkey_curve($pkey);
2633
	/* Return true if any of the following conditions are met:
2634
	 *  * This is not an EC key
2635
	 *  * The private key cannot be read
2636
	 *  * There are no restrictions
2637
	 *  * The requested curve is compatible */
2638
	return (($curve === false) ||
2639
		!array_key_exists($consumer, $cert_curve_compatible) ||
2640
		in_array($curve, $cert_curve_compatible[$consumer]));
2641
}
2642

    
2643
/****f* certs/cert_build_list
2644
 * NAME
2645
 *   cert_build_list - Build an option list of cert or CA entries, checked
2646
 *                     against a specific consumer name.
2647
 * INPUTS
2648
 *   $type    : 'ca' for certificate authority entries, 'cert' for certificates.
2649
 *   $consumer: The consumer name used to filter certificates out of the result.
2650
 *                 See the contents of $cert_curve_compatible for details.
2651
 *   $selectsource: Then true, outputs in a format usable by select_source in
2652
 *                  packages.
2653
 *   $addnone: When true, a 'none' choice is added to the list.
2654
 * RESULT
2655
 *   Returns an option list of entries with incompatible entries removed. This
2656
 *   can be used directly in form option selection lists.
2657
 * NOTES
2658
 *   This can be expanded in the future to allow for other types of restrictions.
2659
 ******/
2660

    
2661
function cert_build_list($type = 'cert', $consumer = '', $selectsource = false, $addnone = false) {
2662
	/* Ensure that $type is valid */
2663
	if (!in_array($type, array('ca', 'cert'))) {
2664
		return array();
2665
	}
2666

    
2667
	$list = array();
2668
	if ($addnone) {
2669
		if ($selectsource) {
2670
			$list[] = array('refid' => 'none', 'descr' => 'None');
2671
		} else {
2672
			$list['none'] = "None";
2673
		}
2674
	}
2675

    
2676
	/* Create a hashed array with the certificate refid as the key and
2677
	 * descriptive name as the value. Exclude incompatible certificates. */
2678
	foreach (config_get_path($type, []) as $cert) {
2679
		if (empty($cert['prv']) && ($type == 'cert')) {
2680
			continue;
2681
		} elseif (cert_check_pkey_compatibility($cert['prv'], $consumer) &&
2682
			  cert_check_digest_compatibility($type, $cert, $consumer)) {
2683
			if ($selectsource) {
2684
				$list[] = array('refid' => $cert['refid'],
2685
						'descr' => $cert['descr']);
2686
			} else {
2687
				$list[$cert['refid']] = $cert['descr'];
2688
			}
2689
		}
2690
	}
2691

    
2692
	return $list;
2693
}
2694

    
2695
/****f* certs/cert_pkcs12_export
2696
 * NAME
2697
 *   cert_pkcs12_export - Export a PKCS#12 archive file for a given certificate
2698
 *                        and optional CA and passphrase.
2699
 * INPUTS
2700
 *   $cert      : Certificate entry array.
2701
 *   $encryption: Strength of encryption to use:
2702
 *                "high" (AES-256 + SHA256)
2703
 *                "low" (3DES + SHA1)
2704
 *                "legacy" (RC2-40 + SHA1)
2705
 *   $passphrase: Optional passphrase used to encrypt the archive contents and
2706
 *                private key.
2707
 *   $add_ca    : Boolean flag which determines whether or not the certificate
2708
 *                CA is included in the archive (if available)
2709
 *   $delivery  : Delivery method for the result: "file", "download", or "data".
2710
 *                See RESULT section for details.
2711
 * RESULT
2712
 *   Returns false on failure, otherwise result depends upon the value passed in
2713
 *   $delivery:
2714
 *       "file"    : Returns the path to the output archive file.
2715
 *                   NOTE: Does not clean up path, caller must clean up the
2716
 *                         entire directory containing the output file.
2717
 *       "download": Sends the archive data to the current GUI browser session.
2718
 *                   Must be called before any output is sent to the user
2719
 *                   session.
2720
 *       "data"    : Returns the contents of the PKCS#12 archive as a string.
2721
 * NOTES
2722
 *   If the certificate entry does not contain a private key, the archive will
2723
 *   also not contain a key.
2724
 ******/
2725

    
2726
function cert_pkcs12_export($cert, $encryption = 'high', $passphrase = '', $add_ca = true, $delivery = 'download') {
2727
	global $g;
2728

    
2729
	/* Unusable certificate entry, bail early. */
2730
	if (!is_array($cert) || empty($cert)) {
2731
		return false;
2732
	}
2733

    
2734
	/* Encryption and Digest */
2735
	switch ($encryption) {
2736
		case 'legacy':
2737
			$algo = '-legacy -certpbe PBE-SHA1-RC2-40 -keypbe PBE-SHA1-RC2-40';
2738
			$hash = '';
2739
			break;
2740
		case 'low':
2741
			$algo = '-certpbe PBE-SHA1-3DES -keypbe PBE-SHA1-3DES';
2742
			$hash = '-macalg SHA1';
2743
			break;
2744
		case 'high':
2745
		default:
2746
			$algo = '-aes256 -certpbe AES-256-CBC -keypbe AES-256-CBC';
2747
			$hash = '-macalg sha256';
2748
	}
2749

    
2750
	/* Make a secure temporary directory */
2751
	$workdir = tempnam("{$g['tmp_path']}/", "p12export");
2752
	@unlink_if_exists($workdir);
2753
	mkdir($workdir, 0600);
2754

    
2755
	/* Set the friendly name to the certificate description, if available */
2756
	$descr = "";
2757
	if (!empty($cert['descr'])) {
2758
		$edescr = escapeshellarg($cert['descr']);
2759
		$descr = "-name {$edescr} -CSP {$edescr}";
2760
		$fileprefix = basename($cert['descr']);
2761
	}
2762

    
2763
	/* If there isn't a usable portion of the description, use the refid */
2764
	if (empty($fileprefix)) {
2765
		$fileprefix = $cert['refid'];
2766
	}
2767

    
2768
	/* Exported output archive filename */
2769
	$outpath = "{$workdir}/{$fileprefix}.p12";
2770
	$eoutpath = escapeshellarg($outpath);
2771

    
2772
	/* Passphrase handling */
2773
	if (!empty($passphrase)) {
2774
		/* Use passphrase text file so the passphrase is not visible in
2775
		 * process list. */
2776
		$passfile = "{$workdir}/passphrase.txt";
2777
		file_put_contents($passfile, $passphrase . "\n");
2778
		$pass = '-passout file:' . escapeshellarg($passfile);
2779
	} else {
2780
		/* Null password + disable encryption of the keys */
2781
		$pass = '-passout pass: -nodes';
2782
	}
2783

    
2784
	/* Certificate file */
2785
	$crtpath = "{$workdir}/cert.pem";
2786
	$ecrtpath = escapeshellarg($crtpath);
2787
	file_put_contents($crtpath, base64_decode($cert['crt']));
2788

    
2789
	/* Private key (if present) */
2790
	if (!empty($cert['prv'])) {
2791
		$keypath = "{$workdir}/key.pem";
2792
		/* Write key to a secure temporary name */
2793
		file_put_contents($keypath, base64_decode($cert['prv']));
2794
		$key = '-inkey ' . escapeshellarg($keypath);
2795
	} else {
2796
		$key = '-nokeys';
2797
	}
2798

    
2799
	/* Add CA if one is defined and requested */
2800
	$eca = '';
2801
	if ($add_ca && !empty($cert['caref'])) {
2802
		$ca = lookup_ca($cert['caref']);
2803
		$ca = $ca['item'];
2804
		if ($ca) {
2805
			$capath = "{$workdir}/ca.pem";
2806
			file_put_contents($capath, base64_decode($ca['crt']));
2807
			$eca = '-certfile ' . escapeshellarg($capath);
2808
		}
2809
	}
2810

    
2811
	/* Export a PKCS#12 archive using these components and settings */
2812
	exec("/usr/bin/openssl pkcs12 -export -in {$ecrtpath} {$eca} {$key} -out {$eoutpath} {$pass} {$descr} {$algo} {$hash}");
2813

    
2814
	/* Bail if the output is invalid */
2815
	if (!file_exists($outpath) ||
2816
	    (filesize($outpath) == 0)) {
2817
		return false;
2818
	}
2819

    
2820
	/* Tailor output as requested by the caller */
2821
	switch ($delivery) {
2822
		case 'file':
2823
			/* Return path to export file, do not clean up, caller must clean up. */
2824
			return $outpath;
2825
			break;
2826
		case 'download':
2827
			/* Send file to user and cleanup */
2828
			$p12_data = file_get_contents($outpath);
2829
			rmdir_recursive($workdir);
2830
			send_user_download('data', $p12_data, "{$cert['descr']}.p12");
2831
			return true;
2832
			break;
2833
		case 'data':
2834
		default:
2835
			/* Return PKCS#12 archive data and cleanup */
2836
			$p12_data = file_get_contents($outpath);
2837
			rmdir_recursive($workdir);
2838
			return $p12_data;
2839
			break;
2840
	}
2841

    
2842
	return null;
2843
}
2844

    
2845
/* Check if a given digest is known to be weak */
2846
function is_weak_digest($digest_alg) {
2847
	global $cert_strict_values;
2848
	/* Check for and remove unnecessary ECDSA digest prefix
2849
	 * See https://redmine.pfsense.org/issues/13437 */
2850
	$ecdsa_prefix = 'ecdsa-with-';
2851
	if (substr(strtolower($digest_alg), 0, strlen($ecdsa_prefix)) == $ecdsa_prefix) {
2852
		$digest_alg = substr($digest_alg, strlen($ecdsa_prefix));
2853
	}
2854
	return in_array($digest_alg, $cert_strict_values['digest_blacklist']);
2855
}
2856

    
2857
/* Determine if a certificate is too weak to function in certain contexts
2858
 * such as for the GUI, Captive Portal, or OpenVPN with OpenSSL 3.x */
2859
function cert_has_weak_digest($str_crt, $decode = true) {
2860
	$digestinfo = cert_get_sigtype($str_crt, $decode);
2861
	return is_weak_digest($digestinfo['shortname']);
2862
}
2863

    
2864
/* Check both certificate and CA when passed a full certificate array entry */
2865
function cert_chain_has_weak_digest($cert) {
2866
	if (!is_array($cert) || !$cert['crt']) {
2867
		/* Empty input so flag as weak anyhow */
2868
		return true;
2869
	}
2870
	$weak = false;
2871
	/* Check CA if present */
2872
	if (!empty($cert['caref'])) {
2873
		$ca = lookup_ca($cert['caref']);
2874
		$ca = $ca['item'];
2875
		$weak = cert_has_weak_digest($ca['crt']);
2876
	}
2877
	/* Only check cert if the CA is not weak (or no self-signed) */
2878
	if (!$weak) {
2879
		$weak = cert_has_weak_digest($cert['crt']);
2880
	}
2881
	return $weak;
2882
}
2883

    
2884
function cert_check_digest_compatibility($type, $cert, $consumer) {
2885
	$nonweak_consumers = [
2886
		'ca'   => ['HTTPS'],
2887
		'cert' => ['HTTPS', 'OpenVPN'],
2888
	];
2889
	$usable = true;
2890
	/* HTTPS certificates must not have a weak entry anywhere in the CA+Cert
2891
	 * chain */
2892
	if (($type == 'cert') &&
2893
	    ($consumer == 'HTTPS')) {
2894
		$usable = !cert_chain_has_weak_digest($cert);
2895
	} else {
2896
		$usable = !cert_has_weak_digest($cert['crt']);
2897
	}
2898
	return ($usable ||
2899
		!in_array($consumer, $nonweak_consumers[$type]));
2900
}
2901

    
2902
function cert_create_selfsigned($descr = '', $hostname = '', $log = true) {
2903
	global $cert_strict_values;
2904

    
2905
	if ($log) {
2906
		log_error(sprintf(gettext("Creating self-signed SSL/TLS certificate (%s)"), $descr));
2907
	}
2908

    
2909
	if (empty($descr)) {
2910
		$descr = gettext("GUI default");
2911
	}
2912

    
2913
	/* Basic cert details */
2914
	$cert = [];
2915
	$cert['refid'] = uniqid();
2916
	$cert['descr'] = "{$descr} ({$cert['refid']})";
2917

    
2918
	if (empty($hostname)) {
2919
		/* Find the system hostname, append unique refid */
2920
		$hostname = config_get_path('system/hostname') . "-{$cert['refid']}";
2921
	}
2922

    
2923
	$dn = [
2924
		'organizationName' => g_get('product_label') . " {$descr} Self-Signed Certificate",
2925
		'commonName' => $hostname,
2926
		'subjectAltName' => "DNS:{$hostname}"
2927
	];
2928

    
2929
	/* Create the self-signed certificate */
2930
	$old_err_level = error_reporting(0); /* otherwise openssl_ functions throw warnings directly to a page screwing menu tab */
2931
	if (!cert_create($cert, null, 2048, $cert_strict_values['max_server_cert_lifetime'], $dn, "self-signed", "sha256")) {
2932
		while ($log && ($ssl_err = openssl_error_string())) {
2933
			log_error(sprintf(gettext("Error creating self-signed certificate: openssl library returns: %s"), $ssl_err));
2934
		}
2935
		error_reporting($old_err_level);
2936
		return null;
2937
	}
2938
	error_reporting($old_err_level);
2939

    
2940
	/* Add the new certificate */
2941
	config_set_path('cert/', $cert);
2942

    
2943
	return $cert;
2944
}
2945

    
2946
?>
(7-7/61)