Project

General

Profile

Download (81.7 KB) Statistics
| Branch: | Tag: | Revision:
1
<?php
2
/*
3
 * certs.inc
4
 *
5
 * part of pfSense (https://www.pfsense.org)
6
 * Copyright (c) 2008-2013 BSD Perimeter
7
 * Copyright (c) 2013-2016 Electric Sheep Fencing
8
 * Copyright (c) 2014-2022 Rubicon Communications, LLC (Netgate)
9
 * Copyright (c) 2008 Shrew Soft Inc. All rights reserved.
10
 * All rights reserved.
11
 *
12
 * Licensed under the Apache License, Version 2.0 (the "License");
13
 * you may not use this file except in compliance with the License.
14
 * You may obtain a copy of the License at
15
 *
16
 * http://www.apache.org/licenses/LICENSE-2.0
17
 *
18
 * Unless required by applicable law or agreed to in writing, software
19
 * distributed under the License is distributed on an "AS IS" BASIS,
20
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21
 * See the License for the specific language governing permissions and
22
 * limitations under the License.
23
 */
24

    
25
define("OPEN_SSL_CONF_PATH", "/etc/ssl/openssl.cnf");
26

    
27
require_once("functions.inc");
28

    
29
global $openssl_digest_algs;
30
$openssl_digest_algs = array("sha1", "sha224", "sha256", "sha384", "sha512");
31

    
32
global $openssl_crl_status;
33
/* Numbers are set in the RFC: https://www.ietf.org/rfc/rfc5280.txt */
34
$openssl_crl_status = array(
35
	-1 => "No Status (default)",
36
	0  => "Unspecified",
37
	1 => "Key Compromise",
38
	2 => "CA Compromise",
39
	3 => "Affiliation Changed",
40
	4 => "Superseded",
41
	5 => "Cessation of Operation",
42
	6 => "Certificate Hold",
43
	9 => 'Privilege Withdrawn',
44
);
45

    
46
global $cert_altname_types;
47
$cert_altname_types = array(
48
	'DNS' => gettext('FQDN or Hostname'),
49
	'IP' => gettext('IP address'),
50
	'URI' => gettext('URI'),
51
	'email' => gettext('email address'),
52
);
53

    
54
global $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
function & lookup_ca($refid) {
68
	global $config;
69

    
70
	if (is_array($config['ca'])) {
71
		foreach ($config['ca'] as & $ca) {
72
			if (empty($ca)) {
73
				continue;
74
			}
75
			if ($ca['refid'] == $refid) {
76
				return $ca;
77
			}
78
		}
79
	}
80

    
81
	return false;
82
}
83

    
84
function & lookup_ca_by_subject($subject) {
85
	global $config;
86

    
87
	if (is_array($config['ca'])) {
88
		foreach ($config['ca'] as & $ca) {
89
			if (empty($ca)) {
90
				continue;
91
			}
92
			$ca_subject = cert_get_subject($ca['crt']);
93
			if ($ca_subject == $subject) {
94
				return $ca;
95
			}
96
		}
97
	}
98

    
99
	return false;
100
}
101

    
102
function & lookup_cert($refid) {
103
	global $config;
104

    
105
	if (is_array($config['cert'])) {
106
		foreach ($config['cert'] as & $cert) {
107
			if (empty($cert)) {
108
				continue;
109
			}
110
			if ($cert['refid'] == $refid) {
111
				return $cert;
112
			}
113
		}
114
	}
115

    
116
	return false;
117
}
118

    
119
function & lookup_cert_by_name($name) {
120
	global $config;
121
	if (is_array($config['cert'])) {
122
		foreach ($config['cert'] as & $cert) {
123
			if (empty($cert)) {
124
				continue;
125
			}
126
			if ($cert['descr'] == $name) {
127
				return $cert;
128
			}
129
		}
130
	}
131
}
132

    
133
function & lookup_crl($refid) {
134
	global $config;
135

    
136
	if (is_array($config['crl'])) {
137
		foreach ($config['crl'] as & $crl) {
138
			if (empty($crl)) {
139
				continue;
140
			}
141
			if ($crl['refid'] == $refid) {
142
				return $crl;
143
			}
144
		}
145
	}
146

    
147
	return false;
148
}
149

    
150
function ca_chain_array(& $cert) {
151
	if ($cert['caref']) {
152
		$chain = array();
153
		$crt = lookup_ca($cert['caref']);
154
		$chain[] = $crt;
155
		while ($crt) {
156
			$caref = $crt['caref'];
157
			if ($caref) {
158
				$crt = lookup_ca($caref);
159
			} else {
160
				$crt = false;
161
			}
162
			if ($crt) {
163
				$chain[] = $crt;
164
			}
165
		}
166
		return $chain;
167
	}
168
	return false;
169
}
170

    
171
function ca_chain(& $cert) {
172
	if ($cert['caref']) {
173
		$ca = "";
174
		$cas = ca_chain_array($cert);
175
		if (is_array($cas)) {
176
			foreach ($cas as & $ca_cert) {
177
				$ca .= base64_decode($ca_cert['crt']);
178
				$ca .= "\n";
179
			}
180
		}
181
		return $ca;
182
	}
183
	return "";
184
}
185

    
186
function ca_import(& $ca, $str, $key = "", $serial = "") {
187
	global $config;
188

    
189
	$ca['crt'] = base64_encode($str);
190
	if (!empty($key)) {
191
		$ca['prv'] = base64_encode($key);
192
	}
193
	if (empty($serial)) {
194
		$ca['serial'] = 0;
195
	} else {
196
		$ca['serial'] = $serial;
197
	}
198
	$subject = cert_get_subject($str, false);
199
	$issuer = cert_get_issuer($str, false);
200
	$serialNumber = cert_get_serial($str, false);
201

    
202
	// Find my issuer unless self-signed
203
	if ($issuer <> $subject) {
204
		$issuer_crt =& lookup_ca_by_subject($issuer);
205
		if ($issuer_crt) {
206
			$ca['caref'] = $issuer_crt['refid'];
207
		}
208
	}
209

    
210
	/* Correct if child certificate was loaded first */
211
	if (is_array($config['ca'])) {
212
		foreach ($config['ca'] as & $oca) {
213
			// check by serial number if CA already exists
214
			$osn = cert_get_serial($oca['crt']);
215
			if (($ca['refid'] <> $oca['refid']) && ($serialNumber == $osn)) {
216
				return false;
217
			}
218
			$issuer = cert_get_issuer($oca['crt']);
219
			if (($ca['refid'] <> $oca['refid']) && ($issuer == $subject)) {
220
				$oca['caref'] = $ca['refid'];
221
			}
222
		}
223
	}
224
	if (is_array($config['cert'])) {
225
		foreach ($config['cert'] as & $cert) {
226
			$issuer = cert_get_issuer($cert['crt']);
227
			if ($issuer == $subject) {
228
				$cert['caref'] = $ca['refid'];
229
			}
230
		}
231
	}
232
	return true;
233
}
234

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

    
237
	$args = array(
238
		"x509_extensions" => "v3_ca",
239
		"digest_alg" => $digest_alg,
240
		"encrypt_key" => false);
241
	if ($keytype == 'ECDSA') {
242
		$args["curve_name"] = $ecname;
243
		$args["private_key_type"] = OPENSSL_KEYTYPE_EC;
244
	} else {
245
		$args["private_key_bits"] = (int)$keylen;
246
		$args["private_key_type"] = OPENSSL_KEYTYPE_RSA;
247
	}
248

    
249
	// generate a new key pair
250
	$res_key = openssl_pkey_new($args);
251
	if (!$res_key) {
252
		return false;
253
	}
254

    
255
	// generate a certificate signing request
256
	$res_csr = openssl_csr_new($dn, $res_key, $args);
257
	if (!$res_csr) {
258
		return false;
259
	}
260

    
261
	// self sign the certificate
262
	$res_crt = openssl_csr_sign($res_csr, null, $res_key, $lifetime, $args, cert_get_random_serial());
263
	if (!$res_crt) {
264
		return false;
265
	}
266

    
267
	// export our certificate data
268
	if (!openssl_pkey_export($res_key, $str_key) ||
269
	    !openssl_x509_export($res_crt, $str_crt)) {
270
		return false;
271
	}
272

    
273
	// return our ca information
274
	$ca['crt'] = base64_encode($str_crt);
275
	$ca['prv'] = base64_encode($str_key);
276
	$ca['serial'] = 0;
277

    
278
	return true;
279
}
280

    
281
function ca_inter_create(& $ca, $keylen, $lifetime, $dn, $caref, $digest_alg = "sha256", $keytype = "RSA", $ecname = "prime256v1") {
282
	// Create Intermediate Certificate Authority
283
	$signing_ca =& lookup_ca($caref);
284
	if (!$signing_ca) {
285
		return false;
286
	}
287

    
288
	$signing_ca_res_crt = openssl_x509_read(base64_decode($signing_ca['crt']));
289
	$signing_ca_res_key = openssl_pkey_get_private(array(0 => base64_decode($signing_ca['prv']) , 1 => ""));
290
	if (!$signing_ca_res_crt || !$signing_ca_res_key) {
291
		return false;
292
	}
293
	$signing_ca_serial = ++$signing_ca['serial'];
294

    
295
	$args = array(
296
		"x509_extensions" => "v3_ca",
297
		"digest_alg" => $digest_alg,
298
		"encrypt_key" => false);
299
	if ($keytype == 'ECDSA') {
300
		$args["curve_name"] = $ecname;
301
		$args["private_key_type"] = OPENSSL_KEYTYPE_EC;
302
	} else {
303
		$args["private_key_bits"] = (int)$keylen;
304
		$args["private_key_type"] = OPENSSL_KEYTYPE_RSA;
305
	}
306

    
307
	// generate a new key pair
308
	$res_key = openssl_pkey_new($args);
309
	if (!$res_key) {
310
		return false;
311
	}
312

    
313
	// generate a certificate signing request
314
	$res_csr = openssl_csr_new($dn, $res_key, $args);
315
	if (!$res_csr) {
316
		return false;
317
	}
318

    
319
	// Sign the certificate
320
	$res_crt = openssl_csr_sign($res_csr, $signing_ca_res_crt, $signing_ca_res_key, $lifetime, $args, $signing_ca_serial);
321
	if (!$res_crt) {
322
		return false;
323
	}
324

    
325
	// export our certificate data
326
	if (!openssl_pkey_export($res_key, $str_key) ||
327
	    !openssl_x509_export($res_crt, $str_crt)) {
328
		return false;
329
	}
330

    
331
	// return our ca information
332
	$ca['crt'] = base64_encode($str_crt);
333
	$ca['prv'] = base64_encode($str_key);
334
	$ca['serial'] = 0;
335
	$ca['caref'] = $caref;
336

    
337
	return true;
338
}
339

    
340
function cert_import(& $cert, $crt_str, $key_str) {
341

    
342
	$cert['crt'] = base64_encode($crt_str);
343
	$cert['prv'] = base64_encode($key_str);
344

    
345
	$subject = cert_get_subject($crt_str, false);
346
	$issuer = cert_get_issuer($crt_str, false);
347

    
348
	// Find my issuer unless self-signed
349
	if ($issuer <> $subject) {
350
		$issuer_crt =& lookup_ca_by_subject($issuer);
351
		if ($issuer_crt) {
352
			$cert['caref'] = $issuer_crt['refid'];
353
		}
354
	}
355
	return true;
356
}
357

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

    
360
	$cert['type'] = $type;
