Project

General

Profile

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

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

    
27
require_once("functions.inc");
28

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

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

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

    
54
global $cert_max_lifetime;
55
$cert_max_lifetime = 12000;
56

    
57
function & lookup_ca($refid) {
58
	global $config;
59

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

    
68
	return false;
69
}
70

    
71
function & lookup_ca_by_subject($subject) {
72
	global $config;
73

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

    
83
	return false;
84
}
85

    
86
function & lookup_cert($refid) {
87
	global $config;
88

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

    
97
	return false;
98
}
99

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

    
111
function & lookup_crl($refid) {
112
	global $config;
113

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

    
122
	return false;
123
}
124

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

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

    
161
function ca_import(& $ca, $str, $key = "", $serial = "") {
162
	global $config;
163

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

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

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

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

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

    
224
	// generate a new key pair
225
	$res_key = openssl_pkey_new($args);
226
	if (!$res_key) {
227
		return false;
228
	}
229

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

    
236
	// self sign the certificate
237
	$res_crt = openssl_csr_sign($res_csr, null, $res_key, $lifetime, $args);
238
	if (!$res_crt) {
239
		return false;
240
	}
241

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

    
248
	// return our ca information
249
	$ca['crt'] = base64_encode($str_crt);
250
	$ca['prv'] = base64_encode($str_key);
251
	$ca['serial'] = 0;
252

    
253
	return true;
254
}
255

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

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

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

    
282
	// generate a new key pair
283
	$res_key = openssl_pkey_new($args);
284
	if (!$res_key) {
285
		return false;
286
	}
287

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

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

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

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

    
312
	return true;
313
}
314

    
315
function cert_import(& $cert, $crt_str, $key_str) {
316

    
317
	$cert['crt'] = base64_encode($crt_str);
318
	$cert['prv'] = base64_encode($key_str);
319

    
320
	$subject = cert_get_subject($crt_str, false);
321
	$issuer = cert_get_issuer($crt_str, false);
322

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

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

    
335
	$cert['type'] = $type;
336

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

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

    
352
		/* Get the next available CA serial number. */
353
		$ca_serial = ca_get_next_serial($ca);
354
	}
355

    
356
	$cert_type = cert_type_config_section($type);
357

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

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

    
378
	// generate a new key pair
379
	$res_key = openssl_pkey_new($args);
380
	if (!$res_key) {
381
		return false;
382
	}
383

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

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

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

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

    
412
	// return our certificate information
413
	$cert['crt'] = base64_encode($str_crt);
414
	$cert['prv'] = base64_encode($str_key);
415

    
416
	return true;
417
}
418

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

    
421
	$cert_type = cert_type_config_section($type);
422

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

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

    
444
	// generate a new key pair
445
	$res_key = openssl_pkey_new($args);
446
	if (!$res_key) {
447
		return false;
448
	}
449

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

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

    
462
	// return our request information
463
	$cert['csr'] = base64_encode($str_csr);
464
	$cert['prv'] = base64_encode($str_key);
465

    
466
	return true;
467
}
468

    
469
function csr_sign($csr, & $ca, $duration, $type = "user", $altnames, $digest_alg = "sha256") {
470
	global $config;
471
	$old_err_level = error_reporting(0);
472

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

    
481
	/* Get the next available CA serial number. */
482
	$ca_serial = ca_get_next_serial($ca);
483

    
484
	$cert_type = cert_type_config_section($type);
485

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

    
491
	$args = array(
492
		"x509_extensions" => $cert_type,
493
		"digest_alg" => $digest_alg,
494
		"req_extensions" => "req_{$cert_type}"
495
	);
496

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

    
501
	return $n509;
502
}
503

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

    
511
function csr_get_subject($str_crt, $decode = true) {
512

    
513
	if ($decode) {
514
		$str_crt = base64_decode($str_crt);
515
	}
516

    
517
	$components = openssl_csr_get_subject($str_crt);
518

    
519
	if (empty($components) || !is_array($components)) {
520
		return "unknown";
521
	}
522

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

    
532
	return $subject;
533
}
534

    
535
function cert_get_subject($str_crt, $decode = true) {
536

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

    
541
	$inf_crt = openssl_x509_parse($str_crt);
542
	$components = $inf_crt['subject'];
543

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

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

    
562
	return $subject;
563
}
564

    
565
function cert_get_subject_array($crt) {
566
	$str_crt = base64_decode($crt);
567
	$inf_crt = openssl_x509_parse($str_crt);
568
	$components = $inf_crt['subject'];
569

    
570
	if (!is_array($components)) {
571
		return;
572
	}
573

    
574
	$subject_array = array();
575

    
576
	foreach ($components as $a => $v) {
577
		$subject_array[] = array('a' => $a, 'v' => $v);
578
	}
579

    
580
	return $subject_array;
581
}
582

    
583
function cert_get_subject_hash($crt) {
584
	$str_crt = base64_decode($crt);
585
	$inf_crt = openssl_x509_parse($str_crt);
586
	return $inf_crt['subject'];
587
}
588

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

    
601
function cert_get_issuer($str_crt, $decode = true) {
602

    
603
	if ($decode) {
604
		$str_crt = base64_decode($str_crt);
605
	}
606

    
607
	$inf_crt = openssl_x509_parse($str_crt);
608
	$components = $inf_crt['issuer'];
609

    
610
	if (empty($components) || !is_array($components)) {
611
		return "unknown";
612
	}
613

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

    
628
	return $issuer;
629
}
630

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

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

    
690
	return $purpose;
