Project

General

Profile

Download (81.7 KB) Statistics
| Branch: | Tag: | Revision:
1 d43ad788 Scott Ullrich
<?php
2
/*
3 ac24dc24 Renato Botelho
 * certs.inc
4
 *
5
 * part of pfSense (https://www.pfsense.org)
6 38809d47 Renato Botelho do Couto
 * Copyright (c) 2008-2013 BSD Perimeter
7
 * Copyright (c) 2013-2016 Electric Sheep Fencing
8 402c98a2 Reid Linnemann
 * Copyright (c) 2014-2023 Rubicon Communications, LLC (Netgate)
9 ac24dc24 Renato Botelho
 * Copyright (c) 2008 Shrew Soft Inc. All rights reserved.
10
 * All rights reserved.
11
 *
12 b12ea3fb Renato Botelho
 * 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 ac24dc24 Renato Botelho
 *
16 b12ea3fb Renato Botelho
 * http://www.apache.org/licenses/LICENSE-2.0
17 ac24dc24 Renato Botelho
 *
18 b12ea3fb Renato Botelho
 * 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 ac24dc24 Renato Botelho
 */
24 d43ad788 Scott Ullrich
25 87b4deb2 jim-p
define("OPEN_SSL_CONF_PATH", "/etc/ssl/openssl.cnf");
26
27 d43ad788 Scott Ullrich
require_once("functions.inc");
28
29 e09b941d jim-p
global $openssl_digest_algs;
30 84141846 jim-p
$openssl_digest_algs = array("sha1", "sha224", "sha256", "sha384", "sha512");
31 ca621902 jim-p
32 e09b941d jim-p
global $openssl_crl_status;
33 2aafa69c jim-p
/* Numbers are set in the RFC: https://www.ietf.org/rfc/rfc5280.txt */
34 e09b941d jim-p
$openssl_crl_status = array(
35 2aafa69c jim-p
	-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 e09b941d jim-p
);
45
46 2e1809dd jim-p
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 a7e50981 jim-p
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 3a877e4a jim-p
global $cert_max_lifetime;
62
$cert_max_lifetime = 12000;
63 2e1809dd jim-p
64 a3c15890 jim-p
global $crl_max_lifetime;
65
$crl_max_lifetime = 9999;
66
67 d43ad788 Scott Ullrich
function & lookup_ca($refid) {
68
	global $config;
69
70 1e0b1727 Phil Davis
	if (is_array($config['ca'])) {
71
		foreach ($config['ca'] as & $ca) {
72 c3a65526 jim-p
			if (empty($ca)) {
73
				continue;
74
			}
75 1e0b1727 Phil Davis
			if ($ca['refid'] == $refid) {
76 d43ad788 Scott Ullrich
				return $ca;
77 1e0b1727 Phil Davis
			}
78
		}
79
	}
80 d43ad788 Scott Ullrich
81
	return false;
82
}
83
84
function & lookup_ca_by_subject($subject) {
85
	global $config;
86
87 1e0b1727 Phil Davis
	if (is_array($config['ca'])) {
88
		foreach ($config['ca'] as & $ca) {
89 c3a65526 jim-p
			if (empty($ca)) {
90
				continue;
91
			}
92 d43ad788 Scott Ullrich
			$ca_subject = cert_get_subject($ca['crt']);
93 1e0b1727 Phil Davis
			if ($ca_subject == $subject) {
94 d43ad788 Scott Ullrich
				return $ca;
95 1e0b1727 Phil Davis
			}
96 d43ad788 Scott Ullrich
		}
97 1e0b1727 Phil Davis
	}
98 d43ad788 Scott Ullrich
99
	return false;
100
}
101
102
function & lookup_cert($refid) {
103
	global $config;
104
105 1e0b1727 Phil Davis
	if (is_array($config['cert'])) {
106
		foreach ($config['cert'] as & $cert) {
107 c3a65526 jim-p
			if (empty($cert)) {
108
				continue;
109
			}
110 1e0b1727 Phil Davis
			if ($cert['refid'] == $refid) {
111 d43ad788 Scott Ullrich
				return $cert;
112 1e0b1727 Phil Davis
			}
113
		}
114
	}
115 d43ad788 Scott Ullrich
116
	return false;
117
}
118
119 c5f010aa jim-p
function & lookup_cert_by_name($name) {
120
	global $config;
121 1e0b1727 Phil Davis
	if (is_array($config['cert'])) {
122
		foreach ($config['cert'] as & $cert) {
123 c3a65526 jim-p
			if (empty($cert)) {
124
				continue;
125
			}
126 1e0b1727 Phil Davis
			if ($cert['descr'] == $name) {
127 c5f010aa jim-p
				return $cert;
128 1e0b1727 Phil Davis
			}
129
		}
130
	}
131 c5f010aa jim-p
}
132
133
function & lookup_crl($refid) {
134
	global $config;
135
136 1e0b1727 Phil Davis
	if (is_array($config['crl'])) {
137
		foreach ($config['crl'] as & $crl) {
138 c3a65526 jim-p
			if (empty($crl)) {
139
				continue;
140
			}
141 1e0b1727 Phil Davis
			if ($crl['refid'] == $refid) {
142 c5f010aa jim-p
				return $crl;
143 1e0b1727 Phil Davis
			}
144
		}
145
	}
146 c5f010aa jim-p
147
	return false;
148
}
149
150 d43ad788 Scott Ullrich
function ca_chain_array(& $cert) {
151 1e0b1727 Phil Davis
	if ($cert['caref']) {
152 d43ad788 Scott Ullrich
		$chain = array();
153 5289dc57 jim-p
		$crt = lookup_ca($cert['caref']);
154 d43ad788 Scott Ullrich
		$chain[] = $crt;
155
		while ($crt) {
156
			$caref = $crt['caref'];
157 1e0b1727 Phil Davis
			if ($caref) {
158 5289dc57 jim-p
				$crt = lookup_ca($caref);
159 1e0b1727 Phil Davis
			} else {
160 d43ad788 Scott Ullrich
				$crt = false;
161 1e0b1727 Phil Davis
			}
162
			if ($crt) {
163 d43ad788 Scott Ullrich
				$chain[] = $crt;
164 1e0b1727 Phil Davis
			}
165 d43ad788 Scott Ullrich
		}
166
		return $chain;
167
	}
168
	return false;
169
}
170
171
function ca_chain(& $cert) {
172 1e0b1727 Phil Davis
	if ($cert['caref']) {
173 d43ad788 Scott Ullrich
		$ca = "";
174
		$cas = ca_chain_array($cert);
175 1e0b1727 Phil Davis
		if (is_array($cas)) {
176
			foreach ($cas as & $ca_cert) {
177 d43ad788 Scott Ullrich
				$ca .= base64_decode($ca_cert['crt']);
178
				$ca .= "\n";
179
			}
180 1e0b1727 Phil Davis
		}
181 d43ad788 Scott Ullrich
		return $ca;
182
	}
183
	return "";
184
}
185
186 ab63443a jim-p
function ca_import(& $ca, $str, $key = "", $serial = "") {
187 d43ad788 Scott Ullrich
	global $config;
188
189
	$ca['crt'] = base64_encode($str);
190 1e0b1727 Phil Davis
	if (!empty($key)) {
191 ecefc738 jim-p
		$ca['prv'] = base64_encode($key);
192 1e0b1727 Phil Davis
	}
193 ab63443a jim-p
	if (empty($serial)) {
194
		$ca['serial'] = 0;
195
	} else {
196 bfa992bc jim-p
		$ca['serial'] = $serial;
197 1e0b1727 Phil Davis
	}
198 d43ad788 Scott Ullrich
	$subject = cert_get_subject($str, false);
199
	$issuer = cert_get_issuer($str, false);
200 96d78012 Viktor Gurov
	$serialNumber = cert_get_serial($str, false);
201 1e0b1727 Phil Davis
202 d43ad788 Scott Ullrich
	// Find my issuer unless self-signed
203 1e0b1727 Phil Davis
	if ($issuer <> $subject) {
204 d43ad788 Scott Ullrich
		$issuer_crt =& lookup_ca_by_subject($issuer);
205 1e0b1727 Phil Davis
		if ($issuer_crt) {
206 d43ad788 Scott Ullrich
			$ca['caref'] = $issuer_crt['refid'];
207 1e0b1727 Phil Davis
		}
208 d43ad788 Scott Ullrich
	}
209
210
	/* Correct if child certificate was loaded first */
211 1e0b1727 Phil Davis
	if (is_array($config['ca'])) {
212
		foreach ($config['ca'] as & $oca) {
213 96d78012 Viktor Gurov
			// 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 d43ad788 Scott Ullrich
			$issuer = cert_get_issuer($oca['crt']);
219 96d78012 Viktor Gurov
			if (($ca['refid'] <> $oca['refid']) && ($issuer == $subject)) {
220 d43ad788 Scott Ullrich
				$oca['caref'] = $ca['refid'];
221 1e0b1727 Phil Davis
			}
222 d43ad788 Scott Ullrich
		}
223 1e0b1727 Phil Davis
	}
224
	if (is_array($config['cert'])) {
225
		foreach ($config['cert'] as & $cert) {
226 d43ad788 Scott Ullrich
			$issuer = cert_get_issuer($cert['crt']);
227 086cf944 Phil Davis
			if ($issuer == $subject) {
228 d43ad788 Scott Ullrich
				$cert['caref'] = $ca['refid'];
229 1e0b1727 Phil Davis
			}
230 d43ad788 Scott Ullrich
		}
231 1e0b1727 Phil Davis
	}
232 d43ad788 Scott Ullrich
	return true;
233
}
234
235 c3cda38e jim-p
function ca_create(& $ca, $keylen, $lifetime, $dn, $digest_alg = "sha256", $keytype = "RSA", $ecname = "prime256v1") {
236 d43ad788 Scott Ullrich
237
	$args = array(
238 87b4deb2 jim-p
		"x509_extensions" => "v3_ca",
239 ca621902 jim-p
		"digest_alg" => $digest_alg,
240 d43ad788 Scott Ullrich
		"encrypt_key" => false);
241 ff5bc49c Viktor Gurov
	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 d43ad788 Scott Ullrich
249
	// generate a new key pair
250 838e27bf jim-p
	$res_key = openssl_pkey_new($args);
251 1e0b1727 Phil Davis
	if (!$res_key) {
252
		return false;
253
	}
254 d43ad788 Scott Ullrich
255
	// generate a certificate signing request
256
	$res_csr = openssl_csr_new($dn, $res_key, $args);
257 1e0b1727 Phil Davis
	if (!$res_csr) {
258
		return false;
259
	}
260 d43ad788 Scott Ullrich
261
	// self sign the certificate
262 4aa7c7ae jim-p
	$res_crt = openssl_csr_sign($res_csr, null, $res_key, $lifetime, $args, cert_get_random_serial());
263 1e0b1727 Phil Davis
	if (!$res_crt) {
264
		return false;
265
	}
266 d43ad788 Scott Ullrich
267
	// export our certificate data
268 1b6d9fa5 Evgeny Yurchenko
	if (!openssl_pkey_export($res_key, $str_key) ||
269 ae52d165 Renato Botelho
	    !openssl_x509_export($res_crt, $str_crt)) {
270 1b6d9fa5 Evgeny Yurchenko
		return false;
271 1e0b1727 Phil Davis
	}
272 d43ad788 Scott Ullrich
273
	// return our ca information
274
	$ca['crt'] = base64_encode($str_crt);
275
	$ca['prv'] = base64_encode($str_key);
276 663e29bb jim-p
	$ca['serial'] = 1;
277 d43ad788 Scott Ullrich
278
	return true;
279
}
280
281 c3cda38e jim-p
function ca_inter_create(& $ca, $keylen, $lifetime, $dn, $caref, $digest_alg = "sha256", $keytype = "RSA", $ecname = "prime256v1") {
282 95c8cf48 Evgeny Yurchenko
	// Create Intermediate Certificate Authority
283
	$signing_ca =& lookup_ca($caref);
284 1e0b1727 Phil Davis
	if (!$signing_ca) {
285 95c8cf48 Evgeny Yurchenko
		return false;
286 1e0b1727 Phil Davis
	}
287 95c8cf48 Evgeny Yurchenko
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 1e0b1727 Phil Davis
	if (!$signing_ca_res_crt || !$signing_ca_res_key) {
291
		return false;
292
	}
293 95c8cf48 Evgeny Yurchenko
	$signing_ca_serial = ++$signing_ca['serial'];
294
295
	$args = array(
296 87b4deb2 jim-p
		"x509_extensions" => "v3_ca",
297 ca621902 jim-p
		"digest_alg" => $digest_alg,
298 95c8cf48 Evgeny Yurchenko
		"encrypt_key" => false);
299 ff5bc49c Viktor Gurov
	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 95c8cf48 Evgeny Yurchenko
307
	// generate a new key pair
308
	$res_key = openssl_pkey_new($args);
309 1e0b1727 Phil Davis
	if (!$res_key) {
310
		return false;
311
	}
312 95c8cf48 Evgeny Yurchenko
313
	// generate a certificate signing request
314
	$res_csr = openssl_csr_new($dn, $res_key, $args);
315 1e0b1727 Phil Davis
	if (!$res_csr) {
316
		return false;
317
	}
318 95c8cf48 Evgeny Yurchenko
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 1e0b1727 Phil Davis
	if (!$res_crt) {
322
		return false;
323
	}
324 95c8cf48 Evgeny Yurchenko
325
	// export our certificate data
326
	if (!openssl_pkey_export($res_key, $str_key) ||
327 ae52d165 Renato Botelho
	    !openssl_x509_export($res_crt, $str_crt)) {
328 95c8cf48 Evgeny Yurchenko
		return false;
329 1e0b1727 Phil Davis
	}
330 95c8cf48 Evgeny Yurchenko
331
	// return our ca information
332
	$ca['crt'] = base64_encode($str_crt);
333
	$ca['prv'] = base64_encode($str_key);
334
	$ca['serial'] = 0;
335 dd76084d Matt Smith
	$ca['caref'] = $caref;
336 95c8cf48 Evgeny Yurchenko
337
	return true;
338
}
339
340 d43ad788 Scott Ullrich
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 1e0b1727 Phil Davis
348 d43ad788 Scott Ullrich
	// Find my issuer unless self-signed