361

    
362
	if ($type != "self-signed") {
363
		$cert['caref'] = $caref;
364
		$ca =& lookup_ca($caref);
365
		if (!$ca) {
366
			return false;
367
		}
368

    
369
		$ca_str_crt = base64_decode($ca['crt']);
370
		$ca_str_key = base64_decode($ca['prv']);
371
		$ca_res_crt = openssl_x509_read($ca_str_crt);
372
		$ca_res_key = openssl_pkey_get_private(array(0 => $ca_str_key, 1 => ""));
373
		if (!$ca_res_key) {
374
			return false;
375
		}
376

    
377
		/* Get the next available CA serial number. */
378
		$ca_serial = ca_get_next_serial($ca);
379
	}
380

    
381
	$cert_type = cert_type_config_section($type);
382

    
383
	// in case of using Subject Alternative Names use other sections (with postfix '_san')
384
	// pass subjectAltName over environment variable 'SAN'
385
	if ($dn['subjectAltName']) {
386
		putenv("SAN={$dn['subjectAltName']}"); // subjectAltName can be set _only_ via configuration file
387
		$cert_type .= '_san';
388
		unset($dn['subjectAltName']);
389
	}
390

    
391
	$args = array(
392
		"x509_extensions" => $cert_type,
393
		"digest_alg" => $digest_alg,
394
		"encrypt_key" => false);
395
	if ($keytype == 'ECDSA') {
396
		$args["curve_name"] = $ecname;
397
		$args["private_key_type"] = OPENSSL_KEYTYPE_EC;
398
	} else {
399
		$args["private_key_bits"] = (int)$keylen;
400
		$args["private_key_type"] = OPENSSL_KEYTYPE_RSA;
401
	}
402

    
403
	// generate a new key pair
404
	$res_key = openssl_pkey_new($args);
405
	if (!$res_key) {
406
		return false;
407
	}
408

    
409
	// If this is a self-signed cert, blank out the CA and sign with the cert's key
410
	if ($type == "self-signed") {
411
		$ca           = null;
412
		$ca_res_crt   = null;
413
		$ca_res_key   = $res_key;
414
		$ca_serial    = cert_get_random_serial();
415
		$cert['type'] = "server";
416
	}
417

    
418
	// generate a certificate signing request
419
	$res_csr = openssl_csr_new($dn, $res_key, $args);
420
	if (!$res_csr) {
421
		return false;
422
	}
423

    
424
	// sign the certificate using an internal CA
425
	$res_crt = openssl_csr_sign($res_csr, $ca_res_crt, $ca_res_key, $lifetime,
426
				 $args, $ca_serial);
427
	if (!$res_crt) {
428
		return false;
429
	}
430

    
431
	// export our certificate data
432
	if (!openssl_pkey_export($res_key, $str_key) ||
433
	    !openssl_x509_export($res_crt, $str_crt)) {
434
		return false;
435
	}
436

    
437
	// return our certificate information
438
	$cert['crt'] = base64_encode($str_crt);
439
	$cert['prv'] = base64_encode($str_key);
440

    
441
	return true;
442
}
443

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

    
446
	$cert_type = cert_type_config_section($type);
447

    
448
	// in case of using Subject Alternative Names use other sections (with postfix '_san')
449
	// pass subjectAltName over environment variable 'SAN'
450
	if ($dn['subjectAltName']) {
451
		putenv("SAN={$dn['subjectAltName']}"); // subjectAltName can be set _only_ via configuration file
452
		$cert_type .= '_san';
453
		unset($dn['subjectAltName']);
454
	}
455

    
456
	$args = array(
457
		"x509_extensions" => $cert_type,
458
		"req_extensions" => "req_{$cert_type}",
459
		"digest_alg" => $digest_alg,
460
		"encrypt_key" => false);
461
	if ($keytype == 'ECDSA') {
462
		$args["curve_name"] = $ecname;
463
		$args["private_key_type"] = OPENSSL_KEYTYPE_EC;
464
	} else {
465
		$args["private_key_bits"] = (int)$keylen;
466
		$args["private_key_type"] = OPENSSL_KEYTYPE_RSA;
467
	}
468

    
469
	// generate a new key pair
470
	$res_key = openssl_pkey_new($args);
471
	if (!$res_key) {
472
		return false;
473
	}
474

    
475
	// generate a certificate signing request
476
	$res_csr = openssl_csr_new($dn, $res_key, $args);
477
	if (!$res_csr) {
478
		return false;
479
	}
480

    
481
	// export our request data
482
	if (!openssl_pkey_export($res_key, $str_key) ||
483
	    !openssl_csr_export($res_csr, $str_csr)) {
484
		return false;
485
	}
486

    
487
	// return our request information
488
	$cert['csr'] = base64_encode($str_csr);
489
	$cert['prv'] = base64_encode($str_key);
490

    
491
	return true;