691
}
692

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

    
705
function cert_format_date($validTS, $validTS_time_t, $outputstring = true) {
706
	$now = new DateTime("now");
707

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

    
724
function cert_get_dates($str_crt, $decode = true, $outputstring = true) {
725
	if ($decode) {
726
		$str_crt = base64_decode($str_crt);
727
	}
728
	$crt_details = openssl_x509_parse($str_crt);
729

    
730
	$start = cert_format_date($crt_details['validFrom'], $crt_details['validFrom_time_t'], $outputstring);
731
	$end   = cert_format_date($crt_details['validTo'], $crt_details['validTo_time_t'], $outputstring);
732

    
733
	return array($start, $end);
734
}
735

    
736
function cert_get_serial($str_crt, $decode = true) {
737
	if ($decode) {
738
		$str_crt = base64_decode($str_crt);
739
	}
740
	$crt_details = openssl_x509_parse($str_crt);
741
	if (isset($crt_details['serialNumber'])) {
742
		return $crt_details['serialNumber'];
743
	} else {
744
		return NULL;
745
	}
746
}
747

    
748
function cert_get_sigtype($str_crt, $decode = true) {
749
	if ($decode) {
750
		$str_crt = base64_decode($str_crt);
751
	}
752
	$crt_details = openssl_x509_parse($str_crt);
753

    
754
	$signature = array();
755
	if (isset($crt_details['signatureTypeSN']) && !empty($crt_details['signatureTypeSN'])) {
756
		$signature['shortname'] = $crt_details['signatureTypeSN'];
757
	}
758
	if (isset($crt_details['signatureTypeLN']) && !empty($crt_details['signatureTypeLN'])) {
759
		$signature['longname'] = $crt_details['signatureTypeLN'];
760
	}
761
	if (isset($crt_details['signatureTypeNID']) && !empty($crt_details['signatureTypeNID'])) {
762
		$signature['nid'] = $crt_details['signatureTypeNID'];
763
	}
764

    
765
	return $signature;
766
}
767

    
768
function is_openvpn_server_ca($caref) {
769
	global $config;
770
	if (!is_array($config['openvpn']['openvpn-server'])) {
771
		return;
772
	}
773
	foreach ($config['openvpn']['openvpn-server'] as $ovpns) {
774
		if ($ovpns['caref'] == $caref) {
775
			return true;
776
		}
777
	}
778
	return false;
779
}
780

    
781
function is_openvpn_client_ca($caref) {
782
	global $config;
783
	if (!is_array($config['openvpn']['openvpn-client'])) {
784
		return;
785
	}
786
	foreach ($config['openvpn']['openvpn-client'] as $ovpnc) {
787
		if ($ovpnc['caref'] == $caref) {
788
			return true;
789
		}
790
	}
791
	return false;
792
}
793

    
794
function is_ipsec_peer_ca($caref) {
795
	global $config;
796
	if (!is_array($config['ipsec']['phase1'])) {
797
		return;
798
	}
799
	foreach ($config['ipsec']['phase1'] as $ipsec) {
800
		if ($ipsec['caref'] == $caref) {
801
			return true;
802
		}
803
	}
804
	return false;
805
}
806

    
807
function is_ldap_peer_ca($caref) {
808
	global $config;
809
	if (!is_array($config['system']['authserver'])) {
810
		return;
811
	}
812
	foreach ($config['system']['authserver'] as $authserver) {
813
		if ($authserver['ldap_caref'] == $caref) {
814
			return true;
815
		}
816
	}
817
	return false;
818
}
819

    
820
function ca_in_use($caref) {
821
	return (is_openvpn_server_ca($caref) ||
822
		is_openvpn_client_ca($caref) ||
823
		is_ipsec_peer_ca($caref) ||
824
		is_ldap_peer_ca($caref));
825
}
826

    
827
function is_user_cert($certref) {
828
	global $config;
829
	if (!is_array($config['system']['user'])) {
830
		return;
831
	}
832
	foreach ($config['system']['user'] as $user) {
833
		if (!is_array($user['cert'])) {
834
			continue;
835
		}
836
		foreach ($user['cert'] as $cert) {
837
			if ($certref == $cert) {
838
				return true;
839
			}
840
		}
841
	}
842
	return false;
843
}
844

    
845
function is_openvpn_server_cert($certref) {
846
	global $config;
847
	if (!is_array($config['openvpn']['openvpn-server'])) {
848
		return;
849
	}
850
	foreach ($config['openvpn']['openvpn-server'] as $ovpns) {
851
		if ($ovpns['certref'] == $certref) {
852
			return true;
853
		}
854
	}
855
	return false;
856
}
857

    
858
function is_openvpn_client_cert($certref) {
859
	global $config;
860
	if (!is_array($config['openvpn']['openvpn-client'])) {
861
		return;
862
	}
863
	foreach ($config['openvpn']['openvpn-client'] as $ovpnc) {
864
		if ($ovpnc['certref'] == $certref) {
865
			return true;
866
		}
867
	}
868
	return false;
869
}
870

    
871
function is_ipsec_cert($certref) {
872
	global $config;
873
	if (!is_array($config['ipsec']['phase1'])) {
874
		return;
875
	}
876
	foreach ($config['ipsec']['phase1'] as $ipsec) {
877
		if ($ipsec['certref'] == $certref) {
878
			return true;
879
		}
880
	}
881
	return false;
882
}
883

    
884
function is_webgui_cert($certref) {
885
	global $config;
886
	if (($config['system']['webgui']['ssl-certref'] == $certref) &&
887
	    ($config['system']['webgui']['protocol'] != "http")) {
888
		return true;
889
	}
890
}
891

    
892
function is_package_cert($certref) {
893
	$pluginparams = array();
894
	$pluginparams['type'] = 'certificates';
895
	$pluginparams['event'] = 'used_certificates';
896

    
897
	$certificates_used_by_packages = pkg_call_plugins('plugin_certificates', $pluginparams);
898

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

    
908
function is_captiveportal_cert($certref) {
909
	global $config;
910
	if (!is_array($config['captiveportal'])) {
911
		return;
912
	}
913
	foreach ($config['captiveportal'] as $portal) {
914
		if (isset($portal['enable']) && isset($portal['httpslogin']) && ($portal['certref'] == $certref)) {
915
			return true;
916
		}
917
	}
918
	return false;
919
}
920

    
921
function cert_in_use($certref) {
922

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

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

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

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

    
966
function crl_create(& $crl, $caref, $name, $serial = 0, $lifetime = 3650) {
967
	global $config;
968
	$max_lifetime = cert_get_max_lifetime();
969
	$ca =& lookup_ca($caref);
970
	if (!$ca) {
971
		return false;
972
	}
973
	$crl['descr'] = $name;
974
	$crl['caref'] = $caref;
975
	$crl['serial'] = $serial;
976
	$crl['lifetime'] = ($lifetime > $max_lifetime) ? $max_lifetime : $lifetime;
977
	$crl['cert'] = array();
978
	$config['crl'][] = $crl;
979
	return $crl;
980
}
981

    
982
function crl_update(& $crl) {
983
	require_once('ASN1.php');
984
	require_once('ASN1_UTF8STRING.php');
985
	require_once('ASN1_ASCIISTRING.php');
986
	require_once('ASN1_BITSTRING.php');
987
	require_once('ASN1_BOOL.php');
988
	require_once('ASN1_GENERALTIME.php');
989
	require_once('ASN1_INT.php');
990
	require_once('ASN1_ENUM.php');
991
	require_once('ASN1_NULL.php');
992
	require_once('ASN1_OCTETSTRING.php');
993
	require_once('ASN1_OID.php');
994
	require_once('ASN1_SEQUENCE.php');
995
	require_once('ASN1_SET.php');
996
	require_once('ASN1_SIMPLE.php');
997
	require_once('ASN1_TELETEXSTRING.php');
998
	require_once('ASN1_UTCTIME.php');
999
	require_once('OID.php');
1000
	require_once('X509.php');
1001
	require_once('X509_CERT.php');
1002
	require_once('X509_CRL.php');
1003

    
1004
	global $config;
1005
	$max_lifetime = cert_get_max_lifetime();
1006
	$ca =& lookup_ca($crl['caref']);
1007
	if (!$ca) {
1008
		return false;
1009
	}
1010
	// If we have text but no certs, it was imported and cannot be updated.
1011
	if (($crl["method"] != "internal") && (!empty($crl['text']) && empty($crl['cert']))) {
1012
		return false;
1013
	}
1014
	$crl['serial']++;
1015
	$ca_cert = \Ukrbublik\openssl_x509_crl\X509::pem2der(base64_decode($ca['crt']));
1016
	$ca_pkey = openssl_pkey_get_private(base64_decode($ca['prv']));
1017

    
1018
	$crlconf = array(
1019
		'no' => $crl['serial'],
1020
		'version' => 2,
1021
		'days' => ($crl['lifetime'] > $max_lifetime) ? $max_lifetime : $crl['lifetime'],
1022
		'alg' => OPENSSL_ALGO_SHA1,
1023
		'revoked' => array()
1024
	);
1025

    
1026
	if (is_array($crl['cert']) && (count($crl['cert']) > 0)) {
1027
		foreach ($crl['cert'] as $cert) {
1028
			/* Determine the serial number to revoke */
1029
			if (isset($cert['serial'])) {
1030
				$serial = $cert['serial'];
1031
			} elseif (isset($cert['crt'])) {
1032
				$serial = cert_get_serial($cert['crt'], true);
1033
			} else {
1034
				continue;
1035
			}
1036
			$crlconf['revoked'][] = array(
1037
				'serial' => $serial,
1038
				'rev_date' => $cert['revoke_time'],
1039
				'reason' => ($cert['reason'] == -1) ? null : (int) $cert['reason'],
1040
			);
1041
		}
1042
	}
1043

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

    
1047
	return $crl['text'];
1048
}
1049

    
1050
function cert_revoke($cert, & $crl, $reason = OCSP_REVOKED_STATUS_UNSPECIFIED) {
1051
	global $config;
1052
	if (is_cert_revoked($cert, $crl['refid'])) {
1053
		return true;
1054
	}
1055
	// If we have text but no certs, it was imported and cannot be updated.
1056
	if (!is_crl_internal($crl)) {
1057
		return false;
1058
	}
1059

    
1060
	if (!is_array($cert)) {
1061
		/* If passed a not an array but a serial string, set it up as an
1062
		 * array with the serial number defined */
1063
		$rcert = array();
1064
		$rcert['serial'] = $cert;
1065
	} else {
1066
		/* If passed a certificate entry, read out the serial and store
1067
		 * it separately. */
1068
		$rcert = $cert;
1069
		$rcert['serial'] = cert_get_serial($cert['crt']);
1070
	}
1071
	$rcert['reason'] = $reason;
1072
	$rcert['revoke_time'] = time();
1073
	$crl['cert'][] = $rcert;
1074
	crl_update($crl);
1075
	return true;
1076
}
1077

    
1078
function cert_unrevoke($cert, & $crl) {
1079
	global $config;
1080
	if (!is_crl_internal($crl)) {
1081
		return false;
1082
	}
1083

    
1084
	$serial = crl_get_entry_serial($cert);
1085

    
1086
	foreach ($crl['cert'] as $id => $rcert) {
1087
		/* Check for a match by refid, name, or serial number */
1088
		if (($rcert['refid'] == $cert['refid']) ||
1089
		    ($rcert['descr'] == $cert['descr']) ||
1090
		    (crl_get_entry_serial($rcert) == $serial)) {
1091
			unset($crl['cert'][$id]);
1092
			if (count($crl['cert']) == 0) {
1093
				// Protect against accidentally switching the type to imported, for older CRLs
1094
				if (!isset($crl['method'])) {
1095
					$crl['method'] = "internal";
1096
				}
1097
				crl_update($crl);
1098
			} else {
1099
				crl_update($crl);
1100
			}
1101
			return true;
1102
		}
1103
	}
1104
	return false;
1105
}
1106

    
1107
/* Compare two certificates to see if they match. */
1108
function cert_compare($cert1, $cert2) {
1109
	/* Ensure two certs are identical by first checking that their issuers match, then
1110
		subjects, then serial numbers, and finally the moduli. Anything less strict
1111
		could accidentally count two similar, but different, certificates as
1112
		being identical. */
1113
	$c1 = base64_decode($cert1['crt']);
1114
	$c2 = base64_decode($cert2['crt']);
1115
	if ((cert_get_issuer($c1, false) == cert_get_issuer($c2, false)) &&
1116
	    (cert_get_subject($c1, false) == cert_get_subject($c2, false)) &&
1117
	    (cert_get_serial($c1, false) == cert_get_serial($c2, false)) &&
1118
	    (cert_get_publickey($c1, false) == cert_get_publickey($c2, false))) {
1119
		return true;
1120
	}
1121
	return false;
1122
}
1123

    
1124
/****f* certs/crl_get_entry_serial
1125
 * NAME
1126
 *   crl_get_entry_serial - Take a CRL entry and determine the associated serial
1127
 * INPUTS
1128
 *   $entry: CRL certificate list entry to inspect, or serial string
1129
 * RESULT
1130
 *   The requested serial string, if present, or null if it cannot be determined.
1131
 ******/
1132

    
1133
function crl_get_entry_serial($entry) {
1134
	/* Check the passed entry several ways to determine the serial */
1135
	if (isset($entry['serial']) && (strlen($entry['serial']) > 0)) {
1136
		/* Entry is an array with a viable 'serial' element */
1137
		return $entry['serial'];
1138
	} elseif (isset($entry['crt'])) {
1139
		/* Entry is an array with certificate text which can be used to
1140
		 * determine the serial */
1141
		return cert_get_serial($entry['crt'], true);
1142
	} elseif (cert_validate_serial($entry, false, true) != null) {
1143
		/* Entry is a valid serial string */
1144
		return $entry;
1145
	}
1146
	/* Unable to find or determine a serial number */
1147
	return null;
1148
}
1149

    
1150
/****f* certs/cert_validate_serial
1151
 * NAME
1152
 *   cert_validate_serial - Validate a given string to test if it can be used as
1153
 *                          a certificate serial.
1154
 * INPUTS
1155
 *   $serial     : Serial number string to test
1156
 *   $returnvalue: Whether to return the parsed value or true/false
1157
 * RESULT
1158
 *   If $returnvalue is true, then the parsed ASN.1 integer value string for
1159
 *     $serial or null if invalid
1160
 *   If $returnvalue is false, then true/false based on whether or not $serial
1161
 *     is valid.
1162
 ******/
1163

    
1164
function cert_validate_serial($serial, $returnvalue = false, $allowlarge = false) {
1165
	require_once('ASN1.php');
1166
	require_once('ASN1_INT.php');
1167
	/* The ASN.1 parsing function will throw an exception if the value is
1168
	 * invalid, so take advantage of that to catch other error as well. */
1169
	try {
1170
		/* If the serial is not a string, then do not bother with
1171
		 * further tests. */
1172
		if (!is_string($serial)) {
1173
			throw new Exception('Not a string');
1174
		}
1175
		/* Process a hex string */
1176
		if ((substr($serial, 0, 2) == '0x')) {
1177
			/* If the string is hex, then it must contain only
1178
			 * valid hex digits */
1179
			if (!ctype_xdigit(substr($serial, 2))) {
1180
				throw new Exception('Not a valid hex string');
1181
			}
1182
			/* Convert to decimal */
1183
			$serial = base_convert($serial, 16, 10);
1184
		}
1185

    
1186
		/* Unfortunately, PHP openssl_csr_sign() limits serial numbers to a
1187
		 * PHP integer, so we cannot generate large numbers up to the maximum
1188
		 * allowed ASN.1 size (2^159). We are limited to PHP_INT_MAX --
1189
		 * As such, numbers larger than that limit should be rejected */
1190
		if ($serial > PHP_INT_MAX) {
1191
			throw new Exception('Serial too large for PHP OpenSSL');
1192
		}
1193

    
1194
		/* Attempt to create an ASN.1 integer, if it fails, an exception will be thrown */
1195
		$asn1serial = new \Ukrbublik\openssl_x509_crl\ASN1_INT( $serial );
1196
		return ($returnvalue) ? $asn1serial->content : true;
1197
	} catch (Exception $ex) {
1198
		/* No mattter what the error is, return null or false depending
1199
		 * on what was requested. */
1200
		return ($returnvalue) ? null : false;
1201
	}
1202
}
1203

    
1204
/****f* certs/cert_generate_serial
1205
 * NAME
1206
 *   cert_generate_serial - Generate a random positive integer usable as a
1207
 *                          certificate serial number
1208
 * INPUTS
1209
 *   None
1210
 * RESULT
1211
 *   Integer representing an ASN.1 compatible certificate serial number.
1212
 ******/
1213

    
1214
function cert_generate_serial() {
1215
	/* Use a separate function for this to make it easier to use a better
1216
	 * randomization function in the future. */
1217

    
1218
	/* Unfortunately, PHP openssl_csr_sign() limits serial numbers to a
1219
	 * PHP integer, so we cannot generate large numbers up to the maximum
1220
	 * allowed ASN.1 size (2^159). We are limited to PHP_INT_MAX */
1221
	return random_int(1, PHP_INT_MAX);
1222
}
1223

    
1224
/****f* certs/ca_has_serial
1225
 * NAME
1226
 *   ca_has_serial - Check if a serial number is used by any certificate in a given CA
1227
 * INPUTS
1228
 *   $ca    : Certificate Authority to check
1229
 *   $serial: Serial number to check
1230
 * RESULT
1231
 *   true if the serial number is in use by a certificate issued by this CA,
1232
 *   false otherwise.
1233
 ******/
1234

    
1235
function ca_has_serial($caref, $serial) {
1236
	global $config;
1237

    
1238
	foreach ($config['cert'] as $cert) {
1239
		if (($cert['caref'] == $caref) &&
1240
		    (cert_get_serial($cert['crt'], true) == $serial)) {
1241
			/* If this certificate is issued by the CA in question
1242
			 * and has a matching serial number, stop processing
1243
			 * and return true. */
1244
			return true;
1245
		}
1246
	}
1247

    
1248
	return false;
1249
}
1250

    
1251
/****f* certs/cert_get_random_serial
1252
 * NAME
1253
 *   cert_get_random_serial - Generate a random certificate serial unique in a CA
1254
 * INPUTS
1255
 *   $caref : Certificate Authority refid to test for serial uniqueness.
1256
 * RESULT
1257
 *   Random serial number which is not in use by any known certificate in a CA
1258
 ******/
1259

    
1260
function cert_get_random_serial($caref) {
1261
	/* Number of attempts to generate a usable serial. Multiple attempts
1262
	 *  are necessary to ensure that the number is usable and unique. */
1263
	$attempts = 10;
1264

    
1265
	/* Default value, -1 indicates an error */
1266
	$serial = -1;
1267

    
1268
	for ($i=0; $i < $attempts; $i++) {
1269
		/* Generate a random serial */
1270
		$serial = cert_generate_serial();
1271
		/* Check that the serial number is usable and unique:
1272
		 *  * Cannot be 0
1273
		 *  * Must be a valid ASN.1 serial number
1274
		 *  * Cannot be used by any other certificate on this CA */
1275
		if (($serial != 0) &&
1276
		    cert_validate_serial($serial) &&
1277
		    !ca_has_serial($caref, $serial)) {
1278
			/* If all conditions are met, we have a good serial, so stop. */
1279
			break;
1280
		}
1281
	}
1282
	return $serial;
1283
}
1284

    
1285
/****f* certs/ca_get_next_serial
1286
 * NAME
1287
 *   ca_get_next_serial - Get the next available serial number for a CA
1288
 * INPUTS
1289
 *   $ca: Reference to a CA entry
1290
 * RESULT
1291
 *   A randomized serial number (if enabled for a CA) or the next sequential value.
1292
 ******/
1293

    
1294
function ca_get_next_serial(& $ca) {
1295
	$ca_serial = null;
1296
	/* Get a randomized serial if enabled */
1297
	if ($ca['randomserial'] == 'enabled') {
1298
		$ca_serial = cert_get_random_serial($ca['refid']);
1299
	}
1300
	/* Initialize the sequential serial to be safe */
1301
	if (empty($ca['serial'])) {
1302
		$ca['serial'] = 0;
1303
	}
1304
	/* If not using a randomized serial, or randomizing the serial
1305
	 * failed, then fall back to sequential serials. */
1306
	return (empty($ca_serial) || ($ca_serial == -1)) ? ++$ca['serial'] : $ca_serial;
1307
}
1308

    
1309
/****f* certs/crl_contains_cert
1310
 * NAME
1311
 *   crl_contains_cert - Check if a certificate is present in a CRL
1312
 * INPUTS
1313
 *   $crl : CRL to check
1314
 *   $cert: Certificate to test
1315
 * RESULT
1316
 *   true if the CRL contains the certificate, false otherwise
1317
 ******/
1318

    
1319
function crl_contains_cert($crl, $cert) {
1320
	global $config;
1321
	if (!is_array($config['crl']) ||
1322
	    !is_array($crl['cert'])) {
1323
		return false;
1324
	}
1325

    
1326
	/* Find the issuer of this CRL */
1327
	$ca = lookup_ca($crl['caref']);
1328
	$crlissuer = is_array($cert) ? cert_get_subject($ca['crt']) : null;
1329
	$serial = crl_get_entry_serial($cert);
1330

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

    
1334
	/* If the requested certificate was not issued by the
1335
	 * same CA as the CRL, then do not bother checking this
1336
	 * CRL. */
1337
	if ($issuer != $crlissuer) {
1338
		return false;
1339
	}
1340

    
1341
	/* Check CRL entries to see if the certificate serial is revoked */
1342
	foreach ($crl['cert'] as $rcert) {
1343
		if (crl_get_entry_serial($rcert) == $serial) {
1344
			return true;
1345
		}
1346
	}
1347

    
1348
	/* Certificate was not found in the CRL */
1349
	return false;
1350
}
1351

    
1352
/****f* certs/is_cert_revoked
1353
 * NAME
1354
 *   is_cert_revoked - Test if a given certificate or serial is revoked
1355
 * INPUTS
1356
 *   $cert  : Certificate entry or serial number to test
1357
 *   $crlref: CRL to check for revoked entries, or empty to check all CRLs
1358
 * RESULT
1359
 *   true if the requested entry is revoked
1360
 *   false if the requested entry is not revoked
1361
 ******/
1362

    
1363
function is_cert_revoked($cert, $crlref = "") {
1364
	global $config;
1365
	if (!is_array($config['crl'])) {
1366
		return false;
1367
	}
1368

    
1369
	if (!empty($crlref)) {
1370
		$crl = lookup_crl($crlref);
1371
		return crl_contains_cert($crl, $cert);
1372
	} else {
1373
		if (!is_array($cert)) {
1374
			/* If passed a serial, then it cannot be definitively
1375
			 * matched in this way since we do not know the CA
1376
			 * associated with the bare serial. */
1377
			return null;
1378
		}
1379

    
1380
		/* Check every CRL in the configuration for a match */
1381
		foreach ($config['crl'] as $crl) {
1382
			if (!is_array($crl['cert'])) {
1383
				continue;
1384
			}
1385
			if (crl_contains_cert($crl, $cert)) {
1386
				return true;
1387
			}
1388
		}
1389
	}
1390
	return false;
1391
}
1392

    
1393
function is_openvpn_server_crl($crlref) {
1394
	global $config;
1395
	if (!is_array($config['openvpn']['openvpn-server'])) {
1396
		return;
1397
	}
1398
	foreach ($config['openvpn']['openvpn-server'] as $ovpns) {
1399
		if (!empty($ovpns['crlref']) && ($ovpns['crlref'] == $crlref)) {
1400
			return true;
1401
		}
1402
	}
1403
	return false;
1404
}
1405

    
1406
// Keep this general to allow for future expansion. See cert_in_use() above.
1407
function crl_in_use($crlref) {
1408
	return (is_openvpn_server_crl($crlref));
1409
}
1410

    
1411
function is_crl_internal($crl) {
1412
	return (!(!empty($crl['text']) && empty($crl['cert'])) || ($crl["method"] == "internal"));
1413
}
1414

    
1415
function cert_get_cn($crt, $isref = false) {
1416
	/* If this is a certref, not an actual cert, look up the cert first */
1417
	if ($isref) {
1418
		$cert = lookup_cert($crt);
1419
		/* If it's not a valid cert, bail. */
1420
		if (!(is_array($cert) && !empty($cert['crt']))) {
1421
			return "";
1422
		}
1423
		$cert = $cert['crt'];
1424
	} else {
1425
		$cert = $crt;
1426
	}
1427
	$sub = cert_get_subject_array($cert);
1428
	if (is_array($sub)) {
1429
		foreach ($sub as $s) {
1430
			if (strtoupper($s['a']) == "CN") {
1431
				return $s['v'];
1432
			}
1433
		}
1434
	}
1435
	return "";
1436
}
1437

    
1438
function cert_escape_x509_chars($str, $reverse = false) {
1439
	/* Characters which need escaped when present in x.509 fields.
1440
	 * See https://www.ietf.org/rfc/rfc4514.txt
1441
	 *
1442
	 * The backslash (\) must be listed first in these arrays!
1443
	 */
1444
	$cert_directory_string_special_chars = array('\\', '"', '#', '+', ',', ';', '<', '=', '>');
1445
	$cert_directory_string_special_chars_esc = array('\\\\', '\"', '\#', '\+', '\,', '\;', '\<', '\=', '\>');
1446
	if ($reverse) {
1447
		return str_replace($cert_directory_string_special_chars_esc, $cert_directory_string_special_chars, $str);
1448
	} else {
1449
		/* First unescape and then escape again, to prevent possible double escaping. */
1450
		return str_replace($cert_directory_string_special_chars, $cert_directory_string_special_chars_esc, cert_escape_x509_chars($str, true));
1451
	}
1452
}
1453

    
1454
function cert_add_altname_type($str) {
1455
	$type = "";
1456
	if (is_ipaddr($str)) {
1457
		$type = "IP";
1458
	} elseif (is_hostname($str, true)) {
1459
		$type = "DNS";
1460
	} elseif (is_URL($str)) {
1461
		$type = "URI";
1462
	} elseif (filter_var($str, FILTER_VALIDATE_EMAIL)) {
1463
		$type = "email";
1464
	}
1465
	if (!empty($type)) {
1466
		return "{$type}:" . cert_escape_x509_chars($str);
1467
	} else {
1468
		return null;
1469
	}
1470
}
1471

    
1472
function cert_type_config_section($type) {
1473
	switch ($type) {
1474
		case "ca":
1475
			$cert_type = "v3_ca";
1476
			break;
1477
		case "server":
1478
		case "self-signed":
1479
			$cert_type = "server";
1480
			break;
1481
		default:
1482
			$cert_type = "usr_cert";
1483
			break;
1484
	}
1485
	return $cert_type;
1486
}
1487

    
1488
/****f* certs/is_cert_locally_renewable
1489
 * NAME
1490
 *   is_cert_locally_renewable - Check to see if an existing certificate can be
1491
 *                               renewed by a local internal CA.
1492
 * INPUTS
1493
 *   $cert : The certificate to be tested
1494
 * RESULT
1495
 *   true if the certificate can be locally renewed, false otherwise.
1496
 ******/
1497

    
1498
function is_cert_locally_renewable($cert) {
1499
	/* If there is no certificate or private key string, this entry is either
1500
	 * invalid or cannot be renewed. */
1501
	if (empty($cert['crt']) || empty($cert['prv'])) {
1502
		return false;
1503
	}
1504

    
1505
	/* Get subject and issuer values to test for self-signed state */
1506
	$subj = cert_get_subject($cert['crt']);
1507
	$issuer = cert_get_issuer($cert['crt']);
1508

    
1509
	/* Lookup CA for this certificate */
1510
	$ca = array();
1511
	if (!empty($cert['caref'])) {
1512
		$ca = lookup_ca($cert['caref']);
1513
	}
1514

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

    
1520
/* Strict certificate requirements based on
1521
 * https://redmine.pfsense.org/issues/9825
1522
 */
1523
global $cert_strict_values;
1524
$cert_strict_values = array(
1525
	'max_server_cert_lifetime' => 398,
1526
	'digest_blacklist' => array('md4', 'RSA-MD4',  'md5', 'RSA-MD5', 'md5-sha1',
1527
					'mdc2', 'RSA-MDC2', 'sha1', 'RSA-SHA1',
1528
					'RSA-SHA1-2'),
1529
	'min_private_key_bits' => 2048,
1530
	'ec_curve' => 'prime256v1',
1531
);
1532

    
1533
/****f* certs/cert_renew
1534
 * NAME
1535
 *   cert_renew - Renew an existing internal CA or certificate
1536
 * INPUTS
1537
 *   $cert : The entry to be renewed (used as a reference so it can be altered directly)
1538
 *   $reusekey : Whether or not to reuse the existing key for the certificate
1539
 *      true: Reuse the existing key (Default)
1540
 *      false: Generate a new key based on current (or enforced minimum) parameters
1541
 *   $strictsecurity : Whether or not to enforce stricter security for specific attributes
1542
 *      true: Enforce maximum lifetime for server certs, minimum digest type, and
1543
 *            minimum private key size. See https://redmine.pfsense.org/issues/9825
1544
 *      false: Use existing values as-is (Default).
1545
 * RESULT
1546
 *   true if successful, false if failure.
1547
 * NOTES
1548
 *   See https://redmine.pfsense.org/issues/9842 for more information on behavior.
1549
 *   Does NOT run write_config(), that must be performed by the caller.
1550
 ******/
1551

    
1552
function cert_renew(& $cert, $reusekey = true, $strictsecurity = false) {
1553
	global $cert_strict_values, $cert_curve_compatible;
1554

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

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

    
1564
	/* No details, must not be valid in some way */
1565
	if (!array($cert_details) || empty($cert_details)) {
1566
		return false;
1567
	}
1568

    
1569
	$subj = cert_get_subject($cert['crt']);
1570
	$issuer = cert_get_issuer($cert['crt']);
1571

    
1572
	$res_key = openssl_pkey_get_private(base64_decode($cert['prv']));
1573
	$key_details = openssl_pkey_get_details($res_key);
1574

    
1575
	/* Form a new Distinguished Name from the existing values.
1576
	 * Note: Deprecated/unsupported DN fields may not be carried forward, but
1577
	 *       may be preserved to avoid altering a subject.
1578
	 */
1579
	$subject_map = array(
1580
		'CN' => 'commonName',
1581
		'C' => 'countryName',
1582
		'ST' => 'stateOrProvinceName',
1583
		'L' => 'localityName',
1584
		'O' => 'organizationName',
1585
		'OU' => 'organizationalUnitName',
1586
		'emailAddress' => 'emailAddress', /* deprecated, but commonly found in older entries */
1587
	);
1588
	$dn = array();
1589
	/* This is necessary to ensure the order of subject components is
1590
	 * identical on the old and new certificate. */
1591
	foreach ($cert_details['subject'] as $p => $v) {
1592
		if (array_key_exists($p, $subject_map)) {
1593
			$dn[$subject_map[$p]] = $v;
1594
		}
1595
	}
1596

    
1597
	/* Test for self-signed or signed by a CA */
1598
	$selfsigned = ($subj == $issuer);
1599

    
1600
	/* Determine the type if it is not specified directly */
1601
	if (array_key_exists('serial', $cert)) {
1602
		/* If a serial value is present, this must be a CA */
1603
		$cert['type'] = 'ca';
1604
	} elseif (empty($cert['type'])) {
1605
		/* Assume server certificate if empty & non-CA (e.g. self-signed) */
1606
		$cert['type'] = 'server';
1607
	}
1608

    
1609
	/* Convert the internal certificate type to an openssl.cnf section name */
1610
	$cert_type = cert_type_config_section($cert['type']);
1611
	if ($cert['type'] != 'ca') {
1612
		$cert_type .= '_san';
1613
	}
1614

    
1615
	/* Reuse lifetime (convert seconds to days) */
1616
	$lifetime = (int) round(($cert_details['validTo_time_t'] - $cert_details['validFrom_time_t']) / 86400);
1617

    
1618
	/* If we are enforcing strict security, then cap the lifetime for server certificates */
1619
	if (($cert_type == 'server_san') && $strictsecurity &&
1620
	    ($lifetime > $cert_strict_values['max_server_cert_lifetime'])) {
1621
		$lifetime = $cert_strict_values['max_server_cert_lifetime'];
1622
	}
1623

    
1624
	/* Reuse SAN list, or, if empty, add CN as SAN. */
1625
	$sans = str_replace("IP Address", "IP", $cert_details['extensions']['subjectAltName']);
1626
	if (empty($sans)) {
1627
		$sans = cert_add_altname_type($dn['commonName']);
1628
	}
1629

    
1630
	/* subjectAltName can be set _only_ via configuration file, so put the
1631
	 * value into the environment where it will be read from the configuration */
1632
	putenv("SAN={$sans}");
1633

    
1634
	/* If we are enforcing strict security, then check the digest against a
1635
	 * blacklist of insecure digest methods. */
1636
	$digest_alg = $cert_details['signatureTypeSN'];
1637
	if ($strictsecurity &&
1638
	    (in_array($digest_alg, $cert_strict_values['digest_blacklist']))) {
1639
		$digest_alg = 'sha256';
1640
	}
1641

    
1642
	/* Validate key type, assume RSA if it cannot be read. */
1643
	if (is_array($key_details) && array_key_exists('type', $key_details)) {
1644
		$private_key_type = $key_details['type'];
1645
	} else {
1646
		$private_key_type = OPENSSL_KEYTYPE_RSA;
1647
	}
1648

    
1649
	/* Setup certificate and key arguments */
1650
	$args = array(
1651
		"x509_extensions" => $cert_type,
1652
		"digest_alg" => $digest_alg,
1653
		"private_key_type" => $private_key_type,
1654
		"encrypt_key" => false);
1655

    
1656
	/* If we are enforcing strict security, then ensure the private key size
1657
	 * is at least 2048 bits or NIST P-256 elliptic curve*/
1658
	$private_key_bits = $key_details['bits'];
1659
	if ($strictsecurity) {
1660
		if (($key_details['type'] == OPENSSL_KEYTYPE_RSA) &&
1661
		    ($private_key_bits < $cert_strict_values['min_private_key_bits'])) {
1662
			$private_key_bits = $cert_strict_values['min_private_key_bits'];
1663
			$reusekey = false;
1664
		} else if (!in_array($key_details['ec']['curve_name'], $curve_compatible_list)) {
1665
			$ec_curve = $cert_strict_values['ec_curve'];
1666
			$reusekey = false;
1667
		}
1668
	}
1669

    
1670
	/* Set key parameters. */
1671
	if ($key_details['type'] ==  OPENSSL_KEYTYPE_RSA) {
1672
		$args['private_key_bits'] = (int)$private_key_bits;
1673
	} else if ($ec_curve) {
1674
		$args['curve_name'] = $ec_curve;
1675
	} else {
1676
		$args['curve_name'] = $key_details['ec']['curve_name'];
1677
	}
1678

    
1679
	/* Make a new key if necessary */
1680
	if (!$res_key || !$reusekey) {
1681
		$res_key = openssl_pkey_new($args);
1682
		if (!$res_key) {
1683
			return false;
1684
		}
1685
	}
1686

    
1687
	/* Create a new CSR from derived parameters and key */
1688
	$res_csr = openssl_csr_new($dn, $res_key, $args);
1689
	/* If the CSR could not be created, bail */
1690
	if (!$res_csr) {
1691
		return false;
1692
	}
1693

    
1694
	if (!empty($cert['caref'])) {
1695
		/* The certificate was signed by a CA, so read the CA details. */
1696
		$ca = & lookup_ca($cert['caref']);
1697
		/* If the referenced CA cannot be found, bail. */
1698
		if (!$ca) {
1699
			return false;
1700
		}
1701
		$ca_str_crt = base64_decode($ca['crt']);
1702
		$ca_str_key = base64_decode($ca['prv']);
1703
		$ca_res_crt = openssl_x509_read($ca_str_crt);
1704
		$ca_res_key = openssl_pkey_get_private(array(0 => $ca_str_key, 1 => ""));
1705
		if (!$ca_res_key) {
1706
			/* If the CA key cannot be read, bail. */
1707
			return false;
1708
		}
1709
		/* If the CA does not have a serial number, assume 0. */
1710
		if (empty($ca['serial'])) {
1711
			$ca['serial'] = 0;
1712
		}
1713
		/* Get the next available CA serial number. */
1714
		$ca_serial = ca_get_next_serial($ca);
1715
	} elseif ($selfsigned) {
1716
		/* For self-signed CAs & certificates, set the CA details to self and
1717
		 * use the key for this entry to sign itself.
1718
		 */
1719
		$ca_res_crt   = null;
1720
		$ca_res_key   = $res_key;
1721
		$ca_serial    = 0; /* TODO: Check if we should increment from previous */
1722
	}
1723

    
1724
	/* Sign the CSR */
1725
	$res_crt = openssl_csr_sign($res_csr, $ca_res_crt, $ca_res_key, $lifetime,
1726
				 $args, $ca_serial);
1727
	/* If the CSR could not be signed, bail */
1728
	if (!$res_crt) {
1729
		return false;
1730
	}
1731

    
1732
	/* Attempt to read the key and certificate and if that fails, bail */
1733
	if (!openssl_pkey_export($res_key, $str_key) ||
1734
	    !openssl_x509_export($res_crt, $str_crt)) {
1735
		return false;
1736
	}
1737

    
1738
	/* Load the new certificate string and key into the configuration */
1739
	$cert['crt'] = base64_encode($str_crt);
1740
	$cert['prv'] = base64_encode($str_key);
1741

    
1742
	return true;
1743
}
1744

    
1745
/****f* certs/cert_get_all_services
1746
 * NAME
1747
 *   cert_get_all_services - Locate services using a given certificate
1748
 * INPUTS
1749
 *   $refid: The refid of a certificate to check
1750
 * RESULT
1751
 *   array containing the services which use this certificate, including:
1752
 *     webgui: Present and true if the WebGUI uses this certificate. Unset otherwise.
1753
 *     services: Array of service definitions using this certificate, with:
1754
 *       name: Name of the service
1755
 *       extras: Extra information needed by some services, such as OpenVPN or Captive Portal.
1756
 *     packages: Array containing package names using this certificate.
1757
 ******/
1758

    
1759
function cert_get_all_services($refid) {
1760
	global $config;
1761
	$services = array();
1762
	$services['services'] = array();
1763
	$services['packages'] = array();
1764

    
1765
	/* Only set if true, otherwise leave unset. */
1766
	if (is_webgui_cert($refid)) {
1767
		$services['webgui'] = true;
1768
	}
1769

    
1770
	init_config_arr(array('openvpn', 'openvpn-server'));
1771
	init_config_arr(array('openvpn', 'openvpn-client'));
1772
	/* Find all OpenVPN clients and servers which use this certificate */
1773
	foreach(array('server', 'client') as $mode) {
1774
		foreach ($config['openvpn']["openvpn-{$mode}"] as $ovpn) {
1775
			if ($ovpn['certref'] == $refid) {
1776
				/* OpenVPN instances are restarted individually,
1777
				 * so we need to note the mode and ID. */
1778
				$services['services'][] = array(
1779
					'name' => 'openvpn',
1780
					'extras' => array(
1781
						'vpnmode' => $mode,
1782
						'id' => $ovpn['vpnid']
1783
					)
1784
				);
1785
			}
1786
		}
1787
	}
1788

    
1789
	/* If any one IPsec tunnel uses this certificate then the whole service
1790
	 * needs a bump. */
1791
	init_config_arr(array('ipsec', 'phase1'));
1792
	foreach ($config['ipsec']['phase1'] as $ipsec) {
1793
		if (($ipsec['authentication_method'] == 'cert') &&
1794
		    ($ipsec['certref'] == $refid)) {
1795
			$services['services'][] = array('name' => 'ipsec');
1796
			/* Stop after finding one, no need to search for more. */
1797
			break;
1798
		}
1799
	}
1800

    
1801
	/* Check to see if any HTTPS-enabled Captive Portal zones use this
1802
	 * certificate. */
1803
	init_config_arr(array('captiveportal'));
1804
	foreach ($config['captiveportal'] as $zone => $portal) {
1805
		if (isset($portal['enable']) && isset($portal['httpslogin']) &&
1806
		    ($portal['certref'] == $refid)) {
1807
			/* Captive Portal zones are restarted individually, so
1808
			 * we need to note the zone name. */
1809
			$services['services'][] = array(
1810
				'name' => 'captiveportal',
1811
				'extras' => array(
1812
					'zone' => $zone,
1813
				)
1814
			);
1815
		}
1816
	}
1817

    
1818
	/* Locate any packages using this certificate */
1819
	$pkgcerts = pkg_call_plugins('plugin_certificates', array('type' => 'certificates', 'event' => 'used_certificates'));
1820
	foreach ($pkgcerts as $name => $package) {
1821
		if (is_array($package['certificatelist'][$refid]) &&
1822
		    isset($package['certificatelist'][$refid]) > 0) {
1823
			$services['packages'][] = $name;
1824
		}
1825
	}
1826

    
1827
	return $services;
1828
}
1829

    
1830
/****f* certs/ca_get_all_services
1831
 * NAME
1832
 *   ca_get_all_services - Locate services using a given certificate authority or its decendents
1833
 * INPUTS
1834
 *   $refid: The refid of a certificate authority to check
1835
 * RESULT
1836
 *   array containing the services which use this certificate authority, including:
1837
 *     webgui: Present and true if the WebGUI uses this certificate. Unset otherwise.
1838
 *     services: Array of service definitions using this certificate, with:
1839
 *       name: Name of the service
1840
 *       extras: Extra information needed by some services, such as OpenVPN or Captive Portal.
1841
 *     packages: Array containing package names using this certificate.
1842
 * NOTES
1843
 *   This searches recursively to find entries using this CA as well as intermediate
1844
 *   CAs and certificates signed by this CA, and returns a single set of all services.
1845
 *   This avoids restarting affected services multiple times when there is overlapping
1846
 *   usage.
1847
 ******/
1848
function ca_get_all_services($refid) {
1849
	global $config;
1850
	$services = array();
1851
	$services['services'] = array();
1852

    
1853
	init_config_arr(array('openvpn', 'openvpn-server'));
1854
	init_config_arr(array('openvpn', 'openvpn-client'));
1855
	foreach(array('server', 'client') as $mode) {
1856
		foreach ($config['openvpn']["openvpn-{$mode}"] as $ovpn) {
1857
			if ($ovpn['caref'] == $refid) {
1858
				$services['services'][] = array(
1859
					'name' => 'openvpn',
1860
					'extras' => array(
1861
						'vpnmode' => $mode,
1862
						'id' => $ovpn['vpnid']
1863
					)
1864
				);
1865
			}
1866
		}
1867
	}
1868
	init_config_arr(array('ipsec', 'phase1'));
1869
	foreach ($config['ipsec']['phase1'] as $ipsec) {
1870
		if ($ipsec['certref'] == $refid) {
1871
			break;
1872
		}
1873
	}
1874
	foreach ($config['ipsec']['phase1'] as $ipsec) {
1875
		if (($ipsec['authentication_method'] == 'cert') &&
1876
		    ($ipsec['caref'] == $refid)) {
1877
			$services['services'][] = array('name' => 'ipsec');
1878
			break;
1879
		}
1880
	}
1881

    
1882
	/* Loop through all certs and get their services as well */
1883
	init_config_arr(array('cert'));
1884
	foreach ($config['cert'] as $cert) {
1885
		if ($cert['caref'] == $refid) {
1886
			$services = array_merge_recursive_unique($services, cert_get_all_services($cert['refid']));
1887
		}
1888
	}
1889

    
1890
	/* Look for intermediate certs and services */
1891
	init_config_arr(array('ca'));
1892
	foreach ($config['ca'] as $cert) {
1893
		if ($cert['caref'] == $refid) {
1894
			$services = array_merge_recursive_unique($services, ca_get_all_services($cert['refid']));
1895
		}
1896
	}
1897

    
1898
	return $services;
1899
}
1900

    
1901
/****f* certs/cert_restart_services
1902
 * NAME
1903
 *   cert_restart_services - Restarts services specific to CA/Certificate usage
1904
 * INPUTS
1905
 *   $services: An array of services returned by cert_get_all_services or ca_get_all_services
1906
 * RESULT
1907
 *   Services in the given array are restarted
1908
 *   returns false if the input is invalid
1909
 *   returns true at the end of execution
1910
 ******/
1911

    
1912
function cert_restart_services($services) {
1913
	require_once("service-utils.inc");
1914
	/* If the input is not an array, it is invalid. */
1915
	if (!is_array($services)) {
1916
		return false;
1917
	}
1918

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

    
1922
	/* Restart GUI: */
1923
	if ($services['webgui']) {
1924
		ob_flush();
1925
		flush();
1926
		log_error(sprintf($restart_string, gettext('service'), 'WebGUI'));
1927
		send_event("service restart webgui");
1928
	}
1929

    
1930
	/* Restart other base services: */
1931
	if (is_array($services['services'])) {
1932
		foreach ($services['services'] as $service) {
1933
			switch ($service['name']) {
1934
				case 'openvpn':
1935
					$service_name = "{$service['name']} {$service['extras']['vpnmode']} {$service['extras']['id']}";
1936
					break;
1937
				case 'captiveportal':
1938
					$service_name = "{$service['name']} zone {$service['extras']['zone']}";
1939
					break;
1940
				default:
1941
					$service_name = $service['name'];
1942
			}
1943
			log_error(sprintf($restart_string, gettext('service'), $service_name));
1944
			service_control_restart($service['name'], $service['extras']);
1945
		}
1946
	}
1947

    
1948
	/* Restart Packages: */
1949
	if (is_array($services['packages'])) {
1950
		foreach ($services['packages'] as $service) {
1951
			log_error(sprintf($restart_string, gettext('package'), $service));
1952
			restart_service($service);
1953
		}
1954
	}
1955
	return true;
1956
}
1957

    
1958
/****f* certs/cert_get_lifetime
1959
 * NAME
1960
 *   cert_get_lifetime - Returns the number of days the certificate is valid
1961
 * INPUTS
1962
 *   $untilexpire: Boolean
1963
 *     true: The number of days returned is from now until the certificate expiration.
1964
 *     false (default): The number of days returned is the total lifetime of the certificate.
1965
 * RESULT
1966
 *   Integer number of days in the certificate total or remaining lifetime
1967
 ******/
1968

    
1969
function cert_get_lifetime($cert, $untilexpire = false) {
1970
	/* If the certificate is not valid, bail. */
1971
	if (!is_array($cert) || empty($cert['crt'])) {
1972
		return null;
1973
	}
1974
	/* Read certificate details */
1975
	list($startdate, $enddate) = cert_get_dates($cert['crt'], true, false);
1976

    
1977
	/* If either of the dates are invalid, there is nothing we can do here. */
1978
	if (($startdate === false) || ($enddate === false)) {
1979
		return false;
1980
	}
1981

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

    
1985
	/* Calculate the requested intervals */
1986
	$interval = $startdate->diff($enddate);
1987

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

    
1992
/****f* certs/cert_analyze_lifetime
1993
 * NAME
1994
 *   cert_analyze_lifetime - Analyze a certificate lifetime for expiration notices
1995
 * INPUTS
1996
 *   $expiredays: Number of days until the certificate expires (See cert_get_lifetime())
1997
 * RESULT
1998
 *   An array of two entries:
1999
 *   0/$lrclass: A bootstrap name for use with classes like text-<x>
2000
 *   1/$expstring: A text analysis describing the expiration timeframe.
2001
 ******/
2002

    
2003
function cert_analyze_lifetime($expiredays) {
2004
	global $config, $g;
2005
	/* Number of days at which to warn of expiration. */
2006
	init_config_arr(array('notifications', 'certexpire'));
2007
	if (!isset($config['notifications']['certexpire']['expiredays']) ||
2008
	    empty($config['notifications']['certexpire']['expiredays'])) {
2009
		$warning_days = $g['default_cert_expiredays'];
2010
	} else {
2011
		$warning_days = $config['notifications']['certexpire']['expiredays'];
2012
	}
2013

    
2014
	if ($expiredays > $warning_days) {
2015
		/* Not expiring soon */
2016
		$lrclass = 'normal';
2017
		$expstring = gettext("%d %s until expiration");
2018
	} elseif ($expiredays > 0) {
2019
		/* Still valid but expiring soon */
2020
		$lrclass = 'warning';
2021
		$expstring = gettext("Expiring soon, in %d %s");
2022
	} else {
2023
		/* Certificate has expired */
2024
		$lrclass = 'danger';
2025
		$expstring = gettext("Expired %d %s ago");
2026
	}
2027
	$days = (abs($expiredays) == 1) ? gettext('day') : gettext('days');
2028
	$expstring = sprintf($expstring, abs($expiredays), $days);
2029
	return array($lrclass, $expstring);
2030
}
2031

    
2032
/****f* certs/cert_print_dates
2033
 * NAME
2034
 *   cert_print_dates - Print the start and end timestamps for the given certificate
2035
 * INPUTS
2036
 *   $cert: CA or Cert entry for which the dates will be printed
2037
 * RESULT
2038
 *   Returns null if the passed entry is invalid
2039
 *   Otherwise, outputs the dates to the user with formatting.
2040
 ******/
2041

    
2042
function cert_print_dates($cert) {
2043
	/* If the certificate is not valid, bail. */
2044
	if (!is_array($cert) || empty($cert['crt'])) {
2045
		return null;
2046
	}
2047
	/* Attempt to extract the dates from the certificate */
2048
	list($startdate, $enddate) = cert_get_dates($cert['crt']);
2049
	/* If either of the timestamps are empty, then do not print anything.
2050
	 * The entry may not be valid or it may just be missing date information */
2051
	if (empty($startdate) || empty($enddate)) {
2052
		return null;
2053
	}
2054
	/* Get the expiration days */
2055
	$expiredays = cert_get_lifetime($cert, true);
2056
	/* Analyze the lifetime value */
2057
	list($lrclass, $expstring) = cert_analyze_lifetime($expiredays);
2058
	/* Output the dates, with a tooltip showing days until expiration, and
2059
	 * a visual indication of warning/expired status. */
2060
	?>
2061
	<br />
2062
	<small>
2063
	<?=gettext("Valid From")?>: <b><?=$startdate ?></b><br />
2064
	<?=gettext("Valid Until")?>:
2065
	<span class="text-<?=$lrclass?>" data-toggle="tooltip" data-placement="bottom" title="<?= $expstring ?>">
2066
	<b><?=$enddate ?></b>
2067
	</span>
2068
	</small>
2069
	<?php
2070
}
2071

    
2072
/****f* certs/cert_print_infoblock
2073
 * NAME
2074
 *   cert_print_infoblock - Print an information block containing certificate details
2075
 * INPUTS
2076
 *   $cert: CA or Cert entry for which the information will be printed
2077
 * RESULT
2078
 *   Returns null if the passed entry is invalid
2079
 *   Otherwise, outputs information to the user with formatting.
2080
 ******/
2081

    
2082
function cert_print_infoblock($cert) {
2083
	/* If the certificate is not valid, bail. */
2084
	if (!is_array($cert) || empty($cert['crt'])) {
2085
		return null;
2086
	}
2087
	/* Variable to hold the formatted information */
2088
	$certextinfo = "";
2089

    
2090
	/* Serial number */
2091
	$cert_details = openssl_x509_parse(base64_decode($cert['crt']));
2092
	if (isset($cert_details['serialNumber']) && (strlen($cert_details['serialNumber']) > 0)) {
2093
		$certextinfo .= '<b>' . gettext("Serial: ") . '</b> ';
2094
		$certextinfo .= htmlspecialchars(cert_escape_x509_chars($cert_details['serialNumber'], true));
2095
		$certextinfo .= '<br/>';
2096
	}
2097

    
2098
	/* Digest type */
2099
	$certsig = cert_get_sigtype($cert['crt']);
2100
	if (is_array($certsig) && !empty($certsig) && !empty($certsig['shortname'])) {
2101
		$certextinfo .= '<b>' . gettext("Signature Digest: ") . '</b> ';
2102
		$certextinfo .= htmlspecialchars(cert_escape_x509_chars($certsig['shortname'], true));
2103
		$certextinfo .= '<br/>';
2104
	}
2105

    
2106
	/* Subject Alternative Name (SAN) list */
2107
	$sans = cert_get_sans($cert['crt']);
2108
	if (is_array($sans) && !empty($sans)) {
2109
		$certextinfo .= '<b>' . gettext("SAN: ") . '</b> ';
2110
		$certextinfo .= htmlspecialchars(implode(', ', cert_escape_x509_chars($sans, true)));
2111
		$certextinfo .= '<br/>';
2112
	}
2113

    
2114
	/* Key usage */
2115
	$purpose = cert_get_purpose($cert['crt']);
2116
	if (is_array($purpose) && !empty($purpose['ku'])) {
2117
		$certextinfo .= '<b>' . gettext("KU: ") . '</b> ';
2118
		$certextinfo .= htmlspecialchars(implode(', ', $purpose['ku']));
2119
		$certextinfo .= '<br/>';
2120
	}
2121

    
2122
	/* Extended key usage */
2123
	if (is_array($purpose) && !empty($purpose['eku'])) {
2124
		$certextinfo .= '<b>' . gettext("EKU: ") . '</b> ';
2125
		$certextinfo .= htmlspecialchars(implode(', ', $purpose['eku']));
2126
		$certextinfo .= '<br/>';
2127
	}
2128

    
2129
	/* OCSP / Must Staple */
2130
	if (cert_get_ocspstaple($cert['crt'])) {
2131
		$certextinfo .= '<b>' . gettext("OCSP: ") . '</b> ';
2132
		$certextinfo .= gettext("Must Staple");
2133
		$certextinfo .= '<br/>';
2134
	}
2135

    
2136
	/* Private key information */
2137
	if (!empty($cert['prv'])) {
2138
		$res_key = openssl_pkey_get_private(base64_decode($cert['prv']));
2139
		$key_details = openssl_pkey_get_details($res_key);
2140

    
2141
		/* Key type (RSA or EC) */
2142
		$certextinfo .= '<b>' . gettext("Key Type: ") . '</b> ';
2143
		if ($key_details['type'] == OPENSSL_KEYTYPE_RSA) {
2144
			/* RSA Key size */
2145
			$certextinfo .= 'RSA<br/>';
2146
			$certextinfo .= '<b>' . gettext("Key Size: ") . '</b> ';
2147
			$certextinfo .= $key_details['bits'] . '<br/>';
2148
		} else {
2149
			/* Elliptic curve (EC) key curve name */
2150
			$certextinfo .= 'ECDSA<br/>';
2151
			$curve = cert_get_pkey_curve($cert['prv']);
2152
			if (!empty($curve)) {
2153
				$certextinfo .= '<b>' . gettext("Elliptic curve name:") . ' </b>';
2154
				$certextinfo .= $curve . '<br/>';
2155
			}
2156
		}
2157
	}
2158

    
2159
	/* Distinguished name (DN) */
2160
	if (!empty($cert_details['name'])) {
2161
		$certextinfo .= '<b>' . gettext("DN: ") . '</b> ';
2162
		$certextinfo .= htmlspecialchars(cert_escape_x509_chars($cert_details['name'], true));
2163
		$certextinfo .= '<br/>';
2164
	}
2165

    
2166
	/* Hash value */
2167
	if (!empty($cert_details['hash'])) {
2168
		$certextinfo .= '<b>' . gettext("Hash: ") . '</b> ';
2169
		$certextinfo .= htmlspecialchars(cert_escape_x509_chars($cert_details['hash'], true));
2170
		$certextinfo .= '<br/>';
2171
	}
2172

    
2173
	/* Subject Key Identifier (SKID) */
2174
	if (is_array($cert_details["extensions"]) && !empty($cert_details["extensions"]["subjectKeyIdentifier"])) {
2175
		$certextinfo .= '<b>' . gettext("Subject Key ID: ") . '</b> ';
2176
		$certextinfo .= htmlspecialchars(cert_escape_x509_chars($cert_details["extensions"]["subjectKeyIdentifier"], true));
2177
		$certextinfo .= '<br/>';
2178
	}
2179

    
2180
	/* Authority Key Identifier (AKID) */
2181
	if (is_array($cert_details["extensions"]) && !empty($cert_details["extensions"]["authorityKeyIdentifier"])) {
2182
		$certextinfo .= '<b>' . gettext("Authority Key ID: ") . '</b> ';
2183
		$certextinfo .= str_replace("\n", '<br/>', htmlspecialchars(cert_escape_x509_chars($cert_details["extensions"]["authorityKeyIdentifier"], true)));
2184
		$certextinfo .= '<br/>';
2185
	}
2186

    
2187
	/* Total Lifetime (days from cert start to end) */
2188
	$lifetime = cert_get_lifetime($cert);
2189
	$certextinfo .= '<b>' . gettext("Total Lifetime: ") . '</b> ';
2190
	$certextinfo .= sprintf("%d %s", $lifetime, (abs($lifetime) == 1) ? gettext('day') : gettext('days'));
2191
	$certextinfo .= '<br/>';
2192

    
2193
	/* Lifetime before certificate expires (days from now to end) */
2194
	$expiredays = cert_get_lifetime($cert, true);
2195
	list($lrclass, $expstring) = cert_analyze_lifetime($expiredays);
2196
	$certextinfo .= '<b>' . gettext("Lifetime Remaining: ") . '</b> ';
2197
	$certextinfo .= "<span class=\"text-{$lrclass}\">{$expstring}</span>";
2198
	$certextinfo .= '<br/>';
2199

    
2200
	if ($purpose['ca'] == 'Yes') {
2201
		/* CA Trust store presence */
2202
		$certextinfo .= '<b>' . gettext("Trust Store: ") . '</b> ';
2203
		$certextinfo .= (isset($cert['trust']) && ($cert['trust'] == "enabled")) ? gettext('Included') : gettext('Excluded');
2204
		$certextinfo .= '<br/>';
2205

    
2206
		if (!empty($cert['prv'])) {
2207
			/* CA Next/Randomize Serial */
2208
			$certextinfo .= '<b>' . gettext("Next Serial: ") . '</b> ';
2209
			$certextinfo .= (isset($cert['randomserial']) && ($cert['randomserial'] == "enabled")) ? gettext('Randomized') : $cert['serial'];
2210
			$certextinfo .= '<br/>';
2211
		}
2212
	}
2213

    
2214
	/* Output the infoblock */
2215
	if (!empty($certextinfo)) { ?>
2216
		<div class="infoblock">
2217
		<? print_info_box($certextinfo, 'info', false); ?>
2218
		</div>
2219
	<?php
2220
	}
2221
}
2222

    
2223
/****f* certs/cert_notify_expiring
2224
 * NAME
2225
 *   cert_notify_expiring - Notify admin about expiring certificates
2226
 * INPUTS
2227
 *   None
2228
 * RESULT
2229
 *   File a notice containing expiring certificate information, which is then
2230
 *   logged, displayed in the GUI, and sent via e-mail (if enabled).
2231
 ******/
2232

    
2233
function cert_notify_expiring() {
2234
	global $config;
2235

    
2236
	/* If certificate expiration notifications are disabled, there is nothing to do. */
2237
	init_config_arr(array('notifications', 'certexpire'));
2238
	if ($config['notifications']['certexpire']['enable'] == "disabled") {
2239
		return;
2240
	}
2241

    
2242
	$notifications = array();
2243

    
2244
	/* Check all CA and Cert entries at once */
2245
	init_config_arr(array('ca'));
2246
	init_config_arr(array('cert'));
2247
	$all_certs = array_merge_recursive($config['ca'], $config['cert']);
2248

    
2249
	foreach ($all_certs as $cert) {
2250
		if (empty($cert)) {
2251
			continue;
2252
		}
2253
		/* Fetch and analyze expiration */
2254
		$expiredays = cert_get_lifetime($cert, true);
2255
		/* If the result is null, then the lifetime data is missing, so skip the invalid entry. */
2256
		if ($expiredays == null) {
2257
			continue;
2258
		}
2259
		list($lrclass, $expstring) = cert_analyze_lifetime($expiredays);
2260
		/* Only notify if the certificate is expiring soon, or has
2261
		 * already expired */
2262
		if ($lrclass != 'normal') {
2263
			$notify_string = (array_key_exists('serial', $cert)) ? gettext('Certificate Authority') : gettext('Certificate');
2264
			$notify_string .= ": {$cert['descr']} ({$cert['refid']}): {$expstring}";
2265
			$notifications[] = $notify_string;
2266
		}
2267
	}
2268
	if (!empty($notifications)) {
2269
		$message = gettext("The following CA/Certificate entries are expiring:") . "\n" .
2270
			implode("\n", $notifications);
2271
		file_notice("Certificate Expiration", $message, "Certificate Manager");
2272
	}
2273
}
2274

    
2275
/****f* certs/ca_setup_trust_store
2276
 * NAME
2277
 *   ca_setup_trust_store - Setup local CA trust store so that CA entries in the
2278
 *                          configuration may be trusted by the operating system.
2279
 * INPUTS
2280
 *   None
2281
 * RESULT
2282
 *   CAs marked as trusted in the configuration will be setup in the OS trust store.
2283
 ******/
2284

    
2285
function ca_setup_trust_store() {
2286
	global $config;
2287

    
2288
	/* This directory is trusted by OpenSSL on FreeBSD by default */
2289
	$trust_store_directory = '/etc/ssl/certs';
2290

    
2291
	/* Create the directory if it does not already exist, and clean it up if it does. */
2292
	safe_mkdir($trust_store_directory);
2293
	unlink_if_exists("{$trust_store_directory}/*.0");
2294

    
2295
	init_config_arr(array('ca'));
2296
	foreach ($config['ca'] as $ca) {
2297
		/* If the entry is invalid or is not trusted, skip it. */
2298
		if (!is_array($ca) ||
2299
		    empty($ca['crt']) ||
2300
		    !isset($ca['trust']) ||
2301
		    ($ca['trust'] != "enabled")) {
2302
			continue;
2303
		}
2304

    
2305
		ca_setup_capath($ca, $trust_store_directory);
2306
	}
2307
}
2308

    
2309
/****f* certs/ca_setup_capath
2310
 * NAME
2311
 *   ca_setup_capath - Setup CApath structure so that CA chains and related CRLs
2312
 *                     may be written and validated by the -CApath option in
2313
 *                     OpenSSL and other compatible validators.
2314
 * INPUTS
2315
 *   $ca     : A CA (not a refid) to write
2316
 *   $basedir: The directory which will contain the CA structure.
2317
 *   $crl    : A CRL (not a refid) associated with the CA to write.
2318
 *   $refresh: Refresh CRLs -- When true, perform no cleanup and increment suffix
2319
 * RESULT
2320
 *   $basedir is populated with CA and CRL files in a format usable by OpenSSL
2321
 *   CApath. This has the filenames as the CA hash with the CA named <hash>.0
2322
 *   and CRLs named <hash>.r0
2323
 ******/
2324

    
2325
function ca_setup_capath($ca, $basedir, $crl = "", $refresh = false) {
2326
	global $config;
2327
	/* Check for an invalid CA */
2328
	if (!$ca || !is_array($ca)) {
2329
		return false;
2330
	}
2331
	/* Check for an invalid CRL, but do not consider it fatal if it's wrong */
2332
	if (!$crl || !is_array($crl) || ($crl['caref'] != $ca['refid'])) {
2333
		unset($crl);
2334
	}
2335

    
2336
	/* Check for an empty base directory, which is invalid */
2337
	if (empty($basedir)) {
2338
		return false;
2339
	}
2340

    
2341
	/* Ensure that $basedir exists and is a directory */
2342
	if (!is_dir($basedir)) {
2343
		/* If it's a file, remove it, otherwise the directory cannot
2344
		 * be created. */
2345
		if (file_exists($basedir)) {
2346
			@unlink_if_exists($basedir);
2347
		}
2348
		@safe_mkdir($basedir);
2349
	}
2350
	/* Decode the certificate contents */
2351
	$cert_contents = base64_decode($ca['crt']);
2352
	/* Get hash value to use for filename */
2353
	$cert_details = openssl_x509_parse($cert_contents);
2354
	$fprefix = "{$basedir}/{$cert_details['hash']}";
2355

    
2356

    
2357
	$ca_filename = "{$fprefix}.0";
2358
	/* Cleanup old CA/CRL files for this hash */
2359
	@unlink_if_exists($ca_filename);
2360
	/* Write CA to base dir and ensure it has correct permissions. */
2361
	file_put_contents($ca_filename, $cert_contents);
2362
	chmod($ca_filename, 0644);
2363
	chown($ca_filename, 'root');
2364
	chgrp($ca_filename, 'wheel');
2365

    
2366
	/* If there is a CRL, process it. */
2367
	if ($crl) {
2368
		$crl_filename = "{$fprefix}.r";
2369
		if (!$refresh) {
2370
			/* Cleanup old CA/CRL files for this hash */
2371
			@unlink_if_exists("{$crl_filename}*");
2372
		}
2373
		/* Find next suffix based on how many existing files there are (start=0) */
2374
		$crl_filename .= count(glob("{$crl_filename}*"));
2375
		/* Write CRL to base dir and ensure it has correct permissions. */
2376
		file_put_contents($crl_filename, base64_decode($crl['text']));
2377
		chmod($crl_filename, 0644);
2378
		chown($crl_filename, 'root');
2379
		chgrp($crl_filename, 'wheel');
2380
	}
2381

    
2382
	return true;
2383
}
2384

    
2385
/****f* certs/cert_get_pkey_curve
2386
 * NAME
2387
 *   cert_get_pkey_curve - Get the ECDSA curve of a private key
2388
 * INPUTS
2389
 *   $pkey  : The private key to check
2390
 *   $decode: true: base64 decode the string before use, false to use as-is.
2391
 * RESULT
2392
 *   false if the private key is not ECDSA or the private key is not present.
2393
 *   Otherwise, the name of the ECDSA curve used for the private key.
2394
 ******/
2395

    
2396
function cert_get_pkey_curve($pkey, $decode = true) {
2397
	if ($decode) {
2398
		$pkey = base64_decode($pkey);
2399
	}
2400

    
2401
	/* Attempt to read the private key, and if successful, its details. */
2402
	$res_key = openssl_pkey_get_private($pkey);
2403
	if ($res_key) {
2404
		$key_details = openssl_pkey_get_details($res_key);
2405
		/* If this is an EC key, and the curve name is not empty, return
2406
		 * that curve name. */
2407
		if ($key_details['type'] ==  OPENSSL_KEYTYPE_EC) {
2408
			if (!empty($key_details['ec']['curve_name'])) {
2409
				return $key_details['ec']['curve_name'];
2410
			} else {
2411
				return $key_details['ec']['curve_oid'];
2412
			}
2413
		}
2414
	}
2415

    
2416
	/* Either the private key could not be read, or this is not an EC certificate. */
2417
	return false;
2418
}
2419

    
2420
/* Array containing ECDSA curve names allowed in certain contexts. For instance,
2421
 * HTTPS servers and web browsers only support specific curves in TLSv1.3. */
2422
global $cert_curve_compatible, $curve_compatible_list;
2423
$cert_curve_compatible = array(
2424
	/* HTTPS list per TLSv1.3 spec and Mozilla compatibility list */
2425
	'HTTPS' => array('prime256v1', 'secp384r1'),
2426
	/* IPsec/EAP/TLS list per strongSwan docs/issues */
2427
	'IPsec' => array('prime256v1', 'secp384r1', 'secp521r1'),
2428
	/* OpenVPN bug limits usable curves, see https://redmine.pfsense.org/issues/9744 */
2429
	'OpenVPN' => array('prime256v1', 'secp384r1', 'secp521r1'),
2430
);
2431
$curve_compatible_list = array_unique(call_user_func_array('array_merge', $cert_curve_compatible));
2432

    
2433
/****f* certs/cert_build_curve_list
2434
 * NAME
2435
 *   cert_build_curve_list - Build an option list of ECDSA curves with notations
2436
 *                           about known compatible uses.
2437
 * INPUTS
2438
 *   None
2439
 * RESULT
2440
 *   Returns an option list of OpenSSL EC names with added notes. This can be
2441
 *   used directly in form option selection lists.
2442
 ******/
2443

    
2444
function cert_build_curve_list() {
2445
	global $cert_curve_compatible;
2446
	/* Get the default list of curve names */
2447
	$openssl_ecnames = openssl_get_curve_names();
2448
	/* Turn this into a hashed array where key==value */
2449
	$curvelist = array_combine($openssl_ecnames, $openssl_ecnames);
2450
	/* Check all known compatible curves and note matches */
2451
	foreach ($cert_curve_compatible as $consumer => $validcurves) {
2452
		/* $consumer will be a name like HTTPS or IPsec
2453
		 * $validcurves will be an array of curves compatible with the consumer */
2454
		foreach ($validcurves as $vc) {
2455
			/* If the valid curve is present in the curve list, add
2456
			 * a note with the consumer name to the value visible to
2457
			 * the user. */
2458
			if (array_key_exists($vc, $curvelist)) {
2459
				$curvelist[$vc] .= " [{$consumer}]";
2460
			}
2461
		}
2462
	}
2463
	return $curvelist;
2464
}
2465

    
2466
/****f* certs/cert_check_pkey_compatibility
2467
 * NAME
2468
 *   cert_check_pkey_compatibility - Check a private key to see if it can be
2469
 *                                   used in a specific compatible context.
2470
 * INPUTS
2471
 *   $pkey    : The private key to check
2472
 *   $consumer: The consumer name used to validate the curve. See the contents
2473
 *                 of $cert_curve_compatible for details.
2474
 * RESULT
2475
 *   true if the private key may be used in requested area, or if there are no
2476
 *        restrictions.
2477
 *   false if the private key cannot be used.
2478
 ******/
2479

    
2480
function cert_check_pkey_compatibility($pkey, $consumer) {
2481
	global $cert_curve_compatible;
2482

    
2483
	/* Read the curve name from the key */
2484
	$curve = cert_get_pkey_curve($pkey);
2485
	/* Return true if any of the following conditions are met:
2486
	 *  * This is not an EC key
2487
	 *  * The private key cannot be read
2488
	 *  * There are no restrictions
2489
	 *  * The requested curve is compatible */
2490
	return (($curve === false) ||
2491
		!array_key_exists($consumer, $cert_curve_compatible) ||
2492
		in_array($curve, $cert_curve_compatible[$consumer]));
2493
}
2494

    
2495
/****f* certs/cert_build_list
2496
 * NAME
2497
 *   cert_build_list - Build an option list of cert or CA entries, checked
2498
 *                     against a specific consumer name.
2499
 * INPUTS
2500
 *   $type    : 'ca' for certificate authority entries, 'cert' for certificates.
2501
 *   $consumer: The consumer name used to filter certificates out of the result.
2502
 *                 See the contents of $cert_curve_compatible for details.
2503
 *   $selectsource: Then true, outputs in a format usable by select_source in
2504
 *                  packages.
2505
 *   $addnone: When true, a 'none' choice is added to the list.
2506
 * RESULT
2507
 *   Returns an option list of entries with incompatible entries removed. This
2508
 *   can be used directly in form option selection lists.
2509
 * NOTES
2510
 *   This can be expanded in the future to allow for other types of restrictions.
2511
 ******/
2512

    
2513
function cert_build_list($type = 'cert', $consumer = '', $selectsource = false, $addnone = false) {
2514
	global $config;
2515

    
2516
	/* Ensure that $type is valid */
2517
	if (!in_array($type, array('ca', 'cert'))) {
2518
		return array();
2519
	}
2520

    
2521
	/* Initialize arrays */
2522
	init_config_arr(array($type));
2523
	$list = array();
2524

    
2525
	if ($addnone) {
2526
		if ($selectsource) {
2527
			$list[] = array('refid' => 'none', 'descr' => 'None');
2528
		} else {
2529
			$list['none'] = "None";
2530
		}
2531
	}
2532

    
2533
	/* Create a hashed array with the certificate refid as the key and
2534
	 * descriptive name as the value. Exclude incompatible certificates. */
2535
	foreach ($config[$type] as $cert) {
2536
		if (empty($cert['prv']) && ($type == 'cert')) {
2537
			continue;
2538
		} else if (cert_check_pkey_compatibility($cert['prv'], $consumer)) {
2539
			if ($selectsource) {
2540
				$list[] = array('refid' => $cert['refid'],
2541
						'descr' => $cert['descr']);
2542
			} else {
2543
				$list[$cert['refid']] = $cert['descr'];
2544
			}
2545
		}
2546
	}
2547

    
2548
	return $list;
2549
}
2550

    
2551
?>
(7-7/61)