Project

General

Profile

Download (88.2 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
	global $config;
523
	$old_err_level = error_reporting(0);
524

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

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

    
536
	$cert_type = cert_type_config_section($type);
537

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

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

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

    
553
	return $n509;
554
}
555

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

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

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

    
569
	$components = openssl_csr_get_subject($str_crt);
570

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

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

    
584
	return $subject;
585
}
586

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

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

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

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

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

    
614
	return $subject;
615
}
616

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

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

    
626
	$subject_array = array();
627

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

    
632
	return $subject_array;
633
}
634

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

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

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

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

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

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

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

    
680
	return $issuer;
681
}
682

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

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

    
742
	return $purpose;
743
}
744

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

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

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

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

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

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

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

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

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

    
821
	return $signature;
822
}
823

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

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

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

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

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

    
868
function is_user_cert($certref) {
869
	foreach (config_get_path('system/user', []) as $user) {
870
		if (!is_array($user['cert'])) {
871
			continue;
872
		}
873
		foreach ($user['cert'] as $cert) {
874
			if ($certref == $cert) {
875
				return true;
876
			}
877
		}
878
	}
879
	return false;
880
}
881

    
882
function is_openvpn_server_cert($certref) {
883
	foreach (config_get_path('openvpn/openvpn-server', []) as $ovpns) {
884
		if ($ovpns['certref'] == $certref) {
885
			return true;
886
		}
887
	}
888
	return false;
889
}
890

    
891
function is_openvpn_client_cert($certref) {
892
	foreach (config_get_path('openvpn/openvpn-client', []) as $ovpnc) {
893
		if ($ovpnc['certref'] == $certref) {
894
			return true;
895
		}
896
	}
897
	return false;
898
}
899

    
900
function is_ipsec_cert($certref) {
901
	foreach(config_get_path('ipsec/phase1', []) as $ipsec) {
902
		if ($ipsec['certref'] == $certref) {
903
			return true;
904
		}
905
	}
906
	return false;
907
}
908

    
909
function is_webgui_cert($certref) {
910
	if ((config_get_path('system/webgui/ssl-certref') == $certref) &&
911
	    (config_get_path('system/webgui/protocol') != "http")) {
912
		return true;
913
	}
914
}
915

    
916
function is_package_cert($certref) {
917
	$pluginparams = array();
918
	$pluginparams['type'] = 'certificates';
919
	$pluginparams['event'] = 'used_certificates';
920

    
921
	$certificates_used_by_packages = pkg_call_plugins('plugin_certificates', $pluginparams);
922

    
923
	/* Check if any package is using certificate */
924
	foreach ($certificates_used_by_packages as $name => $package) {
925
		if (is_array($package['certificatelist'][$certref]) &&
926
		    isset($package['certificatelist'][$certref]) > 0) {
927
			return true;
928
		}
929
	}
930
}
931

    
932
function is_captiveportal_cert($certref) {
933
	foreach (config_get_path('captiveportal', []) as $portal) {
934
		if (isset($portal['enable']) && isset($portal['httpslogin']) && ($portal['certref'] == $certref)) {
935
			return true;
936
		}
937
	}
938
	return false;
939
}
940

    
941
function is_unbound_cert($certref) {
942
	if (config_path_enabled('unbound') &&
943
	    config_path_enabled('unbound','enablessl') &&
944
	    (config_get_path('unbound/sslcertref') == $certref)) {
945
		return true;
946
	}
947
}
948

    
949
function cert_in_use($certref) {
950

    
951
	return (is_webgui_cert($certref) ||
952
		is_user_cert($certref) ||
953
		is_openvpn_server_cert($certref) ||
954
		is_openvpn_client_cert($certref) ||
955
		is_ipsec_cert($certref) ||
956
		is_captiveportal_cert($certref) ||
957
		is_unbound_cert($certref) ||
958
		is_package_cert($certref));
959
}
960

    
961
function cert_usedby_description($refid, $certificates_used_by_packages) {
962
	$result = "";
963
	if (is_array($certificates_used_by_packages)) {
964
		foreach ($certificates_used_by_packages as $name => $package) {
965
			if (isset($package['certificatelist'][$refid])) {
966
				$hint = "" ;
967
				if (is_array($package['certificatelist'][$refid])) {
968
					foreach ($package['certificatelist'][$refid] as $cert_used) {
969
						$hint = $hint . $cert_used['usedby']."\n";
970
					}
971
				}
972
				$count = count($package['certificatelist'][$refid]);
973
				$result .= "<div title='".htmlspecialchars($hint)."'>";
974
				$result .= htmlspecialchars($package['pkgname'])." ($count)<br />";
975
				$result .= "</div>";
976
			}
977
		}
978
	}
979
	return $result;
980
}
981

    
982
/* Detect a rollover at 2038 on some platforms (e.g. ARM)
983
 * See: https://redmine.pfsense.org/issues/9098 */
984
function cert_get_max_lifetime() {
985
	global $cert_max_lifetime;
986
	$max = $cert_max_lifetime;
987

    
988
	$current_time = time();
989
	while ((int)($current_time + ($max * 24 * 60 * 60)) < 0) {
990
		$max--;
991
	}
992
	return min($max, $cert_max_lifetime);
993
}
994

    
995
/* Detect a rollover at 2050 with UTCTime
996
 * See: https://redmine.pfsense.org/issues/9098 */
997
function crl_get_max_lifetime() {
998
	$now = new DateTime("now");
999
	$utctime_before_roll = DateTime::createFromFormat('Ymd', '20491231');
1000
	if ($now !== false) {
1001
		$interval = $now->diff($utctime_before_roll);
1002
		$max_days = abs($interval->days);
1003
		/* Reduce the max well below the rollover time */
1004
		if ($max_days > 1000) {
1005
			$max_days -= 1000;
1006
		}
1007
		return min($max_days, cert_get_max_lifetime());
1008
	}
1009

    
1010
	/* Cannot use date functions, so use a lower default max. */
1011
	return min(7000, cert_get_max_lifetime());
1012
}
1013

    
1014
/**
1015
 * Directly modifes the Certificate Revocation List and adds it to config.
1016
 */
