Project

General

Profile

Download (88.4 KB) Statistics
| Branch: | Tag: | Revision:
1
<?php
2
/*
3
 * certs.inc
4
 *
5
 * part of pfSense (https://www.pfsense.org)
6
 * Copyright (c) 2008-2013 BSD Perimeter
7
 * Copyright (c) 2013-2016 Electric Sheep Fencing
8
 * Copyright (c) 2014-2025 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 (!is_array($ca) || 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 (!is_array($ca) || 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 (!is_array($cert) || 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 (!is_array($cert) || 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']}/serial", ++$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
		$ca           = null;
438
		$ca_res_crt   = null;
439
		$ca_res_key   = $res_key;
440
		$ca_serial    = cert_get_random_serial();
441
		$cert['type'] = "server";
442
	}
443

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

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

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

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

    
467
	return true;
468
}
469

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

    
472
	$cert_type = cert_type_config_section($type);
473

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

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

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

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

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

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

    
517
	return true;
518
}
519

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

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

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

    
534
	$cert_type = cert_type_config_section($type);
535

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

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

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

    
551
	return $n509;
552
}
553

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

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

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

    
567
	$components = openssl_csr_get_subject($str_crt);
568

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

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

    
582
	return $subject;
583
}
584

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

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

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

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

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

    
612
	return $subject;
613
}
614

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

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

    
624
	$subject_array = array();
625

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

    
630
	return $subject_array;
631
}
632

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

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

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

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

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

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

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

    
678
	return $issuer;
679
}
680

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

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

    
740
	return $purpose;
741
}
742

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

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

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

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

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

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

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

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

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

    
819
	return $signature;
820
}
821

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

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

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

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

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

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

    
877
	return false;
878
}
879

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1163
	$serial = crl_get_entry_serial($cert);
1164

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1345
	return false;
1346
}
1347

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1882
	return true;
1883
}
1884

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

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

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

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

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

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

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

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

    
1963
	return $services;
1964
}
1965

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

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

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

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

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

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

    
2030
	return $services;
2031
}
2032

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
2379
	$notifications = array();
2380

    
2381
	/* Check all CA and Cert entries at once */
2382
	$all_certs = array_merge_recursive(config_get_path('ca', []), config_get_path('cert', []));
2383

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

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

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

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

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

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

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

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

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

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

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

    
2497

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

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

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

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

    
2527
	return true;
2528
}
2529

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
2689
	return $list;
2690
}
2691

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
2839
	return null;
2840
}
2841

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

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

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

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

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

    
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
	config_set_path('cert/', $cert);
2939

    
2940
	return $cert;
2941
}
2942

    
2943
?>
(7-7/61)