492
}
493

    
494
function csr_sign($csr, & $ca, $duration, $type, $altnames, $digest_alg = "sha256") {
495
	global $config;
496
	$old_err_level = error_reporting(0);
497

    
498
	// Gather the information required for signed cert
499
	$ca_str_crt = base64_decode($ca['crt']);
500
	$ca_str_key = base64_decode($ca['prv']);
501
	$ca_res_key = openssl_pkey_get_private(array(0 => $ca_str_key, 1 => ""));
502
	if (!$ca_res_key) {
503
		return false;
504
	}
505

    
506
	/* Get the next available CA serial number. */
507
	$ca_serial = ca_get_next_serial($ca);
508

    
509
	$cert_type = cert_type_config_section($type);
510

    
511
	if (!empty($altnames)) {
512
		putenv("SAN={$altnames}"); // subjectAltName can be set _only_ via configuration file
513
		$cert_type .= '_san';
514
	}
515

    
516
	$args = array(
517
		"x509_extensions" => $cert_type,
518
		"digest_alg" => $digest_alg,
519
		"req_extensions" => "req_{$cert_type}"
520
	);
521

    
522
	// Sign the new cert and export it in x509 format
523
	openssl_x509_export(openssl_csr_sign($csr, $ca_str_crt, $ca_str_key, $duration, $args, $ca_serial), $n509);
524
	error_reporting($old_err_level);
525

    
526
	return $n509;
527
}
528

    
529
function csr_complete(& $cert, $str_crt) {
530
	$str_key = base64_decode($cert['prv']);
531
	cert_import($cert, $str_crt, $str_key);
532
	unset($cert['csr']);
533
	return true;
534
}
535

    
536
function csr_get_subject($str_crt, $decode = true) {
537

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

    
542
	$components = openssl_csr_get_subject($str_crt);
543

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

    
548
	ksort($components);
549
	foreach ($components as $a => $v) {
550
		if (!strlen($subject)) {
551
			$subject = "{$a}={$v}";
552
		} else {
553
			$subject = "{$a}={$v}, {$subject}";
554
		}
555
	}
556

    
557
	return $subject;
558
}
559

    
560
function cert_get_subject($str_crt, $decode = true) {
561

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

    
566
	$inf_crt = openssl_x509_parse($str_crt);
567
	$components = $inf_crt['subject'];
568

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

    
573
	ksort($components);
574
	foreach ($components as $a => $v) {
575
		if (is_array($v)) {
576
			ksort($v);
577
			foreach ($v as $w) {
578
				$asubject = "{$a}={$w}";
579
				$subject = (strlen($subject)) ? "{$asubject}, {$subject}" : $asubject;
580
			}
581
		} else {
582
			$asubject = "{$a}={$v}";
583
			$subject = (strlen($subject)) ? "{$asubject}, {$subject}" : $asubject;
584
		}
585
	}
586

    
587
	return $subject;
588
}
589

    
590
function cert_get_subject_array($crt) {
591
	$str_crt = base64_decode($crt);
592
	$inf_crt = openssl_x509_parse($str_crt);
593
	$components = $inf_crt['subject'];
594

    
595
	if (!is_array($components)) {
596
		return;
597
	}
598

    
599
	$subject_array = array();
600

    
601
	foreach ($components as $a => $v) {
602
		$subject_array[] = array('a' => $a, 'v' => $v);
603
	}
604

    
605
	return $subject_array;
606
}
607

    
608
function cert_get_subject_hash($crt) {
609
	$str_crt = base64_decode($crt);
610
	$inf_crt = openssl_x509_parse($str_crt);
611
	return $inf_crt['subject'];
612
}
613

    
614
function cert_get_sans($str_crt, $decode = true) {
615
	if ($decode) {
616
		$str_crt = base64_decode($str_crt);
617
	}
618
	$sans = array();
619
	$crt_details = openssl_x509_parse($str_crt);
620
	if (!empty($crt_details['extensions']['subjectAltName'])) {
621
		$sans = explode(',', $crt_details['extensions']['subjectAltName']);
622
	}
623
	return $sans;
624
}
625

    
626
function cert_get_issuer($str_crt, $decode = true) {
627

    
628
	if ($decode) {
629
		$str_crt = base64_decode($str_crt);
630
	}
631

    
632
	$inf_crt = openssl_x509_parse($str_crt);
633
	$components = $inf_crt['issuer'];
634

    
635
	if (empty($components) || !is_array($components)) {
636
		return "unknown";
637
	}
638

    
639
	ksort($components);
640
	foreach ($components as $a => $v) {
641
		if (is_array($v)) {
642
			ksort($v);
643
			foreach ($v as $w) {
644
				$aissuer = "{$a}={$w}";
645
				$issuer = (strlen($issuer)) ? "{$aissuer}, {$issuer}" : $aissuer;
646
			}
647
		} else {
648
			$aissuer = "{$a}={$v}";
649
			$issuer = (strlen($issuer)) ? "{$aissuer}, {$issuer}" : $aissuer;
650
		}
651
	}
652

    
653
	return $issuer;
654
}
655

    
656
/* Works for both RSA and ECC (crt) and key (prv) */
657
function cert_get_publickey($str_crt, $decode = true, $type = "crt") {
658
	if ($decode) {
659
		$str_crt = base64_decode($str_crt);
660
	}
661
	$certfn = tempnam('/tmp', 'CGPK');
662
	file_put_contents($certfn, $str_crt);
663
	switch ($type) {
664
		case 'prv':
665
			exec("/usr/bin/openssl pkey -in {$certfn} -pubout", $out);
666
			break;
667
		case 'crt':
668
			exec("/usr/bin/openssl x509 -in {$certfn} -inform pem -noout -pubkey", $out);
669
			break;
670
		case 'csr':
671
			exec("/usr/bin/openssl req -in {$certfn} -inform pem -noout -pubkey", $out);
672
			break;
673
		default:
674
			$out = array();
675
			break;
676
	}
677
	unlink($certfn);
678
	return implode("\n", $out);
679
}
680

    
681
function cert_get_purpose($str_crt, $decode = true) {
682
	$extended_oids = array(
683
		"1.3.6.1.5.5.8.2.2" => "IP Security IKE Intermediate",
684
	);
685
	if ($decode) {
686
		$str_crt = base64_decode($str_crt);
687
	}
688
	$crt_details = openssl_x509_parse($str_crt);
689
	$purpose = array();
690
	if (!empty($crt_details['extensions']['keyUsage'])) {
691
		$purpose['ku'] = explode(',', $crt_details['extensions']['keyUsage']);
692
		foreach ($purpose['ku'] as & $ku) {
693
			$ku = trim($ku);
694
			if (array_key_exists($ku, $extended_oids)) {
695
				$ku = $extended_oids[$ku];
696
			}
697
		}
698
	} else {
699
		$purpose['ku'] = array();
700
	}
701
	if (!empty($crt_details['extensions']['extendedKeyUsage'])) {
702
		$purpose['eku'] = explode(',', $crt_details['extensions']['extendedKeyUsage']);
703
		foreach ($purpose['eku'] as & $eku) {
704
			$eku = trim($eku);
705
			if (array_key_exists($eku, $extended_oids)) {
706
				$eku = $extended_oids[$eku];
707
			}
708
		}
709
	} else {
710
		$purpose['eku'] = array();
711
	}
712
	$purpose['ca'] = (stristr($crt_details['extensions']['basicConstraints'], 'CA:TRUE') === false) ? 'No': 'Yes';
713
	$purpose['server'] = (in_array('TLS Web Server Authentication', $purpose['eku'])) ? 'Yes': 'No';
714

    
715
	return $purpose;
716
}
717

    
718
function cert_get_ocspstaple($str_crt, $decode = true) {
719
	if ($decode) {
720
		$str_crt = base64_decode($str_crt);
721
	}
722
	$crt_details = openssl_x509_parse($str_crt);
723
	if (($crt_details['extensions']['tlsfeature'] == "status_request") ||
724
	    !empty($crt_details['extensions']['1.3.6.1.5.5.7.1.24'])) {
725
		return true;
726
	}
727
	return false;
728
}
729

    
730
function cert_format_date($validTS, $validTS_time_t, $outputstring = true) {
731
	$now = new DateTime("now");
732

    
733
	/* Try to create a DateTime object from the full time string */
734
	$date = DateTime::createFromFormat('ymdHis', rtrim($validTS, 'Z'), new DateTimeZone('Z'));
735
	/* If that failed, try using a four digit year */
736
	if ($date === false) {
737
		$date = DateTime::createFromFormat('YmdHis', rtrim($validTS, 'Z'), new DateTimeZone('Z'));
738
	}
739
	/* If that failed, try to create it from the UNIX timestamp */
740
	if (($date === false) && (!empty($validTS_time_t))) {
741
		$date = new DateTime('@' . $validTS_time_t, new DateTimeZone('Z'));
742
	}
743
	/* If we have a valid DateTime object, format it in a nice way */
744
	if ($date !== false) {
745
		$date->setTimezone($now->getTimeZone());
746
		if ($outputstring) {
747
			$date = $date->format(DateTimeInterface::RFC2822);
748
		}
749
	}
750
	return $date;
751
}
752

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

    
759
	$start = cert_format_date($crt_details['validFrom'], $crt_details['validFrom_time_t'], $outputstring);
760
	$end   = cert_format_date($crt_details['validTo'], $crt_details['validTo_time_t'], $outputstring);
761

    
762
	return array($start, $end);
763
}
764

    
765
function cert_get_serial($str_crt, $decode = true) {
766
	if ($decode) {
767
		$str_crt = base64_decode($str_crt);
768
	}
769
	$crt_details = openssl_x509_parse($str_crt);
770
	if (isset($crt_details['serialNumber'])) {
771
		return $crt_details['serialNumber'];
772
	} else {
773
		return NULL;
774
	}
775
}
776

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

    
783
	$signature = array();
784
	if (isset($crt_details['signatureTypeSN']) && !empty($crt_details['signatureTypeSN'])) {
785
		$signature['shortname'] = $crt_details['signatureTypeSN'];
786
	}
787
	if (isset($crt_details['signatureTypeLN']) && !empty($crt_details['signatureTypeLN'])) {
788
		$signature['longname'] = $crt_details['signatureTypeLN'];
789
	}
790
	if (isset($crt_details['signatureTypeNID']) && !empty($crt_details['signatureTypeNID'])) {
791
		$signature['nid'] = $crt_details['signatureTypeNID'];
792
	}
793

    
794
	return $signature;
795
}
796

    
797
function is_openvpn_server_ca($caref) {
798
	foreach(config_get_path('openvpn/openvpn-server', []) as $opvns) {
799
		if ($ovpns['caref'] == $caref) {
800
			return true;
801
		}
802
	}
803
	return false;
804
}
805

    
806
function is_openvpn_client_ca($caref) {
807
	foreach(config_get_path('openvpn/openvpn-client', []) as $ovpnc) {
808
		if ($ovpnc['caref'] == $caref) {
809
			return true;
810
		}
811
	}
812
	return false;
813
}
814

    
815
function is_ipsec_peer_ca($caref) {
816
	foreach (config_get_path('ipsec/phase1', []) as $ipsec) {
817
		if ($ipsec['caref'] == $caref) {
818
			return true;
819
		}
820
	}
821
	return false;
822
}
823

    
824
function is_ldap_peer_ca($caref) {
825
	foreach (config_get_path('system/authserver', []) as $authserver) {
826
		if (($authserver['ldap_caref'] == $caref) &&
827
		    ($authserver['ldap_urltype'] != 'Standard TCP')) {
828
			return true;
829
		}
830
	}
831
	return false;
832
}
833

    
834
function ca_in_use($caref) {
835
	return (is_openvpn_server_ca($caref) ||
836
		is_openvpn_client_ca($caref) ||
837
		is_ipsec_peer_ca($caref) ||
838
		is_ldap_peer_ca($caref));
839
}
840

    
841
function is_user_cert($certref) {
842
	foreach (config_get_path('system/user', []) as $user) {
843
		if (!is_array($user['cert'])) {
844
			continue;
845
		}
846
		foreach ($user['cert'] as $cert) {
847
			if ($certref == $cert) {
848
				return true;
849
			}
850
		}
851
	}
852
	return false;
853
}
854

    
855
function is_openvpn_server_cert($certref) {
856
	foreach (config_get_path('openvpn/openvpn-server', []) as $ovpns) {
857
		if ($ovpns['certref'] == $certref) {
858
			return true;
859
		}
860
	}
861
	return false;
862
}
863

    
864
function is_openvpn_client_cert($certref) {
865
	foreach (config_get_path('openvpn/openvpn-client', []) as $ovpnc) {
866
		if ($ovpnc['certref'] == $certref) {
867
			return true;
868
		}
869
	}
870
	return false;
871
}
872

    
873
function is_ipsec_cert($certref) {
874
	foreach(config_get_path('ipsec/phase1', []) as $ipsec) {
875
		if ($ipsec['certref'] == $certref) {
876
			return true;
877
		}
878
	}
879
	return false;
880
}
881

    
882
function is_webgui_cert($certref) {
883
	if ((config_get_path('system/webgui/ssl-certref') == $certref) &&
884
	    (config_get_path('system/webgui/protocol') != "http")) {
885
		return true;
886
	}
887
}
888

    
889
function is_package_cert($certref) {
890
	$pluginparams = array();
891
	$pluginparams['type'] = 'certificates';
892
	$pluginparams['event'] = 'used_certificates';
893

    
894
	$certificates_used_by_packages = pkg_call_plugins('plugin_certificates', $pluginparams);
895

    
896
	/* Check if any package is using certificate */
897
	foreach ($certificates_used_by_packages as $name => $package) {
898
		if (is_array($package['certificatelist'][$certref]) &&
899
		    isset($package['certificatelist'][$certref]) > 0) {
900
			return true;
901
		}
902
	}
903
}
904

    
905
function is_captiveportal_cert($certref) {
906
	foreach (config_get_path('captiveportal', []) as $portal) {
907
		if (isset($portal['enable']) && isset($portal['httpslogin']) && ($portal['certref'] == $certref)) {
908
			return true;
909
		}
910
	}
911
	return false;
912
}
913

    
914
function is_unbound_cert($certref) {
915
	if (config_path_enabled('unbound') &&
916
	    config_path_enabled('unbound','enablessl') &&
917
	    (config_get_path('unbound/sslcertref') == $certref)) {
918
		return true;
919
	}
920
}
921

    
922
function cert_in_use($certref) {
923

    
924
	return (is_webgui_cert($certref) ||
925
		is_user_cert($certref) ||
926
		is_openvpn_server_cert($certref) ||
927
		is_openvpn_client_cert($certref) ||
928
		is_ipsec_cert($certref) ||
929
		is_captiveportal_cert($certref) ||
930
		is_unbound_cert($certref) ||
931
		is_package_cert($certref));
932
}
933

    
934
function cert_usedby_description($refid, $certificates_used_by_packages) {
935
	$result = "";
936
	if (is_array($certificates_used_by_packages)) {
937
		foreach ($certificates_used_by_packages as $name => $package) {
938
			if (isset($package['certificatelist'][$refid])) {
939
				$hint = "" ;
940
				if (is_array($package['certificatelist'][$refid])) {
941
					foreach ($package['certificatelist'][$refid] as $cert_used) {
942
						$hint = $hint . $cert_used['usedby']."\n";
943
					}
944
				}
945
				$count = count($package['certificatelist'][$refid]);
946
				$result .= "<div title='".htmlspecialchars($hint)."'>";
947
				$result .= htmlspecialchars($package['pkgname'])." ($count)<br />";
948
				$result .= "</div>";
949
			}
950
		}
951
	}
952
	return $result;
953
}
954

    
955
/* Detect a rollover at 2038 on some platforms (e.g. ARM)
956
 * See: https://redmine.pfsense.org/issues/9098 */