1017
function crl_create(& $crl, $caref, $name, $serial = 0, $lifetime = 3650) {
1018
	global $config;
1019
	$max_lifetime = crl_get_max_lifetime();
1020
	$ca = lookup_ca($caref);
1021
	$ca = $ca['item'];
1022
	if (!$ca) {
1023
		return false;
1024
	}
1025
	$crl['descr'] = $name;
1026
	$crl['caref'] = $caref;
1027
	$crl['serial'] = $serial;
1028
	$crl['lifetime'] = ($lifetime > $max_lifetime) ? $max_lifetime : $lifetime;
1029
	$crl['cert'] = array();
1030

    
1031
	$crls = config_get_path('crl', []);
1032
	$crls[] = $crl;
1033
	config_set_path('crl', $crls);
1034
	return $crl;
1035
}
1036

    
1037
/**
1038
 * @param array &$crl_config Must contain the CRL config index and item.
1039
 */
1040
function crl_update(& $crl_config) {
1041
	$crl = &$crl_config['item'];
1042
	require_once('ASN1.php');
1043
	require_once('ASN1_UTF8STRING.php');
1044
	require_once('ASN1_ASCIISTRING.php');
1045
	require_once('ASN1_BITSTRING.php');
1046
	require_once('ASN1_BOOL.php');
1047
	require_once('ASN1_GENERALTIME.php');
1048
	require_once('ASN1_INT.php');
1049
	require_once('ASN1_ENUM.php');
1050
	require_once('ASN1_NULL.php');
1051
	require_once('ASN1_OCTETSTRING.php');
1052
	require_once('ASN1_OID.php');
1053
	require_once('ASN1_SEQUENCE.php');
1054
	require_once('ASN1_SET.php');
1055
	require_once('ASN1_SIMPLE.php');
1056
	require_once('ASN1_TELETEXSTRING.php');
1057
	require_once('ASN1_UTCTIME.php');
1058
	require_once('OID.php');
1059
	require_once('X509.php');
1060
	require_once('X509_CERT.php');
1061
	require_once('X509_CRL.php');
1062

    
1063
	global $config;
1064
	$max_lifetime = crl_get_max_lifetime();
1065
	$ca = lookup_ca($crl['caref']);
1066
	$ca = $ca['item'];
1067
	if (!$ca) {
1068
		return false;
1069
	}
1070
	// If we have text but no certs, it was imported and cannot be updated.
1071
	if (($crl["method"] != "internal") && (!empty($crl['text']) && empty($crl['cert']))) {
1072
		return false;
1073
	}
1074
	$crl['serial']++;
1075
	config_set_path("crl/{$crl_config['idx']}", $crl);
1076
	$ca_cert = \Ukrbublik\openssl_x509_crl\X509::pem2der(base64_decode($ca['crt']));
1077
	$ca_pkey = openssl_pkey_get_private(base64_decode($ca['prv']));
1078

    
1079
	$crlconf = array(
1080
		'no' => $crl['serial'],
1081
		'version' => 2,
1082
		'days' => ($crl['lifetime'] > $max_lifetime) ? $max_lifetime : $crl['lifetime'],
1083
		'alg' => OPENSSL_ALGO_SHA1,
1084
		'revoked' => array()
1085
	);
1086

    
1087
	if (is_array($crl['cert']) && (count($crl['cert']) > 0)) {
1088
		foreach ($crl['cert'] as $cert) {
1089
			/* Determine the serial number to revoke */
1090
			if (isset($cert['serial'])) {
1091
				$serial = $cert['serial'];
1092
			} elseif (isset($cert['crt'])) {
1093
				$serial = cert_get_serial($cert['crt'], true);
1094
			} else {
1095
				continue;
1096
			}
1097
			$crlconf['revoked'][] = array(
1098
				'serial' => $serial,
1099
				'rev_date' => $cert['revoke_time'],
1100
				'reason' => ($cert['reason'] == -1) ? null : (int) $cert['reason'],
1101
			);
1102
		}
1103
	}
1104

    
1105
	$crl_data = \Ukrbublik\openssl_x509_crl\X509_CRL::create($crlconf, $ca_pkey, $ca_cert);
1106
	$crl['text'] = base64_encode(\Ukrbublik\openssl_x509_crl\X509::der2pem4crl($crl_data));
1107
	config_set_path("crl/{$crl_config['idx']}", $crl);
1108

    
1109
	return $crl['text'];
1110
}
1111

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

    
1127
	if (!is_array($cert)) {
1128
		/* If passed a not an array but a serial string, set it up as an
1129
		 * array with the serial number defined */
1130
		$rcert = array();
1131
		$rcert['serial'] = $cert;
1132
	} else {
1133
		/* If passed a certificate entry, read out the serial and store
1134
		 * it separately. */
1135
		$rcert = $cert;
1136
		$rcert['serial'] = cert_get_serial($cert['crt']);
1137
	}
1138
	$rcert['reason'] = $reason;
1139
	$rcert['revoke_time'] = time();
1140
	$crl['cert'][] = $rcert;
1141
	crl_update($crl_config);
1142
	return true;
1143
}
1144

    
1145
/**
1146
 * @param array|string $cert        The cert item or cert serial to unrevoke.
1147
 * @param array        &$crl_config Must contain the CRL config index and item.
1148
 */