349 1e0b1727 Phil Davis
	if ($issuer <> $subject) {
350 d43ad788 Scott Ullrich
		$issuer_crt =& lookup_ca_by_subject($issuer);
351 1e0b1727 Phil Davis
		if ($issuer_crt) {
352 d43ad788 Scott Ullrich
			$cert['caref'] = $issuer_crt['refid'];
353 1e0b1727 Phil Davis
		}
354 d43ad788 Scott Ullrich
	}
355
	return true;
356
}
357
358 c3cda38e jim-p
function cert_create(& $cert, $caref, $keylen, $lifetime, $dn, $type = "user", $digest_alg = "sha256", $keytype = "RSA", $ecname = "prime256v1") {
359 d43ad788 Scott Ullrich
360 7c4c77ee jim-p
	$cert['type'] = $type;
361 d43ad788 Scott Ullrich
362 7c4c77ee jim-p
	if ($type != "self-signed") {
363
		$cert['caref'] = $caref;
364
		$ca =& lookup_ca($caref);
365 1e0b1727 Phil Davis
		if (!$ca) {
366 7c4c77ee jim-p
			return false;
367 1e0b1727 Phil Davis
		}
368 7c4c77ee jim-p
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 1e0b1727 Phil Davis
		if (!$ca_res_key) {
374
			return false;
375
		}
376 2c9601c9 jim-p
377
		/* Get the next available CA serial number. */
378
		$ca_serial = ca_get_next_serial($ca);
379 7c4c77ee jim-p
	}
380 d43ad788 Scott Ullrich
381 0c82b8c2 jim-p
	$cert_type = cert_type_config_section($type);
382 7aaabd69 jim-p
383 3cb773da yarick123
	// 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 d43ad788 Scott Ullrich
	$args = array(
392 7aaabd69 jim-p
		"x509_extensions" => $cert_type,
393 ca621902 jim-p
		"digest_alg" => $digest_alg,
394 d43ad788 Scott Ullrich
		"encrypt_key" => false);
395 68690e0d Viktor Gurov
	if ($keytype == 'ECDSA') {
396 e0f8d364 Viktor Gurov
		$args["curve_name"] = $ecname;
397 68690e0d Viktor Gurov
		$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 d43ad788 Scott Ullrich
403
	// generate a new key pair
404 838e27bf jim-p
	$res_key = openssl_pkey_new($args);
405 1e0b1727 Phil Davis
	if (!$res_key) {
406
		return false;
407
	}
408 d43ad788 Scott Ullrich
409 7c4c77ee jim-p
	// 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 4aa7c7ae jim-p
		$ca_serial    = cert_get_random_serial();
415 7c4c77ee jim-p
		$cert['type'] = "server";
416
	}
417
418 d43ad788 Scott Ullrich
	// generate a certificate signing request
419
	$res_csr = openssl_csr_new($dn, $res_key, $args);
420 1e0b1727 Phil Davis
	if (!$res_csr) {
421
		return false;
422
	}
423 d43ad788 Scott Ullrich
424 7c4c77ee jim-p
	// sign the certificate using an internal CA
425 d43ad788 Scott Ullrich
	$res_crt = openssl_csr_sign($res_csr, $ca_res_crt, $ca_res_key, $lifetime,
426
				 $args, $ca_serial);
427 1e0b1727 Phil Davis
	if (!$res_crt) {
428
		return false;
429
	}
430 d43ad788 Scott Ullrich
431
	// export our certificate data
432 22b380aa Evgeny Yurchenko
	if (!openssl_pkey_export($res_key, $str_key) ||
433 ae52d165 Renato Botelho
	    !openssl_x509_export($res_crt, $str_crt)) {
434 22b380aa Evgeny Yurchenko
		return false;
435 1e0b1727 Phil Davis
	}
436 d43ad788 Scott Ullrich
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 c3cda38e jim-p
function csr_generate(& $cert, $keylen, $dn, $type = "user", $digest_alg = "sha256", $keytype = "RSA", $ecname = "prime256v1") {
445 282b6c66 jim-p
446 0c82b8c2 jim-p
	$cert_type = cert_type_config_section($type);
447 282b6c66 jim-p
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 d43ad788 Scott Ullrich
456
	$args = array(
457 282b6c66 jim-p
		"x509_extensions" => $cert_type,
458
		"req_extensions" => "req_{$cert_type}",
459 ca621902 jim-p
		"digest_alg" => $digest_alg,
460 d43ad788 Scott Ullrich
		"encrypt_key" => false);
461 68690e0d Viktor Gurov
	if ($keytype == 'ECDSA') {
462 e0f8d364 Viktor Gurov
		$args["curve_name"] = $ecname;
463 68690e0d Viktor Gurov
		$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 d43ad788 Scott Ullrich
469
	// generate a new key pair
470 838e27bf jim-p
	$res_key = openssl_pkey_new($args);
471 1e0b1727 Phil Davis
	if (!$res_key) {
472
		return false;
473
	}
474 d43ad788 Scott Ullrich
475
	// generate a certificate signing request
476
	$res_csr = openssl_csr_new($dn, $res_key, $args);
477 1e0b1727 Phil Davis
	if (!$res_csr) {
478
		return false;
479
	}
480 d43ad788 Scott Ullrich
481
	// export our request data
482 22b380aa Evgeny Yurchenko
	if (!openssl_pkey_export($res_key, $str_key) ||
483 ae52d165 Renato Botelho
	    !openssl_csr_export($res_csr, $str_csr)) {
484 22b380aa Evgeny Yurchenko
		return false;
485 1e0b1727 Phil Davis
	}
486 d43ad788 Scott Ullrich
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 de3f6463 Reid Linnemann
function csr_sign($csr, & $ca, $duration, $type, $altnames, $digest_alg = "sha256") {
495 0c82b8c2 jim-p
	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 2c9601c9 jim-p
506
	/* Get the next available CA serial number. */
507
	$ca_serial = ca_get_next_serial($ca);
508 0c82b8c2 jim-p
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 aec3a259 jim-p
		"digest_alg" => $digest_alg,
519 0c82b8c2 jim-p
		"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 d43ad788 Scott Ullrich
function csr_complete(& $cert, $str_crt) {
530 7fd7fbcf PiBa-NL
	$str_key = base64_decode($cert['prv']);
531
	cert_import($cert, $str_crt, $str_key);
532 d43ad788 Scott Ullrich
	unset($cert['csr']);
533
	return true;
534
}
535
536
function csr_get_subject($str_crt, $decode = true) {
537
538 1e0b1727 Phil Davis
	if ($decode) {
539 d43ad788 Scott Ullrich
		$str_crt = base64_decode($str_crt);
540 1e0b1727 Phil Davis
	}
541 d43ad788 Scott Ullrich
542
	$components = openssl_csr_get_subject($str_crt);
543
544 1e0b1727 Phil Davis
	if (empty($components) || !is_array($components)) {
545 d43ad788 Scott Ullrich
		return "unknown";
546 1e0b1727 Phil Davis
	}
547 d43ad788 Scott Ullrich
548 311f93cd Ermal
	ksort($components);
549 d43ad788 Scott Ullrich
	foreach ($components as $a => $v) {
550 1e0b1727 Phil Davis
		if (!strlen($subject)) {
551 d43ad788 Scott Ullrich
			$subject = "{$a}={$v}";
552 1e0b1727 Phil Davis
		} else {
553 d43ad788 Scott Ullrich
			$subject = "{$a}={$v}, {$subject}";
554 1e0b1727 Phil Davis
		}
555 d43ad788 Scott Ullrich
	}
556
557
	return $subject;
558
}
559
560
function cert_get_subject($str_crt, $decode = true) {
561
562 1e0b1727 Phil Davis
	if ($decode) {
563 d43ad788 Scott Ullrich
		$str_crt = base64_decode($str_crt);
564 1e0b1727 Phil Davis
	}
565 d43ad788 Scott Ullrich
566
	$inf_crt = openssl_x509_parse($str_crt);
567
	$components = $inf_crt['subject'];
568
569 1e0b1727 Phil Davis
	if (empty($components) || !is_array($components)) {
570 d43ad788 Scott Ullrich
		return "unknown";
571 1e0b1727 Phil Davis
	}
572 d43ad788 Scott Ullrich
573 b89c34aa Ermal
	ksort($components);
574 d43ad788 Scott Ullrich
	foreach ($components as $a => $v) {
575 b89c34aa Ermal
		if (is_array($v)) {
576
			ksort($v);
577 5479df47 jim-p
			foreach ($v as $w) {
578
				$asubject = "{$a}={$w}";
579
				$subject = (strlen($subject)) ? "{$asubject}, {$subject}" : $asubject;
580
			}
581 b89c34aa Ermal
		} else {
582 5479df47 jim-p
			$asubject = "{$a}={$v}";
583
			$subject = (strlen($subject)) ? "{$asubject}, {$subject}" : $asubject;
584
		}
585 d43ad788 Scott Ullrich
	}
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 e4d7a064 jim-p
595 1e0b1727 Phil Davis
	if (!is_array($components)) {
596 e4d7a064 jim-p
		return;
597 1e0b1727 Phil Davis
	}
598 e4d7a064 jim-p
599 d43ad788 Scott Ullrich
	$subject_array = array();
600
601 1e0b1727 Phil Davis
	foreach ($components as $a => $v) {
602 d43ad788 Scott Ullrich
		$subject_array[] = array('a' => $a, 'v' => $v);
603 1e0b1727 Phil Davis
	}
604 d43ad788 Scott Ullrich
605
	return $subject_array;
606
}
607
608 a84eb838 jim-p
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 4906f4ee jim-p
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 d43ad788 Scott Ullrich
function cert_get_issuer($str_crt, $decode = true) {
627
628 1e0b1727 Phil Davis
	if ($decode) {
629 d43ad788 Scott Ullrich
		$str_crt = base64_decode($str_crt);
630 1e0b1727 Phil Davis
	}
631 d43ad788 Scott Ullrich
632
	$inf_crt = openssl_x509_parse($str_crt);
633
	$components = $inf_crt['issuer'];
634 1e0b1727 Phil Davis
635
	if (empty($components) || !is_array($components)) {
636 d43ad788 Scott Ullrich
		return "unknown";
637 1e0b1727 Phil Davis
	}
638 5ca13f69 Ermal
639
	ksort($components);
640 d43ad788 Scott Ullrich
	foreach ($components as $a => $v) {
641 2a08b814 vsquared56
		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 d43ad788 Scott Ullrich
	}
652
653
	return $issuer;
654
}
655
656 1746c5ce PiBa-NL
/* Works for both RSA and ECC (crt) and key (prv) */
657
function cert_get_publickey($str_crt, $decode = true, $type = "crt") {
658 1e0b1727 Phil Davis
	if ($decode) {
659 9038f44c Steve Beaver
		$str_crt = base64_decode($str_crt);
660 1e0b1727 Phil Davis
	}
661 b6dcbd64 jim-p
	$certfn = tempnam('/tmp', 'CGPK');
662
	file_put_contents($certfn, $str_crt);
663 6d6ba660 PiBa-NL
	switch ($type) {
664
		case 'prv':
665 b6dcbd64 jim-p
			exec("/usr/bin/openssl pkey -in {$certfn} -pubout", $out);
666 6d6ba660 PiBa-NL
			break;
667
		case 'crt':
668 b6dcbd64 jim-p
			exec("/usr/bin/openssl x509 -in {$certfn} -inform pem -noout -pubkey", $out);
669 6d6ba660 PiBa-NL
			break;
670
		case 'csr':
671 b6dcbd64 jim-p
			exec("/usr/bin/openssl req -in {$certfn} -inform pem -noout -pubkey", $out);
672 6d6ba660 PiBa-NL
			break;
673
		default:
674
			$out = array();
675
			break;
676 1746c5ce PiBa-NL
	}
677 b6dcbd64 jim-p
	unlink($certfn);
678 6d6ba660 PiBa-NL
	return implode("\n", $out);
679 a828210b yakatz
}
680 1379d66f jim-p
681
function cert_get_purpose($str_crt, $decode = true) {
682 4906f4ee jim-p
	$extended_oids = array(
683
		"1.3.6.1.5.5.8.2.2" => "IP Security IKE Intermediate",
684
	);
685 1e0b1727 Phil Davis
	if ($decode) {
686 1379d66f jim-p
		$str_crt = base64_decode($str_crt);
687 1e0b1727 Phil Davis
	}
688 1379d66f jim-p
	$crt_details = openssl_x509_parse($str_crt);
689
	$purpose = array();
690 4906f4ee jim-p
	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 1379d66f jim-p
	$purpose['ca'] = (stristr($crt_details['extensions']['basicConstraints'], 'CA:TRUE') === false) ? 'No': 'Yes';
713 4906f4ee jim-p
	$purpose['server'] = (in_array('TLS Web Server Authentication', $purpose['eku'])) ? 'Yes': 'No';
714
715 1379d66f jim-p
	return $purpose;
716
}
717
718 37e1aecf jim-p
function cert_get_ocspstaple($str_crt, $decode = true) {
719 00e54150 jim-p
	if ($decode) {
720
		$str_crt = base64_decode($str_crt);
721
	}
722
	$crt_details = openssl_x509_parse($str_crt);
723 0d82f93b jim-p
	if (($crt_details['extensions']['tlsfeature'] == "status_request") ||
724
	    !empty($crt_details['extensions']['1.3.6.1.5.5.7.1.24'])) {
725 00e54150 jim-p
		return true;
726
	}
727
	return false;
728
}
729
730 1120b85c jim-p
function cert_format_date($validTS, $validTS_time_t, $outputstring = true) {
731 b8b33a3e jim-p
	$now = new DateTime("now");
732 1120b85c jim-p
733
	/* Try to create a DateTime object from the full time string */
734
	$date = DateTime::createFromFormat('ymdHis', rtrim($validTS, 'Z'), new DateTimeZone('Z'));
735 bdaa35dc jim-p
	/* 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 1120b85c jim-p
	/* If that failed, try to create it from the UNIX timestamp */
740 29804b9e jim-p
	if (($date === false) && (!empty($validTS_time_t))) {
741 1120b85c jim-p
		$date = new DateTime('@' . $validTS_time_t, new DateTimeZone('Z'));
742 1e0b1727 Phil Davis
	}
743 1120b85c jim-p
	/* If we have a valid DateTime object, format it in a nice way */
744
	if ($date !== false) {
745
		$date->setTimezone($now->getTimeZone());
746 b8b33a3e jim-p
		if ($outputstring) {
747 1120b85c jim-p
			$date = $date->format(DateTimeInterface::RFC2822);
748 3fec2470 jim-p
		}
749 1e0b1727 Phil Davis
	}
750 1120b85c jim-p
	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 2b333210 jim-p
	return array($start, $end);
763
}
764
765 04761344 jim-p
function cert_get_serial($str_crt, $decode = true) {
766 1e0b1727 Phil Davis
	if ($decode) {
767 04761344 jim-p
		$str_crt = base64_decode($str_crt);
768 1e0b1727 Phil Davis
	}
769 04761344 jim-p
	$crt_details = openssl_x509_parse($str_crt);
770 03a84081 jim-p
	if (isset($crt_details['serialNumber'])) {
771 04761344 jim-p
		return $crt_details['serialNumber'];
772 1e0b1727 Phil Davis
	} else {
773 04761344 jim-p
		return NULL;
774 1e0b1727 Phil Davis
	}
775 04761344 jim-p
}
776
777 aec3a259 jim-p
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 e2c718c8 jim-p
function is_openvpn_server_ca($caref) {
798 25ab4237 Christian McDonald
	foreach(config_get_path('openvpn/openvpn-server', []) as $opvns) {
799 e2c718c8 jim-p
		if ($ovpns['caref'] == $caref) {
800
			return true;
801
		}
802
	}
803
	return false;
804
}
805
806
function is_openvpn_client_ca($caref) {
807 25ab4237 Christian McDonald
	foreach(config_get_path('openvpn/openvpn-client', []) as $ovpnc) {
808 e2c718c8 jim-p
		if ($ovpnc['caref'] == $caref) {
809
			return true;
810
		}
811
	}
812
	return false;
813
}
814
815
function is_ipsec_peer_ca($caref) {
816 843ee1ac jim-p
	foreach (config_get_path('ipsec/phase1', []) as $ipsec) {
817 e2c718c8 jim-p
		if ($ipsec['caref'] == $caref) {
818
			return true;
819
		}
820
	}
821
	return false;
822
}
823
824
function is_ldap_peer_ca($caref) {
825 843ee1ac jim-p
	foreach (config_get_path('system/authserver', []) as $authserver) {
826 d74bd052 Viktor G
		if (($authserver['ldap_caref'] == $caref) &&
827
		    ($authserver['ldap_urltype'] != 'Standard TCP')) {
828 e2c718c8 jim-p
			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 dea98903 jim-p
function is_user_cert($certref) {
842 843ee1ac jim-p
	foreach (config_get_path('system/user', []) as $user) {
843 1e0b1727 Phil Davis
		if (!is_array($user['cert'])) {
844 dab2e769 jim-p
			continue;
845 1e0b1727 Phil Davis
		}
846 dab2e769 jim-p
		foreach ($user['cert'] as $cert) {
847 1e0b1727 Phil Davis
			if ($certref == $cert) {
848 dea98903 jim-p
				return true;
849 1e0b1727 Phil Davis
			}
850 dab2e769 jim-p
		}
851
	}
852 dea98903 jim-p
	return false;
853 dab2e769 jim-p
}
854
855 dea98903 jim-p
function is_openvpn_server_cert($certref) {
856 35bf4437 Christian McDonald
	foreach (config_get_path('openvpn/openvpn-server', []) as $ovpns) {
857 1e0b1727 Phil Davis
		if ($ovpns['certref'] == $certref) {
858 dea98903 jim-p
			return true;
859 1e0b1727 Phil Davis
		}
860 dea98903 jim-p
	}
861
	return false;
862
}
863
864
function is_openvpn_client_cert($certref) {
865 35bf4437 Christian McDonald
	foreach (config_get_path('openvpn/openvpn-client', []) as $ovpnc) {
866 1e0b1727 Phil Davis
		if ($ovpnc['certref'] == $certref) {
867 dea98903 jim-p
			return true;
868 1e0b1727 Phil Davis
		}
869 dea98903 jim-p
	}
870
	return false;
871
}
872
873
function is_ipsec_cert($certref) {
874 35bf4437 Christian McDonald
	foreach(config_get_path('ipsec/phase1', []) as $ipsec) {
875 1e0b1727 Phil Davis
		if ($ipsec['certref'] == $certref) {
876 dea98903 jim-p
			return true;
877 1e0b1727 Phil Davis
		}
878 dea98903 jim-p
	}
879
	return false;
880
}
881
882
function is_webgui_cert($certref) {
883 843ee1ac jim-p
	if ((config_get_path('system/webgui/ssl-certref') == $certref) &&
884
	    (config_get_path('system/webgui/protocol') != "http")) {
885 dea98903 jim-p
		return true;
886 1e0b1727 Phil Davis
	}
887 dea98903 jim-p
}
888
889 29e6a815 Renato Botelho
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 36f6ed35 bcyrill
function is_captiveportal_cert($certref) {
906 35bf4437 Christian McDonald
	foreach (config_get_path('captiveportal', []) as $portal) {
907 1e0b1727 Phil Davis
		if (isset($portal['enable']) && isset($portal['httpslogin']) && ($portal['certref'] == $certref)) {
908 36f6ed35 bcyrill
			return true;
909 1e0b1727 Phil Davis
		}
910 36f6ed35 bcyrill
	}
911
	return false;
912
}
913
914 39d83c73 Viktor G
function is_unbound_cert($certref) {
915 843ee1ac jim-p
	if (config_path_enabled('unbound') &&
916
	    config_path_enabled('unbound','enablessl') &&
917
	    (config_get_path('unbound/sslcertref') == $certref)) {
918 39d83c73 Viktor G
		return true;
919
	}
920
}
921
922 dea98903 jim-p
function cert_in_use($certref) {
923 29e6a815 Renato Botelho
924 dea98903 jim-p
	return (is_webgui_cert($certref) ||
925
		is_user_cert($certref) ||
926
		is_openvpn_server_cert($certref) ||
927
		is_openvpn_client_cert($certref) ||
928 36f6ed35 bcyrill
		is_ipsec_cert($certref) ||
929 29e6a815 Renato Botelho
		is_captiveportal_cert($certref) ||
930 39d83c73 Viktor G
		is_unbound_cert($certref) ||
931 29e6a815 Renato Botelho
		is_package_cert($certref));
932 dab2e769 jim-p
}
933
934 3bde5cdd PiBa-NL
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 9aa8f6a8 jim-p
/* Detect a rollover at 2038 on some platforms (e.g. ARM)
956
 * See: https://redmine.pfsense.org/issues/9098 */
957 3a877e4a jim-p
function cert_get_max_lifetime() {
958
	global $cert_max_lifetime;
959
	$max = $cert_max_lifetime;
960
961 9aa8f6a8 jim-p
	$current_time = time();
962
	while ((int)($current_time + ($max * 24 * 60 * 60)) < 0) {
963
		$max--;
964
	}
965 3a877e4a jim-p
	return min($max, $cert_max_lifetime);
966 9aa8f6a8 jim-p
}
967
968 a3c15890 jim-p
/* 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 9aa8f6a8 jim-p
function crl_create(& $crl, $caref, $name, $serial = 0, $lifetime = 3650) {
991 c5f010aa jim-p
	global $config;
992 a3c15890 jim-p
	$max_lifetime = crl_get_max_lifetime();
993 c5f010aa jim-p
	$ca =& lookup_ca($caref);
994 1e0b1727 Phil Davis
	if (!$ca) {
995 c5f010aa jim-p
		return false;
996 1e0b1727 Phil Davis
	}
997 f2a86ca9 jim-p
	$crl['descr'] = $name;
998 c5f010aa jim-p
	$crl['caref'] = $caref;
999
	$crl['serial'] = $serial;
1000 9aa8f6a8 jim-p
	$crl['lifetime'] = ($lifetime > $max_lifetime) ? $max_lifetime : $lifetime;
1001 c5f010aa jim-p
	$crl['cert'] = array();
1002 843ee1ac jim-p
1003
	$crls = config_get_path('crl', []);
1004
	$crls[] = $crl;
1005
	config_set_path('crl', $crls);
1006 981d6364 jim-p
	return $crl;
1007 c5f010aa jim-p
}
1008
1009
function crl_update(& $crl) {
1010 981d6364 jim-p
	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 c5f010aa jim-p
	global $config;
1032 a3c15890 jim-p
	$max_lifetime = crl_get_max_lifetime();
1033 c5f010aa jim-p
	$ca =& lookup_ca($crl['caref']);
1034 1e0b1727 Phil Davis
	if (!$ca) {
1035 c5f010aa jim-p
		return false;
1036 1e0b1727 Phil Davis
	}
1037 7b757d1b jim-p
	// If we have text but no certs, it was imported and cannot be updated.
1038 1e0b1727 Phil Davis
	if (($crl["method"] != "internal") && (!empty($crl['text']) && empty($crl['cert']))) {
1039 7b757d1b jim-p
		return false;
1040 1e0b1727 Phil Davis
	}
1041 c5f010aa jim-p
	$crl['serial']++;
1042 981d6364 jim-p
	$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 9aa8f6a8 jim-p
		'days' => ($crl['lifetime'] > $max_lifetime) ? $max_lifetime : $crl['lifetime'],
1049 981d6364 jim-p
		'alg' => OPENSSL_ALGO_SHA1,
1050
		'revoked' => array()
1051
	);
1052
1053 4bc2c676 jim-p
	if (is_array($crl['cert']) && (count($crl['cert']) > 0)) {
1054
		foreach ($crl['cert'] as $cert) {
1055 63fb68d7 jim-p
			/* 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 981d6364 jim-p
			$crlconf['revoked'][] = array(
1064 63fb68d7 jim-p
				'serial' => $serial,
1065
				'rev_date' => $cert['revoke_time'],
1066
				'reason' => ($cert['reason'] == -1) ? null : (int) $cert['reason'],
1067 981d6364 jim-p
			);
1068 4bc2c676 jim-p
		}
1069 c5f010aa jim-p
	}
1070 981d6364 jim-p
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 c5f010aa jim-p
}
1076
1077 6c07db48 Phil Davis
function cert_revoke($cert, & $crl, $reason = OCSP_REVOKED_STATUS_UNSPECIFIED) {
1078 c5f010aa jim-p
	global $config;
1079 1e0b1727 Phil Davis
	if (is_cert_revoked($cert, $crl['refid'])) {
1080 c5f010aa jim-p
		return true;
1081 1e0b1727 Phil Davis
	}
1082 7b757d1b jim-p
	// If we have text but no certs, it was imported and cannot be updated.
1083 1e0b1727 Phil Davis
	if (!is_crl_internal($crl)) {
1084 7b757d1b jim-p
		return false;
1085 1e0b1727 Phil Davis
	}
1086 63fb68d7 jim-p
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 c5f010aa jim-p
	crl_update($crl);
1102 fb3f1993 jim-p
	return true;
1103 c5f010aa jim-p
}
1104
1105
function cert_unrevoke($cert, & $crl) {
1106
	global $config;
1107 1e0b1727 Phil Davis
	if (!is_crl_internal($crl)) {
1108 7b757d1b jim-p
		return false;
1109 1e0b1727 Phil Davis
	}
1110 63fb68d7 jim-p
1111
	$serial = crl_get_entry_serial($cert);
1112
1113 c5f010aa jim-p
	foreach ($crl['cert'] as $id => $rcert) {
1114 63fb68d7 jim-p
		/* 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 c5f010aa jim-p
			unset($crl['cert'][$id]);
1119 728003c8 jim-p
			if (count($crl['cert']) == 0) {
1120
				// Protect against accidentally switching the type to imported, for older CRLs
1121 1e0b1727 Phil Davis
				if (!isset($crl['method'])) {
1122 728003c8 jim-p
					$crl['method'] = "internal";
1123 1e0b1727 Phil Davis
				}
1124 728003c8 jim-p
				crl_update($crl);
1125 1e0b1727 Phil Davis
			} else {
1126 a59831e7 jim-p
				crl_update($crl);
1127 1e0b1727 Phil Davis
			}
1128 c5f010aa jim-p
			return true;
1129
		}
1130
	}
1131
	return false;
1132
}
1133
1134 04761344 jim-p
/* 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 ae52d165 Renato Botelho
	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 1746c5ce PiBa-NL
	    (cert_get_publickey($c1, false) == cert_get_publickey($c2, false))) {
1146 04761344 jim-p
		return true;
1147 1e0b1727 Phil Davis
	}
1148 04761344 jim-p
	return false;
1149
}
1150
1151 63fb68d7 jim-p
/****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 a6bd9e78 jim-p
	} elseif (cert_validate_serial($entry, false, true) != null) {
1170 63fb68d7 jim-p
		/* 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 a6bd9e78 jim-p
function cert_validate_serial($serial, $returnvalue = false, $allowlarge = false) {
1192 63fb68d7 jim-p
	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 a6bd9e78 jim-p
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 63fb68d7 jim-p
		/* 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 4864d7f6 Josh Soref
		/* No matter what the error is, return null or false depending
1226 63fb68d7 jim-p
		 * on what was requested. */
1227
		return ($returnvalue) ? null : false;
1228
	}
1229
}
1230
1231 2c9601c9 jim-p
/****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 4aa7c7ae jim-p
	/* Check certs first -- more likely to find a hit */
1266 2c9601c9 jim-p
	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 4864d7f6 Josh Soref
	/* Check the CA itself */
1277 4aa7c7ae jim-p
	$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 2c9601c9 jim-p
	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 4aa7c7ae jim-p
function cert_get_random_serial($caref = '') {
1307 2c9601c9 jim-p
	/* 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 63fb68d7 jim-p
/****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 84041dcf jim-p
	$crlissuer = is_array($cert) ? cert_get_subject($ca['crt']) : null;
1375 63fb68d7 jim-p
	$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 fb3f1993 jim-p
function is_cert_revoked($cert, $crlref = "") {
1410 c5f010aa jim-p
	global $config;
1411 1e0b1727 Phil Davis
	if (!is_array($config['crl'])) {
1412 c5f010aa jim-p
		return false;
1413 1e0b1727 Phil Davis
	}
1414 c5f010aa jim-p
1415 fb3f1993 jim-p
	if (!empty($crlref)) {
1416 28ff7ace jim-p
		$crl = lookup_crl($crlref);
1417 63fb68d7 jim-p
		return crl_contains_cert($crl, $cert);
1418 fb3f1993 jim-p
	} else {
1419 63fb68d7 jim-p
		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 fb3f1993 jim-p
		foreach ($config['crl'] as $crl) {
1428 1e0b1727 Phil Davis
			if (!is_array($crl['cert'])) {
1429 fb3f1993 jim-p
				continue;
1430 1e0b1727 Phil Davis
			}
1431 63fb68d7 jim-p
			if (crl_contains_cert($crl, $cert)) {
1432
				return true;
1433 fb3f1993 jim-p
			}
1434
		}
1435
	}
1436
	return false;
1437
}
1438
1439
function is_openvpn_server_crl($crlref) {
1440 35bf4437 Christian McDonald
	foreach (config_get_path('openvpn/openvpn-server', []) as $ovpns) {
1441 1e0b1727 Phil Davis
		if (!empty($ovpns['crlref']) && ($ovpns['crlref'] == $crlref)) {
1442 fb3f1993 jim-p
			return true;
1443 1e0b1727 Phil Davis
		}
1444 c5f010aa jim-p
	}
1445
	return false;
1446
}
1447
1448 1b1723da Mark Silinio
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 cfec2190 Mark Silinio
		    (count($package['certificatelist'][$crlref]) > 0)) {
1459 1b1723da Mark Silinio
			return true;
1460
		}
1461
	}
1462
}
1463
1464 fb3f1993 jim-p
// Keep this general to allow for future expansion. See cert_in_use() above.
1465
function crl_in_use($crlref) {
1466 1b1723da Mark Silinio
	return (is_openvpn_server_crl($crlref) ||
1467
		is_package_crl($crlref));
1468 fb3f1993 jim-p
}
1469
1470
function is_crl_internal($crl) {
1471 728003c8 jim-p
	return (!(!empty($crl['text']) && empty($crl['cert'])) || ($crl["method"] == "internal"));
1472 fb3f1993 jim-p
}
1473
1474 b34b2b7d jim-p
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 1e0b1727 Phil Davis
		if (!(is_array($cert) && !empty($cert['crt']))) {
1480 b34b2b7d jim-p
			return "";
1481 1e0b1727 Phil Davis
		}
1482 b34b2b7d jim-p
		$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 1e0b1727 Phil Davis
			if (strtoupper($s['a']) == "CN") {
1490 b34b2b7d jim-p
				return $s['v'];
1491 1e0b1727 Phil Davis
			}
1492 b34b2b7d jim-p
		}
1493
	}
1494
	return "";
1495
}
1496
1497 83d2b83a jim-p
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 2e1809dd jim-p
function cert_add_altname_type($str) {
1514
	$type = "";
1515
	if (is_ipaddr($str)) {
1516
		$type = "IP";
1517 70b70f9d jim-p
	} elseif (is_hostname($str, true)) {
1518 2e1809dd jim-p
		$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 e562fca2 jim-p
		return null;
1528 2e1809dd jim-p
	}
1529
}
1530
1531 0c82b8c2 jim-p
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 9e80dd44 jim-p
/****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 f944f4a7 jim-p
	'max_server_cert_lifetime' => 398,
1585 9e80dd44 jim-p
	'digest_blacklist' => array('md4', 'RSA-MD4',  'md5', 'RSA-MD5', 'md5-sha1',
1586
					'mdc2', 'RSA-MDC2', 'sha1', 'RSA-SHA1',
1587 9484a1cb jim-p
					'RSA-SHA1-2', 'sha224', 'RSA-SHA224'),
1588 9e80dd44 jim-p
	'min_private_key_bits' => 2048,
1589 941470ef Viktor G
	'ec_curve' => 'prime256v1',
1590 9e80dd44 jim-p
);
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 ab7ad5f9 jim-p
function cert_renew(& $cert, $reusekey = true, $strictsecurity = false, $reuseserial = false) {
1612 009a3d4e jim-p
	global $cert_strict_values, $cert_curve_compatible, $curve_compatible_list;
1613 9e80dd44 jim-p
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 009a3d4e jim-p
	$purpose = cert_get_purpose($cert['crt']);
1631 9e80dd44 jim-p
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 03a84081 jim-p
	$selfsigned = ($subj == $issuer);
1659 9e80dd44 jim-p
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 009a3d4e jim-p
		/* Automatically determine certificate type if unset based on purpose value */
1666
		$cert['type'] = ($purpose['server'] == 'Yes') ? 'server' : 'user';
1667 9e80dd44 jim-p
	}
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 009a3d4e jim-p
	if (($cert_type == 'server') && $strictsecurity &&
1677 9e80dd44 jim-p
	    ($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 3fdd559e Viktor Gurov
	$sans = str_replace("IP Address", "IP", $cert_details['extensions']['subjectAltName']);
1683 9e80dd44 jim-p
	if (empty($sans)) {
1684
		$sans = cert_add_altname_type($dn['commonName']);
1685
	}
1686
1687 09d3fe62 jim-p
	/* 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 9e80dd44 jim-p
1698 9484a1cb jim-p
	/* 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 9e80dd44 jim-p
	/* 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 941470ef Viktor G
	/* If we are enforcing strict security, then ensure the private key size
1730 00d9ce91 Viktor G
	 * is at least 2048 bits or NIST P-256 elliptic curve*/
1731 941470ef Viktor G
	$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 7ee29634 Viktor Gurov
		} else if (!in_array($key_details['ec']['curve_name'], $curve_compatible_list)) {
1738 941470ef Viktor G
			$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 9e80dd44 jim-p
		$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 2c9601c9 jim-p
		/* Get the next available CA serial number. */
1787
		$ca_serial = ca_get_next_serial($ca);
1788 9e80dd44 jim-p
	} 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 4aa7c7ae jim-p
		/* Use random serial from this CA/Self-Signed Cert */
1795
		$ca_serial    = cert_get_random_serial($cert['refid']);
1796 9e80dd44 jim-p
	}
1797
1798 ab7ad5f9 jim-p
	/* Did the user choose to keep the serial? */
1799
	$ca_serial = ($reuseserial) ? cert_get_serial($cert['crt']) : $ca_serial;
1800
1801 9e80dd44 jim-p
	/* 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 03a84081 jim-p
/****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 843ee1ac jim-p
	foreach (array('server', 'client') as $mode) {
1848
		foreach (config_get_path("openvpn/openvpn-{$mode}", []) as $ovpn) {
1849 03a84081 jim-p
			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 843ee1ac jim-p
1866
	foreach (config_get_path('ipsec/phase1', []) as $ipsec) {
1867 d1f5587d jim-p
		if (($ipsec['authentication_method'] == 'cert') &&
1868 03a84081 jim-p
		    ($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 843ee1ac jim-p
	foreach (config_get_path('captiveportal', []) as $zone => $portal) {
1878 03a84081 jim-p
		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 843ee1ac jim-p
	foreach (array('server', 'client') as $mode) {
1926
		foreach (config_get_path("openvpn/openvpn-{$mode}", []) as $ovpn) {
1927 03a84081 jim-p
			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 843ee1ac jim-p
1939
	foreach (config_get_path('ipsec/phase1', []) as $ipsec) {
1940 03a84081 jim-p
		if ($ipsec['certref'] == $refid) {
1941
			break;
1942
		}
1943
	}
1944 843ee1ac jim-p
1945
	foreach (config_get_path('ipsec/phase1', []) as $ipsec) {
1946 d1f5587d jim-p
		if (($ipsec['authentication_method'] == 'cert') &&
1947 03a84081 jim-p
		    ($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 843ee1ac jim-p
	foreach (config_get_path('cert', []) as $cert) {
1955 03a84081 jim-p
		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 843ee1ac jim-p
	foreach (config_get_path('ca', []) as $cert) {
1962 03a84081 jim-p
		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 81423583 jim-p
	require_once("service-utils.inc");
1983 03a84081 jim-p
	/* 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 b6196922 jim-p
2027 93f1121f jim-p
/****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 b6196922 jim-p
	if (!is_array($cert) || empty($cert['crt'])) {
2041
		return null;
2042
	}
2043 93f1121f jim-p
	/* Read certificate details */
2044 b8b33a3e jim-p
	list($startdate, $enddate) = cert_get_dates($cert['crt'], true, false);
2045
2046 1120b85c jim-p
	/* 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 93f1121f jim-p
	/* Determine which start time to use (now, or cert start) */
2052 b8b33a3e jim-p
	$startdate = ($untilexpire) ? new DateTime("now") : $startdate;
2053
2054
	/* Calculate the requested intervals */
2055
	$interval = $startdate->diff($enddate);
2056
2057 1120b85c jim-p
	/* DateTime diff is always positive, check if we need to negate the result. */
2058
	return ($startdate > $enddate) ? -1 * $interval->days : $interval->days;
2059 93f1121f jim-p
}
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 843ee1ac jim-p
	global $g;
2074 38e7b336 jim-p
	/* Number of days at which to warn of expiration. */
2075 2568e151 Christian McDonald
	$warning_days = config_get_path('notifications/certexpire/expiredays', g_get('default_cert_expiredays'));
2076 93f1121f jim-p
2077
	if ($expiredays > $warning_days) {
2078
		/* Not expiring soon */
2079
		$lrclass = 'normal';
2080
		$expstring = gettext("%d %s until expiration");
2081 a86ab279 ilmarranen
	} elseif ($expiredays >= 0) {
2082 93f1121f jim-p
		/* 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 46bd32bb Steve Beaver
function cert_print_dates($cert) {
2106 93f1121f jim-p
	/* 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 46bd32bb Steve Beaver
	?>
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 88774881 Christian McDonald
<?php
2133 b6196922 jim-p
}
2134
2135 93f1121f jim-p
/****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 b6196922 jim-p
function cert_print_infoblock($cert) {
2146 93f1121f jim-p
	/* If the certificate is not valid, bail. */
2147 b6196922 jim-p
	if (!is_array($cert) || empty($cert['crt'])) {
2148
		return null;
2149
	}
2150 93f1121f jim-p
	/* Variable to hold the formatted information */
2151 b6196922 jim-p
	$certextinfo = "";
2152 93f1121f jim-p
2153
	/* Serial number */
2154 b6196922 jim-p
	$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 93f1121f jim-p
2161
	/* Digest type */
2162 b6196922 jim-p
	$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 93f1121f jim-p
2169
	/* Subject Alternative Name (SAN) list */
2170 b6196922 jim-p
	$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 93f1121f jim-p
2177
	/* Key usage */
2178 b6196922 jim-p
	$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 93f1121f jim-p
2185
	/* Extended key usage */
2186 b6196922 jim-p
	if (is_array($purpose) && !empty($purpose['eku'])) {
2187
		$certextinfo .= '<b>' . gettext("EKU: ") . '</b> ';
2188
		$certextinfo .= htmlspecialchars(implode(', ', $purpose['eku']));
2189
		$certextinfo .= '<br/>';
2190
	}
2191 93f1121f jim-p
2192
	/* OCSP / Must Staple */
2193 b6196922 jim-p
	if (cert_get_ocspstaple($cert['crt'])) {
2194
		$certextinfo .= '<b>' . gettext("OCSP: ") . '</b> ';
2195
		$certextinfo .= gettext("Must Staple");
2196 b0790fc0 jim-p
		$certextinfo .= '<br/>';
2197 b6196922 jim-p
	}
2198 93f1121f jim-p
2199
	/* Private key information */
2200 b6196922 jim-p
	if (!empty($cert['prv'])) {
2201
		$res_key = openssl_pkey_get_private(base64_decode($cert['prv']));
2202
		$certextinfo .= '<b>' . gettext("Key Type: ") . '</b> ';
2203 55dc0070 jim-p
		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 9dfd57c0 jim-p
			}
2220 55dc0070 jim-p
		} else {
2221
			$certextinfo .= '<i>' . gettext("Unknown (Key could not be parsed)") . '</i><br/>';
2222 b6196922 jim-p
		}
2223
	}
2224 93f1121f jim-p
2225
	/* Distinguished name (DN) */
2226 b6196922 jim-p
	if (!empty($cert_details['name'])) {
2227
		$certextinfo .= '<b>' . gettext("DN: ") . '</b> ';
2228 8abff49b Viktor G
		/* 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 b6196922 jim-p
		$certextinfo .= '<br/>';
2232
	}
2233 93f1121f jim-p
2234
	/* Hash value */
2235 b6196922 jim-p
	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 93f1121f jim-p
2241
	/* Subject Key Identifier (SKID) */
2242 b6196922 jim-p
	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 93f1121f jim-p
2248
	/* Authority Key Identifier (AKID) */
2249 b6196922 jim-p
	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 93f1121f jim-p
2255
	/* Total Lifetime (days from cert start to end) */
2256
	$lifetime = cert_get_lifetime($cert);
2257 29804b9e jim-p
	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 93f1121f jim-p
2262 29804b9e jim-p
		/* 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 93f1121f jim-p
2270 94ce250e jim-p
	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 7daab3d8 jim-p
2284 46bd32bb Steve Beaver
	/* Output the infoblock */
2285
	if (!empty($certextinfo)) { ?>
2286
		<div class="infoblock">
2287
		<? print_info_box($certextinfo, 'info', false); ?>
2288
		</div>
2289
	<?php
2290
	}
2291 b6196922 jim-p
}
2292
2293 b5d2d8d8 jim-p
/****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 843ee1ac jim-p
	if (config_get_path('notifications/certexpire/enable') == "disabled") {
2308 b5d2d8d8 jim-p
		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 b8b33a3e jim-p
	$all_certs = array_merge_recursive($config['ca'], $config['cert']);
2317 b5d2d8d8 jim-p
2318
	foreach ($all_certs as $cert) {
2319 b8b33a3e jim-p
		if (empty($cert)) {
2320
			continue;
2321
		}
2322 4ed695f2 ilmarranen
		/* Proceed only for not revoked certificate if ignore setting enabled */
2323 843ee1ac jim-p
		if ((config_get_path('notifications/certexpire/ignore_revoked') == "enabled") &&
2324
		    is_cert_revoked($cert)) {
2325 226cb195 ilmarranen
			continue;
2326
		}
2327 b5d2d8d8 jim-p
		/* 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 a86ab279 ilmarranen
		if ($expiredays === null) {
2331 b5d2d8d8 jim-p
			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 7daab3d8 jim-p
/****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 843ee1ac jim-p
	foreach (config_get_path('ca', []) as $ca) {
2368 7daab3d8 jim-p
		/* 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 348c2af1 jim-p
		ca_setup_capath($ca, $trust_store_directory);
2377 7daab3d8 jim-p
	}
2378
}
2379
2380 348c2af1 jim-p
/****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 475d712b jim-p
 *   $refresh: Refresh CRLs -- When true, perform no cleanup and increment suffix
2390 348c2af1 jim-p
 * 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 475d712b jim-p
function ca_setup_capath($ca, $basedir, $crl = "", $refresh = false) {
2397 348c2af1 jim-p
	/* 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 475d712b jim-p
	/* 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 348c2af1 jim-p
	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 475d712b jim-p
		$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 348c2af1 jim-p
		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 cffcf9bf jim-p
/****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 9dfd57c0 jim-p
		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 cffcf9bf jim-p
		}
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 7ee29634 Viktor Gurov
global $cert_curve_compatible, $curve_compatible_list;
2493 cffcf9bf jim-p
$cert_curve_compatible = array(
2494 bc3e78ab jim-p
	/* 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 ca3cddbe jim-p
	/* OpenVPN bug limits usable curves, see https://redmine.pfsense.org/issues/9744 */
2499
	'OpenVPN' => array('prime256v1', 'secp384r1', 'secp521r1'),
2500 cffcf9bf jim-p
);
2501 22c89db3 Reid Linnemann
$curve_compatible_list = array_unique(call_user_func_array('array_merge', array_values($cert_curve_compatible)));
2502 cffcf9bf jim-p
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 59fac81f jim-p
 *   $selectsource: Then true, outputs in a format usable by select_source in
2574
 *                  packages.
2575 3c1249b3 jim-p
 *   $addnone: When true, a 'none' choice is added to the list.
2576 cffcf9bf jim-p
 * 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 3c1249b3 jim-p
function cert_build_list($type = 'cert', $consumer = '', $selectsource = false, $addnone = false) {
2584 cffcf9bf jim-p
	/* Ensure that $type is valid */
2585
	if (!in_array($type, array('ca', 'cert'))) {
2586
		return array();
2587
	}
2588
2589
	$list = array();
2590 3c1249b3 jim-p
	if ($addnone) {
2591
		if ($selectsource) {
2592
			$list[] = array('refid' => 'none', 'descr' => 'None');
2593
		} else {
2594
			$list['none'] = "None";
2595
		}
2596
	}
2597
2598 cffcf9bf jim-p
	/* Create a hashed array with the certificate refid as the key and
2599
	 * descriptive name as the value. Exclude incompatible certificates. */
2600 843ee1ac jim-p
	foreach (config_get_path($type, []) as $cert) {
2601 4e8cb2fc Viktor Gurov
		if (empty($cert['prv']) && ($type == 'cert')) {
2602 e43c71ce Viktor Gurov
			continue;
2603
		} else if (cert_check_pkey_compatibility($cert['prv'], $consumer)) {
2604 59fac81f jim-p
			if ($selectsource) {
2605
				$list[] = array('refid' => $cert['refid'],
2606
						'descr' => $cert['descr']);
2607
			} else {
2608
				$list[$cert['refid']] = $cert['descr'];
2609
			}
2610 cffcf9bf jim-p
		}
2611
	}
2612
2613
	return $list;
2614
}
2615
2616 9efec277 Jim Pingle
/****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 a7e50981 jim-p
 *   $encryption: Strength of encryption to use:
2623
 *                "high" (AES-256 + SHA256)
2624
 *                "low" (3DES + SHA1)
2625
 *                "legacy" (RC2-40 + SHA1)
2626 9efec277 Jim Pingle
 *   $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 a7e50981 jim-p
function cert_pkcs12_export($cert, $encryption = 'high', $passphrase = '', $add_ca = true, $delivery = 'download') {
2648 9efec277 Jim Pingle
	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 a7e50981 jim-p
	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 9efec277 Jim Pingle
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 b89c34aa Ermal
?>