957
function cert_get_max_lifetime() {
958
	global $cert_max_lifetime;
959
	$max = $cert_max_lifetime;
960

    
961
	$current_time = time();
962
	while ((int)($current_time + ($max * 24 * 60 * 60)) < 0) {
963
		$max--;
964
	}
965
	return min($max, $cert_max_lifetime);
966
}
967

    
968
/* Detect a rollover at 2050 with UTCTime
969
 * See: https://redmine.pfsense.org/issues/9098 */
970
function crl_get_max_lifetime() {
971
	global $crl_max_lifetime;
972
	$max = $crl_max_lifetime;
973

    
974
	$now = new DateTime("now");
975
	$utctime_before_roll = DateTime::createFromFormat('Ymd', '20491231');
976
	if ($date !== false) {
977
		$interval = $now->diff($utctime_before_roll);
978
		$max_days = abs($interval->days);
979
		/* Reduce the max well below the rollover time */
980
		if ($max_days > 1000) {
981
			$max_days -= 1000;
982
		}
983
		return min($max_days, cert_get_max_lifetime());
984
	}
985

    
986
	/* Cannot use date functions, so use a lower default max. */
987
	return min(7000, cert_get_max_lifetime());
988
}
989

    
990
function crl_create(& $crl, $caref, $name, $serial = 0, $lifetime = 3650) {
991
	global $config;
992
	$max_lifetime = crl_get_max_lifetime();
993
	$ca =& lookup_ca($caref);
994
	if (!$ca) {
995
		return false;
996
	}
997
	$crl['descr'] = $name;
998
	$crl['caref'] = $caref;
999
	$crl['serial'] = $serial;
1000
	$crl['lifetime'] = ($lifetime > $max_lifetime) ? $max_lifetime : $lifetime;
1001
	$crl['cert'] = array();
1002

    
1003
	$crls = config_get_path('crl', []);
1004
	$crls[] = $crl;
1005
	config_set_path('crl', $crls);
1006
	return $crl;
1007
}
1008

    
1009
function crl_update(& $crl) {
1010
	require_once('ASN1.php');
1011
	require_once('ASN1_UTF8STRING.php');
1012
	require_once('ASN1_ASCIISTRING.php');
1013
	require_once('ASN1_BITSTRING.php');
1014
	require_once('ASN1_BOOL.php');
1015
	require_once('ASN1_GENERALTIME.php');
1016
	require_once('ASN1_INT.php');
1017
	require_once('ASN1_ENUM.php');
1018
	require_once('ASN1_NULL.php');
1019
	require_once('ASN1_OCTETSTRING.php');
1020
	require_once('ASN1_OID.php');
1021
	require_once('ASN1_SEQUENCE.php');
1022
	require_once('ASN1_SET.php');
1023
	require_once('ASN1_SIMPLE.php');
1024
	require_once('ASN1_TELETEXSTRING.php');
1025
	require_once('ASN1_UTCTIME.php');
1026
	require_once('OID.php');
1027
	require_once('X509.php');
1028
	require_once('X509_CERT.php');
1029
	require_once('X509_CRL.php');
1030

    
1031
	global $config;
1032
	$max_lifetime = crl_get_max_lifetime();
1033
	$ca =& lookup_ca($crl['caref']);
1034
	if (!$ca) {
1035
		return false;
1036
	}
1037
	// If we have text but no certs, it was imported and cannot be updated.
1038
	if (($crl["method"] != "internal") && (!empty($crl['text']) && empty($crl['cert']))) {
1039
		return false;
1040
	}
1041
	$crl['serial']++;
1042
	$ca_cert = \Ukrbublik\openssl_x509_crl\X509::pem2der(base64_decode($ca['crt']));
1043
	$ca_pkey = openssl_pkey_get_private(base64_decode($ca['prv']));
1044

    
1045
	$crlconf = array(
1046
		'no' => $crl['serial'],
1047
		'version' => 2,
1048
		'days' => ($crl['lifetime'] > $max_lifetime) ? $max_lifetime : $crl['lifetime'],
1049
		'alg' => OPENSSL_ALGO_SHA1,
1050
		'revoked' => array()
1051
	);
1052

    
1053
	if (is_array($crl['cert']) && (count($crl['cert']) > 0)) {
1054
		foreach ($crl['cert'] as $cert) {
1055
			/* Determine the serial number to revoke */
1056
			if (isset($cert['serial'])) {
1057
				$serial = $cert['serial'];
1058
			} elseif (isset($cert['crt'])) {
1059
				$serial = cert_get_serial($cert['crt'], true);
1060
			} else {
1061
				continue;
1062
			}
1063
			$crlconf['revoked'][] = array(
1064
				'serial' => $serial,
1065
				'rev_date' => $cert['revoke_time'],
1066
				'reason' => ($cert['reason'] == -1) ? null : (int) $cert['reason'],
1067
			);
1068
		}
1069
	}
1070

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

    
1074
	return $crl['text'];
1075
}
1076

    
1077
function cert_revoke($cert, & $crl, $reason = OCSP_REVOKED_STATUS_UNSPECIFIED) {
1078
	global $config;
1079
	if (is_cert_revoked($cert, $crl['refid'])) {
1080
		return true;
1081
	}
1082
	// If we have text but no certs, it was imported and cannot be updated.
1083
	if (!is_crl_internal($crl)) {
1084
		return false;
1085
	}
1086

    
1087
	if (!is_array($cert)) {
1088
		/* If passed a not an array but a serial string, set it up as an
1089
		 * array with the serial number defined */
1090
		$rcert = array();
1091
		$rcert['serial'] = $cert;
1092
	} else {
1093
		/* If passed a certificate entry, read out the serial and store
1094
		 * it separately. */
1095
		$rcert = $cert;
1096
		$rcert['serial'] = cert_get_serial($cert['crt']);
1097
	}
1098
	$rcert['reason'] = $reason;
1099
	$rcert['revoke_time'] = time();
1100
	$crl['cert'][] = $rcert;
1101
	crl_update($crl);
1102
	return true;
1103
}
1104

    
1105
function cert_unrevoke($cert, & $crl) {
1106
	global $config;
1107
	if (!is_crl_internal($crl)) {
1108
		return false;
1109
	}
1110

    
1111
	$serial = crl_get_entry_serial($cert);
1112

    
1113
	foreach ($crl['cert'] as $id => $rcert) {
1114
		/* Check for a match by refid, name, or serial number */
1115
		if (($rcert['refid'] == $cert['refid']) ||
1116
		    ($rcert['descr'] == $cert['descr']) ||
1117
		    (crl_get_entry_serial($rcert) == $serial)) {
1118
			unset($crl['cert'][$id]);
1119
			if (count($crl['cert']) == 0) {
1120
				// Protect against accidentally switching the type to imported, for older CRLs
1121
				if (!isset($crl['method'])) {
1122
					$crl['method'] = "internal";
1123
				}
1124
				crl_update($crl);
1125
			} else {
1126
				crl_update($crl);
1127
			}
1128
			return true;
1129
		}
1130
	}
1131
	return false;
1132
}
1133

    
1134
/* Compare two certificates to see if they match. */
1135
function cert_compare($cert1, $cert2) {
1136
	/* Ensure two certs are identical by first checking that their issuers match, then
1137
		subjects, then serial numbers, and finally the moduli. Anything less strict
1138
		could accidentally count two similar, but different, certificates as
1139
		being identical. */
1140
	$c1 = base64_decode($cert1['crt']);
1141
	$c2 = base64_decode($cert2['crt']);
1142
	if ((cert_get_issuer($c1, false) == cert_get_issuer($c2, false)) &&
1143
	    (cert_get_subject($c1, false) == cert_get_subject($c2, false)) &&
1144
	    (cert_get_serial($c1, false) == cert_get_serial($c2, false)) &&
1145
	    (cert_get_publickey($c1, false) == cert_get_publickey($c2, false))) {
1146
		return true;
1147
	}
1148
	return false;
1149
}
1150

    
1151
/****f* certs/crl_get_entry_serial
1152
 * NAME
1153
 *   crl_get_entry_serial - Take a CRL entry and determine the associated serial
1154
 * INPUTS
1155
 *   $entry: CRL certificate list entry to inspect, or serial string
1156
 * RESULT
1157
 *   The requested serial string, if present, or null if it cannot be determined.
1158
 ******/