1149
function cert_unrevoke($cert, &$crl_config) {
1150
	$crl = &$crl_config['item'];
1151
	if (!is_crl_internal($crl)) {
1152
		return false;
1153
	}
1154

    
1155
	$serial = crl_get_entry_serial($cert);
1156

    
1157
	foreach ($crl['cert'] as $id => $rcert) {
1158
		/* Check for a match by refid, name, or serial number */
1159
		if (($rcert['refid'] == $cert['refid']) ||
1160
		    ($rcert['descr'] == $cert['descr']) ||
1161
		    (crl_get_entry_serial($rcert) == $serial)) {
1162
			unset($crl['cert'][$id]);
1163
			if (count($crl['cert']) == 0) {
1164
				// Protect against accidentally switching the type to imported, for older CRLs
1165
				if (!isset($crl['method'])) {
1166
					$crl['method'] = "internal";
1167
				}
1168
				crl_update($crl_config);
1169
			} else {
1170
				crl_update($crl_config);
1171
			}
1172
			return true;
1173
		}
1174
	}
1175
	return false;
1176
}
1177

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

    
1195
/****f* certs/crl_get_entry_serial
1196
 * NAME
1197
 *   crl_get_entry_serial - Take a CRL entry and determine the associated serial
1198
 * INPUTS
1199
 *   $entry: CRL certificate list entry to inspect, or serial string
1200
 * RESULT
1201
 *   The requested serial string, if present, or null if it cannot be determined.
1202
 ******/
1203

    
1204
function crl_get_entry_serial($entry) {
1205
	/* Check the passed entry several ways to determine the serial */
1206
	if (isset($entry['serial']) && (strlen($entry['serial']) > 0)) {
1207
		/* Entry is an array with a viable 'serial' element */
1208
		return $entry['serial'];
1209
	} elseif (isset($entry['crt'])) {
1210
		/* Entry is an array with certificate text which can be used to
1211
		 * determine the serial */
1212
		return cert_get_serial($entry['crt'], true);
1213
	} elseif (cert_validate_serial($entry, false, true) != null) {
1214
		/* Entry is a valid serial string */
1215
		return $entry;
1216
	}
1217
	/* Unable to find or determine a serial number */
1218
	return null;
1219
}
1220

    
1221
/****f* certs/cert_validate_serial
1222
 * NAME
1223
 *   cert_validate_serial - Validate a given string to test if it can be used as
1224
 *                          a certificate serial.
1225
 * INPUTS
1226
 *   $serial     : Serial number string to test
1227
 *   $returnvalue: Whether to return the parsed value or true/false
1228
 * RESULT
1229
 *   If $returnvalue is true, then the parsed ASN.1 integer value string for
1230
 *     $serial or null if invalid
1231
 *   If $returnvalue is false, then true/false based on whether or not $serial
1232
 *     is valid.
1233
 ******/
1234

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

    
1257
		/* Unfortunately, PHP openssl_csr_sign() limits serial numbers to a
1258
		 * PHP integer, so we cannot generate large numbers up to the maximum
1259
		 * allowed ASN.1 size (2^159). We are limited to PHP_INT_MAX --
1260
		 * As such, numbers larger than that limit should be rejected */
1261
		if ($serial > PHP_INT_MAX) {
1262
			throw new Exception('Serial too large for PHP OpenSSL');
1263
		}
1264

    
1265
		/* Attempt to create an ASN.1 integer, if it fails, an exception will be thrown */
1266
		$asn1serial = new \Ukrbublik\openssl_x509_crl\ASN1_INT( $serial );
1267
		return ($returnvalue) ? $asn1serial->content : true;
1268
	} catch (Exception $ex) {
1269
		/* No matter what the error is, return null or false depending
1270
		 * on what was requested. */
1271
		return ($returnvalue) ? null : false;
1272
	}
1273
}
1274

    
1275
/****f* certs/cert_generate_serial
1276
 * NAME
1277
 *   cert_generate_serial - Generate a random positive integer usable as a
1278
 *                          certificate serial number
1279
 * INPUTS
1280
 *   None
1281
 * RESULT
1282
 *   Integer representing an ASN.1 compatible certificate serial number.
1283
 ******/
1284

    
1285
function cert_generate_serial() {
1286
	/* Use a separate function for this to make it easier to use a better
1287
	 * randomization function in the future. */
1288

    
1289
	/* Unfortunately, PHP openssl_csr_sign() limits serial numbers to a
1290
	 * PHP integer, so we cannot generate large numbers up to the maximum
1291
	 * allowed ASN.1 size (2^159). We are limited to PHP_INT_MAX */
1292
	return random_int(1, PHP_INT_MAX);
1293
}
1294

    
1295
/****f* certs/ca_has_serial
1296
 * NAME
1297
 *   ca_has_serial - Check if a serial number is used by any certificate in a given CA
1298
 * INPUTS
1299
 *   $ca    : Certificate Authority to check
1300
 *   $serial: Serial number to check
1301
 * RESULT
1302
 *   true if the serial number is in use by a certificate issued by this CA,
1303
 *   false otherwise.
1304
 ******/
1305

    
1306
function ca_has_serial($caref, $serial) {
1307
	global $config;
1308

    
1309
	/* Check certs first -- more likely to find a hit */
1310
	foreach (config_get_path('cert', []) as $cert) {
1311
		if (($cert['caref'] == $caref) &&
1312
		    (cert_get_serial($cert['crt'], true) == $serial)) {
1313
			/* If this certificate is issued by the CA in question
1314
			 * and has a matching serial number, stop processing
1315
			 * and return true. */
1316
			return true;
1317
		}
1318
	}
1319

    
1320
	/* Check the CA itself */
1321
	$this_ca = lookup_ca($caref);
1322
	$this_ca = $this_ca['item'];
1323
	$this_serial = cert_get_serial($this_ca['crt'], true);
1324
	if ($serial == $this_serial) {
1325
		return true;
1326
	}
1327

    
1328
	/* Check other CAs for a match (intermediates signed by this CA) */
1329
	foreach (config_get_path('ca', []) as $ca) {
1330
		if (($ca['caref'] == $caref) &&
1331
		    (cert_get_serial($ca['crt'], true) == $serial)) {
1332
			/* If this CA is issued by the CA in question
1333
			 * and has a matching serial number, stop processing
1334
			 * and return true. */
1335
			return true;
1336
		}
1337
	}
1338

    
1339
	return false;
1340
}
1341

    
1342
/****f* certs/cert_get_random_serial
1343
 * NAME
1344
 *   cert_get_random_serial - Generate a random certificate serial unique in a CA
1345
 * INPUTS
1346
 *   $caref : Certificate Authority refid to test for serial uniqueness.
1347
 * RESULT
1348
 *   Random serial number which is not in use by any known certificate in a CA
1349
 ******/