1159

    
1160
function crl_get_entry_serial($entry) {
1161
	/* Check the passed entry several ways to determine the serial */
1162
	if (isset($entry['serial']) && (strlen($entry['serial']) > 0)) {
1163
		/* Entry is an array with a viable 'serial' element */
1164
		return $entry['serial'];
1165
	} elseif (isset($entry['crt'])) {
1166
		/* Entry is an array with certificate text which can be used to
1167
		 * determine the serial */
1168
		return cert_get_serial($entry['crt'], true);
1169
	} elseif (cert_validate_serial($entry, false, true) != null) {
1170
		/* Entry is a valid serial string */
1171
		return $entry;
1172
	}
1173
	/* Unable to find or determine a serial number */
1174
	return null;
1175
}
1176

    
1177
/****f* certs/cert_validate_serial
1178
 * NAME
1179
 *   cert_validate_serial - Validate a given string to test if it can be used as
1180
 *                          a certificate serial.
1181
 * INPUTS
1182
 *   $serial     : Serial number string to test
1183
 *   $returnvalue: Whether to return the parsed value or true/false
1184
 * RESULT
1185
 *   If $returnvalue is true, then the parsed ASN.1 integer value string for
1186
 *     $serial or null if invalid
1187
 *   If $returnvalue is false, then true/false based on whether or not $serial
1188
 *     is valid.
1189
 ******/
1190

    
1191
function cert_validate_serial($serial, $returnvalue = false, $allowlarge = false) {
1192
	require_once('ASN1.php');
1193
	require_once('ASN1_INT.php');
1194
	/* The ASN.1 parsing function will throw an exception if the value is
1195
	 * invalid, so take advantage of that to catch other error as well. */
1196
	try {
1197
		/* If the serial is not a string, then do not bother with
1198
		 * further tests. */
1199
		if (!is_string($serial)) {
1200
			throw new Exception('Not a string');
1201
		}
1202
		/* Process a hex string */
1203
		if ((substr($serial, 0, 2) == '0x')) {
1204
			/* If the string is hex, then it must contain only
1205
			 * valid hex digits */
1206
			if (!ctype_xdigit(substr($serial, 2))) {
1207
				throw new Exception('Not a valid hex string');
1208
			}
1209
			/* Convert to decimal */
1210
			$serial = base_convert($serial, 16, 10);
1211
		}
1212

    
1213
		/* Unfortunately, PHP openssl_csr_sign() limits serial numbers to a
1214
		 * PHP integer, so we cannot generate large numbers up to the maximum
1215
		 * allowed ASN.1 size (2^159). We are limited to PHP_INT_MAX --
1216
		 * As such, numbers larger than that limit should be rejected */
1217
		if ($serial > PHP_INT_MAX) {
1218
			throw new Exception('Serial too large for PHP OpenSSL');
1219
		}
1220

    
1221
		/* Attempt to create an ASN.1 integer, if it fails, an exception will be thrown */
1222
		$asn1serial = new \Ukrbublik\openssl_x509_crl\ASN1_INT( $serial );
1223
		return ($returnvalue) ? $asn1serial->content : true;
1224
	} catch (Exception $ex) {
1225
		/* No mattter what the error is, return null or false depending
1226
		 * on what was requested. */
1227
		return ($returnvalue) ? null : false;
1228
	}
1229
}
1230

    
1231
/****f* certs/cert_generate_serial
1232
 * NAME
1233
 *   cert_generate_serial - Generate a random positive integer usable as a
1234
 *                          certificate serial number
1235
 * INPUTS
1236
 *   None
1237
 * RESULT
1238
 *   Integer representing an ASN.1 compatible certificate serial number.
1239
 ******/
1240

    
1241
function cert_generate_serial() {
1242
	/* Use a separate function for this to make it easier to use a better
1243
	 * randomization function in the future. */
1244

    
1245
	/* Unfortunately, PHP openssl_csr_sign() limits serial numbers to a
1246
	 * PHP integer, so we cannot generate large numbers up to the maximum
1247
	 * allowed ASN.1 size (2^159). We are limited to PHP_INT_MAX */
1248
	return random_int(1, PHP_INT_MAX);
1249
}
1250

    
1251
/****f* certs/ca_has_serial
1252
 * NAME
1253
 *   ca_has_serial - Check if a serial number is used by any certificate in a given CA
1254
 * INPUTS
1255
 *   $ca    : Certificate Authority to check
1256
 *   $serial: Serial number to check
1257
 * RESULT
1258
 *   true if the serial number is in use by a certificate issued by this CA,
1259
 *   false otherwise.
1260
 ******/
1261

    
1262
function ca_has_serial($caref, $serial) {
1263
	global $config;
1264

    
1265
	/* Check certs first -- more likely to find a hit */
1266
	foreach ($config['cert'] as $cert) {
1267
		if (($cert['caref'] == $caref) &&
1268
		    (cert_get_serial($cert['crt'], true) == $serial)) {
1269
			/* If this certificate is issued by the CA in question
1270
			 * and has a matching serial number, stop processing
1271
			 * and return true. */
1272
			return true;
1273
		}
1274
	}
1275

    
1276
	/* Check the CA iteself */
1277
	$this_ca = lookup_ca($caref);
1278
	$this_serial = cert_get_serial($this_ca['crt'], true);
1279
	if ($serial == $this_serial) {
1280
		return true;
1281
	}
1282

    
1283
	/* Check other CAs for a match (intermediates signed by this CA) */
1284
	foreach ($config['ca'] as $ca) {
1285
		if (($ca['caref'] == $caref) &&
1286
		    (cert_get_serial($ca['crt'], true) == $serial)) {
1287
			/* If this CA is issued by the CA in question
1288
			 * and has a matching serial number, stop processing
1289
			 * and return true. */
1290
			return true;
1291
		}
1292
	}
1293

    
1294
	return false;
1295
}
1296

    
1297
/****f* certs/cert_get_random_serial
1298
 * NAME
1299
 *   cert_get_random_serial - Generate a random certificate serial unique in a CA
1300
 * INPUTS
1301
 *   $caref : Certificate Authority refid to test for serial uniqueness.
1302
 * RESULT
1303
 *   Random serial number which is not in use by any known certificate in a CA
1304
 ******/
1305

    
1306
function cert_get_random_serial($caref = '') {
1307
	/* Number of attempts to generate a usable serial. Multiple attempts
1308
	 *  are necessary to ensure that the number is usable and unique. */
1309
	$attempts = 10;
1310

    
1311
	/* Default value, -1 indicates an error */
1312
	$serial = -1;
1313

    
1314
	for ($i=0; $i < $attempts; $i++) {
1315
		/* Generate a random serial */
1316
		$serial = cert_generate_serial();
1317
		/* Check that the serial number is usable and unique:
1318
		 *  * Cannot be 0
1319
		 *  * Must be a valid ASN.1 serial number
1320
		 *  * Cannot be used by any other certificate on this CA */
1321
		if (($serial != 0) &&
1322
		    cert_validate_serial($serial) &&
1323
		    !ca_has_serial($caref, $serial)) {
1324
			/* If all conditions are met, we have a good serial, so stop. */
1325
			break;
1326
		}
1327
	}
1328
	return $serial;
1329
}
1330

    
1331
/****f* certs/ca_get_next_serial
1332
 * NAME
1333
 *   ca_get_next_serial - Get the next available serial number for a CA
1334
 * INPUTS
1335
 *   $ca: Reference to a CA entry
1336
 * RESULT
1337
 *   A randomized serial number (if enabled for a CA) or the next sequential value.
1338
 ******/
1339

    
1340
function ca_get_next_serial(& $ca) {
1341
	$ca_serial = null;
1342
	/* Get a randomized serial if enabled */
1343
	if ($ca['randomserial'] == 'enabled') {
1344
		$ca_serial = cert_get_random_serial($ca['refid']);
1345
	}
1346
	/* Initialize the sequential serial to be safe */
1347
	if (empty($ca['serial'])) {
1348
		$ca['serial'] = 0;
1349
	}
1350
	/* If not using a randomized serial, or randomizing the serial
1351
	 * failed, then fall back to sequential serials. */
1352
	return (empty($ca_serial) || ($ca_serial == -1)) ? ++$ca['serial'] : $ca_serial;
1353
}
1354

    
1355
/****f* certs/crl_contains_cert
1356
 * NAME
1357
 *   crl_contains_cert - Check if a certificate is present in a CRL
1358
 * INPUTS
1359
 *   $crl : CRL to check
1360
 *   $cert: Certificate to test
1361
 * RESULT
1362
 *   true if the CRL contains the certificate, false otherwise
1363
 ******/
1364

    
1365
function crl_contains_cert($crl, $cert) {
1366
	global $config;
1367
	if (!is_array($config['crl']) ||
1368
	    !is_array($crl['cert'])) {
1369
		return false;
1370
	}
1371

    
1372
	/* Find the issuer of this CRL */
1373
	$ca = lookup_ca($crl['caref']);
1374
	$crlissuer = is_array($cert) ? cert_get_subject($ca['crt']) : null;
1375
	$serial = crl_get_entry_serial($cert);
1376

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

    
1380
	/* If the requested certificate was not issued by the
1381
	 * same CA as the CRL, then do not bother checking this
1382
	 * CRL. */
1383
	if ($issuer != $crlissuer) {
1384
		return false;
1385
	}
1386

    
1387
	/* Check CRL entries to see if the certificate serial is revoked */
1388
	foreach ($crl['cert'] as $rcert) {
1389
		if (crl_get_entry_serial($rcert) == $serial) {
1390
			return true;
1391
		}
1392
	}
1393

    
1394
	/* Certificate was not found in the CRL */
1395
	return false;
1396
}
1397

    
1398
/****f* certs/is_cert_revoked
1399
 * NAME
1400
 *   is_cert_revoked - Test if a given certificate or serial is revoked
1401
 * INPUTS
1402
 *   $cert  : Certificate entry or serial number to test
1403
 *   $crlref: CRL to check for revoked entries, or empty to check all CRLs
1404
 * RESULT
1405
 *   true if the requested entry is revoked
1406
 *   false if the requested entry is not revoked
1407
 ******/
1408

    
1409
function is_cert_revoked($cert, $crlref = "") {
1410
	global $config;
1411
	if (!is_array($config['crl'])) {
1412
		return false;
1413
	}
1414

    
1415
	if (!empty($crlref)) {
1416
		$crl = lookup_crl($crlref);
1417
		return crl_contains_cert($crl, $cert);
1418
	} else {
1419
		if (!is_array($cert)) {
1420
			/* If passed a serial, then it cannot be definitively
1421
			 * matched in this way since we do not know the CA
1422
			 * associated with the bare serial. */
1423
			return null;
1424
		}
1425

    
1426
		/* Check every CRL in the configuration for a match */
1427
		foreach ($config['crl'] as $crl) {
1428
			if (!is_array($crl['cert'])) {
1429
				continue;
1430
			}
1431
			if (crl_contains_cert($crl, $cert)) {
1432
				return true;
1433
			}
1434
		}
1435
	}
1436
	return false;
1437
}
1438

    
1439
function is_openvpn_server_crl($crlref) {
1440
	foreach (config_get_path('openvpn/openvpn-server', []) as $ovpns) {
1441
		if (!empty($ovpns['crlref']) && ($ovpns['crlref'] == $crlref)) {
1442
			return true;
1443
		}
1444
	}
1445
	return false;
1446
}
1447

    
1448
function is_package_crl($crlref) {
1449
	$pluginparams = array();
1450
	$pluginparams['type'] = 'certificates';
1451
	$pluginparams['event'] = 'used_crl';
1452

    
1453
	$certificates_used_by_packages = pkg_call_plugins('plugin_certificates', $pluginparams);
1454

    
1455
	/* Check if any package is using CRL */
1456
	foreach ($certificates_used_by_packages as $name => $package) {
1457
		if (is_array($package['certificatelist'][$crlref]) &&
1458
		    (count($package['certificatelist'][$crlref]) > 0)) {
1459
			return true;
1460
		}
1461
	}
1462
}
1463

    
1464
// Keep this general to allow for future expansion. See cert_in_use() above.
1465
function crl_in_use($crlref) {
1466
	return (is_openvpn_server_crl($crlref) ||
1467
		is_package_crl($crlref));
1468
}
1469

    
1470
function is_crl_internal($crl) {
1471
	return (!(!empty($crl['text']) && empty($crl['cert'])) || ($crl["method"] == "internal"));
1472
}
1473

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

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

    
1513
function cert_add_altname_type($str) {
1514
	$type = "";
1515
	if (is_ipaddr($str)) {
1516
		$type = "IP";
1517
	} elseif (is_hostname($str, true)) {
1518
		$type = "DNS";
1519
	} elseif (is_URL($str)) {
1520
		$type = "URI";
1521
	} elseif (filter_var($str, FILTER_VALIDATE_EMAIL)) {
1522
		$type = "email";
1523
	}
1524
	if (!empty($type)) {
1525
		return "{$type}:" . cert_escape_x509_chars($str);
1526
	} else {
1527
		return null;
1528
	}
1529
}
1530

    
1531
function cert_type_config_section($type) {
1532
	switch ($type) {
1533
		case "ca":
1534
			$cert_type = "v3_ca";
1535
			break;
1536
		case "server":
1537
		case "self-signed":
1538
			$cert_type = "server";
1539
			break;
1540
		default:
1541
			$cert_type = "usr_cert";
1542
			break;
1543
	}
1544
	return $cert_type;
1545
}
1546

    
1547
/****f* certs/is_cert_locally_renewable
1548
 * NAME
1549
 *   is_cert_locally_renewable - Check to see if an existing certificate can be
1550
 *                               renewed by a local internal CA.
1551
 * INPUTS
1552
 *   $cert : The certificate to be tested
1553
 * RESULT
1554
 *   true if the certificate can be locally renewed, false otherwise.
1555
 ******/