1350

    
1351
function cert_get_random_serial($caref = '') {
1352
	/* Number of attempts to generate a usable serial. Multiple attempts
1353
	 *  are necessary to ensure that the number is usable and unique. */
1354
	$attempts = 10;
1355

    
1356
	/* Default value, -1 indicates an error */
1357
	$serial = -1;
1358

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

    
1376
/****f* certs/ca_get_next_serial
1377
 * NAME
1378
 *   ca_get_next_serial - Get the next available serial number for a CA
1379
 * INPUTS
1380
 *   $ca: Reference to a CA entry
1381
 * RESULT
1382
 *   A randomized serial number (if enabled for a CA) or the next sequential value.
1383
 ******/
1384

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

    
1400
/****f* certs/crl_contains_cert
1401
 * NAME
1402
 *   crl_contains_cert - Check if a certificate is present in a CRL
1403
 * INPUTS
1404
 *   $crl : CRL to check
1405
 *   $cert: Certificate to test
1406
 * RESULT
1407
 *   true if the CRL contains the certificate, false otherwise
1408
 ******/
1409

    
1410
function crl_contains_cert($crl, $cert) {
1411
	global $config;
1412
	if (!is_array($config['crl']) ||
1413
	    !is_array($crl['cert'])) {
1414
		return false;
1415
	}
1416

    
1417
	/* Find the issuer of this CRL */
1418
	$ca = lookup_ca($crl['caref']);
1419
	$ca = $ca['item'];
1420
	$crlissuer = is_array($cert) ? cert_get_subject($ca['crt']) : null;
1421
	$serial = crl_get_entry_serial($cert);
1422

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

    
1426
	/* If the requested certificate was not issued by the
1427
	 * same CA as the CRL, then do not bother checking this
1428
	 * CRL. */
1429
	if ($issuer != $crlissuer) {
1430
		return false;
1431
	}
1432

    
1433
	/* Check CRL entries to see if the certificate serial is revoked */
1434
	foreach ($crl['cert'] as $rcert) {
1435
		if (crl_get_entry_serial($rcert) == $serial) {
1436
			return true;
1437
		}
1438
	}
1439

    
1440
	/* Certificate was not found in the CRL */
1441
	return false;
1442
}
1443

    
1444
/****f* certs/is_cert_revoked
1445
 * NAME
1446
 *   is_cert_revoked - Test if a given certificate or serial is revoked
1447
 * INPUTS
1448
 *   $cert  : Certificate entry or serial number to test
1449
 *   $crlref: CRL to check for revoked entries, or empty to check all CRLs
1450
 * RESULT
1451
 *   true if the requested entry is revoked
1452
 *   false if the requested entry is not revoked
1453
 ******/
1454

    
1455
function is_cert_revoked($cert, $crlref = "") {
1456
	global $config;
1457
	if (!is_array($config['crl'])) {
1458
		return false;
1459
	}
1460

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

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

    
1486
function is_openvpn_server_crl($crlref) {
1487
	foreach (config_get_path('openvpn/openvpn-server', []) as $ovpns) {
1488
		if (!empty($ovpns['crlref']) && ($ovpns['crlref'] == $crlref)) {
1489
			return true;
1490
		}
1491
	}
1492
	return false;
1493
}
1494

    
1495
function is_package_crl($crlref) {
1496
	$pluginparams = array();
1497
	$pluginparams['type'] = 'certificates';
1498
	$pluginparams['event'] = 'used_crl';
1499

    
1500
	$certificates_used_by_packages = pkg_call_plugins('plugin_certificates', $pluginparams);
1501

    
1502
	/* Check if any package is using CRL */
1503
	foreach ($certificates_used_by_packages as $name => $package) {
1504
		if (is_array($package['certificatelist'][$crlref]) &&
1505
		    (count($package['certificatelist'][$crlref]) > 0)) {
1506
			return true;
1507
		}
1508
	}
1509
}
1510

    
1511
// Keep this general to allow for future expansion. See cert_in_use() above.
1512
function crl_in_use($crlref) {
1513
	return (is_openvpn_server_crl($crlref) ||
1514
		is_package_crl($crlref));
1515
}
1516

    
1517
function is_crl_internal($crl) {
1518
	return (!(!empty($crl['text']) && empty($crl['cert'])) || ($crl["method"] == "internal"));
1519
}
1520

    
1521
function cert_get_cn($crt, $isref = false) {
1522
	/* If this is a certref, not an actual cert, look up the cert first */
1523
	if ($isref) {
1524
		$cert = lookup_cert($crt);
1525
		$cert = $cert['item'];
1526
		/* If it's not a valid cert, bail. */
1527
		if (!(is_array($cert) && !empty($cert['crt']))) {
1528
			return "";
1529
		}
1530
		$cert = $cert['crt'];
1531
	} else {
1532
		$cert = $crt;
1533
	}
1534
	$sub = cert_get_subject_array($cert);
1535
	if (is_array($sub)) {
1536
		foreach ($sub as $s) {
1537
			if (strtoupper($s['a']) == "CN") {
1538
				return $s['v'];
1539
			}
1540
		}
1541
	}
1542
	return "";
1543
}
1544

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

    
1561
function cert_add_altname_type($str) {
1562
	$type = "";
1563
	if (is_ipaddr($str)) {
1564
		$type = "IP";
1565
	} elseif (is_hostname($str, true)) {
1566
		$type = "DNS";
1567
	} elseif (is_URL($str)) {
1568
		$type = "URI";
1569
	} elseif (filter_var($str, FILTER_VALIDATE_EMAIL)) {
1570
		$type = "email";
1571
	}
1572
	if (!empty($type)) {
1573
		return "{$type}:" . cert_escape_x509_chars($str);
1574
	} else {
1575
		return null;
1576
	}
1577
}
1578

    
1579
function cert_type_config_section($type) {
1580
	switch ($type) {
1581
		case "ca":
1582
			$cert_type = "v3_ca";
1583
			break;
1584
		case "server":
1585
		case "self-signed":
1586
			$cert_type = "server";
1587
			break;
1588
		default:
1589
			$cert_type = "usr_cert";
1590
			break;
1591
	}
1592
	return $cert_type;
1593
}
1594

    
1595
/****f* certs/is_cert_locally_renewable
1596
 * NAME
1597
 *   is_cert_locally_renewable - Check to see if an existing certificate can be
1598
 *                               renewed by a local internal CA.
1599
 * INPUTS
1600
 *   $cert : The certificate to be tested
1601
 * RESULT
1602
 *   true if the certificate can be locally renewed, false otherwise.
1603
 ******/
1604

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

    
1612
	/* Get subject and issuer values to test for self-signed state */
1613
	$subj = cert_get_subject($cert['crt']);
1614
	$issuer = cert_get_issuer($cert['crt']);
1615

    
1616
	/* Lookup CA for this certificate */
1617
	$ca = array();
1618
	if (!empty($cert['caref'])) {
1619
		$ca = lookup_ca($cert['caref']);
1620
		$ca = $ca['item'];
1621
	}
1622

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

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

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

    
1661
function cert_renew(& $cert_config, $reusekey = true, $strictsecurity = false, $reuseserial = false) {
1662
	global $cert_strict_values, $cert_curve_compatible, $curve_compatible_list;
1663

    
1664
	$cert = &$cert_config['item'];
1665
	/* If there is no certificate or private key string, this entry is either
1666
	 *  invalid or cannot be renewed by this function. */
1667
	if (empty($cert['crt']) || empty($cert['prv'])) {
1668
		return false;
1669
	}
1670

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

    
1674
	/* No details, must not be valid in some way */
1675
	if (!array($cert_details) || empty($cert_details)) {
1676
		return false;
1677
	}
1678

    
1679
	$subj = cert_get_subject($cert['crt']);
1680
	$issuer = cert_get_issuer($cert['crt']);
1681
	$purpose = cert_get_purpose($cert['crt']);
1682

    
1683
	$res_key = openssl_pkey_get_private(base64_decode($cert['prv']));
1684
	$key_details = openssl_pkey_get_details($res_key);
1685

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

    
1708
	/* Test for self-signed or signed by a CA */
1709
	$selfsigned = ($subj == $issuer);
1710

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

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

    
1724
	/* Reuse lifetime (convert seconds to days) */
1725
	$lifetime = (int) round(($cert_details['validTo_time_t'] - $cert_details['validFrom_time_t']) / 86400);
1726

    
1727
	/* If we are enforcing strict security, then cap the lifetime for server certificates */
1728
	if (($cert_type == 'server') && $strictsecurity &&
1729
	    ($lifetime > $cert_strict_values['max_server_cert_lifetime'])) {
1730
		$lifetime = $cert_strict_values['max_server_cert_lifetime'];
1731
	}
1732

    
1733
	/* Reuse SAN list, or, if empty, add CN as SAN. */
1734
	$sans = str_replace("IP Address", "IP", $cert_details['extensions']['subjectAltName']);
1735
	if (empty($sans)) {
1736
		$sans = cert_add_altname_type($dn['commonName']);
1737
	}
1738

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

    
1750
	/* Determine current digest algorithm. */
1751
	$digest_alg = strtolower($cert_details['signatureTypeSN']);
1752

    
1753
	/* Check for and remove unnecessary ECDSA digest prefix
1754
	 * See https://redmine.pfsense.org/issues/13437 */
1755
	$ecdsa_prefix = 'ecdsa-with-';
1756
	if (substr($digest_alg, 0, strlen($ecdsa_prefix)) == $ecdsa_prefix) {
1757
		$digest_alg = substr($digest_alg, strlen($ecdsa_prefix));
1758
	}
1759

    
1760
	/* If we are enforcing strict security, then check the digest against a
1761
	 * blacklist of insecure digest methods. */
1762
	if ($strictsecurity &&
1763
	    is_weak_digest($cert_details['signatureTypeSN'])) {
1764
		$digest_alg = 'sha256';
1765
	}
1766

    
1767
	/* Validate key type, assume RSA if it cannot be read. */
1768
	if (is_array($key_details) && array_key_exists('type', $key_details)) {
1769
		$private_key_type = $key_details['type'];
1770
	} else {
1771
		$private_key_type = OPENSSL_KEYTYPE_RSA;
1772
	}
1773

    
1774
	/* Setup certificate and key arguments */
1775
	$args = array(
1776
		"x509_extensions" => $cert_type,
1777
		"digest_alg" => $digest_alg,
1778
		"private_key_type" => $private_key_type,
1779
		"encrypt_key" => false);
1780

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

    
1795
	/* Set key parameters. */
1796
	if ($key_details['type'] ==  OPENSSL_KEYTYPE_RSA) {
1797
		$args['private_key_bits'] = (int)$private_key_bits;
1798
	} else if ($ec_curve) {
1799
		$args['curve_name'] = $ec_curve;
1800
	} else {
1801
		$args['curve_name'] = $key_details['ec']['curve_name'];
1802
	}
1803

    
1804
	/* Make a new key if necessary */
1805
	if (!$res_key || !$reusekey) {
1806
		$res_key = openssl_pkey_new($args);
1807
		if (!$res_key) {
1808
			return false;
1809
		}
1810
	}
1811

    
1812
	/* Create a new CSR from derived parameters and key */
1813
	$res_csr = openssl_csr_new($dn, $res_key, $args);
1814
	/* If the CSR could not be created, bail */
1815
	if (!$res_csr) {
1816
		return false;
1817
	}
1818

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

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

    
1858
	/* Sign the CSR */
1859
	$res_crt = openssl_csr_sign($res_csr, $ca_res_crt, $ca_res_key, $lifetime,
1860
				 $args, $ca_serial);
1861
	/* If the CSR could not be signed, bail */
1862
	if (!$res_crt) {
1863
		return false;
1864
	}
1865

    
1866
	/* Attempt to read the key and certificate and if that fails, bail */
1867
	if (!openssl_pkey_export($res_key, $str_key) ||
1868
	    !openssl_x509_export($res_crt, $str_crt)) {
1869
		return false;
1870
	}
1871

    
1872
	/* Load the new certificate string and key into the configuration */
1873
	$cert['crt'] = base64_encode($str_crt);
1874
	$cert['prv'] = base64_encode($str_key);
1875
	config_set_path("{$cert_config['path']}", $cert);
1876

    
1877
	return true;
1878
}
1879

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

    
1894
function cert_get_all_services($refid) {
1895
	$services = array();
1896
	$services['services'] = array();
1897
	$services['packages'] = array();
1898

    
1899
	/* Only set if true, otherwise leave unset. */
1900
	if (is_webgui_cert($refid)) {
1901
		$services['webgui'] = true;
1902
	}
1903

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

    
1921
	/* If any one IPsec tunnel uses this certificate then the whole service
1922
	 * needs a bump. */
1923

    
1924
	foreach (config_get_path('ipsec/phase1', []) as $ipsec) {
1925
		if (($ipsec['authentication_method'] == 'cert') &&
1926
		    ($ipsec['certref'] == $refid)) {
1927
			$services['services'][] = array('name' => 'ipsec');
1928
			/* Stop after finding one, no need to search for more. */
1929
			break;
1930
		}
1931
	}
1932

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

    
1949
	/* Locate any packages using this certificate */
1950
	$pkgcerts = pkg_call_plugins('plugin_certificates', array('type' => 'certificates', 'event' => 'used_certificates'));
1951
	foreach ($pkgcerts as $name => $package) {
1952
		if (is_array($package['certificatelist'][$refid]) &&
1953
		    isset($package['certificatelist'][$refid]) > 0) {
1954
			$services['packages'][] = $name;
1955
		}
1956
	}
1957

    
1958
	return $services;
1959
}
1960

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

    
1983
	foreach (array('server', 'client') as $mode) {
1984
		foreach (config_get_path("openvpn/openvpn-{$mode}", []) as $ovpn) {
1985
			if ($ovpn['caref'] == $refid) {
1986
				$services['services'][] = array(
1987
					'name' => 'openvpn',
1988
					'extras' => array(
1989
						'vpnmode' => $mode,
1990
						'id' => $ovpn['vpnid']
1991
					)
1992
				);
1993
			}
1994
		}
1995
	}
1996

    
1997
	foreach (config_get_path('ipsec/phase1', []) as $ipsec) {
1998
		if ($ipsec['certref'] == $refid) {
1999
			break;
2000
		}
2001
	}
2002

    
2003
	foreach (config_get_path('ipsec/phase1', []) as $ipsec) {
2004
		if (($ipsec['authentication_method'] == 'cert') &&
2005
		    ($ipsec['caref'] == $refid)) {
2006
			$services['services'][] = array('name' => 'ipsec');
2007
			break;
2008
		}
2009
	}
2010

    
2011
	/* Loop through all certs and get their services as well */
2012
	foreach (config_get_path('cert', []) as $cert) {
2013
		if ($cert['caref'] == $refid) {
2014
			$services = array_merge_recursive_unique($services, cert_get_all_services($cert['refid']));
2015
		}
2016
	}
2017

    
2018
	/* Look for intermediate certs and services */
2019
	foreach (config_get_path('ca', []) as $cert) {
2020
		if ($cert['caref'] == $refid) {
2021
			$services = array_merge_recursive_unique($services, ca_get_all_services($cert['refid']));
2022
		}
2023
	}
2024

    
2025
	return $services;
2026
}
2027

    
2028
/****f* certs/cert_restart_services
2029
 * NAME
2030
 *   cert_restart_services - Restarts services specific to CA/Certificate usage
2031
 * INPUTS
2032
 *   $services: An array of services returned by cert_get_all_services or ca_get_all_services
2033
 * RESULT
2034
 *   Services in the given array are restarted
2035
 *   returns false if the input is invalid
2036
 *   returns true at the end of execution
2037
 ******/
2038

    
2039
function cert_restart_services($services) {
2040
	require_once("service-utils.inc");
2041
	/* If the input is not an array, it is invalid. */
2042
	if (!is_array($services)) {
2043
		return false;
2044
	}
2045

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

    
2049
	/* Restart GUI: */
2050
	if ($services['webgui']) {
2051
		ob_flush();
2052
		flush();
2053
		log_error(sprintf($restart_string, gettext('service'), 'WebGUI'));
2054
		send_event("service restart webgui");
2055
	}
2056

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

    
2075
	/* Restart Packages: */
2076
	if (is_array($services['packages'])) {
2077
		foreach ($services['packages'] as $service) {
2078
			log_error(sprintf($restart_string, gettext('package'), $service));
2079
			restart_service($service);
2080
		}
2081
	}
2082
	return true;
2083
}
2084

    
2085
/****f* certs/cert_get_lifetime
2086
 * NAME
2087
 *   cert_get_lifetime - Returns the number of days the certificate is valid
2088
 * INPUTS
2089
 *   $untilexpire: Boolean
2090
 *     true: The number of days returned is from now until the certificate expiration.
2091
 *     false (default): The number of days returned is the total lifetime of the certificate.
2092
 * RESULT
2093
 *   Integer number of days in the certificate total or remaining lifetime
2094
 ******/
2095

    
2096
function cert_get_lifetime($cert, $untilexpire = false) {
2097
	/* If the certificate is not valid, bail. */
2098
	if (!is_array($cert) || empty($cert['crt'])) {
2099
		return null;
2100
	}
2101
	/* Read certificate details */
2102
	list($startdate, $enddate) = cert_get_dates($cert['crt'], true, false);
2103

    
2104
	/* If either of the dates are invalid, there is nothing we can do here. */
2105
	if (($startdate === false) || ($enddate === false)) {
2106
		return false;
2107
	}
2108

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

    
2112
	/* Calculate the requested intervals */
2113
	$interval = $startdate->diff($enddate);
2114

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

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

    
2130
function cert_analyze_lifetime($expiredays) {
2131
	global $g;
2132
	/* Number of days at which to warn of expiration. */
2133
	$warning_days = config_get_path('notifications/certexpire/expiredays', g_get('default_cert_expiredays'));
2134

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

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

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

    
2193
/****f* certs/cert_print_infoblock
2194
 * NAME
2195
 *   cert_print_infoblock - Print an information block containing certificate details
2196
 * INPUTS
2197
 *   $cert: CA or Cert entry for which the information will be printed
2198
 * RESULT
2199
 *   Returns null if the passed entry is invalid
2200
 *   Otherwise, outputs information to the user with formatting.
2201
 ******/
2202

    
2203
function cert_print_infoblock($cert) {
2204
	/* If the certificate is not valid, bail. */
2205
	if (!is_array($cert) || empty($cert['crt'])) {
2206
		return null;
2207
	}
2208
	/* Variable to hold the formatted information */
2209
	$certextinfo = "";
2210

    
2211
	/* Serial number */
2212
	$cert_details = openssl_x509_parse(base64_decode($cert['crt']));
2213
	if (isset($cert_details['serialNumber']) && (strlen($cert_details['serialNumber']) > 0)) {
2214
		$certextinfo .= '<b>' . gettext("Serial: ") . '</b> ';
2215
		$certextinfo .= htmlspecialchars(cert_escape_x509_chars($cert_details['serialNumber'], true));
2216
		$certextinfo .= '<br/>';
2217
	}
2218

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

    
2234
	/* Subject Alternative Name (SAN) list */
2235
	$sans = cert_get_sans($cert['crt']);
2236
	if (is_array($sans) && !empty($sans)) {
2237
		$certextinfo .= '<b>' . gettext("SAN: ") . '</b> ';
2238
		$certextinfo .= htmlspecialchars(implode(', ', cert_escape_x509_chars($sans, true)));
2239
		$certextinfo .= '<br/>';
2240
	}
2241

    
2242
	/* Key usage */
2243
	$purpose = cert_get_purpose($cert['crt']);
2244
	if (is_array($purpose) && !empty($purpose['ku'])) {
2245
		$certextinfo .= '<b>' . gettext("KU: ") . '</b> ';
2246
		$certextinfo .= htmlspecialchars(implode(', ', $purpose['ku']));
2247
		$certextinfo .= '<br/>';
2248
	}
2249

    
2250
	/* Extended key usage */
2251
	if (is_array($purpose) && !empty($purpose['eku'])) {
2252
		$certextinfo .= '<b>' . gettext("EKU: ") . '</b> ';
2253
		$certextinfo .= htmlspecialchars(implode(', ', $purpose['eku']));
2254
		$certextinfo .= '<br/>';
2255
	}
2256

    
2257
	/* OCSP / Must Staple */
2258
	if (cert_get_ocspstaple($cert['crt'])) {
2259
		$certextinfo .= '<b>' . gettext("OCSP: ") . '</b> ';
2260
		$certextinfo .= gettext("Must Staple");
2261
		$certextinfo .= '<br/>';
2262
	}
2263

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

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

    
2299
	/* Hash value */
2300
	if (!empty($cert_details['hash'])) {
2301
		$certextinfo .= '<b>' . gettext("Hash: ") . '</b> ';
2302
		$certextinfo .= htmlspecialchars(cert_escape_x509_chars($cert_details['hash'], true));
2303
		$certextinfo .= '<br/>';
2304
	}
2305

    
2306
	/* Subject Key Identifier (SKID) */
2307
	if (is_array($cert_details["extensions"]) && !empty($cert_details["extensions"]["subjectKeyIdentifier"])) {
2308
		$certextinfo .= '<b>' . gettext("Subject Key ID: ") . '</b> ';
2309
		$certextinfo .= htmlspecialchars(cert_escape_x509_chars($cert_details["extensions"]["subjectKeyIdentifier"], true));
2310
		$certextinfo .= '<br/>';
2311
	}
2312

    
2313
	/* Authority Key Identifier (AKID) */
2314
	if (is_array($cert_details["extensions"]) && !empty($cert_details["extensions"]["authorityKeyIdentifier"])) {
2315
		$certextinfo .= '<b>' . gettext("Authority Key ID: ") . '</b> ';
2316
		$certextinfo .= str_replace("\n", '<br/>', htmlspecialchars(cert_escape_x509_chars($cert_details["extensions"]["authorityKeyIdentifier"], true)));
2317
		$certextinfo .= '<br/>';
2318
	}
2319

    
2320
	/* Total Lifetime (days from cert start to end) */
2321
	$lifetime = cert_get_lifetime($cert);
2322
	if ($lifetime !== false) {
2323
		$certextinfo .= '<b>' . gettext("Total Lifetime: ") . '</b> ';
2324
		$certextinfo .= sprintf("%d %s", $lifetime, (abs($lifetime) == 1) ? gettext('day') : gettext('days'));
2325
		$certextinfo .= '<br/>';
2326

    
2327
		/* Lifetime before certificate expires (days from now to end) */
2328
		$expiredays = cert_get_lifetime($cert, true);
2329
		list($lrclass, $expstring) = cert_analyze_lifetime($expiredays);
2330
		$certextinfo .= '<b>' . gettext("Lifetime Remaining: ") . '</b> ';
2331
		$certextinfo .= "<span class=\"text-{$lrclass}\">{$expstring}</span>";
2332
		$certextinfo .= '<br/>';
2333
	}
2334

    
2335
	if ($purpose['ca'] == 'Yes') {
2336
		/* CA Trust store presence */
2337
		$certextinfo .= '<b>' . gettext("Trust Store: ") . '</b> ';
2338
		$certextinfo .= (isset($cert['trust']) && ($cert['trust'] == "enabled")) ? gettext('Included') : gettext('Excluded');
2339
		$certextinfo .= '<br/>';
2340

    
2341
		if (!empty($cert['prv'])) {
2342
			/* CA Next/Randomize Serial */
2343
			$certextinfo .= '<b>' . gettext("Next Serial: ") . '</b> ';
2344
			$certextinfo .= (isset($cert['randomserial']) && ($cert['randomserial'] == "enabled")) ? gettext('Randomized') : $cert['serial'];
2345
			$certextinfo .= '<br/>';
2346
		}
2347
	}
2348

    
2349
	/* Output the infoblock */
2350
	if (!empty($certextinfo)) { ?>
2351
		<div class="infoblock">
2352
		<? print_info_box($certextinfo, 'info', false); ?>
2353
		</div>
2354
	<?php
2355
	}
2356
}
2357

    
2358
/****f* certs/cert_notify_expiring
2359
 * NAME
2360
 *   cert_notify_expiring - Notify admin about expiring certificates
2361
 * INPUTS
2362
 *   None
2363
 * RESULT
2364
 *   File a notice containing expiring certificate information, which is then
2365
 *   logged, displayed in the GUI, and sent via e-mail (if enabled).
2366
 ******/
2367

    
2368
function cert_notify_expiring() {
2369
	global $config;
2370

    
2371
	/* If certificate expiration notifications are disabled, there is nothing to do. */
2372
	if (config_get_path('notifications/certexpire/enable') == "disabled") {
2373
		return;
2374
	}
2375

    
2376
	$notifications = array();
2377

    
2378
	/* Check all CA and Cert entries at once */
2379
	init_config_arr(array('ca'));
2380
	init_config_arr(array('cert'));
2381
	$all_certs = array_merge_recursive($config['ca'], $config['cert']);
2382

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

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

    
2424
function ca_setup_trust_store() {
2425
	/* This directory is trusted by OpenSSL on FreeBSD by default */
2426
	$trust_store_directory = '/usr/local/etc/ssl/certs';
2427

    
2428
	/* Create the directory if it does not already exist, and clean it up if it does. */
2429
	safe_mkdir($trust_store_directory);
2430
	unlink_if_exists("{$trust_store_directory}/*.crt");
2431
	unlink_if_exists("{$trust_store_directory}/*.crl");
2432

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

    
2442
		ca_setup_capath($ca, $trust_store_directory, '', false, 'crt', 'crl');
2443
	}
2444

    
2445
	mwexec_bg('/usr/sbin/certctl rehash');
2446
}
2447

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

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

    
2476
	/* Check for an empty base directory, which is invalid */
2477
	if (empty($basedir)) {
2478
		return false;
2479
	}
2480

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

    
2496

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

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

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

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

    
2526
	return true;
2527
}
2528

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

    
2540
function cert_get_pkey_curve($pkey, $decode = true) {
2541
	if ($decode) {
2542
		$pkey = base64_decode($pkey);
2543
	}
2544

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

    
2560
	/* Either the private key could not be read, or this is not an EC certificate. */
2561
	return false;
2562
}
2563

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

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

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

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

    
2624
function cert_check_pkey_compatibility($pkey, $consumer) {
2625
	global $cert_curve_compatible;
2626

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

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

    
2657
function cert_build_list($type = 'cert', $consumer = '', $selectsource = false, $addnone = false) {
2658
	/* Ensure that $type is valid */
2659
	if (!in_array($type, array('ca', 'cert'))) {
2660
		return array();
2661
	}
2662

    
2663
	$list = array();
2664
	if ($addnone) {
2665
		if ($selectsource) {
2666
			$list[] = array('refid' => 'none', 'descr' => 'None');
2667
		} else {
2668
			$list['none'] = "None";
2669
		}
2670
	}
2671

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

    
2688
	return $list;
2689
}
2690

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

    
2722
function cert_pkcs12_export($cert, $encryption = 'high', $passphrase = '', $add_ca = true, $delivery = 'download') {
2723
	global $g;
2724

    
2725
	/* Unusable certificate entry, bail early. */
2726
	if (!is_array($cert) || empty($cert)) {
2727
		return false;
2728
	}
2729

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

    
2746
	/* Make a secure temporary directory */
2747
	$workdir = tempnam("{$g['tmp_path']}/", "p12export");
2748
	@unlink_if_exists($workdir);
2749
	mkdir($workdir, 0600);
2750

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

    
2759
	/* If there isn't a usable portion of the description, use the refid */
2760
	if (empty($fileprefix)) {
2761
		$fileprefix = $cert['refid'];
2762
	}
2763

    
2764
	/* Exported output archive filename */
2765
	$outpath = "{$workdir}/{$fileprefix}.p12";
2766
	$eoutpath = escapeshellarg($outpath);
2767

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

    
2780
	/* Certificate file */
2781
	$crtpath = "{$workdir}/cert.pem";
2782
	$ecrtpath = escapeshellarg($crtpath);
2783
	file_put_contents($crtpath, base64_decode($cert['crt']));
2784

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

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

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

    
2810
	/* Bail if the output is invalid */
2811
	if (!file_exists($outpath) ||
2812
	    (filesize($outpath) == 0)) {
2813
		return false;
2814
	}
2815

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

    
2838
	return null;
2839
}
2840

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

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

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

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

    
2898
function cert_create_selfsigned($descr = '', $hostname = '', $log = true) {
2899
	global $cert_strict_values;
2900

    
2901
	$a_cert = config_get_path('cert', []);
2902
	if ($log) {
2903
		log_error(sprintf(gettext("Creating self-signed SSL/TLS certificate (%s)"), $descr));
2904
	}
2905

    
2906
	if (empty($descr)) {
2907
		$descr = gettext("GUI default");
2908
	}
2909

    
2910
	/* Basic cert details */
2911
	$cert = [];
2912
	$cert['refid'] = uniqid();
2913
	$cert['descr'] = "{$descr} ({$cert['refid']})";
2914

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

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

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

    
2937
	/* Add the new certificate */
2938
	$a_cert[] = $cert;
2939
	config_set_path('cert', $a_cert);
2940

    
2941
	return $cert;
2942
}
2943

    
2944
?>
(7-7/61)