1556

    
1557
function is_cert_locally_renewable($cert) {
1558
	/* If there is no certificate or private key string, this entry is either
1559
	 * invalid or cannot be renewed. */
1560
	if (empty($cert['crt']) || empty($cert['prv'])) {
1561
		return false;
1562
	}
1563

    
1564
	/* Get subject and issuer values to test for self-signed state */
1565
	$subj = cert_get_subject($cert['crt']);
1566
	$issuer = cert_get_issuer($cert['crt']);
1567

    
1568
	/* Lookup CA for this certificate */
1569
	$ca = array();
1570
	if (!empty($cert['caref'])) {
1571
		$ca = lookup_ca($cert['caref']);
1572
	}
1573

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

    
1579
/* Strict certificate requirements based on
1580
 * https://redmine.pfsense.org/issues/9825
1581
 */
1582
global $cert_strict_values;
1583
$cert_strict_values = array(
1584
	'max_server_cert_lifetime' => 398,
1585
	'digest_blacklist' => array('md4', 'RSA-MD4',  'md5', 'RSA-MD5', 'md5-sha1',
1586
					'mdc2', 'RSA-MDC2', 'sha1', 'RSA-SHA1',
1587
					'RSA-SHA1-2', 'sha224', 'RSA-SHA224'),
1588
	'min_private_key_bits' => 2048,
1589
	'ec_curve' => 'prime256v1',
1590
);
1591

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

    
1611
function cert_renew(& $cert, $reusekey = true, $strictsecurity = false, $reuseserial = false) {
1612
	global $cert_strict_values, $cert_curve_compatible, $curve_compatible_list;
1613

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

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

    
1623
	/* No details, must not be valid in some way */
1624
	if (!array($cert_details) || empty($cert_details)) {
1625
		return false;
1626
	}
1627

    
1628
	$subj = cert_get_subject($cert['crt']);
1629
	$issuer = cert_get_issuer($cert['crt']);
1630
	$purpose = cert_get_purpose($cert['crt']);
1631

    
1632
	$res_key = openssl_pkey_get_private(base64_decode($cert['prv']));
1633
	$key_details = openssl_pkey_get_details($res_key);
1634

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

    
1657
	/* Test for self-signed or signed by a CA */
1658
	$selfsigned = ($subj == $issuer);
1659

    
1660
	/* Determine the type if it is not specified directly */
1661
	if (array_key_exists('serial', $cert)) {
1662
		/* If a serial value is present, this must be a CA */
1663
		$cert['type'] = 'ca';
1664
	} elseif (empty($cert['type'])) {
1665
		/* Automatically determine certificate type if unset based on purpose value */
1666
		$cert['type'] = ($purpose['server'] == 'Yes') ? 'server' : 'user';
1667
	}
1668

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

    
1672
	/* Reuse lifetime (convert seconds to days) */
1673
	$lifetime = (int) round(($cert_details['validTo_time_t'] - $cert_details['validFrom_time_t']) / 86400);
1674

    
1675
	/* If we are enforcing strict security, then cap the lifetime for server certificates */
1676
	if (($cert_type == 'server') && $strictsecurity &&
1677
	    ($lifetime > $cert_strict_values['max_server_cert_lifetime'])) {
1678
		$lifetime = $cert_strict_values['max_server_cert_lifetime'];
1679
	}
1680

    
1681
	/* Reuse SAN list, or, if empty, add CN as SAN. */
1682
	$sans = str_replace("IP Address", "IP", $cert_details['extensions']['subjectAltName']);
1683
	if (empty($sans)) {
1684
		$sans = cert_add_altname_type($dn['commonName']);
1685
	}
1686

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

    
1698
	/* Determine current digest algorithm. */
1699
	$digest_alg = strtolower($cert_details['signatureTypeSN']);
1700

    
1701
	/* Check for and remove unnecessary ECDSA digest prefix
1702
	 * See https://redmine.pfsense.org/issues/13437 */
1703
	$ecdsa_prefix = 'ecdsa-with-';
1704
	if (substr($digest_alg, 0, strlen($ecdsa_prefix)) == $ecdsa_prefix) {
1705
		$digest_alg = substr($digest_alg, strlen($ecdsa_prefix));
1706
	}
1707

    
1708
	/* If we are enforcing strict security, then check the digest against a
1709
	 * blacklist of insecure digest methods. */
1710
	if ($strictsecurity &&
1711
	    (in_array($digest_alg, $cert_strict_values['digest_blacklist']))) {
1712
		$digest_alg = 'sha256';
1713
	}
1714

    
1715
	/* Validate key type, assume RSA if it cannot be read. */
1716
	if (is_array($key_details) && array_key_exists('type', $key_details)) {
1717
		$private_key_type = $key_details['type'];
1718
	} else {
1719
		$private_key_type = OPENSSL_KEYTYPE_RSA;
1720
	}
1721

    
1722
	/* Setup certificate and key arguments */
1723
	$args = array(
1724
		"x509_extensions" => $cert_type,
1725
		"digest_alg" => $digest_alg,
1726
		"private_key_type" => $private_key_type,
1727
		"encrypt_key" => false);
1728

    
1729
	/* If we are enforcing strict security, then ensure the private key size
1730
	 * is at least 2048 bits or NIST P-256 elliptic curve*/
1731
	$private_key_bits = $key_details['bits'];
1732
	if ($strictsecurity) {
1733
		if (($key_details['type'] == OPENSSL_KEYTYPE_RSA) &&
1734
		    ($private_key_bits < $cert_strict_values['min_private_key_bits'])) {
1735
			$private_key_bits = $cert_strict_values['min_private_key_bits'];
1736
			$reusekey = false;
1737
		} else if (!in_array($key_details['ec']['curve_name'], $curve_compatible_list)) {
1738
			$ec_curve = $cert_strict_values['ec_curve'];
1739
			$reusekey = false;
1740
		}
1741
	}
1742

    
1743
	/* Set key parameters. */
1744
	if ($key_details['type'] ==  OPENSSL_KEYTYPE_RSA) {
1745
		$args['private_key_bits'] = (int)$private_key_bits;
1746
	} else if ($ec_curve) {
1747
		$args['curve_name'] = $ec_curve;
1748
	} else {
1749
		$args['curve_name'] = $key_details['ec']['curve_name'];
1750
	}
1751

    
1752
	/* Make a new key if necessary */
1753
	if (!$res_key || !$reusekey) {
1754
		$res_key = openssl_pkey_new($args);
1755
		if (!$res_key) {
1756
			return false;
1757
		}
1758
	}
1759

    
1760
	/* Create a new CSR from derived parameters and key */
1761
	$res_csr = openssl_csr_new($dn, $res_key, $args);
1762
	/* If the CSR could not be created, bail */
1763
	if (!$res_csr) {
1764
		return false;
1765
	}
1766

    
1767
	if (!empty($cert['caref'])) {
1768
		/* The certificate was signed by a CA, so read the CA details. */
1769
		$ca = & lookup_ca($cert['caref']);
1770
		/* If the referenced CA cannot be found, bail. */
1771
		if (!$ca) {
1772
			return false;
1773
		}
1774
		$ca_str_crt = base64_decode($ca['crt']);
1775
		$ca_str_key = base64_decode($ca['prv']);
1776
		$ca_res_crt = openssl_x509_read($ca_str_crt);
1777
		$ca_res_key = openssl_pkey_get_private(array(0 => $ca_str_key, 1 => ""));
1778
		if (!$ca_res_key) {
1779
			/* If the CA key cannot be read, bail. */
1780
			return false;
1781
		}
1782
		/* If the CA does not have a serial number, assume 0. */
1783
		if (empty($ca['serial'])) {
1784
			$ca['serial'] = 0;
1785
		}
1786
		/* Get the next available CA serial number. */
1787
		$ca_serial = ca_get_next_serial($ca);
1788
	} elseif ($selfsigned) {
1789
		/* For self-signed CAs & certificates, set the CA details to self and
1790
		 * use the key for this entry to sign itself.
1791
		 */
1792
		$ca_res_crt   = null;
1793
		$ca_res_key   = $res_key;
1794
		/* Use random serial from this CA/Self-Signed Cert */
1795
		$ca_serial    = cert_get_random_serial($cert['refid']);
1796
	}
1797

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

    
1801
	/* Sign the CSR */
1802
	$res_crt = openssl_csr_sign($res_csr, $ca_res_crt, $ca_res_key, $lifetime,
1803
				 $args, $ca_serial);
1804
	/* If the CSR could not be signed, bail */
1805
	if (!$res_crt) {
1806
		return false;
1807
	}
1808

    
1809
	/* Attempt to read the key and certificate and if that fails, bail */
1810
	if (!openssl_pkey_export($res_key, $str_key) ||
1811
	    !openssl_x509_export($res_crt, $str_crt)) {
1812
		return false;
1813
	}
1814

    
1815
	/* Load the new certificate string and key into the configuration */
1816
	$cert['crt'] = base64_encode($str_crt);
1817
	$cert['prv'] = base64_encode($str_key);
1818

    
1819
	return true;
1820
}
1821

    
1822
/****f* certs/cert_get_all_services
1823
 * NAME
1824
 *   cert_get_all_services - Locate services using a given certificate
1825
 * INPUTS
1826
 *   $refid: The refid of a certificate to check
1827
 * RESULT
1828
 *   array containing the services which use this certificate, including:
1829
 *     webgui: Present and true if the WebGUI uses this certificate. Unset otherwise.
1830
 *     services: Array of service definitions using this certificate, with:
1831
 *       name: Name of the service
1832
 *       extras: Extra information needed by some services, such as OpenVPN or Captive Portal.
1833
 *     packages: Array containing package names using this certificate.
1834
 ******/
1835

    
1836
function cert_get_all_services($refid) {
1837
	$services = array();
1838
	$services['services'] = array();
1839
	$services['packages'] = array();
1840

    
1841
	/* Only set if true, otherwise leave unset. */
1842
	if (is_webgui_cert($refid)) {
1843
		$services['webgui'] = true;
1844
	}
1845

    
1846
	/* Find all OpenVPN clients and servers which use this certificate */
1847
	foreach (array('server', 'client') as $mode) {
1848
		foreach (config_get_path("openvpn/openvpn-{$mode}", []) as $ovpn) {
1849
			if ($ovpn['certref'] == $refid) {
1850
				/* OpenVPN instances are restarted individually,
1851
				 * so we need to note the mode and ID. */
1852
				$services['services'][] = array(
1853
					'name' => 'openvpn',
1854
					'extras' => array(
1855
						'vpnmode' => $mode,
1856
						'id' => $ovpn['vpnid']
1857
					)
1858
				);
1859
			}
1860
		}
1861
	}
1862

    
1863
	/* If any one IPsec tunnel uses this certificate then the whole service
1864
	 * needs a bump. */
1865

    
1866
	foreach (config_get_path('ipsec/phase1', []) as $ipsec) {
1867
		if (($ipsec['authentication_method'] == 'cert') &&
1868
		    ($ipsec['certref'] == $refid)) {
1869
			$services['services'][] = array('name' => 'ipsec');
1870
			/* Stop after finding one, no need to search for more. */
1871
			break;
1872
		}
1873
	}
1874

    
1875
	/* Check to see if any HTTPS-enabled Captive Portal zones use this
1876
	 * certificate. */
1877
	foreach (config_get_path('captiveportal', []) as $zone => $portal) {
1878
		if (isset($portal['enable']) && isset($portal['httpslogin']) &&
1879
		    ($portal['certref'] == $refid)) {
1880
			/* Captive Portal zones are restarted individually, so
1881
			 * we need to note the zone name. */
1882
			$services['services'][] = array(
1883
				'name' => 'captiveportal',
1884
				'extras' => array(
1885
					'zone' => $zone,
1886
				)
1887
			);
1888
		}
1889
	}
1890

    
1891
	/* Locate any packages using this certificate */
1892
	$pkgcerts = pkg_call_plugins('plugin_certificates', array('type' => 'certificates', 'event' => 'used_certificates'));
1893
	foreach ($pkgcerts as $name => $package) {
1894
		if (is_array($package['certificatelist'][$refid]) &&
1895
		    isset($package['certificatelist'][$refid]) > 0) {
1896
			$services['packages'][] = $name;
1897
		}
1898
	}
1899

    
1900
	return $services;
1901
}
1902

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

    
1925
	foreach (array('server', 'client') as $mode) {
1926
		foreach (config_get_path("openvpn/openvpn-{$mode}", []) as $ovpn) {
1927
			if ($ovpn['caref'] == $refid) {
1928
				$services['services'][] = array(
1929
					'name' => 'openvpn',
1930
					'extras' => array(
1931
						'vpnmode' => $mode,
1932
						'id' => $ovpn['vpnid']
1933
					)
1934
				);
1935
			}
1936
		}
1937
	}
1938

    
1939
	foreach (config_get_path('ipsec/phase1', []) as $ipsec) {
1940
		if ($ipsec['certref'] == $refid) {
1941
			break;
1942
		}
1943
	}
1944

    
1945
	foreach (config_get_path('ipsec/phase1', []) as $ipsec) {
1946
		if (($ipsec['authentication_method'] == 'cert') &&
1947
		    ($ipsec['caref'] == $refid)) {
1948
			$services['services'][] = array('name' => 'ipsec');
1949
			break;
1950
		}
1951
	}
1952

    
1953
	/* Loop through all certs and get their services as well */
1954
	foreach (config_get_path('cert', []) as $cert) {
1955
		if ($cert['caref'] == $refid) {
1956
			$services = array_merge_recursive_unique($services, cert_get_all_services($cert['refid']));
1957
		}
1958
	}
1959

    
1960
	/* Look for intermediate certs and services */
1961
	foreach (config_get_path('ca', []) as $cert) {
1962
		if ($cert['caref'] == $refid) {
1963
			$services = array_merge_recursive_unique($services, ca_get_all_services($cert['refid']));
1964
		}
1965
	}
1966

    
1967
	return $services;
1968
}
1969

    
1970
/****f* certs/cert_restart_services
1971
 * NAME
1972
 *   cert_restart_services - Restarts services specific to CA/Certificate usage
1973
 * INPUTS
1974
 *   $services: An array of services returned by cert_get_all_services or ca_get_all_services
1975
 * RESULT
1976
 *   Services in the given array are restarted
1977
 *   returns false if the input is invalid
1978
 *   returns true at the end of execution
1979
 ******/
1980

    
1981
function cert_restart_services($services) {
1982
	require_once("service-utils.inc");
1983
	/* If the input is not an array, it is invalid. */
1984
	if (!is_array($services)) {
1985
		return false;
1986
	}
1987

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

    
1991
	/* Restart GUI: */
1992
	if ($services['webgui']) {
1993
		ob_flush();
1994
		flush();
1995
		log_error(sprintf($restart_string, gettext('service'), 'WebGUI'));
1996
		send_event("service restart webgui");
1997
	}
1998

    
1999
	/* Restart other base services: */
2000
	if (is_array($services['services'])) {
2001
		foreach ($services['services'] as $service) {
2002
			switch ($service['name']) {
2003
				case 'openvpn':
2004
					$service_name = "{$service['name']} {$service['extras']['vpnmode']} {$service['extras']['id']}";
2005
					break;
2006
				case 'captiveportal':
2007
					$service_name = "{$service['name']} zone {$service['extras']['zone']}";
2008
					break;
2009
				default:
2010
					$service_name = $service['name'];
2011
			}
2012
			log_error(sprintf($restart_string, gettext('service'), $service_name));
2013
			service_control_restart($service['name'], $service['extras']);
2014
		}
2015
	}
2016

    
2017
	/* Restart Packages: */
2018
	if (is_array($services['packages'])) {
2019
		foreach ($services['packages'] as $service) {
2020
			log_error(sprintf($restart_string, gettext('package'), $service));
2021
			restart_service($service);
2022
		}
2023
	}
2024
	return true;
2025
}
2026

    
2027
/****f* certs/cert_get_lifetime
2028
 * NAME
2029
 *   cert_get_lifetime - Returns the number of days the certificate is valid
2030
 * INPUTS
2031
 *   $untilexpire: Boolean
2032
 *     true: The number of days returned is from now until the certificate expiration.
2033
 *     false (default): The number of days returned is the total lifetime of the certificate.
2034
 * RESULT
2035
 *   Integer number of days in the certificate total or remaining lifetime
2036
 ******/
2037

    
2038
function cert_get_lifetime($cert, $untilexpire = false) {
2039
	/* If the certificate is not valid, bail. */
2040
	if (!is_array($cert) || empty($cert['crt'])) {
2041
		return null;
2042
	}
2043
	/* Read certificate details */
2044
	list($startdate, $enddate) = cert_get_dates($cert['crt'], true, false);
2045

    
2046
	/* If either of the dates are invalid, there is nothing we can do here. */
2047
	if (($startdate === false) || ($enddate === false)) {
2048
		return false;
2049
	}
2050

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

    
2054
	/* Calculate the requested intervals */
2055
	$interval = $startdate->diff($enddate);
2056

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

    
2061
/****f* certs/cert_analyze_lifetime
2062
 * NAME
2063
 *   cert_analyze_lifetime - Analyze a certificate lifetime for expiration notices
2064
 * INPUTS
2065
 *   $expiredays: Number of days until the certificate expires (See cert_get_lifetime())
2066
 * RESULT
2067
 *   An array of two entries:
2068
 *   0/$lrclass: A bootstrap name for use with classes like text-<x>
2069
 *   1/$expstring: A text analysis describing the expiration timeframe.
2070
 ******/
2071

    
2072
function cert_analyze_lifetime($expiredays) {
2073
	global $g;
2074
	/* Number of days at which to warn of expiration. */
2075
	$warning_days = config_get_path('notifications/certexpire/expiredays', $g['default_cert_expiredays']);
2076

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
2303
function cert_notify_expiring() {
2304
	global $config;
2305

    
2306
	/* If certificate expiration notifications are disabled, there is nothing to do. */
2307
	if (config_get_path('notifications/certexpire/enable') == "disabled") {
2308
		return;
2309
	}
2310

    
2311
	$notifications = array();
2312

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

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

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

    
2359
function ca_setup_trust_store() {
2360
	/* This directory is trusted by OpenSSL on FreeBSD by default */
2361
	$trust_store_directory = '/etc/ssl/certs';
2362

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

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

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

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

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

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

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

    
2426

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

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

    
2452
	return true;
2453
}
2454

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

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

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

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

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

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

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

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

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

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

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

    
2583
function cert_build_list($type = 'cert', $consumer = '', $selectsource = false, $addnone = false) {
2584
	/* Ensure that $type is valid */
2585
	if (!in_array($type, array('ca', 'cert'))) {
2586
		return array();
2587
	}
2588

    
2589
	$list = array();
2590
	if ($addnone) {
2591
		if ($selectsource) {
2592
			$list[] = array('refid' => 'none', 'descr' => 'None');
2593
		} else {
2594
			$list['none'] = "None";
2595
		}
2596
	}
2597

    
2598
	/* Create a hashed array with the certificate refid as the key and
2599
	 * descriptive name as the value. Exclude incompatible certificates. */
2600
	foreach (config_get_path($type, []) as $cert) {
2601
		if (empty($cert['prv']) && ($type == 'cert')) {
2602
			continue;
2603
		} else if (cert_check_pkey_compatibility($cert['prv'], $consumer)) {
2604
			if ($selectsource) {
2605
				$list[] = array('refid' => $cert['refid'],
2606
						'descr' => $cert['descr']);
2607
			} else {
2608
				$list[$cert['refid']] = $cert['descr'];
2609
			}
2610
		}
2611
	}
2612

    
2613
	return $list;
2614
}
2615

    
2616
/****f* certs/cert_pkcs12_export
2617
 * NAME
2618
 *   cert_pkcs12_export - Export a PKCS#12 archive file for a given certificate
2619
 *                        and optional CA and passphrase.
2620
 * INPUTS
2621
 *   $cert      : Certificate entry array.
2622
 *   $encryption: Strength of encryption to use:
2623
 *                "high" (AES-256 + SHA256)
2624
 *                "low" (3DES + SHA1)
2625
 *                "legacy" (RC2-40 + SHA1)
2626
 *   $passphrase: Optional passphrase used to encrypt the archive contents and
2627
 *                private key.
2628
 *   $add_ca    : Boolean flag which determines whether or not the certificate
2629
 *                CA is included in the archive (if available)
2630
 *   $delivery  : Delivery method for the result: "file", "download", or "data".
2631
 *                See RESULT section for details.
2632
 * RESULT
2633
 *   Returns false on failure, otherwise result depends upon the value passed in
2634
 *   $delivery:
2635
 *       "file"    : Returns the path to the output archive file.
2636
 *                   NOTE: Does not clean up path, caller must clean up the
2637
 *                         entire directory containing the output file.
2638
 *       "download": Sends the archive data to the current GUI browser session.
2639
 *                   Must be called before any output is sent to the user
2640
 *                   session.
2641
 *       "data"    : Returns the contents of the PKCS#12 archive as a string.
2642
 * NOTES
2643
 *   If the certificate entry does not contain a private key, the archive will
2644
 *   also not contain a key.
2645
 ******/
2646

    
2647
function cert_pkcs12_export($cert, $encryption = 'high', $passphrase = '', $add_ca = true, $delivery = 'download') {
2648
	global $g;
2649

    
2650
	/* Unusable certificate entry, bail early. */
2651
	if (!is_array($cert) || empty($cert)) {
2652
		return false;
2653
	}
2654

    
2655
	/* Encryption and Digest */
2656
	switch ($encryption) {
2657
		case 'legacy':
2658
			$algo = '-certpbe PBE-SHA1-RC2-40 -keypbe PBE-SHA1-RC2-40';
2659
			$hash = '';
2660
			break;
2661
		case 'low':
2662
			$algo = '-certpbe PBE-SHA1-3DES -keypbe PBE-SHA1-3DES';
2663
			$hash = '-macalg SHA1';
2664
			break;
2665
		case 'high':
2666
		default:
2667
			$algo = '-aes256 -certpbe AES-256-CBC -keypbe AES-256-CBC';
2668
			$hash = '-macalg sha256';
2669
	}
2670

    
2671
	/* Make a secure temporary directory */
2672
	$workdir = tempnam("{$g['tmp_path']}/", "p12export");
2673
	@unlink_if_exists($workdir);
2674
	mkdir($workdir, 0600);
2675

    
2676
	/* Set the friendly name to the certificate description, if available */
2677
	$descr = "";
2678
	if (!empty($cert['descr'])) {
2679
		$edescr = escapeshellarg($cert['descr']);
2680
		$descr = "-name {$edescr} -CSP {$edescr}";
2681
		$fileprefix = basename($cert['descr']);
2682
	}
2683

    
2684
	/* If there isn't a usable portion of the description, use the refid */
2685
	if (empty($fileprefix)) {
2686
		$fileprefix = $cert['refid'];
2687
	}
2688

    
2689
	/* Exported output archive filename */
2690
	$outpath = "{$workdir}/{$fileprefix}.p12";
2691
	$eoutpath = escapeshellarg($outpath);
2692

    
2693
	/* Passphrase handling */
2694
	if (!empty($passphrase)) {
2695
		/* Use passphrase text file so the passphrase is not visible in
2696
		 * process list. */
2697
		$passfile = "{$workdir}/passphrase.txt";
2698
		file_put_contents($passfile, $passphrase . "\n");
2699
		$pass = '-passout file:' . escapeshellarg($passfile);
2700
	} else {
2701
		/* Null password + disable encryption of the keys */
2702
		$pass = '-passout pass: -nodes';
2703
	}
2704

    
2705
	/* Certificate file */
2706
	$crtpath = "{$workdir}/cert.pem";
2707
	$ecrtpath = escapeshellarg($crtpath);
2708
	file_put_contents($crtpath, base64_decode($cert['crt']));
2709

    
2710
	/* Private key (if present) */
2711
	if (!empty($cert['prv'])) {
2712
		$keypath = "{$workdir}/key.pem";
2713
		/* Write key to a secure temporary name */
2714
		file_put_contents($keypath, base64_decode($cert['prv']));
2715
		$key = '-inkey ' . escapeshellarg($keypath);
2716
	} else {
2717
		$key = '-nokeys';
2718
	}
2719

    
2720
	/* Add CA if one is defined and requested */
2721
	$eca = '';
2722
	if ($add_ca && !empty($cert['caref'])) {
2723
		$ca = lookup_ca($cert['caref']);
2724
		if ($ca) {
2725
			$capath = "{$workdir}/ca.pem";
2726
			file_put_contents($capath, base64_decode($ca['crt']));
2727
			$eca = '-certfile ' . escapeshellarg($capath);
2728
		}
2729
	}
2730

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

    
2734
	/* Bail if the output is invalid */
2735
	if (!file_exists($outpath) ||
2736
	    (filesize($outpath) == 0)) {
2737
		return false;
2738
	}
2739

    
2740
	/* Tailor output as requested by the caller */
2741
	switch ($delivery) {
2742
		case 'file':
2743
			/* Return path to export file, do not clean up, caller must clean up. */
2744
			return $outpath;
2745
			break;
2746
		case 'download':
2747
			/* Send file to user and cleanup */
2748
			$p12_data = file_get_contents($outpath);
2749
			rmdir_recursive($workdir);
2750
			send_user_download('data', $p12_data, "{$cert['descr']}.p12");
2751
			return true;
2752
			break;
2753
		case 'data':
2754
		default:
2755
			/* Return PKCS#12 archive data and cleanup */
2756
			$p12_data = file_get_contents($outpath);
2757
			rmdir_recursive($workdir);
2758
			return $p12_data;
2759
			break;
2760
	}
2761

    
2762
	return null;
2763
}
2764
?>
(8-8/62)