Project

General

Profile

Download (88.3 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 a68f7a3d Luiz Otavio O Souza
 * Copyright (c) 2014-2024 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 26308930 Marcos Mendoza
/**
68
 * @return array Contains the CA config index and item.
69
 */
70
function lookup_ca($refid) {
71
	$ca_item = ['idx' => null, 'item' => null];
72
	foreach (config_get_path('ca', []) as $idx => $ca) {
73
		if (empty($ca)) {
74
			continue;
75
		}
76
		if ($ca['refid'] == $refid) {
77
			$ca_item = ['idx' => $idx, 'item' => $ca];
78
			break;
79 1e0b1727 Phil Davis
		}
80
	}
81 d43ad788 Scott Ullrich
82 26308930 Marcos Mendoza
	return $ca_item;
83 d43ad788 Scott Ullrich
}
84
85 26308930 Marcos Mendoza
/**
86
 * @return array Contains the CA config index and item.
87
 */
88
function lookup_ca_by_subject($subject) {
89
	$ca_item = ['idx' => null, 'item' => null];
90
	foreach (config_get_path('ca', []) as $idx => $ca) {
91
		if (empty($ca)) {
92
			continue;
93
		}
94
		$ca_subject = cert_get_subject($ca['crt']);
95
		if ($ca_subject == $subject) {
96
			$ca_item = ['idx' => $idx, 'item' => $ca];
97
			break;
98 d43ad788 Scott Ullrich
		}
99 1e0b1727 Phil Davis
	}
100 d43ad788 Scott Ullrich
101 26308930 Marcos Mendoza
	return $ca_item;
102 d43ad788 Scott Ullrich
}
103
104 26308930 Marcos Mendoza
/**
105
 * @return array Contains the cert config index and item.
106
 */
107
function lookup_cert($refid) {
108
	$cert_item = ['idx' => null, 'item' => null];
109
	foreach (config_get_path('cert', []) as $idx => $cert) {
110
		if (empty($cert)) {
111
			continue;
112
		}
113
		if ($cert['refid'] == $refid) {
114
			$cert_item = ['idx' => $idx, 'item' => $cert];
115
			break;
116 1e0b1727 Phil Davis
		}
117
	}
118 d43ad788 Scott Ullrich
119 26308930 Marcos Mendoza
	return $cert_item;
120 d43ad788 Scott Ullrich
}
121
122 26308930 Marcos Mendoza
/**
123
 * @return array Contains the cert config index and item.
124
 */
125
function lookup_cert_by_name($name) {
126
	$cert_item = ['idx' => null, 'item' => null];
127
	foreach (config_get_path('cert', []) as $idx => $cert) {
128
		if (empty($cert)) {
129
			continue;
130
		}
131
		if ($cert['descr'] == $name) {
132
			$cert_item = ['idx' => $idx, 'item' => $cert];
133
			break;
134 1e0b1727 Phil Davis
		}
135
	}
136 c5f010aa jim-p
137 26308930 Marcos Mendoza
	return $cert_item;
138
}
139 c5f010aa jim-p
140 26308930 Marcos Mendoza
/**
141
 * @return array Contains the CRL config index and item.
142
 */
143
function lookup_crl($refid) {
144
	$crl_item = ['idx' => null, 'item' => null];
145
	foreach (config_get_path('crl', []) as $idx => $crl) {
146
		if (empty($crl)) {
147
			continue;
148
		}
149
		if ($crl['refid'] == $refid) {
150
			$crl_item = ['idx' => $idx, 'item' => $crl];
151
			break;
152 1e0b1727 Phil Davis
		}
153
	}
154 c5f010aa jim-p
155 26308930 Marcos Mendoza
	return $crl_item;
156 c5f010aa jim-p
}
157
158 26308930 Marcos Mendoza
function ca_chain_array($cert) {
159 1e0b1727 Phil Davis
	if ($cert['caref']) {
160 d43ad788 Scott Ullrich
		$chain = array();
161 5289dc57 jim-p
		$crt = lookup_ca($cert['caref']);
162 26308930 Marcos Mendoza
		$crt = $crt['item'];
163 d43ad788 Scott Ullrich
		$chain[] = $crt;
164
		while ($crt) {
165
			$caref = $crt['caref'];
166 1e0b1727 Phil Davis
			if ($caref) {
167 5289dc57 jim-p
				$crt = lookup_ca($caref);
168 26308930 Marcos Mendoza
				$crt = $crt['item'];
169 1e0b1727 Phil Davis
			} else {
170 d43ad788 Scott Ullrich
				$crt = false;
171 1e0b1727 Phil Davis
			}
172
			if ($crt) {
173 d43ad788 Scott Ullrich
				$chain[] = $crt;
174 1e0b1727 Phil Davis
			}
175 d43ad788 Scott Ullrich
		}
176
		return $chain;
177
	}
178
	return false;
179
}
180
181 26308930 Marcos Mendoza
function ca_chain($cert) {
182 1e0b1727 Phil Davis
	if ($cert['caref']) {
183 d43ad788 Scott Ullrich
		$ca = "";
184
		$cas = ca_chain_array($cert);
185 1e0b1727 Phil Davis
		if (is_array($cas)) {
186 26308930 Marcos Mendoza
			foreach ($cas as $ca_cert) {
187 d43ad788 Scott Ullrich
				$ca .= base64_decode($ca_cert['crt']);
188
				$ca .= "\n";
189
			}
190 1e0b1727 Phil Davis
		}
191 d43ad788 Scott Ullrich
		return $ca;
192
	}
193
	return "";
194
}
195
196 26308930 Marcos Mendoza
/**
197
 * Writes the given CA to config and updates certs that reference it.
198
 * 
199
 * @param array  &$ca    Directly modified; written to config if it does not exist.
200
 * @param string $str    CA cert - not encoded.
201
 * @param string $key    Optional. CA key - not encoded.
202
 * @param string $serial Optional. CA serial; defaults to 0.
203
 * 
204
 * @return bool False if CA already exists; True otherwise.
205
 */
206 ab63443a jim-p
function ca_import(& $ca, $str, $key = "", $serial = "") {
207 d43ad788 Scott Ullrich
	$ca['crt'] = base64_encode($str);
208 1e0b1727 Phil Davis
	if (!empty($key)) {
209 ecefc738 jim-p
		$ca['prv'] = base64_encode($key);
210 1e0b1727 Phil Davis
	}
211 ab63443a jim-p
	if (empty($serial)) {
212
		$ca['serial'] = 0;
213
	} else {
214 bfa992bc jim-p
		$ca['serial'] = $serial;
215 1e0b1727 Phil Davis
	}
216 d43ad788 Scott Ullrich
	$subject = cert_get_subject($str, false);
217
	$issuer = cert_get_issuer($str, false);
218 96d78012 Viktor Gurov
	$serialNumber = cert_get_serial($str, false);
219 1e0b1727 Phil Davis
220 d43ad788 Scott Ullrich
	// Find my issuer unless self-signed
221 1e0b1727 Phil Davis
	if ($issuer <> $subject) {
222 26308930 Marcos Mendoza
		$issuer_crt = lookup_ca_by_subject($issuer);
223
		$issuer_crt = $issuer_crt['item'];
224 1e0b1727 Phil Davis
		if ($issuer_crt) {
225 d43ad788 Scott Ullrich
			$ca['caref'] = $issuer_crt['refid'];
226 1e0b1727 Phil Davis
		}
227 d43ad788 Scott Ullrich
	}
228
229
	/* Correct if child certificate was loaded first */
230 26308930 Marcos Mendoza
	foreach (config_get_path('ca', []) as $idx => $oca) {
231
		// check by serial number if CA already exists
232
		$osn = cert_get_serial($oca['crt']);
233
		if (($ca['refid'] <> $oca['refid']) && ($serialNumber == $osn)) {
234
			return false;
235
		}
236
		$issuer = cert_get_issuer($oca['crt']);
237
		if (($ca['refid'] <> $oca['refid']) && ($issuer == $subject)) {
238
			config_set_path("ca/{$idx}/caref", $ca['refid']);
239 d43ad788 Scott Ullrich
		}
240 1e0b1727 Phil Davis
	}
241 26308930 Marcos Mendoza
	$cert_config = config_get_path('cert');
242
	if (is_array($cert_config)) {
243
		foreach ($cert_config as & $cert) {
244 d43ad788 Scott Ullrich
			$issuer = cert_get_issuer($cert['crt']);
245 086cf944 Phil Davis
			if ($issuer == $subject) {
246 d43ad788 Scott Ullrich
				$cert['caref'] = $ca['refid'];
247 1e0b1727 Phil Davis
			}
248 d43ad788 Scott Ullrich
		}
249 26308930 Marcos Mendoza
		config_set_path('cert', $cert_config);
250 1e0b1727 Phil Davis
	}
251 d43ad788 Scott Ullrich
	return true;
252
}
253
254 c3cda38e jim-p
function ca_create(& $ca, $keylen, $lifetime, $dn, $digest_alg = "sha256", $keytype = "RSA", $ecname = "prime256v1") {
255 d43ad788 Scott Ullrich
256
	$args = array(
257 87b4deb2 jim-p
		"x509_extensions" => "v3_ca",
258 ca621902 jim-p
		"digest_alg" => $digest_alg,
259 d43ad788 Scott Ullrich
		"encrypt_key" => false);
260 ff5bc49c Viktor Gurov
	if ($keytype == 'ECDSA') {
261
		$args["curve_name"] = $ecname;
262
		$args["private_key_type"] = OPENSSL_KEYTYPE_EC;
263
	} else {
264
		$args["private_key_bits"] = (int)$keylen;
265
		$args["private_key_type"] = OPENSSL_KEYTYPE_RSA;
266
	}
267 d43ad788 Scott Ullrich
268
	// generate a new key pair
269 838e27bf jim-p
	$res_key = openssl_pkey_new($args);
270 1e0b1727 Phil Davis
	if (!$res_key) {
271
		return false;
272
	}
273 d43ad788 Scott Ullrich
274
	// generate a certificate signing request
275
	$res_csr = openssl_csr_new($dn, $res_key, $args);
276 1e0b1727 Phil Davis
	if (!$res_csr) {
277
		return false;
278
	}
279 d43ad788 Scott Ullrich
280
	// self sign the certificate
281 4aa7c7ae jim-p
	$res_crt = openssl_csr_sign($res_csr, null, $res_key, $lifetime, $args, cert_get_random_serial());
282 1e0b1727 Phil Davis
	if (!$res_crt) {
283
		return false;
284
	}
285 d43ad788 Scott Ullrich
286
	// export our certificate data
287 1b6d9fa5 Evgeny Yurchenko
	if (!openssl_pkey_export($res_key, $str_key) ||
288 ae52d165 Renato Botelho
	    !openssl_x509_export($res_crt, $str_crt)) {
289 1b6d9fa5 Evgeny Yurchenko
		return false;
290 1e0b1727 Phil Davis
	}
291 d43ad788 Scott Ullrich
292
	// return our ca information
293
	$ca['crt'] = base64_encode($str_crt);
294
	$ca['prv'] = base64_encode($str_key);
295 663e29bb jim-p
	$ca['serial'] = 1;
296 d43ad788 Scott Ullrich
297
	return true;
298
}
299
300 26308930 Marcos Mendoza
/**
301
 * Creates intermediate Certificate Authority; increments the CA's serial and writes it to config.
302
 */
303 c3cda38e jim-p
function ca_inter_create(& $ca, $keylen, $lifetime, $dn, $caref, $digest_alg = "sha256", $keytype = "RSA", $ecname = "prime256v1") {
304 95c8cf48 Evgeny Yurchenko
	// Create Intermediate Certificate Authority
305 26308930 Marcos Mendoza
	$ca_item_config = lookup_ca($caref);
306
	$signing_ca = &$ca_item_config['item'];
307 1e0b1727 Phil Davis
	if (!$signing_ca) {
308 95c8cf48 Evgeny Yurchenko
		return false;
309 1e0b1727 Phil Davis
	}
310 95c8cf48 Evgeny Yurchenko
311
	$signing_ca_res_crt = openssl_x509_read(base64_decode($signing_ca['crt']));
312
	$signing_ca_res_key = openssl_pkey_get_private(array(0 => base64_decode($signing_ca['prv']) , 1 => ""));
313 1e0b1727 Phil Davis
	if (!$signing_ca_res_crt || !$signing_ca_res_key) {
314
		return false;
315
	}
316 26308930 Marcos Mendoza
	$signing_ca_serial = config_set_path("ca/{$ca_item_config['idx']}", ++$signing_ca['serial']);
317 95c8cf48 Evgeny Yurchenko
318
	$args = array(
319 87b4deb2 jim-p
		"x509_extensions" => "v3_ca",
320 ca621902 jim-p
		"digest_alg" => $digest_alg,
321 95c8cf48 Evgeny Yurchenko
		"encrypt_key" => false);
322 ff5bc49c Viktor Gurov
	if ($keytype == 'ECDSA') {
323
		$args["curve_name"] = $ecname;
324
		$args["private_key_type"] = OPENSSL_KEYTYPE_EC;
325
	} else {
326
		$args["private_key_bits"] = (int)$keylen;
327
		$args["private_key_type"] = OPENSSL_KEYTYPE_RSA;
328
	}
329 95c8cf48 Evgeny Yurchenko
330
	// generate a new key pair
331
	$res_key = openssl_pkey_new($args);
332 1e0b1727 Phil Davis
	if (!$res_key) {
333
		return false;
334
	}
335 95c8cf48 Evgeny Yurchenko
336
	// generate a certificate signing request
337
	$res_csr = openssl_csr_new($dn, $res_key, $args);
338 1e0b1727 Phil Davis
	if (!$res_csr) {
339
		return false;
340
	}
341 95c8cf48 Evgeny Yurchenko
342
	// Sign the certificate
343
	$res_crt = openssl_csr_sign($res_csr, $signing_ca_res_crt, $signing_ca_res_key, $lifetime, $args, $signing_ca_serial);
344 1e0b1727 Phil Davis
	if (!$res_crt) {
345
		return false;
346
	}
347 95c8cf48 Evgeny Yurchenko
348
	// export our certificate data
349
	if (!openssl_pkey_export($res_key, $str_key) ||
350 ae52d165 Renato Botelho
	    !openssl_x509_export($res_crt, $str_crt)) {
351 95c8cf48 Evgeny Yurchenko
		return false;
352 1e0b1727 Phil Davis
	}
353 95c8cf48 Evgeny Yurchenko
354
	// return our ca information
355
	$ca['crt'] = base64_encode($str_crt);
356
	$ca['prv'] = base64_encode($str_key);
357
	$ca['serial'] = 0;
358 dd76084d Matt Smith
	$ca['caref'] = $caref;
359 95c8cf48 Evgeny Yurchenko
360
	return true;
361
}
362
363 d43ad788 Scott Ullrich
function cert_import(& $cert, $crt_str, $key_str) {
364
365
	$cert['crt'] = base64_encode($crt_str);
366
	$cert['prv'] = base64_encode($key_str);
367
368
	$subject = cert_get_subject($crt_str, false);
369
	$issuer = cert_get_issuer($crt_str, false);
370 1e0b1727 Phil Davis
371 d43ad788 Scott Ullrich
	// Find my issuer unless self-signed
372 1e0b1727 Phil Davis
	if ($issuer <> $subject) {
373 26308930 Marcos Mendoza
		$issuer_crt = lookup_ca_by_subject($issuer);
374
		$issuer_crt = $issuer_crt['item'];
375 1e0b1727 Phil Davis
		if ($issuer_crt) {
376 d43ad788 Scott Ullrich
			$cert['caref'] = $issuer_crt['refid'];
377 1e0b1727 Phil Davis
		}
378 d43ad788 Scott Ullrich
	}
379
	return true;
380
}
381
382 c3cda38e jim-p
function cert_create(& $cert, $caref, $keylen, $lifetime, $dn, $type = "user", $digest_alg = "sha256", $keytype = "RSA", $ecname = "prime256v1") {
383 d43ad788 Scott Ullrich
384 7c4c77ee jim-p
	$cert['type'] = $type;
385 d43ad788 Scott Ullrich
386 7c4c77ee jim-p
	if ($type != "self-signed") {
387
		$cert['caref'] = $caref;
388 26308930 Marcos Mendoza
		$ca_item_config = lookup_ca($caref);
389
		$ca = &$ca_item_config['item'];
390 1e0b1727 Phil Davis
		if (!$ca) {
391 7c4c77ee jim-p
			return false;
392 1e0b1727 Phil Davis
		}
393 7c4c77ee jim-p
394
		$ca_str_crt = base64_decode($ca['crt']);
395
		$ca_str_key = base64_decode($ca['prv']);
396
		$ca_res_crt = openssl_x509_read($ca_str_crt);
397
		$ca_res_key = openssl_pkey_get_private(array(0 => $ca_str_key, 1 => ""));
398 1e0b1727 Phil Davis
		if (!$ca_res_key) {
399
			return false;
400
		}
401 2c9601c9 jim-p
402
		/* Get the next available CA serial number. */
403
		$ca_serial = ca_get_next_serial($ca);
404 26308930 Marcos Mendoza
		config_set_path("ca/{$ca_item_config['idx']}", $ca);
405 7c4c77ee jim-p
	}
406 d43ad788 Scott Ullrich
407 0c82b8c2 jim-p
	$cert_type = cert_type_config_section($type);
408 7aaabd69 jim-p
409 3cb773da yarick123
	// in case of using Subject Alternative Names use other sections (with postfix '_san')
410
	// pass subjectAltName over environment variable 'SAN'
411
	if ($dn['subjectAltName']) {
412
		putenv("SAN={$dn['subjectAltName']}"); // subjectAltName can be set _only_ via configuration file
413
		$cert_type .= '_san';
414
		unset($dn['subjectAltName']);
415
	}
416
417 d43ad788 Scott Ullrich
	$args = array(
418 7aaabd69 jim-p
		"x509_extensions" => $cert_type,
419 ca621902 jim-p
		"digest_alg" => $digest_alg,
420 d43ad788 Scott Ullrich
		"encrypt_key" => false);
421 68690e0d Viktor Gurov
	if ($keytype == 'ECDSA') {
422 e0f8d364 Viktor Gurov
		$args["curve_name"] = $ecname;
423 68690e0d Viktor Gurov
		$args["private_key_type"] = OPENSSL_KEYTYPE_EC;
424
	} else {
425
		$args["private_key_bits"] = (int)$keylen;
426
		$args["private_key_type"] = OPENSSL_KEYTYPE_RSA;
427
	}
428 d43ad788 Scott Ullrich
429
	// generate a new key pair
430 838e27bf jim-p
	$res_key = openssl_pkey_new($args);
431 1e0b1727 Phil Davis
	if (!$res_key) {
432
		return false;
433
	}
434 d43ad788 Scott Ullrich
435 7c4c77ee jim-p
	// If this is a self-signed cert, blank out the CA and sign with the cert's key
436
	if ($type == "self-signed") {
437
		$ca           = null;
438
		$ca_res_crt   = null;
439
		$ca_res_key   = $res_key;
440 4aa7c7ae jim-p
		$ca_serial    = cert_get_random_serial();
441 7c4c77ee jim-p
		$cert['type'] = "server";
442
	}
443
444 d43ad788 Scott Ullrich
	// generate a certificate signing request
445
	$res_csr = openssl_csr_new($dn, $res_key, $args);
446 1e0b1727 Phil Davis
	if (!$res_csr) {
447
		return false;
448
	}
449 d43ad788 Scott Ullrich
450 7c4c77ee jim-p
	// sign the certificate using an internal CA
451 d43ad788 Scott Ullrich
	$res_crt = openssl_csr_sign($res_csr, $ca_res_crt, $ca_res_key, $lifetime,
452
				 $args, $ca_serial);
453 1e0b1727 Phil Davis
	if (!$res_crt) {
454
		return false;
455
	}
456 d43ad788 Scott Ullrich
457
	// export our certificate data
458 22b380aa Evgeny Yurchenko
	if (!openssl_pkey_export($res_key, $str_key) ||
459 ae52d165 Renato Botelho
	    !openssl_x509_export($res_crt, $str_crt)) {
460 22b380aa Evgeny Yurchenko
		return false;
461 1e0b1727 Phil Davis
	}
462 d43ad788 Scott Ullrich
463
	// return our certificate information
464
	$cert['crt'] = base64_encode($str_crt);
465
	$cert['prv'] = base64_encode($str_key);
466
467
	return true;
468
}
469
470 c3cda38e jim-p
function csr_generate(& $cert, $keylen, $dn, $type = "user", $digest_alg = "sha256", $keytype = "RSA", $ecname = "prime256v1") {
471 282b6c66 jim-p
472 0c82b8c2 jim-p
	$cert_type = cert_type_config_section($type);
473 282b6c66 jim-p
474
	// in case of using Subject Alternative Names use other sections (with postfix '_san')
475
	// pass subjectAltName over environment variable 'SAN'
476
	if ($dn['subjectAltName']) {
477
		putenv("SAN={$dn['subjectAltName']}"); // subjectAltName can be set _only_ via configuration file
478
		$cert_type .= '_san';
479
		unset($dn['subjectAltName']);
480
	}
481 d43ad788 Scott Ullrich
482
	$args = array(
483 282b6c66 jim-p
		"x509_extensions" => $cert_type,
484
		"req_extensions" => "req_{$cert_type}",
485 ca621902 jim-p
		"digest_alg" => $digest_alg,
486 d43ad788 Scott Ullrich
		"encrypt_key" => false);
487 68690e0d Viktor Gurov
	if ($keytype == 'ECDSA') {
488 e0f8d364 Viktor Gurov
		$args["curve_name"] = $ecname;
489 68690e0d Viktor Gurov
		$args["private_key_type"] = OPENSSL_KEYTYPE_EC;
490
	} else {
491
		$args["private_key_bits"] = (int)$keylen;
492
		$args["private_key_type"] = OPENSSL_KEYTYPE_RSA;
493
	}
494 d43ad788 Scott Ullrich
495
	// generate a new key pair
496 838e27bf jim-p
	$res_key = openssl_pkey_new($args);
497 1e0b1727 Phil Davis
	if (!$res_key) {
498
		return false;
499
	}
500 d43ad788 Scott Ullrich
501
	// generate a certificate signing request
502
	$res_csr = openssl_csr_new($dn, $res_key, $args);
503 1e0b1727 Phil Davis
	if (!$res_csr) {
504
		return false;
505
	}
506 d43ad788 Scott Ullrich
507
	// export our request data
508 22b380aa Evgeny Yurchenko
	if (!openssl_pkey_export($res_key, $str_key) ||
509 ae52d165 Renato Botelho
	    !openssl_csr_export($res_csr, $str_csr)) {
510 22b380aa Evgeny Yurchenko
		return false;
511 1e0b1727 Phil Davis
	}
512 d43ad788 Scott Ullrich
513
	// return our request information
514
	$cert['csr'] = base64_encode($str_csr);
515
	$cert['prv'] = base64_encode($str_key);
516
517
	return true;
518
}
519
520 de3f6463 Reid Linnemann
function csr_sign($csr, & $ca, $duration, $type, $altnames, $digest_alg = "sha256") {
521 0c82b8c2 jim-p
	$old_err_level = error_reporting(0);
522
523
	// Gather the information required for signed cert
524
	$ca_str_crt = base64_decode($ca['crt']);
525
	$ca_str_key = base64_decode($ca['prv']);
526
	$ca_res_key = openssl_pkey_get_private(array(0 => $ca_str_key, 1 => ""));
527
	if (!$ca_res_key) {
528
		return false;
529
	}
530 2c9601c9 jim-p
531
	/* Get the next available CA serial number. */
532
	$ca_serial = ca_get_next_serial($ca);
533 0c82b8c2 jim-p
534
	$cert_type = cert_type_config_section($type);
535
536
	if (!empty($altnames)) {
537
		putenv("SAN={$altnames}"); // subjectAltName can be set _only_ via configuration file
538
		$cert_type .= '_san';
539
	}
540
541
	$args = array(
542
		"x509_extensions" => $cert_type,
543 aec3a259 jim-p
		"digest_alg" => $digest_alg,
544 0c82b8c2 jim-p
		"req_extensions" => "req_{$cert_type}"
545
	);
546
547
	// Sign the new cert and export it in x509 format
548
	openssl_x509_export(openssl_csr_sign($csr, $ca_str_crt, $ca_str_key, $duration, $args, $ca_serial), $n509);
549
	error_reporting($old_err_level);
550
551
	return $n509;
552
}
553
554 d43ad788 Scott Ullrich
function csr_complete(& $cert, $str_crt) {
555 7fd7fbcf PiBa-NL
	$str_key = base64_decode($cert['prv']);
556
	cert_import($cert, $str_crt, $str_key);
557 d43ad788 Scott Ullrich
	unset($cert['csr']);
558
	return true;
559
}
560
561
function csr_get_subject($str_crt, $decode = true) {
562
563 1e0b1727 Phil Davis
	if ($decode) {
564 d43ad788 Scott Ullrich
		$str_crt = base64_decode($str_crt);
565 1e0b1727 Phil Davis
	}
566 d43ad788 Scott Ullrich
567
	$components = openssl_csr_get_subject($str_crt);
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 311f93cd Ermal
	ksort($components);
574 d43ad788 Scott Ullrich
	foreach ($components as $a => $v) {
575 1e0b1727 Phil Davis
		if (!strlen($subject)) {
576 d43ad788 Scott Ullrich
			$subject = "{$a}={$v}";
577 1e0b1727 Phil Davis
		} else {
578 d43ad788 Scott Ullrich
			$subject = "{$a}={$v}, {$subject}";
579 1e0b1727 Phil Davis
		}
580 d43ad788 Scott Ullrich
	}
581
582
	return $subject;
583
}
584
585
function cert_get_subject($str_crt, $decode = true) {
586
587 1e0b1727 Phil Davis
	if ($decode) {
588 d43ad788 Scott Ullrich
		$str_crt = base64_decode($str_crt);
589 1e0b1727 Phil Davis
	}
590 d43ad788 Scott Ullrich
591
	$inf_crt = openssl_x509_parse($str_crt);
592
	$components = $inf_crt['subject'];
593
594 1e0b1727 Phil Davis
	if (empty($components) || !is_array($components)) {
595 d43ad788 Scott Ullrich
		return "unknown";
596 1e0b1727 Phil Davis
	}
597 d43ad788 Scott Ullrich
598 b89c34aa Ermal
	ksort($components);
599 d43ad788 Scott Ullrich
	foreach ($components as $a => $v) {
600 b89c34aa Ermal
		if (is_array($v)) {
601
			ksort($v);
602 5479df47 jim-p
			foreach ($v as $w) {
603
				$asubject = "{$a}={$w}";
604
				$subject = (strlen($subject)) ? "{$asubject}, {$subject}" : $asubject;
605
			}
606 b89c34aa Ermal
		} else {
607 5479df47 jim-p
			$asubject = "{$a}={$v}";
608
			$subject = (strlen($subject)) ? "{$asubject}, {$subject}" : $asubject;
609
		}
610 d43ad788 Scott Ullrich
	}
611
612
	return $subject;
613
}
614
615
function cert_get_subject_array($crt) {
616
	$str_crt = base64_decode($crt);
617
	$inf_crt = openssl_x509_parse($str_crt);
618
	$components = $inf_crt['subject'];
619 e4d7a064 jim-p
620 1e0b1727 Phil Davis
	if (!is_array($components)) {
621 e4d7a064 jim-p
		return;
622 1e0b1727 Phil Davis
	}
623 e4d7a064 jim-p
624 d43ad788 Scott Ullrich
	$subject_array = array();
625
626 1e0b1727 Phil Davis
	foreach ($components as $a => $v) {
627 d43ad788 Scott Ullrich
		$subject_array[] = array('a' => $a, 'v' => $v);
628 1e0b1727 Phil Davis
	}
629 d43ad788 Scott Ullrich
630
	return $subject_array;
631
}
632
633 a84eb838 jim-p
function cert_get_subject_hash($crt) {
634
	$str_crt = base64_decode($crt);
635
	$inf_crt = openssl_x509_parse($str_crt);
636
	return $inf_crt['subject'];
637
}
638
639 4906f4ee jim-p
function cert_get_sans($str_crt, $decode = true) {
640
	if ($decode) {
641
		$str_crt = base64_decode($str_crt);
642
	}
643
	$sans = array();
644
	$crt_details = openssl_x509_parse($str_crt);
645
	if (!empty($crt_details['extensions']['subjectAltName'])) {
646
		$sans = explode(',', $crt_details['extensions']['subjectAltName']);
647
	}
648
	return $sans;
649
}
650
651 d43ad788 Scott Ullrich
function cert_get_issuer($str_crt, $decode = true) {
652
653 1e0b1727 Phil Davis
	if ($decode) {
654 d43ad788 Scott Ullrich
		$str_crt = base64_decode($str_crt);
655 1e0b1727 Phil Davis
	}
656 d43ad788 Scott Ullrich
657
	$inf_crt = openssl_x509_parse($str_crt);
658
	$components = $inf_crt['issuer'];
659 1e0b1727 Phil Davis
660
	if (empty($components) || !is_array($components)) {
661 d43ad788 Scott Ullrich
		return "unknown";
662 1e0b1727 Phil Davis
	}
663 5ca13f69 Ermal
664
	ksort($components);
665 d43ad788 Scott Ullrich
	foreach ($components as $a => $v) {
666 2a08b814 vsquared56
		if (is_array($v)) {
667
			ksort($v);
668
			foreach ($v as $w) {
669
				$aissuer = "{$a}={$w}";
670
				$issuer = (strlen($issuer)) ? "{$aissuer}, {$issuer}" : $aissuer;
671
			}
672
		} else {
673
			$aissuer = "{$a}={$v}";
674
			$issuer = (strlen($issuer)) ? "{$aissuer}, {$issuer}" : $aissuer;
675
		}
676 d43ad788 Scott Ullrich
	}
677
678
	return $issuer;
679
}
680
681 1746c5ce PiBa-NL
/* Works for both RSA and ECC (crt) and key (prv) */
682
function cert_get_publickey($str_crt, $decode = true, $type = "crt") {
683 1e0b1727 Phil Davis
	if ($decode) {
684 9038f44c Steve Beaver
		$str_crt = base64_decode($str_crt);
685 1e0b1727 Phil Davis
	}
686 b6dcbd64 jim-p
	$certfn = tempnam('/tmp', 'CGPK');
687
	file_put_contents($certfn, $str_crt);
688 6d6ba660 PiBa-NL
	switch ($type) {
689
		case 'prv':
690 b6dcbd64 jim-p
			exec("/usr/bin/openssl pkey -in {$certfn} -pubout", $out);
691 6d6ba660 PiBa-NL
			break;
692
		case 'crt':
693 b6dcbd64 jim-p
			exec("/usr/bin/openssl x509 -in {$certfn} -inform pem -noout -pubkey", $out);
694 6d6ba660 PiBa-NL
			break;
695
		case 'csr':
696 b6dcbd64 jim-p
			exec("/usr/bin/openssl req -in {$certfn} -inform pem -noout -pubkey", $out);
697 6d6ba660 PiBa-NL
			break;
698
		default:
699
			$out = array();
700
			break;
701 1746c5ce PiBa-NL
	}
702 b6dcbd64 jim-p
	unlink($certfn);
703 6d6ba660 PiBa-NL
	return implode("\n", $out);
704 a828210b yakatz
}
705 1379d66f jim-p
706
function cert_get_purpose($str_crt, $decode = true) {
707 4906f4ee jim-p
	$extended_oids = array(
708
		"1.3.6.1.5.5.8.2.2" => "IP Security IKE Intermediate",
709
	);
710 1e0b1727 Phil Davis
	if ($decode) {
711 1379d66f jim-p
		$str_crt = base64_decode($str_crt);
712 1e0b1727 Phil Davis
	}
713 1379d66f jim-p
	$crt_details = openssl_x509_parse($str_crt);
714
	$purpose = array();
715 4906f4ee jim-p
	if (!empty($crt_details['extensions']['keyUsage'])) {
716
		$purpose['ku'] = explode(',', $crt_details['extensions']['keyUsage']);
717
		foreach ($purpose['ku'] as & $ku) {
718
			$ku = trim($ku);
719
			if (array_key_exists($ku, $extended_oids)) {
720
				$ku = $extended_oids[$ku];
721
			}
722
		}
723
	} else {
724
		$purpose['ku'] = array();
725
	}
726
	if (!empty($crt_details['extensions']['extendedKeyUsage'])) {
727
		$purpose['eku'] = explode(',', $crt_details['extensions']['extendedKeyUsage']);
728
		foreach ($purpose['eku'] as & $eku) {
729
			$eku = trim($eku);
730
			if (array_key_exists($eku, $extended_oids)) {
731
				$eku = $extended_oids[$eku];
732
			}
733
		}
734
	} else {
735
		$purpose['eku'] = array();
736
	}
737 1379d66f jim-p
	$purpose['ca'] = (stristr($crt_details['extensions']['basicConstraints'], 'CA:TRUE') === false) ? 'No': 'Yes';
738 4906f4ee jim-p
	$purpose['server'] = (in_array('TLS Web Server Authentication', $purpose['eku'])) ? 'Yes': 'No';
739
740 1379d66f jim-p
	return $purpose;
741
}
742
743 37e1aecf jim-p
function cert_get_ocspstaple($str_crt, $decode = true) {
744 00e54150 jim-p
	if ($decode) {
745
		$str_crt = base64_decode($str_crt);
746
	}
747
	$crt_details = openssl_x509_parse($str_crt);
748 0d82f93b jim-p
	if (($crt_details['extensions']['tlsfeature'] == "status_request") ||
749
	    !empty($crt_details['extensions']['1.3.6.1.5.5.7.1.24'])) {
750 00e54150 jim-p
		return true;
751
	}
752
	return false;
753
}
754
755 1120b85c jim-p
function cert_format_date($validTS, $validTS_time_t, $outputstring = true) {
756 b8b33a3e jim-p
	$now = new DateTime("now");
757 1120b85c jim-p
758
	/* Try to create a DateTime object from the full time string */
759
	$date = DateTime::createFromFormat('ymdHis', rtrim($validTS, 'Z'), new DateTimeZone('Z'));
760 bdaa35dc jim-p
	/* If that failed, try using a four digit year */
761
	if ($date === false) {
762
		$date = DateTime::createFromFormat('YmdHis', rtrim($validTS, 'Z'), new DateTimeZone('Z'));
763
	}
764 1120b85c jim-p
	/* If that failed, try to create it from the UNIX timestamp */
765 29804b9e jim-p
	if (($date === false) && (!empty($validTS_time_t))) {
766 1120b85c jim-p
		$date = new DateTime('@' . $validTS_time_t, new DateTimeZone('Z'));
767 1e0b1727 Phil Davis
	}
768 1120b85c jim-p
	/* If we have a valid DateTime object, format it in a nice way */
769
	if ($date !== false) {
770
		$date->setTimezone($now->getTimeZone());
771 b8b33a3e jim-p
		if ($outputstring) {
772 1120b85c jim-p
			$date = $date->format(DateTimeInterface::RFC2822);
773 3fec2470 jim-p
		}
774 1e0b1727 Phil Davis
	}
775 1120b85c jim-p
	return $date;
776
}
777
778
function cert_get_dates($str_crt, $decode = true, $outputstring = true) {
779
	if ($decode) {
780
		$str_crt = base64_decode($str_crt);
781
	}
782
	$crt_details = openssl_x509_parse($str_crt);
783
784
	$start = cert_format_date($crt_details['validFrom'], $crt_details['validFrom_time_t'], $outputstring);
785
	$end   = cert_format_date($crt_details['validTo'], $crt_details['validTo_time_t'], $outputstring);
786
787 2b333210 jim-p
	return array($start, $end);
788
}
789
790 04761344 jim-p
function cert_get_serial($str_crt, $decode = true) {
791 1e0b1727 Phil Davis
	if ($decode) {
792 04761344 jim-p
		$str_crt = base64_decode($str_crt);
793 1e0b1727 Phil Davis
	}
794 04761344 jim-p
	$crt_details = openssl_x509_parse($str_crt);
795 03a84081 jim-p
	if (isset($crt_details['serialNumber'])) {
796 04761344 jim-p
		return $crt_details['serialNumber'];
797 1e0b1727 Phil Davis
	} else {
798 04761344 jim-p
		return NULL;
799 1e0b1727 Phil Davis
	}
800 04761344 jim-p
}
801
802 aec3a259 jim-p
function cert_get_sigtype($str_crt, $decode = true) {
803
	if ($decode) {
804
		$str_crt = base64_decode($str_crt);
805
	}
806
	$crt_details = openssl_x509_parse($str_crt);
807
808
	$signature = array();
809
	if (isset($crt_details['signatureTypeSN']) && !empty($crt_details['signatureTypeSN'])) {
810
		$signature['shortname'] = $crt_details['signatureTypeSN'];
811
	}
812
	if (isset($crt_details['signatureTypeLN']) && !empty($crt_details['signatureTypeLN'])) {
813
		$signature['longname'] = $crt_details['signatureTypeLN'];
814
	}
815
	if (isset($crt_details['signatureTypeNID']) && !empty($crt_details['signatureTypeNID'])) {
816
		$signature['nid'] = $crt_details['signatureTypeNID'];
817
	}
818
819
	return $signature;
820
}
821
822 e2c718c8 jim-p
function is_openvpn_server_ca($caref) {
823 1ec82c30 Marcos Mendoza
	foreach(config_get_path('openvpn/openvpn-server', []) as $ovpns) {
824 e2c718c8 jim-p
		if ($ovpns['caref'] == $caref) {
825
			return true;
826
		}
827
	}
828
	return false;
829
}
830
831
function is_openvpn_client_ca($caref) {
832 25ab4237 Christian McDonald
	foreach(config_get_path('openvpn/openvpn-client', []) as $ovpnc) {
833 e2c718c8 jim-p
		if ($ovpnc['caref'] == $caref) {
834
			return true;
835
		}
836
	}
837
	return false;
838
}
839
840
function is_ipsec_peer_ca($caref) {
841 843ee1ac jim-p
	foreach (config_get_path('ipsec/phase1', []) as $ipsec) {
842 e2c718c8 jim-p
		if ($ipsec['caref'] == $caref) {
843
			return true;
844
		}
845
	}
846
	return false;
847
}
848
849
function is_ldap_peer_ca($caref) {
850 843ee1ac jim-p
	foreach (config_get_path('system/authserver', []) as $authserver) {
851 d74bd052 Viktor G
		if (($authserver['ldap_caref'] == $caref) &&
852
		    ($authserver['ldap_urltype'] != 'Standard TCP')) {
853 e2c718c8 jim-p
			return true;
854
		}
855
	}
856
	return false;
857
}
858
859
function ca_in_use($caref) {
860
	return (is_openvpn_server_ca($caref) ||
861
		is_openvpn_client_ca($caref) ||
862
		is_ipsec_peer_ca($caref) ||
863
		is_ldap_peer_ca($caref));
864
}
865
866 a4cd7de1 Christian McDonald
function is_kea_cert(string $certref): bool {
867
	if (config_path_enabled('kea/ha', 'tls')) {
868
		if (config_get_path('kea/ha/scertref') === $certref) {
869
			return true;
870
		}
871
		if (config_path_enabled('kea/ha', 'mutualtls') &&
872
		    (config_get_path('kea/ha/ccertref') === $certref)) {
873
			return true;
874
		}
875
	}
876
877
	return false;
878
}
879
880 dea98903 jim-p
function is_user_cert($certref) {
881 843ee1ac jim-p
	foreach (config_get_path('system/user', []) as $user) {
882 1e0b1727 Phil Davis
		if (!is_array($user['cert'])) {
883 dab2e769 jim-p
			continue;
884 1e0b1727 Phil Davis
		}
885 dab2e769 jim-p
		foreach ($user['cert'] as $cert) {
886 1e0b1727 Phil Davis
			if ($certref == $cert) {
887 dea98903 jim-p
				return true;
888 1e0b1727 Phil Davis
			}
889 dab2e769 jim-p
		}
890
	}
891 dea98903 jim-p
	return false;
892 dab2e769 jim-p
}
893
894 dea98903 jim-p
function is_openvpn_server_cert($certref) {
895 35bf4437 Christian McDonald
	foreach (config_get_path('openvpn/openvpn-server', []) as $ovpns) {
896 1e0b1727 Phil Davis
		if ($ovpns['certref'] == $certref) {
897 dea98903 jim-p
			return true;
898 1e0b1727 Phil Davis
		}
899 dea98903 jim-p
	}
900
	return false;
901
}
902
903
function is_openvpn_client_cert($certref) {
904 35bf4437 Christian McDonald
	foreach (config_get_path('openvpn/openvpn-client', []) as $ovpnc) {
905 1e0b1727 Phil Davis
		if ($ovpnc['certref'] == $certref) {
906 dea98903 jim-p
			return true;
907 1e0b1727 Phil Davis
		}
908 dea98903 jim-p
	}
909
	return false;
910
}
911
912
function is_ipsec_cert($certref) {
913 35bf4437 Christian McDonald
	foreach(config_get_path('ipsec/phase1', []) as $ipsec) {
914 1e0b1727 Phil Davis
		if ($ipsec['certref'] == $certref) {
915 dea98903 jim-p
			return true;
916 1e0b1727 Phil Davis
		}
917 dea98903 jim-p
	}
918
	return false;
919
}
920
921
function is_webgui_cert($certref) {
922 843ee1ac jim-p
	if ((config_get_path('system/webgui/ssl-certref') == $certref) &&
923
	    (config_get_path('system/webgui/protocol') != "http")) {
924 dea98903 jim-p
		return true;
925 1e0b1727 Phil Davis
	}
926 dea98903 jim-p
}
927
928 29e6a815 Renato Botelho
function is_package_cert($certref) {
929
	$pluginparams = array();
930
	$pluginparams['type'] = 'certificates';
931
	$pluginparams['event'] = 'used_certificates';
932
933
	$certificates_used_by_packages = pkg_call_plugins('plugin_certificates', $pluginparams);
934
935
	/* Check if any package is using certificate */
936
	foreach ($certificates_used_by_packages as $name => $package) {
937
		if (is_array($package['certificatelist'][$certref]) &&
938
		    isset($package['certificatelist'][$certref]) > 0) {
939
			return true;
940
		}
941
	}
942
}
943
944 36f6ed35 bcyrill
function is_captiveportal_cert($certref) {
945 35bf4437 Christian McDonald
	foreach (config_get_path('captiveportal', []) as $portal) {
946 1e0b1727 Phil Davis
		if (isset($portal['enable']) && isset($portal['httpslogin']) && ($portal['certref'] == $certref)) {
947 36f6ed35 bcyrill
			return true;
948 1e0b1727 Phil Davis
		}
949 36f6ed35 bcyrill
	}
950
	return false;
951
}
952
953 39d83c73 Viktor G
function is_unbound_cert($certref) {
954 843ee1ac jim-p
	if (config_path_enabled('unbound') &&
955
	    config_path_enabled('unbound','enablessl') &&
956
	    (config_get_path('unbound/sslcertref') == $certref)) {
957 39d83c73 Viktor G
		return true;
958
	}
959
}
960
961 dea98903 jim-p
function cert_in_use($certref) {
962 a4cd7de1 Christian McDonald
	return (is_kea_cert($certref) ||
963
		is_webgui_cert($certref) ||
964 dea98903 jim-p
		is_user_cert($certref) ||
965
		is_openvpn_server_cert($certref) ||
966
		is_openvpn_client_cert($certref) ||
967 36f6ed35 bcyrill
		is_ipsec_cert($certref) ||
968 29e6a815 Renato Botelho
		is_captiveportal_cert($certref) ||
969 39d83c73 Viktor G
		is_unbound_cert($certref) ||
970 29e6a815 Renato Botelho
		is_package_cert($certref));
971 dab2e769 jim-p
}
972
973 3bde5cdd PiBa-NL
function cert_usedby_description($refid, $certificates_used_by_packages) {
974
	$result = "";
975
	if (is_array($certificates_used_by_packages)) {
976
		foreach ($certificates_used_by_packages as $name => $package) {
977
			if (isset($package['certificatelist'][$refid])) {
978
				$hint = "" ;
979
				if (is_array($package['certificatelist'][$refid])) {
980
					foreach ($package['certificatelist'][$refid] as $cert_used) {
981
						$hint = $hint . $cert_used['usedby']."\n";
982
					}
983
				}
984
				$count = count($package['certificatelist'][$refid]);
985
				$result .= "<div title='".htmlspecialchars($hint)."'>";
986
				$result .= htmlspecialchars($package['pkgname'])." ($count)<br />";
987
				$result .= "</div>";
988
			}
989
		}
990
	}
991
	return $result;
992
}
993
994 9aa8f6a8 jim-p
/* Detect a rollover at 2038 on some platforms (e.g. ARM)
995
 * See: https://redmine.pfsense.org/issues/9098 */
996 3a877e4a jim-p
function cert_get_max_lifetime() {
997
	global $cert_max_lifetime;
998
	$max = $cert_max_lifetime;
999
1000 9aa8f6a8 jim-p
	$current_time = time();
1001
	while ((int)($current_time + ($max * 24 * 60 * 60)) < 0) {
1002
		$max--;
1003
	}
1004 3a877e4a jim-p
	return min($max, $cert_max_lifetime);
1005 9aa8f6a8 jim-p
}
1006
1007 a3c15890 jim-p
/* Detect a rollover at 2050 with UTCTime
1008
 * See: https://redmine.pfsense.org/issues/9098 */
1009
function crl_get_max_lifetime() {
1010
	$now = new DateTime("now");
1011
	$utctime_before_roll = DateTime::createFromFormat('Ymd', '20491231');
1012 1ec82c30 Marcos Mendoza
	if ($now !== false) {
1013 a3c15890 jim-p
		$interval = $now->diff($utctime_before_roll);
1014
		$max_days = abs($interval->days);
1015
		/* Reduce the max well below the rollover time */
1016
		if ($max_days > 1000) {
1017
			$max_days -= 1000;
1018
		}
1019
		return min($max_days, cert_get_max_lifetime());
1020
	}
1021
1022
	/* Cannot use date functions, so use a lower default max. */
1023
	return min(7000, cert_get_max_lifetime());
1024
}
1025
1026 26308930 Marcos Mendoza
/**
1027
 * Directly modifes the Certificate Revocation List and adds it to config.
1028
 */
1029 9aa8f6a8 jim-p
function crl_create(& $crl, $caref, $name, $serial = 0, $lifetime = 3650) {
1030 a3c15890 jim-p
	$max_lifetime = crl_get_max_lifetime();
1031 26308930 Marcos Mendoza
	$ca = lookup_ca($caref);
1032
	$ca = $ca['item'];
1033 1e0b1727 Phil Davis
	if (!$ca) {
1034 c5f010aa jim-p
		return false;
1035 1e0b1727 Phil Davis
	}
1036 f2a86ca9 jim-p
	$crl['descr'] = $name;
1037 c5f010aa jim-p
	$crl['caref'] = $caref;
1038
	$crl['serial'] = $serial;
1039 9aa8f6a8 jim-p
	$crl['lifetime'] = ($lifetime > $max_lifetime) ? $max_lifetime : $lifetime;
1040 c5f010aa jim-p
	$crl['cert'] = array();
1041 843ee1ac jim-p
1042 63d6bb4f Marcos Mendoza
	config_set_path('crl/', $crl);
1043 981d6364 jim-p
	return $crl;
1044 c5f010aa jim-p
}
1045
1046 26308930 Marcos Mendoza
/**
1047
 * @param array &$crl_config Must contain the CRL config index and item.
1048
 */
1049
function crl_update(& $crl_config) {
1050
	$crl = &$crl_config['item'];
1051 981d6364 jim-p
	require_once('ASN1.php');
1052
	require_once('ASN1_UTF8STRING.php');
1053
	require_once('ASN1_ASCIISTRING.php');
1054
	require_once('ASN1_BITSTRING.php');
1055
	require_once('ASN1_BOOL.php');
1056
	require_once('ASN1_GENERALTIME.php');
1057
	require_once('ASN1_INT.php');
1058
	require_once('ASN1_ENUM.php');
1059
	require_once('ASN1_NULL.php');
1060
	require_once('ASN1_OCTETSTRING.php');
1061
	require_once('ASN1_OID.php');
1062
	require_once('ASN1_SEQUENCE.php');
1063
	require_once('ASN1_SET.php');
1064
	require_once('ASN1_SIMPLE.php');
1065
	require_once('ASN1_TELETEXSTRING.php');
1066
	require_once('ASN1_UTCTIME.php');
1067
	require_once('OID.php');
1068
	require_once('X509.php');
1069
	require_once('X509_CERT.php');
1070
	require_once('X509_CRL.php');
1071
1072 a3c15890 jim-p
	$max_lifetime = crl_get_max_lifetime();
1073 26308930 Marcos Mendoza
	$ca = lookup_ca($crl['caref']);
1074
	$ca = $ca['item'];
1075 1e0b1727 Phil Davis
	if (!$ca) {
1076 c5f010aa jim-p
		return false;
1077 1e0b1727 Phil Davis
	}
1078 7b757d1b jim-p
	// If we have text but no certs, it was imported and cannot be updated.
1079 1e0b1727 Phil Davis
	if (($crl["method"] != "internal") && (!empty($crl['text']) && empty($crl['cert']))) {
1080 7b757d1b jim-p
		return false;
1081 1e0b1727 Phil Davis
	}
1082 c5f010aa jim-p
	$crl['serial']++;
1083 26308930 Marcos Mendoza
	config_set_path("crl/{$crl_config['idx']}", $crl);
1084 981d6364 jim-p
	$ca_cert = \Ukrbublik\openssl_x509_crl\X509::pem2der(base64_decode($ca['crt']));
1085
	$ca_pkey = openssl_pkey_get_private(base64_decode($ca['prv']));
1086
1087
	$crlconf = array(
1088
		'no' => $crl['serial'],
1089
		'version' => 2,
1090 9aa8f6a8 jim-p
		'days' => ($crl['lifetime'] > $max_lifetime) ? $max_lifetime : $crl['lifetime'],
1091 981d6364 jim-p
		'alg' => OPENSSL_ALGO_SHA1,
1092
		'revoked' => array()
1093
	);
1094
1095 4bc2c676 jim-p
	if (is_array($crl['cert']) && (count($crl['cert']) > 0)) {
1096
		foreach ($crl['cert'] as $cert) {
1097 63fb68d7 jim-p
			/* Determine the serial number to revoke */
1098
			if (isset($cert['serial'])) {
1099
				$serial = $cert['serial'];
1100
			} elseif (isset($cert['crt'])) {
1101
				$serial = cert_get_serial($cert['crt'], true);
1102
			} else {
1103
				continue;
1104
			}
1105 981d6364 jim-p
			$crlconf['revoked'][] = array(
1106 63fb68d7 jim-p
				'serial' => $serial,
1107
				'rev_date' => $cert['revoke_time'],
1108
				'reason' => ($cert['reason'] == -1) ? null : (int) $cert['reason'],
1109 981d6364 jim-p
			);
1110 4bc2c676 jim-p
		}
1111 c5f010aa jim-p
	}
1112 981d6364 jim-p
1113
	$crl_data = \Ukrbublik\openssl_x509_crl\X509_CRL::create($crlconf, $ca_pkey, $ca_cert);
1114
	$crl['text'] = base64_encode(\Ukrbublik\openssl_x509_crl\X509::der2pem4crl($crl_data));
1115 26308930 Marcos Mendoza
	config_set_path("crl/{$crl_config['idx']}", $crl);
1116 981d6364 jim-p
1117
	return $crl['text'];
1118 c5f010aa jim-p
}
1119
1120 26308930 Marcos Mendoza
/**
1121
 * @param array|string $cert        The cert item or cert serial to revoke.
1122
 * @param array        &$crl_config Must contain the CRL config index and item.
1123
 * @param int          $reason      Revocation reason; defined by RFC5280.
1124
 */
1125
function cert_revoke($cert, &$crl_config, $reason = -1) {
1126
	$crl = &$crl_config['item'];
1127 1e0b1727 Phil Davis
	if (is_cert_revoked($cert, $crl['refid'])) {
1128 c5f010aa jim-p
		return true;
1129 1e0b1727 Phil Davis
	}
1130 7b757d1b jim-p
	// If we have text but no certs, it was imported and cannot be updated.
1131 1e0b1727 Phil Davis
	if (!is_crl_internal($crl)) {
1132 7b757d1b jim-p
		return false;
1133 1e0b1727 Phil Davis
	}
1134 63fb68d7 jim-p
1135
	if (!is_array($cert)) {
1136
		/* If passed a not an array but a serial string, set it up as an
1137
		 * array with the serial number defined */
1138
		$rcert = array();
1139
		$rcert['serial'] = $cert;
1140
	} else {
1141
		/* If passed a certificate entry, read out the serial and store
1142
		 * it separately. */
1143
		$rcert = $cert;
1144
		$rcert['serial'] = cert_get_serial($cert['crt']);
1145
	}
1146
	$rcert['reason'] = $reason;
1147
	$rcert['revoke_time'] = time();
1148
	$crl['cert'][] = $rcert;
1149 26308930 Marcos Mendoza
	crl_update($crl_config);
1150 fb3f1993 jim-p
	return true;
1151 c5f010aa jim-p
}
1152
1153 26308930 Marcos Mendoza
/**
1154
 * @param array|string $cert        The cert item or cert serial to unrevoke.
1155
 * @param array        &$crl_config Must contain the CRL config index and item.
1156
 */
1157
function cert_unrevoke($cert, &$crl_config) {
1158
	$crl = &$crl_config['item'];
1159 1e0b1727 Phil Davis
	if (!is_crl_internal($crl)) {
1160 7b757d1b jim-p
		return false;
1161 1e0b1727 Phil Davis
	}
1162 63fb68d7 jim-p
1163
	$serial = crl_get_entry_serial($cert);
1164
1165 c5f010aa jim-p
	foreach ($crl['cert'] as $id => $rcert) {
1166 63fb68d7 jim-p
		/* Check for a match by refid, name, or serial number */
1167
		if (($rcert['refid'] == $cert['refid']) ||
1168
		    ($rcert['descr'] == $cert['descr']) ||
1169
		    (crl_get_entry_serial($rcert) == $serial)) {
1170 c5f010aa jim-p
			unset($crl['cert'][$id]);
1171 728003c8 jim-p
			if (count($crl['cert']) == 0) {
1172
				// Protect against accidentally switching the type to imported, for older CRLs
1173 1e0b1727 Phil Davis
				if (!isset($crl['method'])) {
1174 728003c8 jim-p
					$crl['method'] = "internal";
1175 1e0b1727 Phil Davis
				}
1176 26308930 Marcos Mendoza
				crl_update($crl_config);
1177 1e0b1727 Phil Davis
			} else {
1178 26308930 Marcos Mendoza
				crl_update($crl_config);
1179 1e0b1727 Phil Davis
			}
1180 c5f010aa jim-p
			return true;
1181
		}
1182
	}
1183
	return false;
1184
}
1185
1186 04761344 jim-p
/* Compare two certificates to see if they match. */
1187
function cert_compare($cert1, $cert2) {
1188
	/* Ensure two certs are identical by first checking that their issuers match, then
1189
		subjects, then serial numbers, and finally the moduli. Anything less strict
1190
		could accidentally count two similar, but different, certificates as
1191
		being identical. */
1192
	$c1 = base64_decode($cert1['crt']);
1193
	$c2 = base64_decode($cert2['crt']);
1194 ae52d165 Renato Botelho
	if ((cert_get_issuer($c1, false) == cert_get_issuer($c2, false)) &&
1195
	    (cert_get_subject($c1, false) == cert_get_subject($c2, false)) &&
1196
	    (cert_get_serial($c1, false) == cert_get_serial($c2, false)) &&
1197 1746c5ce PiBa-NL
	    (cert_get_publickey($c1, false) == cert_get_publickey($c2, false))) {
1198 04761344 jim-p
		return true;
1199 1e0b1727 Phil Davis
	}
1200 04761344 jim-p
	return false;
1201
}
1202
1203 63fb68d7 jim-p
/****f* certs/crl_get_entry_serial
1204
 * NAME
1205
 *   crl_get_entry_serial - Take a CRL entry and determine the associated serial
1206
 * INPUTS
1207
 *   $entry: CRL certificate list entry to inspect, or serial string
1208
 * RESULT
1209
 *   The requested serial string, if present, or null if it cannot be determined.
1210
 ******/
1211
1212
function crl_get_entry_serial($entry) {
1213
	/* Check the passed entry several ways to determine the serial */
1214
	if (isset($entry['serial']) && (strlen($entry['serial']) > 0)) {
1215
		/* Entry is an array with a viable 'serial' element */
1216
		return $entry['serial'];
1217
	} elseif (isset($entry['crt'])) {
1218
		/* Entry is an array with certificate text which can be used to
1219
		 * determine the serial */
1220
		return cert_get_serial($entry['crt'], true);
1221 a6bd9e78 jim-p
	} elseif (cert_validate_serial($entry, false, true) != null) {
1222 63fb68d7 jim-p
		/* Entry is a valid serial string */
1223
		return $entry;
1224
	}
1225
	/* Unable to find or determine a serial number */
1226
	return null;
1227
}
1228
1229
/****f* certs/cert_validate_serial
1230
 * NAME
1231
 *   cert_validate_serial - Validate a given string to test if it can be used as
1232
 *                          a certificate serial.
1233
 * INPUTS
1234
 *   $serial     : Serial number string to test
1235
 *   $returnvalue: Whether to return the parsed value or true/false
1236
 * RESULT
1237
 *   If $returnvalue is true, then the parsed ASN.1 integer value string for
1238
 *     $serial or null if invalid
1239
 *   If $returnvalue is false, then true/false based on whether or not $serial
1240
 *     is valid.
1241
 ******/
1242
1243 a6bd9e78 jim-p
function cert_validate_serial($serial, $returnvalue = false, $allowlarge = false) {
1244 63fb68d7 jim-p
	require_once('ASN1.php');
1245
	require_once('ASN1_INT.php');
1246
	/* The ASN.1 parsing function will throw an exception if the value is
1247
	 * invalid, so take advantage of that to catch other error as well. */
1248
	try {
1249
		/* If the serial is not a string, then do not bother with
1250
		 * further tests. */
1251
		if (!is_string($serial)) {
1252
			throw new Exception('Not a string');
1253
		}
1254
		/* Process a hex string */
1255
		if ((substr($serial, 0, 2) == '0x')) {
1256
			/* If the string is hex, then it must contain only
1257
			 * valid hex digits */
1258
			if (!ctype_xdigit(substr($serial, 2))) {
1259
				throw new Exception('Not a valid hex string');
1260
			}
1261
			/* Convert to decimal */
1262
			$serial = base_convert($serial, 16, 10);
1263
		}
1264 a6bd9e78 jim-p
1265
		/* Unfortunately, PHP openssl_csr_sign() limits serial numbers to a
1266
		 * PHP integer, so we cannot generate large numbers up to the maximum
1267
		 * allowed ASN.1 size (2^159). We are limited to PHP_INT_MAX --
1268
		 * As such, numbers larger than that limit should be rejected */
1269
		if ($serial > PHP_INT_MAX) {
1270
			throw new Exception('Serial too large for PHP OpenSSL');
1271
		}
1272
1273 63fb68d7 jim-p
		/* Attempt to create an ASN.1 integer, if it fails, an exception will be thrown */
1274
		$asn1serial = new \Ukrbublik\openssl_x509_crl\ASN1_INT( $serial );
1275
		return ($returnvalue) ? $asn1serial->content : true;
1276
	} catch (Exception $ex) {
1277 4864d7f6 Josh Soref
		/* No matter what the error is, return null or false depending
1278 63fb68d7 jim-p
		 * on what was requested. */
1279
		return ($returnvalue) ? null : false;
1280
	}
1281
}
1282
1283 2c9601c9 jim-p
/****f* certs/cert_generate_serial
1284
 * NAME
1285
 *   cert_generate_serial - Generate a random positive integer usable as a
1286
 *                          certificate serial number
1287
 * INPUTS
1288
 *   None
1289
 * RESULT
1290
 *   Integer representing an ASN.1 compatible certificate serial number.
1291
 ******/
1292
1293
function cert_generate_serial() {
1294
	/* Use a separate function for this to make it easier to use a better
1295
	 * randomization function in the future. */
1296
1297
	/* Unfortunately, PHP openssl_csr_sign() limits serial numbers to a
1298
	 * PHP integer, so we cannot generate large numbers up to the maximum
1299
	 * allowed ASN.1 size (2^159). We are limited to PHP_INT_MAX */
1300
	return random_int(1, PHP_INT_MAX);
1301
}
1302
1303
/****f* certs/ca_has_serial
1304
 * NAME
1305
 *   ca_has_serial - Check if a serial number is used by any certificate in a given CA
1306
 * INPUTS
1307
 *   $ca    : Certificate Authority to check
1308
 *   $serial: Serial number to check
1309
 * RESULT
1310
 *   true if the serial number is in use by a certificate issued by this CA,
1311
 *   false otherwise.
1312
 ******/
1313
1314
function ca_has_serial($caref, $serial) {
1315 4aa7c7ae jim-p
	/* Check certs first -- more likely to find a hit */
1316 ac0a027f Christian McDonald
	foreach (config_get_path('cert', []) as $cert) {
1317 2c9601c9 jim-p
		if (($cert['caref'] == $caref) &&
1318
		    (cert_get_serial($cert['crt'], true) == $serial)) {
1319
			/* If this certificate is issued by the CA in question
1320
			 * and has a matching serial number, stop processing
1321
			 * and return true. */
1322
			return true;
1323
		}
1324
	}
1325
1326 4864d7f6 Josh Soref
	/* Check the CA itself */
1327 4aa7c7ae jim-p
	$this_ca = lookup_ca($caref);
1328 26308930 Marcos Mendoza
	$this_ca = $this_ca['item'];
1329 4aa7c7ae jim-p
	$this_serial = cert_get_serial($this_ca['crt'], true);
1330
	if ($serial == $this_serial) {
1331
		return true;
1332
	}
1333
1334
	/* Check other CAs for a match (intermediates signed by this CA) */
1335 ac0a027f Christian McDonald
	foreach (config_get_path('ca', []) as $ca) {
1336 4aa7c7ae jim-p
		if (($ca['caref'] == $caref) &&
1337
		    (cert_get_serial($ca['crt'], true) == $serial)) {
1338
			/* If this CA is issued by the CA in question
1339
			 * and has a matching serial number, stop processing
1340
			 * and return true. */
1341
			return true;
1342
		}
1343
	}
1344
1345 2c9601c9 jim-p
	return false;
1346
}
1347
1348
/****f* certs/cert_get_random_serial
1349
 * NAME
1350
 *   cert_get_random_serial - Generate a random certificate serial unique in a CA
1351
 * INPUTS
1352
 *   $caref : Certificate Authority refid to test for serial uniqueness.
1353
 * RESULT
1354
 *   Random serial number which is not in use by any known certificate in a CA
1355
 ******/
1356
1357 4aa7c7ae jim-p
function cert_get_random_serial($caref = '') {
1358 2c9601c9 jim-p
	/* Number of attempts to generate a usable serial. Multiple attempts
1359
	 *  are necessary to ensure that the number is usable and unique. */
1360
	$attempts = 10;
1361
1362
	/* Default value, -1 indicates an error */
1363
	$serial = -1;
1364
1365
	for ($i=0; $i < $attempts; $i++) {
1366
		/* Generate a random serial */
1367
		$serial = cert_generate_serial();
1368
		/* Check that the serial number is usable and unique:
1369
		 *  * Cannot be 0
1370
		 *  * Must be a valid ASN.1 serial number
1371
		 *  * Cannot be used by any other certificate on this CA */
1372
		if (($serial != 0) &&
1373
		    cert_validate_serial($serial) &&
1374
		    !ca_has_serial($caref, $serial)) {
1375
			/* If all conditions are met, we have a good serial, so stop. */
1376
			break;
1377
		}
1378
	}
1379
	return $serial;
1380
}
1381
1382
/****f* certs/ca_get_next_serial
1383
 * NAME
1384
 *   ca_get_next_serial - Get the next available serial number for a CA
1385
 * INPUTS
1386
 *   $ca: Reference to a CA entry
1387
 * RESULT
1388
 *   A randomized serial number (if enabled for a CA) or the next sequential value.
1389
 ******/
1390
1391
function ca_get_next_serial(& $ca) {
1392
	$ca_serial = null;
1393
	/* Get a randomized serial if enabled */
1394
	if ($ca['randomserial'] == 'enabled') {
1395
		$ca_serial = cert_get_random_serial($ca['refid']);
1396
	}
1397
	/* Initialize the sequential serial to be safe */
1398
	if (empty($ca['serial'])) {
1399
		$ca['serial'] = 0;
1400
	}
1401
	/* If not using a randomized serial, or randomizing the serial
1402
	 * failed, then fall back to sequential serials. */
1403
	return (empty($ca_serial) || ($ca_serial == -1)) ? ++$ca['serial'] : $ca_serial;
1404
}
1405
1406 63fb68d7 jim-p
/****f* certs/crl_contains_cert
1407
 * NAME
1408
 *   crl_contains_cert - Check if a certificate is present in a CRL
1409
 * INPUTS
1410
 *   $crl : CRL to check
1411
 *   $cert: Certificate to test
1412
 * RESULT
1413
 *   true if the CRL contains the certificate, false otherwise
1414
 ******/
1415
1416
function crl_contains_cert($crl, $cert) {
1417 63d6bb4f Marcos Mendoza
	if (!is_array(config_get_path('crl')) ||
1418 63fb68d7 jim-p
	    !is_array($crl['cert'])) {
1419
		return false;
1420
	}
1421
1422
	/* Find the issuer of this CRL */
1423
	$ca = lookup_ca($crl['caref']);
1424 26308930 Marcos Mendoza
	$ca = $ca['item'];
1425 84041dcf jim-p
	$crlissuer = is_array($cert) ? cert_get_subject($ca['crt']) : null;
1426 63fb68d7 jim-p
	$serial = crl_get_entry_serial($cert);
1427
1428
	/* Skip issuer match when sarching by serial instead of certificate */
1429
	$issuer = is_array($cert) ? cert_get_issuer($cert['crt']) : null;
1430
1431
	/* If the requested certificate was not issued by the
1432
	 * same CA as the CRL, then do not bother checking this
1433
	 * CRL. */
1434
	if ($issuer != $crlissuer) {
1435
		return false;
1436
	}
1437
1438
	/* Check CRL entries to see if the certificate serial is revoked */
1439
	foreach ($crl['cert'] as $rcert) {
1440
		if (crl_get_entry_serial($rcert) == $serial) {
1441
			return true;
1442
		}
1443
	}
1444
1445
	/* Certificate was not found in the CRL */
1446
	return false;
1447
}
1448
1449
/****f* certs/is_cert_revoked
1450
 * NAME
1451
 *   is_cert_revoked - Test if a given certificate or serial is revoked
1452
 * INPUTS
1453
 *   $cert  : Certificate entry or serial number to test
1454
 *   $crlref: CRL to check for revoked entries, or empty to check all CRLs
1455
 * RESULT
1456
 *   true if the requested entry is revoked
1457
 *   false if the requested entry is not revoked
1458
 ******/
1459
1460 fb3f1993 jim-p
function is_cert_revoked($cert, $crlref = "") {
1461 63d6bb4f Marcos Mendoza
	if (!is_array(config_get_path('crl'))) {
1462 c5f010aa jim-p
		return false;
1463 1e0b1727 Phil Davis
	}
1464 c5f010aa jim-p
1465 fb3f1993 jim-p
	if (!empty($crlref)) {
1466 28ff7ace jim-p
		$crl = lookup_crl($crlref);
1467 26308930 Marcos Mendoza
		$crl = $crl['item'];
1468 63fb68d7 jim-p
		return crl_contains_cert($crl, $cert);
1469 fb3f1993 jim-p
	} else {
1470 63fb68d7 jim-p
		if (!is_array($cert)) {
1471
			/* If passed a serial, then it cannot be definitively
1472
			 * matched in this way since we do not know the CA
1473
			 * associated with the bare serial. */
1474
			return null;
1475
		}
1476
1477
		/* Check every CRL in the configuration for a match */
1478 ac0a027f Christian McDonald
		foreach (config_get_path('crl', []) as $crl) {
1479 1e0b1727 Phil Davis
			if (!is_array($crl['cert'])) {
1480 fb3f1993 jim-p
				continue;
1481 1e0b1727 Phil Davis
			}
1482 63fb68d7 jim-p
			if (crl_contains_cert($crl, $cert)) {
1483
				return true;
1484 fb3f1993 jim-p
			}
1485
		}
1486
	}
1487
	return false;
1488
}
1489
1490
function is_openvpn_server_crl($crlref) {
1491 35bf4437 Christian McDonald
	foreach (config_get_path('openvpn/openvpn-server', []) as $ovpns) {
1492 1e0b1727 Phil Davis
		if (!empty($ovpns['crlref']) && ($ovpns['crlref'] == $crlref)) {
1493 fb3f1993 jim-p
			return true;
1494 1e0b1727 Phil Davis
		}
1495 c5f010aa jim-p
	}
1496
	return false;
1497
}
1498
1499 1b1723da Mark Silinio
function is_package_crl($crlref) {
1500
	$pluginparams = array();
1501
	$pluginparams['type'] = 'certificates';
1502
	$pluginparams['event'] = 'used_crl';
1503
1504
	$certificates_used_by_packages = pkg_call_plugins('plugin_certificates', $pluginparams);
1505
1506
	/* Check if any package is using CRL */
1507
	foreach ($certificates_used_by_packages as $name => $package) {
1508
		if (is_array($package['certificatelist'][$crlref]) &&
1509 cfec2190 Mark Silinio
		    (count($package['certificatelist'][$crlref]) > 0)) {
1510 1b1723da Mark Silinio
			return true;
1511
		}
1512
	}
1513
}
1514
1515 fb3f1993 jim-p
// Keep this general to allow for future expansion. See cert_in_use() above.
1516
function crl_in_use($crlref) {
1517 1b1723da Mark Silinio
	return (is_openvpn_server_crl($crlref) ||
1518
		is_package_crl($crlref));
1519 fb3f1993 jim-p
}
1520
1521
function is_crl_internal($crl) {
1522 728003c8 jim-p
	return (!(!empty($crl['text']) && empty($crl['cert'])) || ($crl["method"] == "internal"));
1523 fb3f1993 jim-p
}
1524
1525 b34b2b7d jim-p
function cert_get_cn($crt, $isref = false) {
1526
	/* If this is a certref, not an actual cert, look up the cert first */
1527
	if ($isref) {
1528
		$cert = lookup_cert($crt);
1529 26308930 Marcos Mendoza
		$cert = $cert['item'];
1530 b34b2b7d jim-p
		/* If it's not a valid cert, bail. */
1531 1e0b1727 Phil Davis
		if (!(is_array($cert) && !empty($cert['crt']))) {
1532 b34b2b7d jim-p
			return "";
1533 1e0b1727 Phil Davis
		}
1534 b34b2b7d jim-p
		$cert = $cert['crt'];
1535
	} else {
1536
		$cert = $crt;
1537
	}
1538
	$sub = cert_get_subject_array($cert);
1539
	if (is_array($sub)) {
1540
		foreach ($sub as $s) {
1541 1e0b1727 Phil Davis
			if (strtoupper($s['a']) == "CN") {
1542 70defd0f Marcos Mendoza
				// Multiple CNs are not supported so use the first available; see RFC9525, RFC5280
1543
				return is_array($s['v']) ? $s['v'][array_key_first($s['v'])] : $s['v'];
1544 1e0b1727 Phil Davis
			}
1545 b34b2b7d jim-p
		}
1546
	}
1547
	return "";
1548
}
1549
1550 83d2b83a jim-p
function cert_escape_x509_chars($str, $reverse = false) {
1551
	/* Characters which need escaped when present in x.509 fields.
1552
	 * See https://www.ietf.org/rfc/rfc4514.txt
1553
	 *
1554
	 * The backslash (\) must be listed first in these arrays!
1555
	 */
1556
	$cert_directory_string_special_chars = array('\\', '"', '#', '+', ',', ';', '<', '=', '>');
1557
	$cert_directory_string_special_chars_esc = array('\\\\', '\"', '\#', '\+', '\,', '\;', '\<', '\=', '\>');
1558
	if ($reverse) {
1559
		return str_replace($cert_directory_string_special_chars_esc, $cert_directory_string_special_chars, $str);
1560
	} else {
1561
		/* First unescape and then escape again, to prevent possible double escaping. */
1562
		return str_replace($cert_directory_string_special_chars, $cert_directory_string_special_chars_esc, cert_escape_x509_chars($str, true));
1563
	}
1564
}
1565
1566 2e1809dd jim-p
function cert_add_altname_type($str) {
1567
	$type = "";
1568
	if (is_ipaddr($str)) {
1569
		$type = "IP";
1570 70b70f9d jim-p
	} elseif (is_hostname($str, true)) {
1571 2e1809dd jim-p
		$type = "DNS";
1572
	} elseif (is_URL($str)) {
1573
		$type = "URI";
1574
	} elseif (filter_var($str, FILTER_VALIDATE_EMAIL)) {
1575
		$type = "email";
1576
	}
1577
	if (!empty($type)) {
1578
		return "{$type}:" . cert_escape_x509_chars($str);
1579
	} else {
1580 e562fca2 jim-p
		return null;
1581 2e1809dd jim-p
	}
1582
}
1583
1584 0c82b8c2 jim-p
function cert_type_config_section($type) {
1585
	switch ($type) {
1586
		case "ca":
1587
			$cert_type = "v3_ca";
1588
			break;
1589
		case "server":
1590
		case "self-signed":
1591
			$cert_type = "server";
1592
			break;
1593
		default:
1594
			$cert_type = "usr_cert";
1595
			break;
1596
	}
1597
	return $cert_type;
1598
}
1599
1600 9e80dd44 jim-p
/****f* certs/is_cert_locally_renewable
1601
 * NAME
1602
 *   is_cert_locally_renewable - Check to see if an existing certificate can be
1603
 *                               renewed by a local internal CA.
1604
 * INPUTS
1605
 *   $cert : The certificate to be tested
1606
 * RESULT
1607
 *   true if the certificate can be locally renewed, false otherwise.
1608
 ******/
1609
1610
function is_cert_locally_renewable($cert) {
1611
	/* If there is no certificate or private key string, this entry is either
1612
	 * invalid or cannot be renewed. */
1613
	if (empty($cert['crt']) || empty($cert['prv'])) {
1614
		return false;
1615
	}
1616
1617
	/* Get subject and issuer values to test for self-signed state */
1618
	$subj = cert_get_subject($cert['crt']);
1619
	$issuer = cert_get_issuer($cert['crt']);
1620
1621
	/* Lookup CA for this certificate */
1622
	$ca = array();
1623
	if (!empty($cert['caref'])) {
1624
		$ca = lookup_ca($cert['caref']);
1625 26308930 Marcos Mendoza
		$ca = $ca['item'];
1626 9e80dd44 jim-p
	}
1627
1628
	/* If the CA exists and we have the private key, or if the cert is
1629
	 *  self-signed, then it can be locally renewed. */
1630
	return ((!empty($ca) && !empty($ca['prv'])) || ($subj == $issuer));
1631
}
1632
1633
/* Strict certificate requirements based on
1634
 * https://redmine.pfsense.org/issues/9825
1635
 */
1636
global $cert_strict_values;
1637
$cert_strict_values = array(
1638 f944f4a7 jim-p
	'max_server_cert_lifetime' => 398,
1639 9e80dd44 jim-p
	'digest_blacklist' => array('md4', 'RSA-MD4',  'md5', 'RSA-MD5', 'md5-sha1',
1640
					'mdc2', 'RSA-MDC2', 'sha1', 'RSA-SHA1',
1641 9484a1cb jim-p
					'RSA-SHA1-2', 'sha224', 'RSA-SHA224'),
1642 9e80dd44 jim-p
	'min_private_key_bits' => 2048,
1643 941470ef Viktor G
	'ec_curve' => 'prime256v1',
1644 9e80dd44 jim-p
);
1645
1646
/****f* certs/cert_renew
1647
 * NAME
1648
 *   cert_renew - Renew an existing internal CA or certificate
1649
 * INPUTS
1650 26308930 Marcos Mendoza
 *   $cert_config : An array containing the config array path and the ca or
1651
 *                  cert item; it is modified directly and written to config.
1652 9e80dd44 jim-p
 *   $reusekey : Whether or not to reuse the existing key for the certificate
1653
 *      true: Reuse the existing key (Default)
1654
 *      false: Generate a new key based on current (or enforced minimum) parameters
1655
 *   $strictsecurity : Whether or not to enforce stricter security for specific attributes
1656
 *      true: Enforce maximum lifetime for server certs, minimum digest type, and
1657
 *            minimum private key size. See https://redmine.pfsense.org/issues/9825
1658
 *      false: Use existing values as-is (Default).
1659
 * RESULT
1660
 *   true if successful, false if failure.
1661
 * NOTES
1662
 *   See https://redmine.pfsense.org/issues/9842 for more information on behavior.
1663
 *   Does NOT run write_config(), that must be performed by the caller.
1664
 ******/
1665
1666 26308930 Marcos Mendoza
function cert_renew(& $cert_config, $reusekey = true, $strictsecurity = false, $reuseserial = false) {
1667 009a3d4e jim-p
	global $cert_strict_values, $cert_curve_compatible, $curve_compatible_list;
1668 9e80dd44 jim-p
1669 26308930 Marcos Mendoza
	$cert = &$cert_config['item'];
1670 9e80dd44 jim-p
	/* If there is no certificate or private key string, this entry is either
1671
	 *  invalid or cannot be renewed by this function. */
1672
	if (empty($cert['crt']) || empty($cert['prv'])) {
1673
		return false;
1674
	}
1675
1676
	/* Read certificate information necessary to create a new request */
1677
	$cert_details = openssl_x509_parse(base64_decode($cert['crt']));
1678
1679
	/* No details, must not be valid in some way */
1680
	if (!array($cert_details) || empty($cert_details)) {
1681
		return false;
1682
	}
1683
1684
	$subj = cert_get_subject($cert['crt']);
1685
	$issuer = cert_get_issuer($cert['crt']);
1686 009a3d4e jim-p
	$purpose = cert_get_purpose($cert['crt']);
1687 9e80dd44 jim-p
1688
	$res_key = openssl_pkey_get_private(base64_decode($cert['prv']));
1689
	$key_details = openssl_pkey_get_details($res_key);
1690
1691
	/* Form a new Distinguished Name from the existing values.
1692
	 * Note: Deprecated/unsupported DN fields may not be carried forward, but
1693
	 *       may be preserved to avoid altering a subject.
1694
	 */
1695
	$subject_map = array(
1696
		'CN' => 'commonName',
1697
		'C' => 'countryName',
1698
		'ST' => 'stateOrProvinceName',
1699
		'L' => 'localityName',
1700
		'O' => 'organizationName',
1701
		'OU' => 'organizationalUnitName',
1702
		'emailAddress' => 'emailAddress', /* deprecated, but commonly found in older entries */
1703
	);
1704
	$dn = array();
1705
	/* This is necessary to ensure the order of subject components is
1706
	 * identical on the old and new certificate. */
1707
	foreach ($cert_details['subject'] as $p => $v) {
1708
		if (array_key_exists($p, $subject_map)) {
1709
			$dn[$subject_map[$p]] = $v;
1710
		}
1711
	}
1712
1713
	/* Test for self-signed or signed by a CA */
1714 03a84081 jim-p
	$selfsigned = ($subj == $issuer);
1715 9e80dd44 jim-p
1716
	/* Determine the type if it is not specified directly */
1717
	if (array_key_exists('serial', $cert)) {
1718
		/* If a serial value is present, this must be a CA */
1719
		$cert['type'] = 'ca';
1720
	} elseif (empty($cert['type'])) {
1721 009a3d4e jim-p
		/* Automatically determine certificate type if unset based on purpose value */
1722
		$cert['type'] = ($purpose['server'] == 'Yes') ? 'server' : 'user';
1723 9e80dd44 jim-p
	}
1724 26308930 Marcos Mendoza
	config_set_path("{$cert_config['path']}", $cert);
1725 9e80dd44 jim-p
1726
	/* Convert the internal certificate type to an openssl.cnf section name */
1727
	$cert_type = cert_type_config_section($cert['type']);
1728
1729
	/* Reuse lifetime (convert seconds to days) */
1730
	$lifetime = (int) round(($cert_details['validTo_time_t'] - $cert_details['validFrom_time_t']) / 86400);
1731
1732
	/* If we are enforcing strict security, then cap the lifetime for server certificates */
1733 009a3d4e jim-p
	if (($cert_type == 'server') && $strictsecurity &&
1734 9e80dd44 jim-p
	    ($lifetime > $cert_strict_values['max_server_cert_lifetime'])) {
1735
		$lifetime = $cert_strict_values['max_server_cert_lifetime'];
1736
	}
1737
1738
	/* Reuse SAN list, or, if empty, add CN as SAN. */
1739 3fdd559e Viktor Gurov
	$sans = str_replace("IP Address", "IP", $cert_details['extensions']['subjectAltName']);
1740 9e80dd44 jim-p
	if (empty($sans)) {
1741
		$sans = cert_add_altname_type($dn['commonName']);
1742
	}
1743
1744 09d3fe62 jim-p
	/* Do not setup SANs if the SAN list is empty (e.g. no SAN list and/or
1745
	 * CN cannot be mapped to a valid SAN type) */
1746
	if (!empty($sans)) {
1747
		if ($cert['type'] != 'ca') {
1748
			$cert_type .= '_san';
1749
		}
1750
		/* subjectAltName can be set _only_ via configuration file, so put the
1751
		 * value into the environment where it will be read from the configuration */
1752
		putenv("SAN={$sans}");
1753
	}
1754 9e80dd44 jim-p
1755 9484a1cb jim-p
	/* Determine current digest algorithm. */
1756
	$digest_alg = strtolower($cert_details['signatureTypeSN']);
1757
1758
	/* Check for and remove unnecessary ECDSA digest prefix
1759
	 * See https://redmine.pfsense.org/issues/13437 */
1760
	$ecdsa_prefix = 'ecdsa-with-';
1761
	if (substr($digest_alg, 0, strlen($ecdsa_prefix)) == $ecdsa_prefix) {
1762
		$digest_alg = substr($digest_alg, strlen($ecdsa_prefix));
1763
	}
1764
1765 9e80dd44 jim-p
	/* If we are enforcing strict security, then check the digest against a
1766
	 * blacklist of insecure digest methods. */
1767
	if ($strictsecurity &&
1768 3ad1e1cb jim-p
	    is_weak_digest($cert_details['signatureTypeSN'])) {
1769 9e80dd44 jim-p
		$digest_alg = 'sha256';
1770
	}
1771
1772
	/* Validate key type, assume RSA if it cannot be read. */
1773
	if (is_array($key_details) && array_key_exists('type', $key_details)) {
1774
		$private_key_type = $key_details['type'];
1775
	} else {
1776
		$private_key_type = OPENSSL_KEYTYPE_RSA;
1777
	}
1778
1779
	/* Setup certificate and key arguments */
1780
	$args = array(
1781
		"x509_extensions" => $cert_type,
1782
		"digest_alg" => $digest_alg,
1783
		"private_key_type" => $private_key_type,
1784
		"encrypt_key" => false);
1785
1786 941470ef Viktor G
	/* If we are enforcing strict security, then ensure the private key size
1787 00d9ce91 Viktor G
	 * is at least 2048 bits or NIST P-256 elliptic curve*/
1788 941470ef Viktor G
	$private_key_bits = $key_details['bits'];
1789
	if ($strictsecurity) {
1790
		if (($key_details['type'] == OPENSSL_KEYTYPE_RSA) &&
1791
		    ($private_key_bits < $cert_strict_values['min_private_key_bits'])) {
1792
			$private_key_bits = $cert_strict_values['min_private_key_bits'];
1793
			$reusekey = false;
1794 7ee29634 Viktor Gurov
		} else if (!in_array($key_details['ec']['curve_name'], $curve_compatible_list)) {
1795 941470ef Viktor G
			$ec_curve = $cert_strict_values['ec_curve'];
1796
			$reusekey = false;
1797
		}
1798
	}
1799
1800
	/* Set key parameters. */
1801
	if ($key_details['type'] ==  OPENSSL_KEYTYPE_RSA) {
1802
		$args['private_key_bits'] = (int)$private_key_bits;
1803
	} else if ($ec_curve) {
1804
		$args['curve_name'] = $ec_curve;
1805
	} else {
1806 9e80dd44 jim-p
		$args['curve_name'] = $key_details['ec']['curve_name'];
1807
	}
1808
1809
	/* Make a new key if necessary */
1810
	if (!$res_key || !$reusekey) {
1811
		$res_key = openssl_pkey_new($args);
1812
		if (!$res_key) {
1813
			return false;
1814
		}
1815
	}
1816
1817
	/* Create a new CSR from derived parameters and key */
1818
	$res_csr = openssl_csr_new($dn, $res_key, $args);
1819
	/* If the CSR could not be created, bail */
1820
	if (!$res_csr) {
1821
		return false;
1822
	}
1823
1824
	if (!empty($cert['caref'])) {
1825
		/* The certificate was signed by a CA, so read the CA details. */
1826 26308930 Marcos Mendoza
		$ca_item_config = lookup_ca($cert['caref']);
1827
		$ca = &$ca_item_config['item'];
1828 9e80dd44 jim-p
		/* If the referenced CA cannot be found, bail. */
1829
		if (!$ca) {
1830
			return false;
1831
		}
1832
		$ca_str_crt = base64_decode($ca['crt']);
1833
		$ca_str_key = base64_decode($ca['prv']);
1834
		$ca_res_crt = openssl_x509_read($ca_str_crt);
1835
		$ca_res_key = openssl_pkey_get_private(array(0 => $ca_str_key, 1 => ""));
1836
		if (!$ca_res_key) {
1837
			/* If the CA key cannot be read, bail. */
1838
			return false;
1839
		}
1840
		/* If the CA does not have a serial number, assume 0. */
1841
		if (empty($ca['serial'])) {
1842
			$ca['serial'] = 0;
1843
		}
1844 2c9601c9 jim-p
		/* Get the next available CA serial number. */
1845
		$ca_serial = ca_get_next_serial($ca);
1846 26308930 Marcos Mendoza
		config_set_path("ca/{$ca_item_config['idx']}", $ca);
1847
		if ($cert['type'] == 'ca') {
1848
			$cert = $ca;
1849
		}
1850 9e80dd44 jim-p
	} elseif ($selfsigned) {
1851
		/* For self-signed CAs & certificates, set the CA details to self and
1852
		 * use the key for this entry to sign itself.
1853
		 */
1854
		$ca_res_crt   = null;
1855
		$ca_res_key   = $res_key;
1856 4aa7c7ae jim-p
		/* Use random serial from this CA/Self-Signed Cert */
1857
		$ca_serial    = cert_get_random_serial($cert['refid']);
1858 9e80dd44 jim-p
	}
1859
1860 ab7ad5f9 jim-p
	/* Did the user choose to keep the serial? */
1861
	$ca_serial = ($reuseserial) ? cert_get_serial($cert['crt']) : $ca_serial;
1862
1863 9e80dd44 jim-p
	/* Sign the CSR */
1864
	$res_crt = openssl_csr_sign($res_csr, $ca_res_crt, $ca_res_key, $lifetime,
1865
				 $args, $ca_serial);
1866
	/* If the CSR could not be signed, bail */
1867
	if (!$res_crt) {
1868
		return false;
1869
	}
1870
1871
	/* Attempt to read the key and certificate and if that fails, bail */
1872
	if (!openssl_pkey_export($res_key, $str_key) ||
1873
	    !openssl_x509_export($res_crt, $str_crt)) {
1874
		return false;
1875
	}
1876
1877
	/* Load the new certificate string and key into the configuration */
1878
	$cert['crt'] = base64_encode($str_crt);
1879
	$cert['prv'] = base64_encode($str_key);
1880 26308930 Marcos Mendoza
	config_set_path("{$cert_config['path']}", $cert);
1881 9e80dd44 jim-p
1882
	return true;
1883
}
1884
1885 03a84081 jim-p
/****f* certs/cert_get_all_services
1886
 * NAME
1887
 *   cert_get_all_services - Locate services using a given certificate
1888
 * INPUTS
1889
 *   $refid: The refid of a certificate to check
1890
 * RESULT
1891
 *   array containing the services which use this certificate, including:
1892
 *     webgui: Present and true if the WebGUI uses this certificate. Unset otherwise.
1893
 *     services: Array of service definitions using this certificate, with:
1894
 *       name: Name of the service
1895
 *       extras: Extra information needed by some services, such as OpenVPN or Captive Portal.
1896
 *     packages: Array containing package names using this certificate.
1897
 ******/
1898
1899
function cert_get_all_services($refid) {
1900
	$services = array();
1901
	$services['services'] = array();
1902
	$services['packages'] = array();
1903
1904
	/* Only set if true, otherwise leave unset. */
1905
	if (is_webgui_cert($refid)) {
1906
		$services['webgui'] = true;
1907
	}
1908
1909
	/* Find all OpenVPN clients and servers which use this certificate */
1910 843ee1ac jim-p
	foreach (array('server', 'client') as $mode) {
1911
		foreach (config_get_path("openvpn/openvpn-{$mode}", []) as $ovpn) {
1912 03a84081 jim-p
			if ($ovpn['certref'] == $refid) {
1913
				/* OpenVPN instances are restarted individually,
1914
				 * so we need to note the mode and ID. */
1915
				$services['services'][] = array(
1916
					'name' => 'openvpn',
1917
					'extras' => array(
1918
						'vpnmode' => $mode,
1919
						'id' => $ovpn['vpnid']
1920
					)
1921
				);
1922
			}
1923
		}
1924
	}
1925
1926
	/* If any one IPsec tunnel uses this certificate then the whole service
1927
	 * needs a bump. */
1928 843ee1ac jim-p
1929
	foreach (config_get_path('ipsec/phase1', []) as $ipsec) {
1930 d1f5587d jim-p
		if (($ipsec['authentication_method'] == 'cert') &&
1931 03a84081 jim-p
		    ($ipsec['certref'] == $refid)) {
1932
			$services['services'][] = array('name' => 'ipsec');
1933
			/* Stop after finding one, no need to search for more. */
1934
			break;
1935
		}
1936
	}
1937
1938
	/* Check to see if any HTTPS-enabled Captive Portal zones use this
1939
	 * certificate. */
1940 843ee1ac jim-p
	foreach (config_get_path('captiveportal', []) as $zone => $portal) {
1941 03a84081 jim-p
		if (isset($portal['enable']) && isset($portal['httpslogin']) &&
1942
		    ($portal['certref'] == $refid)) {
1943
			/* Captive Portal zones are restarted individually, so
1944
			 * we need to note the zone name. */
1945
			$services['services'][] = array(
1946
				'name' => 'captiveportal',
1947
				'extras' => array(
1948
					'zone' => $zone,
1949
				)
1950
			);
1951
		}
1952
	}
1953
1954
	/* Locate any packages using this certificate */
1955
	$pkgcerts = pkg_call_plugins('plugin_certificates', array('type' => 'certificates', 'event' => 'used_certificates'));
1956
	foreach ($pkgcerts as $name => $package) {
1957
		if (is_array($package['certificatelist'][$refid]) &&
1958
		    isset($package['certificatelist'][$refid]) > 0) {
1959
			$services['packages'][] = $name;
1960
		}
1961
	}
1962
1963
	return $services;
1964
}
1965
1966
/****f* certs/ca_get_all_services
1967
 * NAME
1968
 *   ca_get_all_services - Locate services using a given certificate authority or its decendents
1969
 * INPUTS
1970
 *   $refid: The refid of a certificate authority to check
1971
 * RESULT
1972
 *   array containing the services which use this certificate authority, including:
1973
 *     webgui: Present and true if the WebGUI uses this certificate. Unset otherwise.
1974
 *     services: Array of service definitions using this certificate, with:
1975
 *       name: Name of the service
1976
 *       extras: Extra information needed by some services, such as OpenVPN or Captive Portal.
1977
 *     packages: Array containing package names using this certificate.
1978
 * NOTES
1979
 *   This searches recursively to find entries using this CA as well as intermediate
1980
 *   CAs and certificates signed by this CA, and returns a single set of all services.
1981
 *   This avoids restarting affected services multiple times when there is overlapping
1982
 *   usage.
1983
 ******/
1984
function ca_get_all_services($refid) {
1985
	$services = array();
1986
	$services['services'] = array();
1987
1988 843ee1ac jim-p
	foreach (array('server', 'client') as $mode) {
1989
		foreach (config_get_path("openvpn/openvpn-{$mode}", []) as $ovpn) {
1990 03a84081 jim-p
			if ($ovpn['caref'] == $refid) {
1991
				$services['services'][] = array(
1992
					'name' => 'openvpn',
1993
					'extras' => array(
1994
						'vpnmode' => $mode,
1995
						'id' => $ovpn['vpnid']
1996
					)
1997
				);
1998
			}
1999
		}
2000
	}
2001 843ee1ac jim-p
2002
	foreach (config_get_path('ipsec/phase1', []) as $ipsec) {
2003 03a84081 jim-p
		if ($ipsec['certref'] == $refid) {
2004
			break;
2005
		}
2006
	}
2007 843ee1ac jim-p
2008
	foreach (config_get_path('ipsec/phase1', []) as $ipsec) {
2009 d1f5587d jim-p
		if (($ipsec['authentication_method'] == 'cert') &&
2010 03a84081 jim-p
		    ($ipsec['caref'] == $refid)) {
2011
			$services['services'][] = array('name' => 'ipsec');
2012
			break;
2013
		}
2014
	}
2015
2016
	/* Loop through all certs and get their services as well */
2017 843ee1ac jim-p
	foreach (config_get_path('cert', []) as $cert) {
2018 03a84081 jim-p
		if ($cert['caref'] == $refid) {
2019
			$services = array_merge_recursive_unique($services, cert_get_all_services($cert['refid']));
2020
		}
2021
	}
2022
2023
	/* Look for intermediate certs and services */
2024 843ee1ac jim-p
	foreach (config_get_path('ca', []) as $cert) {
2025 03a84081 jim-p
		if ($cert['caref'] == $refid) {
2026
			$services = array_merge_recursive_unique($services, ca_get_all_services($cert['refid']));
2027
		}
2028
	}
2029
2030
	return $services;
2031
}
2032
2033
/****f* certs/cert_restart_services
2034
 * NAME
2035
 *   cert_restart_services - Restarts services specific to CA/Certificate usage
2036
 * INPUTS
2037
 *   $services: An array of services returned by cert_get_all_services or ca_get_all_services
2038
 * RESULT
2039
 *   Services in the given array are restarted
2040
 *   returns false if the input is invalid
2041
 *   returns true at the end of execution
2042
 ******/
2043
2044
function cert_restart_services($services) {
2045 81423583 jim-p
	require_once("service-utils.inc");
2046 03a84081 jim-p
	/* If the input is not an array, it is invalid. */
2047
	if (!is_array($services)) {
2048
		return false;
2049
	}
2050
2051
	/* Base string to log when restarting a service */
2052
	$restart_string = gettext('Restarting %s %s due to certificate change');
2053
2054
	/* Restart GUI: */
2055
	if ($services['webgui']) {
2056
		ob_flush();
2057
		flush();
2058
		log_error(sprintf($restart_string, gettext('service'), 'WebGUI'));
2059
		send_event("service restart webgui");
2060
	}
2061
2062
	/* Restart other base services: */
2063
	if (is_array($services['services'])) {
2064
		foreach ($services['services'] as $service) {
2065
			switch ($service['name']) {
2066
				case 'openvpn':
2067
					$service_name = "{$service['name']} {$service['extras']['vpnmode']} {$service['extras']['id']}";
2068
					break;
2069
				case 'captiveportal':
2070
					$service_name = "{$service['name']} zone {$service['extras']['zone']}";
2071
					break;
2072
				default:
2073
					$service_name = $service['name'];
2074
			}
2075
			log_error(sprintf($restart_string, gettext('service'), $service_name));
2076
			service_control_restart($service['name'], $service['extras']);
2077
		}
2078
	}
2079
2080
	/* Restart Packages: */
2081
	if (is_array($services['packages'])) {
2082
		foreach ($services['packages'] as $service) {
2083
			log_error(sprintf($restart_string, gettext('package'), $service));
2084
			restart_service($service);
2085
		}
2086
	}
2087
	return true;
2088
}
2089 b6196922 jim-p
2090 93f1121f jim-p
/****f* certs/cert_get_lifetime
2091
 * NAME
2092
 *   cert_get_lifetime - Returns the number of days the certificate is valid
2093
 * INPUTS
2094
 *   $untilexpire: Boolean
2095
 *     true: The number of days returned is from now until the certificate expiration.
2096
 *     false (default): The number of days returned is the total lifetime of the certificate.
2097
 * RESULT
2098
 *   Integer number of days in the certificate total or remaining lifetime
2099
 ******/
2100
2101
function cert_get_lifetime($cert, $untilexpire = false) {
2102
	/* If the certificate is not valid, bail. */
2103 b6196922 jim-p
	if (!is_array($cert) || empty($cert['crt'])) {
2104
		return null;
2105
	}
2106 93f1121f jim-p
	/* Read certificate details */
2107 b8b33a3e jim-p
	list($startdate, $enddate) = cert_get_dates($cert['crt'], true, false);
2108
2109 1120b85c jim-p
	/* If either of the dates are invalid, there is nothing we can do here. */
2110
	if (($startdate === false) || ($enddate === false)) {
2111
		return false;
2112
	}
2113
2114 93f1121f jim-p
	/* Determine which start time to use (now, or cert start) */
2115 b8b33a3e jim-p
	$startdate = ($untilexpire) ? new DateTime("now") : $startdate;
2116
2117
	/* Calculate the requested intervals */
2118
	$interval = $startdate->diff($enddate);
2119
2120 1120b85c jim-p
	/* DateTime diff is always positive, check if we need to negate the result. */
2121
	return ($startdate > $enddate) ? -1 * $interval->days : $interval->days;
2122 93f1121f jim-p
}
2123
2124
/****f* certs/cert_analyze_lifetime
2125
 * NAME
2126
 *   cert_analyze_lifetime - Analyze a certificate lifetime for expiration notices
2127
 * INPUTS
2128
 *   $expiredays: Number of days until the certificate expires (See cert_get_lifetime())
2129
 * RESULT
2130
 *   An array of two entries:
2131
 *   0/$lrclass: A bootstrap name for use with classes like text-<x>
2132
 *   1/$expstring: A text analysis describing the expiration timeframe.
2133
 ******/
2134
2135
function cert_analyze_lifetime($expiredays) {
2136 843ee1ac jim-p
	global $g;
2137 38e7b336 jim-p
	/* Number of days at which to warn of expiration. */
2138 2568e151 Christian McDonald
	$warning_days = config_get_path('notifications/certexpire/expiredays', g_get('default_cert_expiredays'));
2139 93f1121f jim-p
2140
	if ($expiredays > $warning_days) {
2141
		/* Not expiring soon */
2142
		$lrclass = 'normal';
2143
		$expstring = gettext("%d %s until expiration");
2144 a86ab279 ilmarranen
	} elseif ($expiredays >= 0) {
2145 93f1121f jim-p
		/* Still valid but expiring soon */
2146
		$lrclass = 'warning';
2147
		$expstring = gettext("Expiring soon, in %d %s");
2148
	} else {
2149
		/* Certificate has expired */
2150
		$lrclass = 'danger';
2151
		$expstring = gettext("Expired %d %s ago");
2152
	}
2153
	$days = (abs($expiredays) == 1) ? gettext('day') : gettext('days');
2154
	$expstring = sprintf($expstring, abs($expiredays), $days);
2155
	return array($lrclass, $expstring);
2156
}
2157
2158
/****f* certs/cert_print_dates
2159
 * NAME
2160
 *   cert_print_dates - Print the start and end timestamps for the given certificate
2161
 * INPUTS
2162
 *   $cert: CA or Cert entry for which the dates will be printed
2163
 * RESULT
2164
 *   Returns null if the passed entry is invalid
2165
 *   Otherwise, outputs the dates to the user with formatting.
2166
 ******/
2167
2168 46bd32bb Steve Beaver
function cert_print_dates($cert) {
2169 93f1121f jim-p
	/* If the certificate is not valid, bail. */
2170
	if (!is_array($cert) || empty($cert['crt'])) {
2171
		return null;
2172
	}
2173
	/* Attempt to extract the dates from the certificate */
2174
	list($startdate, $enddate) = cert_get_dates($cert['crt']);
2175
	/* If either of the timestamps are empty, then do not print anything.
2176
	 * The entry may not be valid or it may just be missing date information */
2177
	if (empty($startdate) || empty($enddate)) {
2178
		return null;
2179
	}
2180
	/* Get the expiration days */
2181
	$expiredays = cert_get_lifetime($cert, true);
2182
	/* Analyze the lifetime value */
2183
	list($lrclass, $expstring) = cert_analyze_lifetime($expiredays);
2184
	/* Output the dates, with a tooltip showing days until expiration, and
2185
	 * a visual indication of warning/expired status. */
2186 46bd32bb Steve Beaver
	?>
2187
	<br />
2188
	<small>
2189
	<?=gettext("Valid From")?>: <b><?=$startdate ?></b><br />
2190
	<?=gettext("Valid Until")?>:
2191
	<span class="text-<?=$lrclass?>" data-toggle="tooltip" data-placement="bottom" title="<?= $expstring ?>">
2192
	<b><?=$enddate ?></b>
2193
	</span>
2194
	</small>
2195 88774881 Christian McDonald
<?php
2196 b6196922 jim-p
}
2197
2198 93f1121f jim-p
/****f* certs/cert_print_infoblock
2199
 * NAME
2200
 *   cert_print_infoblock - Print an information block containing certificate details
2201
 * INPUTS
2202
 *   $cert: CA or Cert entry for which the information will be printed
2203
 * RESULT
2204
 *   Returns null if the passed entry is invalid
2205
 *   Otherwise, outputs information to the user with formatting.
2206
 ******/
2207
2208 b6196922 jim-p
function cert_print_infoblock($cert) {
2209 93f1121f jim-p
	/* If the certificate is not valid, bail. */
2210 b6196922 jim-p
	if (!is_array($cert) || empty($cert['crt'])) {
2211
		return null;
2212
	}
2213 93f1121f jim-p
	/* Variable to hold the formatted information */
2214 b6196922 jim-p
	$certextinfo = "";
2215 93f1121f jim-p
2216
	/* Serial number */
2217 b6196922 jim-p
	$cert_details = openssl_x509_parse(base64_decode($cert['crt']));
2218
	if (isset($cert_details['serialNumber']) && (strlen($cert_details['serialNumber']) > 0)) {
2219
		$certextinfo .= '<b>' . gettext("Serial: ") . '</b> ';
2220
		$certextinfo .= htmlspecialchars(cert_escape_x509_chars($cert_details['serialNumber'], true));
2221
		$certextinfo .= '<br/>';
2222
	}
2223 93f1121f jim-p
2224
	/* Digest type */
2225 b6196922 jim-p
	$certsig = cert_get_sigtype($cert['crt']);
2226
	if (is_array($certsig) && !empty($certsig) && !empty($certsig['shortname'])) {
2227
		$certextinfo .= '<b>' . gettext("Signature Digest: ") . '</b> ';
2228 3ad1e1cb jim-p
		$is_weak = is_weak_digest($certsig['shortname']);
2229
		$csgclass = ($is_weak) ? 'warning' : 'normal';
2230
		$certextinfo .= '<span class="text-' . $csgclass . '" data-placement="bottom">';
2231 b6196922 jim-p
		$certextinfo .= htmlspecialchars(cert_escape_x509_chars($certsig['shortname'], true));
2232 3ad1e1cb jim-p
		if ($is_weak) {
2233
			$certextinfo .= ' (' . gettext("Weak Digest") . ')';
2234
		}
2235
		$certextinfo .= '</span>';
2236 b6196922 jim-p
		$certextinfo .= '<br/>';
2237
	}
2238 93f1121f jim-p
2239
	/* Subject Alternative Name (SAN) list */
2240 b6196922 jim-p
	$sans = cert_get_sans($cert['crt']);
2241
	if (is_array($sans) && !empty($sans)) {
2242
		$certextinfo .= '<b>' . gettext("SAN: ") . '</b> ';
2243
		$certextinfo .= htmlspecialchars(implode(', ', cert_escape_x509_chars($sans, true)));
2244
		$certextinfo .= '<br/>';
2245
	}
2246 93f1121f jim-p
2247
	/* Key usage */
2248 b6196922 jim-p
	$purpose = cert_get_purpose($cert['crt']);
2249
	if (is_array($purpose) && !empty($purpose['ku'])) {
2250
		$certextinfo .= '<b>' . gettext("KU: ") . '</b> ';
2251
		$certextinfo .= htmlspecialchars(implode(', ', $purpose['ku']));
2252
		$certextinfo .= '<br/>';
2253
	}
2254 93f1121f jim-p
2255
	/* Extended key usage */
2256 b6196922 jim-p
	if (is_array($purpose) && !empty($purpose['eku'])) {
2257
		$certextinfo .= '<b>' . gettext("EKU: ") . '</b> ';
2258
		$certextinfo .= htmlspecialchars(implode(', ', $purpose['eku']));
2259
		$certextinfo .= '<br/>';
2260
	}
2261 93f1121f jim-p
2262
	/* OCSP / Must Staple */
2263 b6196922 jim-p
	if (cert_get_ocspstaple($cert['crt'])) {
2264
		$certextinfo .= '<b>' . gettext("OCSP: ") . '</b> ';
2265
		$certextinfo .= gettext("Must Staple");
2266 b0790fc0 jim-p
		$certextinfo .= '<br/>';
2267 b6196922 jim-p
	}
2268 93f1121f jim-p
2269
	/* Private key information */
2270 b6196922 jim-p
	if (!empty($cert['prv'])) {
2271
		$res_key = openssl_pkey_get_private(base64_decode($cert['prv']));
2272
		$certextinfo .= '<b>' . gettext("Key Type: ") . '</b> ';
2273 55dc0070 jim-p
		if ($res_key) {
2274
			$key_details = openssl_pkey_get_details($res_key);
2275
			/* Key type (RSA or EC) */
2276
			if ($key_details['type'] == OPENSSL_KEYTYPE_RSA) {
2277
				/* RSA Key size */
2278
				$certextinfo .= 'RSA<br/>';
2279
				$certextinfo .= '<b>' . gettext("Key Size: ") . '</b> ';
2280
				$certextinfo .= $key_details['bits'] . '<br/>';
2281
			} else {
2282
				/* Elliptic curve (EC) key curve name */
2283
				$certextinfo .= 'ECDSA<br/>';
2284
				$curve = cert_get_pkey_curve($cert['prv']);
2285
				if (!empty($curve)) {
2286
					$certextinfo .= '<b>' . gettext("Elliptic curve name:") . ' </b>';
2287
					$certextinfo .= $curve . '<br/>';
2288
				}
2289 9dfd57c0 jim-p
			}
2290 55dc0070 jim-p
		} else {
2291
			$certextinfo .= '<i>' . gettext("Unknown (Key could not be parsed)") . '</i><br/>';
2292 b6196922 jim-p
		}
2293
	}
2294 93f1121f jim-p
2295
	/* Distinguished name (DN) */
2296 b6196922 jim-p
	if (!empty($cert_details['name'])) {
2297
		$certextinfo .= '<b>' . gettext("DN: ") . '</b> ';
2298 8abff49b Viktor G
		/* UTF8 DN support, see https://redmine.pfsense.org/issues/12041 */
2299
		$certdnstring = preg_replace_callback('/\\\\x([0-9A-F]{2})/', function ($a) { return pack('H*', $a[1]); }, $cert_details['name']);
2300
		$certextinfo .= htmlspecialchars(cert_escape_x509_chars($certdnstring, true));
2301 b6196922 jim-p
		$certextinfo .= '<br/>';
2302
	}
2303 93f1121f jim-p
2304
	/* Hash value */
2305 b6196922 jim-p
	if (!empty($cert_details['hash'])) {
2306
		$certextinfo .= '<b>' . gettext("Hash: ") . '</b> ';
2307
		$certextinfo .= htmlspecialchars(cert_escape_x509_chars($cert_details['hash'], true));
2308
		$certextinfo .= '<br/>';
2309
	}
2310 93f1121f jim-p
2311
	/* Subject Key Identifier (SKID) */
2312 b6196922 jim-p
	if (is_array($cert_details["extensions"]) && !empty($cert_details["extensions"]["subjectKeyIdentifier"])) {
2313
		$certextinfo .= '<b>' . gettext("Subject Key ID: ") . '</b> ';
2314
		$certextinfo .= htmlspecialchars(cert_escape_x509_chars($cert_details["extensions"]["subjectKeyIdentifier"], true));
2315
		$certextinfo .= '<br/>';
2316
	}
2317 93f1121f jim-p
2318
	/* Authority Key Identifier (AKID) */
2319 b6196922 jim-p
	if (is_array($cert_details["extensions"]) && !empty($cert_details["extensions"]["authorityKeyIdentifier"])) {
2320
		$certextinfo .= '<b>' . gettext("Authority Key ID: ") . '</b> ';
2321
		$certextinfo .= str_replace("\n", '<br/>', htmlspecialchars(cert_escape_x509_chars($cert_details["extensions"]["authorityKeyIdentifier"], true)));
2322
		$certextinfo .= '<br/>';
2323
	}
2324 93f1121f jim-p
2325
	/* Total Lifetime (days from cert start to end) */
2326
	$lifetime = cert_get_lifetime($cert);
2327 29804b9e jim-p
	if ($lifetime !== false) {
2328
		$certextinfo .= '<b>' . gettext("Total Lifetime: ") . '</b> ';
2329
		$certextinfo .= sprintf("%d %s", $lifetime, (abs($lifetime) == 1) ? gettext('day') : gettext('days'));
2330
		$certextinfo .= '<br/>';
2331 93f1121f jim-p
2332 29804b9e jim-p
		/* Lifetime before certificate expires (days from now to end) */
2333
		$expiredays = cert_get_lifetime($cert, true);
2334
		list($lrclass, $expstring) = cert_analyze_lifetime($expiredays);
2335
		$certextinfo .= '<b>' . gettext("Lifetime Remaining: ") . '</b> ';
2336
		$certextinfo .= "<span class=\"text-{$lrclass}\">{$expstring}</span>";
2337
		$certextinfo .= '<br/>';
2338
	}
2339 93f1121f jim-p
2340 94ce250e jim-p
	if ($purpose['ca'] == 'Yes') {
2341
		/* CA Trust store presence */
2342
		$certextinfo .= '<b>' . gettext("Trust Store: ") . '</b> ';
2343
		$certextinfo .= (isset($cert['trust']) && ($cert['trust'] == "enabled")) ? gettext('Included') : gettext('Excluded');
2344
		$certextinfo .= '<br/>';
2345
2346
		if (!empty($cert['prv'])) {
2347
			/* CA Next/Randomize Serial */
2348
			$certextinfo .= '<b>' . gettext("Next Serial: ") . '</b> ';
2349
			$certextinfo .= (isset($cert['randomserial']) && ($cert['randomserial'] == "enabled")) ? gettext('Randomized') : $cert['serial'];
2350
			$certextinfo .= '<br/>';
2351
		}
2352
	}
2353 7daab3d8 jim-p
2354 46bd32bb Steve Beaver
	/* Output the infoblock */
2355
	if (!empty($certextinfo)) { ?>
2356
		<div class="infoblock">
2357
		<? print_info_box($certextinfo, 'info', false); ?>
2358
		</div>
2359
	<?php
2360
	}
2361 b6196922 jim-p
}
2362
2363 b5d2d8d8 jim-p
/****f* certs/cert_notify_expiring
2364
 * NAME
2365
 *   cert_notify_expiring - Notify admin about expiring certificates
2366
 * INPUTS
2367
 *   None
2368
 * RESULT
2369
 *   File a notice containing expiring certificate information, which is then
2370
 *   logged, displayed in the GUI, and sent via e-mail (if enabled).
2371
 ******/
2372
2373
function cert_notify_expiring() {
2374
	/* If certificate expiration notifications are disabled, there is nothing to do. */
2375 843ee1ac jim-p
	if (config_get_path('notifications/certexpire/enable') == "disabled") {
2376 b5d2d8d8 jim-p
		return;
2377
	}
2378
2379
	$notifications = array();
2380
2381
	/* Check all CA and Cert entries at once */
2382 b8f18410 Marcos Mendoza
	$all_certs = array_merge_recursive(config_get_path('ca', []), config_get_path('cert', []));
2383 b5d2d8d8 jim-p
2384
	foreach ($all_certs as $cert) {
2385 b8b33a3e jim-p
		if (empty($cert)) {
2386
			continue;
2387
		}
2388 4ed695f2 ilmarranen
		/* Proceed only for not revoked certificate if ignore setting enabled */
2389 843ee1ac jim-p
		if ((config_get_path('notifications/certexpire/ignore_revoked') == "enabled") &&
2390
		    is_cert_revoked($cert)) {
2391 226cb195 ilmarranen
			continue;
2392
		}
2393 b5d2d8d8 jim-p
		/* Fetch and analyze expiration */
2394
		$expiredays = cert_get_lifetime($cert, true);
2395
		/* If the result is null, then the lifetime data is missing, so skip the invalid entry. */
2396 a86ab279 ilmarranen
		if ($expiredays === null) {
2397 b5d2d8d8 jim-p
			continue;
2398
		}
2399
		list($lrclass, $expstring) = cert_analyze_lifetime($expiredays);
2400
		/* Only notify if the certificate is expiring soon, or has
2401
		 * already expired */
2402
		if ($lrclass != 'normal') {
2403
			$notify_string = (array_key_exists('serial', $cert)) ? gettext('Certificate Authority') : gettext('Certificate');
2404
			$notify_string .= ": {$cert['descr']} ({$cert['refid']}): {$expstring}";
2405
			$notifications[] = $notify_string;
2406
		}
2407
	}
2408
	if (!empty($notifications)) {
2409
		$message = gettext("The following CA/Certificate entries are expiring:") . "\n" .
2410
			implode("\n", $notifications);
2411
		file_notice("Certificate Expiration", $message, "Certificate Manager");
2412
	}
2413
}
2414
2415 7daab3d8 jim-p
/****f* certs/ca_setup_trust_store
2416
 * NAME
2417
 *   ca_setup_trust_store - Setup local CA trust store so that CA entries in the
2418
 *                          configuration may be trusted by the operating system.
2419
 * INPUTS
2420
 *   None
2421
 * RESULT
2422
 *   CAs marked as trusted in the configuration will be setup in the OS trust store.
2423
 ******/
2424
2425
function ca_setup_trust_store() {
2426
	/* This directory is trusted by OpenSSL on FreeBSD by default */
2427 27fc5a30 jim-p
	$trust_store_directory = '/usr/local/etc/ssl/certs';
2428 7daab3d8 jim-p
2429
	/* Create the directory if it does not already exist, and clean it up if it does. */
2430
	safe_mkdir($trust_store_directory);
2431 27fc5a30 jim-p
	unlink_if_exists("{$trust_store_directory}/*.crt");
2432
	unlink_if_exists("{$trust_store_directory}/*.crl");
2433 7daab3d8 jim-p
2434 843ee1ac jim-p
	foreach (config_get_path('ca', []) as $ca) {
2435 7daab3d8 jim-p
		/* If the entry is invalid or is not trusted, skip it. */
2436
		if (!is_array($ca) ||
2437
		    empty($ca['crt']) ||
2438
		    !isset($ca['trust']) ||
2439
		    ($ca['trust'] != "enabled")) {
2440
			continue;
2441
		}
2442
2443 27fc5a30 jim-p
		ca_setup_capath($ca, $trust_store_directory, '', false, 'crt', 'crl');
2444 7daab3d8 jim-p
	}
2445 27fc5a30 jim-p
2446 72c441e9 jim-p
	mwexec_bg('/usr/sbin/certctl rehash');
2447 7daab3d8 jim-p
}
2448
2449 348c2af1 jim-p
/****f* certs/ca_setup_capath
2450
 * NAME
2451
 *   ca_setup_capath - Setup CApath structure so that CA chains and related CRLs
2452
 *                     may be written and validated by the -CApath option in
2453
 *                     OpenSSL and other compatible validators.
2454
 * INPUTS
2455
 *   $ca     : A CA (not a refid) to write
2456
 *   $basedir: The directory which will contain the CA structure.
2457
 *   $crl    : A CRL (not a refid) associated with the CA to write.
2458 475d712b jim-p
 *   $refresh: Refresh CRLs -- When true, perform no cleanup and increment suffix
2459 27fc5a30 jim-p
 *   $crtext : Certificate file extension
2460
 *   $crlext : CRL file extension
2461 348c2af1 jim-p
 * RESULT
2462
 *   $basedir is populated with CA and CRL files in a format usable by OpenSSL
2463
 *   CApath. This has the filenames as the CA hash with the CA named <hash>.0
2464
 *   and CRLs named <hash>.r0
2465
 ******/
2466
2467 27fc5a30 jim-p
function ca_setup_capath($ca, $basedir, $crl = "", $refresh = false, $crtext = '0', $crlext = 'r') {
2468 348c2af1 jim-p
	/* Check for an invalid CA */
2469
	if (!$ca || !is_array($ca)) {
2470
		return false;
2471
	}
2472
	/* Check for an invalid CRL, but do not consider it fatal if it's wrong */
2473
	if (!$crl || !is_array($crl) || ($crl['caref'] != $ca['refid'])) {
2474
		unset($crl);
2475
	}
2476
2477
	/* Check for an empty base directory, which is invalid */
2478
	if (empty($basedir)) {
2479
		return false;
2480
	}
2481
2482
	/* Ensure that $basedir exists and is a directory */
2483
	if (!is_dir($basedir)) {
2484
		/* If it's a file, remove it, otherwise the directory cannot
2485
		 * be created. */
2486
		if (file_exists($basedir)) {
2487
			@unlink_if_exists($basedir);
2488
		}
2489
		@safe_mkdir($basedir);
2490
	}
2491
	/* Decode the certificate contents */
2492
	$cert_contents = base64_decode($ca['crt']);
2493
	/* Get hash value to use for filename */
2494
	$cert_details = openssl_x509_parse($cert_contents);
2495
	$fprefix = "{$basedir}/{$cert_details['hash']}";
2496
2497
2498 27fc5a30 jim-p
	$ca_filename = "{$fprefix}.{$crtext}";
2499 475d712b jim-p
	/* Cleanup old CA/CRL files for this hash */
2500
	@unlink_if_exists($ca_filename);
2501
	/* Write CA to base dir and ensure it has correct permissions. */
2502 348c2af1 jim-p
	file_put_contents($ca_filename, $cert_contents);
2503
	chmod($ca_filename, 0644);
2504
	chown($ca_filename, 'root');
2505
	chgrp($ca_filename, 'wheel');
2506
2507
	/* If there is a CRL, process it. */
2508
	if ($crl) {
2509 27fc5a30 jim-p
		$crl_filename = "{$fprefix}.{$crlext}";
2510 475d712b jim-p
		if (!$refresh) {
2511
			/* Cleanup old CA/CRL files for this hash */
2512
			@unlink_if_exists("{$crl_filename}*");
2513
		}
2514 27fc5a30 jim-p
2515
		if ($crlext == 'r') {
2516
			/* Find next suffix based on how many existing files there are (start=0) */
2517
			$crl_filename .= count(glob("{$crl_filename}*"));
2518
		}
2519
2520 475d712b jim-p
		/* Write CRL to base dir and ensure it has correct permissions. */
2521 348c2af1 jim-p
		file_put_contents($crl_filename, base64_decode($crl['text']));
2522
		chmod($crl_filename, 0644);
2523
		chown($crl_filename, 'root');
2524
		chgrp($crl_filename, 'wheel');
2525
	}
2526
2527
	return true;
2528
}
2529
2530 cffcf9bf jim-p
/****f* certs/cert_get_pkey_curve
2531
 * NAME
2532
 *   cert_get_pkey_curve - Get the ECDSA curve of a private key
2533
 * INPUTS
2534
 *   $pkey  : The private key to check
2535
 *   $decode: true: base64 decode the string before use, false to use as-is.
2536
 * RESULT
2537
 *   false if the private key is not ECDSA or the private key is not present.
2538
 *   Otherwise, the name of the ECDSA curve used for the private key.
2539
 ******/
2540
2541
function cert_get_pkey_curve($pkey, $decode = true) {
2542
	if ($decode) {
2543
		$pkey = base64_decode($pkey);
2544
	}
2545
2546
	/* Attempt to read the private key, and if successful, its details. */
2547
	$res_key = openssl_pkey_get_private($pkey);
2548
	if ($res_key) {
2549
		$key_details = openssl_pkey_get_details($res_key);
2550
		/* If this is an EC key, and the curve name is not empty, return
2551
		 * that curve name. */
2552 9dfd57c0 jim-p
		if ($key_details['type'] ==  OPENSSL_KEYTYPE_EC) {
2553
			if (!empty($key_details['ec']['curve_name'])) {
2554
				return $key_details['ec']['curve_name'];
2555
			} else {
2556
				return $key_details['ec']['curve_oid'];
2557
			}
2558 cffcf9bf jim-p
		}
2559
	}
2560
2561
	/* Either the private key could not be read, or this is not an EC certificate. */
2562
	return false;
2563
}
2564
2565
/* Array containing ECDSA curve names allowed in certain contexts. For instance,
2566
 * HTTPS servers and web browsers only support specific curves in TLSv1.3. */
2567 7ee29634 Viktor Gurov
global $cert_curve_compatible, $curve_compatible_list;
2568 cffcf9bf jim-p
$cert_curve_compatible = array(
2569 bc3e78ab jim-p
	/* HTTPS list per TLSv1.3 spec and Mozilla compatibility list */
2570
	'HTTPS' => array('prime256v1', 'secp384r1'),
2571
	/* IPsec/EAP/TLS list per strongSwan docs/issues */
2572
	'IPsec' => array('prime256v1', 'secp384r1', 'secp521r1'),
2573 ca3cddbe jim-p
	/* OpenVPN bug limits usable curves, see https://redmine.pfsense.org/issues/9744 */
2574
	'OpenVPN' => array('prime256v1', 'secp384r1', 'secp521r1'),
2575 cffcf9bf jim-p
);
2576 22c89db3 Reid Linnemann
$curve_compatible_list = array_unique(call_user_func_array('array_merge', array_values($cert_curve_compatible)));
2577 cffcf9bf jim-p
2578
/****f* certs/cert_build_curve_list
2579
 * NAME
2580
 *   cert_build_curve_list - Build an option list of ECDSA curves with notations
2581
 *                           about known compatible uses.
2582
 * INPUTS
2583
 *   None
2584
 * RESULT
2585
 *   Returns an option list of OpenSSL EC names with added notes. This can be
2586
 *   used directly in form option selection lists.
2587
 ******/
2588
2589
function cert_build_curve_list() {
2590
	global $cert_curve_compatible;
2591
	/* Get the default list of curve names */
2592
	$openssl_ecnames = openssl_get_curve_names();
2593
	/* Turn this into a hashed array where key==value */
2594
	$curvelist = array_combine($openssl_ecnames, $openssl_ecnames);
2595
	/* Check all known compatible curves and note matches */
2596
	foreach ($cert_curve_compatible as $consumer => $validcurves) {
2597
		/* $consumer will be a name like HTTPS or IPsec
2598
		 * $validcurves will be an array of curves compatible with the consumer */
2599
		foreach ($validcurves as $vc) {
2600
			/* If the valid curve is present in the curve list, add
2601
			 * a note with the consumer name to the value visible to
2602
			 * the user. */
2603
			if (array_key_exists($vc, $curvelist)) {
2604
				$curvelist[$vc] .= " [{$consumer}]";
2605
			}
2606
		}
2607
	}
2608
	return $curvelist;
2609
}
2610
2611
/****f* certs/cert_check_pkey_compatibility
2612
 * NAME
2613
 *   cert_check_pkey_compatibility - Check a private key to see if it can be
2614
 *                                   used in a specific compatible context.
2615
 * INPUTS
2616
 *   $pkey    : The private key to check
2617
 *   $consumer: The consumer name used to validate the curve. See the contents
2618
 *                 of $cert_curve_compatible for details.
2619
 * RESULT
2620
 *   true if the private key may be used in requested area, or if there are no
2621
 *        restrictions.
2622
 *   false if the private key cannot be used.
2623
 ******/
2624
2625
function cert_check_pkey_compatibility($pkey, $consumer) {
2626
	global $cert_curve_compatible;
2627
2628
	/* Read the curve name from the key */
2629
	$curve = cert_get_pkey_curve($pkey);
2630
	/* Return true if any of the following conditions are met:
2631
	 *  * This is not an EC key
2632
	 *  * The private key cannot be read
2633
	 *  * There are no restrictions
2634
	 *  * The requested curve is compatible */
2635
	return (($curve === false) ||
2636
		!array_key_exists($consumer, $cert_curve_compatible) ||
2637
		in_array($curve, $cert_curve_compatible[$consumer]));
2638
}
2639
2640
/****f* certs/cert_build_list
2641
 * NAME
2642
 *   cert_build_list - Build an option list of cert or CA entries, checked
2643
 *                     against a specific consumer name.
2644
 * INPUTS
2645
 *   $type    : 'ca' for certificate authority entries, 'cert' for certificates.
2646
 *   $consumer: The consumer name used to filter certificates out of the result.
2647
 *                 See the contents of $cert_curve_compatible for details.
2648 59fac81f jim-p
 *   $selectsource: Then true, outputs in a format usable by select_source in
2649
 *                  packages.
2650 3c1249b3 jim-p
 *   $addnone: When true, a 'none' choice is added to the list.
2651 cffcf9bf jim-p
 * RESULT
2652
 *   Returns an option list of entries with incompatible entries removed. This
2653
 *   can be used directly in form option selection lists.
2654
 * NOTES
2655
 *   This can be expanded in the future to allow for other types of restrictions.
2656
 ******/
2657
2658 3c1249b3 jim-p
function cert_build_list($type = 'cert', $consumer = '', $selectsource = false, $addnone = false) {
2659 cffcf9bf jim-p
	/* Ensure that $type is valid */
2660
	if (!in_array($type, array('ca', 'cert'))) {
2661
		return array();
2662
	}
2663
2664
	$list = array();
2665 3c1249b3 jim-p
	if ($addnone) {
2666
		if ($selectsource) {
2667
			$list[] = array('refid' => 'none', 'descr' => 'None');
2668
		} else {
2669
			$list['none'] = "None";
2670
		}
2671
	}
2672
2673 cffcf9bf jim-p
	/* Create a hashed array with the certificate refid as the key and
2674
	 * descriptive name as the value. Exclude incompatible certificates. */
2675 843ee1ac jim-p
	foreach (config_get_path($type, []) as $cert) {
2676 4e8cb2fc Viktor Gurov
		if (empty($cert['prv']) && ($type == 'cert')) {
2677 e43c71ce Viktor Gurov
			continue;
2678 3ad1e1cb jim-p
		} elseif (cert_check_pkey_compatibility($cert['prv'], $consumer) &&
2679 ffcb4247 jim-p
			  cert_check_digest_compatibility($type, $cert, $consumer)) {
2680 59fac81f jim-p
			if ($selectsource) {
2681
				$list[] = array('refid' => $cert['refid'],
2682
						'descr' => $cert['descr']);
2683
			} else {
2684
				$list[$cert['refid']] = $cert['descr'];
2685
			}
2686 cffcf9bf jim-p
		}
2687
	}
2688
2689
	return $list;
2690
}
2691
2692 9efec277 Jim Pingle
/****f* certs/cert_pkcs12_export
2693
 * NAME
2694
 *   cert_pkcs12_export - Export a PKCS#12 archive file for a given certificate
2695
 *                        and optional CA and passphrase.
2696
 * INPUTS
2697
 *   $cert      : Certificate entry array.
2698 a7e50981 jim-p
 *   $encryption: Strength of encryption to use:
2699
 *                "high" (AES-256 + SHA256)
2700
 *                "low" (3DES + SHA1)
2701
 *                "legacy" (RC2-40 + SHA1)
2702 9efec277 Jim Pingle
 *   $passphrase: Optional passphrase used to encrypt the archive contents and
2703
 *                private key.
2704
 *   $add_ca    : Boolean flag which determines whether or not the certificate
2705
 *                CA is included in the archive (if available)
2706
 *   $delivery  : Delivery method for the result: "file", "download", or "data".
2707
 *                See RESULT section for details.
2708
 * RESULT
2709
 *   Returns false on failure, otherwise result depends upon the value passed in
2710
 *   $delivery:
2711
 *       "file"    : Returns the path to the output archive file.
2712
 *                   NOTE: Does not clean up path, caller must clean up the
2713
 *                         entire directory containing the output file.
2714
 *       "download": Sends the archive data to the current GUI browser session.
2715
 *                   Must be called before any output is sent to the user
2716
 *                   session.
2717
 *       "data"    : Returns the contents of the PKCS#12 archive as a string.
2718
 * NOTES
2719
 *   If the certificate entry does not contain a private key, the archive will
2720
 *   also not contain a key.
2721
 ******/
2722
2723 a7e50981 jim-p
function cert_pkcs12_export($cert, $encryption = 'high', $passphrase = '', $add_ca = true, $delivery = 'download') {
2724 9efec277 Jim Pingle
	global $g;
2725
2726
	/* Unusable certificate entry, bail early. */
2727
	if (!is_array($cert) || empty($cert)) {
2728
		return false;
2729
	}
2730
2731
	/* Encryption and Digest */
2732 a7e50981 jim-p
	switch ($encryption) {
2733
		case 'legacy':
2734 9b9eaaea jim-p
			$algo = '-legacy -certpbe PBE-SHA1-RC2-40 -keypbe PBE-SHA1-RC2-40';
2735 a7e50981 jim-p
			$hash = '';
2736
			break;
2737
		case 'low':
2738
			$algo = '-certpbe PBE-SHA1-3DES -keypbe PBE-SHA1-3DES';
2739
			$hash = '-macalg SHA1';
2740
			break;
2741
		case 'high':
2742
		default:
2743
			$algo = '-aes256 -certpbe AES-256-CBC -keypbe AES-256-CBC';
2744
			$hash = '-macalg sha256';
2745
	}
2746 9efec277 Jim Pingle
2747
	/* Make a secure temporary directory */
2748
	$workdir = tempnam("{$g['tmp_path']}/", "p12export");
2749
	@unlink_if_exists($workdir);
2750
	mkdir($workdir, 0600);
2751
2752
	/* Set the friendly name to the certificate description, if available */
2753
	$descr = "";
2754
	if (!empty($cert['descr'])) {
2755
		$edescr = escapeshellarg($cert['descr']);
2756
		$descr = "-name {$edescr} -CSP {$edescr}";
2757
		$fileprefix = basename($cert['descr']);
2758
	}
2759
2760
	/* If there isn't a usable portion of the description, use the refid */
2761
	if (empty($fileprefix)) {
2762
		$fileprefix = $cert['refid'];
2763
	}
2764
2765
	/* Exported output archive filename */
2766
	$outpath = "{$workdir}/{$fileprefix}.p12";
2767
	$eoutpath = escapeshellarg($outpath);
2768
2769
	/* Passphrase handling */
2770
	if (!empty($passphrase)) {
2771
		/* Use passphrase text file so the passphrase is not visible in
2772
		 * process list. */
2773
		$passfile = "{$workdir}/passphrase.txt";
2774
		file_put_contents($passfile, $passphrase . "\n");
2775
		$pass = '-passout file:' . escapeshellarg($passfile);
2776
	} else {
2777
		/* Null password + disable encryption of the keys */
2778
		$pass = '-passout pass: -nodes';
2779
	}
2780
2781
	/* Certificate file */
2782
	$crtpath = "{$workdir}/cert.pem";
2783
	$ecrtpath = escapeshellarg($crtpath);
2784
	file_put_contents($crtpath, base64_decode($cert['crt']));
2785
2786
	/* Private key (if present) */
2787
	if (!empty($cert['prv'])) {
2788
		$keypath = "{$workdir}/key.pem";
2789
		/* Write key to a secure temporary name */
2790
		file_put_contents($keypath, base64_decode($cert['prv']));
2791
		$key = '-inkey ' . escapeshellarg($keypath);
2792
	} else {
2793
		$key = '-nokeys';
2794
	}
2795
2796
	/* Add CA if one is defined and requested */
2797
	$eca = '';
2798
	if ($add_ca && !empty($cert['caref'])) {
2799
		$ca = lookup_ca($cert['caref']);
2800 26308930 Marcos Mendoza
		$ca = $ca['item'];
2801 9efec277 Jim Pingle
		if ($ca) {
2802
			$capath = "{$workdir}/ca.pem";
2803
			file_put_contents($capath, base64_decode($ca['crt']));
2804
			$eca = '-certfile ' . escapeshellarg($capath);
2805
		}
2806
	}
2807
2808
	/* Export a PKCS#12 archive using these components and settings */
2809
	exec("/usr/bin/openssl pkcs12 -export -in {$ecrtpath} {$eca} {$key} -out {$eoutpath} {$pass} {$descr} {$algo} {$hash}");
2810
2811
	/* Bail if the output is invalid */
2812
	if (!file_exists($outpath) ||
2813
	    (filesize($outpath) == 0)) {
2814
		return false;
2815
	}
2816
2817
	/* Tailor output as requested by the caller */
2818
	switch ($delivery) {
2819
		case 'file':
2820
			/* Return path to export file, do not clean up, caller must clean up. */
2821
			return $outpath;
2822
			break;
2823
		case 'download':
2824
			/* Send file to user and cleanup */
2825
			$p12_data = file_get_contents($outpath);
2826
			rmdir_recursive($workdir);
2827
			send_user_download('data', $p12_data, "{$cert['descr']}.p12");
2828
			return true;
2829
			break;
2830
		case 'data':
2831
		default:
2832
			/* Return PKCS#12 archive data and cleanup */
2833
			$p12_data = file_get_contents($outpath);
2834
			rmdir_recursive($workdir);
2835
			return $p12_data;
2836
			break;
2837
	}
2838
2839
	return null;
2840
}
2841 3ad1e1cb jim-p
2842
/* Check if a given digest is known to be weak */
2843
function is_weak_digest($digest_alg) {
2844
	global $cert_strict_values;
2845
	/* Check for and remove unnecessary ECDSA digest prefix
2846
	 * See https://redmine.pfsense.org/issues/13437 */
2847
	$ecdsa_prefix = 'ecdsa-with-';
2848
	if (substr(strtolower($digest_alg), 0, strlen($ecdsa_prefix)) == $ecdsa_prefix) {
2849
		$digest_alg = substr($digest_alg, strlen($ecdsa_prefix));
2850
	}
2851
	return in_array($digest_alg, $cert_strict_values['digest_blacklist']);
2852
}
2853
2854
/* Determine if a certificate is too weak to function in certain contexts
2855
 * such as for the GUI, Captive Portal, or OpenVPN with OpenSSL 3.x */
2856
function cert_has_weak_digest($str_crt, $decode = true) {
2857
	$digestinfo = cert_get_sigtype($str_crt, $decode);
2858
	return is_weak_digest($digestinfo['shortname']);
2859
}
2860
2861 f78ae299 jim-p
/* Check both certificate and CA when passed a full certificate array entry */
2862
function cert_chain_has_weak_digest($cert) {
2863
	if (!is_array($cert) || !$cert['crt']) {
2864
		/* Empty input so flag as weak anyhow */
2865
		return true;
2866
	}
2867
	$weak = false;
2868
	/* Check CA if present */
2869
	if (!empty($cert['caref'])) {
2870
		$ca = lookup_ca($cert['caref']);
2871 26308930 Marcos Mendoza
		$ca = $ca['item'];
2872 f78ae299 jim-p
		$weak = cert_has_weak_digest($ca['crt']);
2873
	}
2874
	/* Only check cert if the CA is not weak (or no self-signed) */
2875
	if (!$weak) {
2876
		$weak = cert_has_weak_digest($cert['crt']);
2877
	}
2878
	return $weak;
2879
}
2880
2881 3ad1e1cb jim-p
function cert_check_digest_compatibility($type, $cert, $consumer) {
2882
	$nonweak_consumers = [
2883
		'ca'   => ['HTTPS'],
2884
		'cert' => ['HTTPS', 'OpenVPN'],
2885
	];
2886 ffcb4247 jim-p
	$usable = true;
2887
	/* HTTPS certificates must not have a weak entry anywhere in the CA+Cert
2888
	 * chain */
2889
	if (($type == 'cert') &&
2890
	    ($consumer == 'HTTPS')) {
2891
		$usable = !cert_chain_has_weak_digest($cert);
2892
	} else {
2893
		$usable = !cert_has_weak_digest($cert['crt']);
2894
	}
2895
	return ($usable ||
2896 3ad1e1cb jim-p
		!in_array($consumer, $nonweak_consumers[$type]));
2897
}
2898
2899 f78ae299 jim-p
function cert_create_selfsigned($descr = '', $hostname = '', $log = true) {
2900
	global $cert_strict_values;
2901
2902
	if ($log) {
2903
		log_error(sprintf(gettext("Creating self-signed SSL/TLS certificate (%s)"), $descr));
2904
	}
2905
2906
	if (empty($descr)) {
2907
		$descr = gettext("GUI default");
2908
	}
2909
2910
	/* Basic cert details */
2911
	$cert = [];
2912
	$cert['refid'] = uniqid();
2913
	$cert['descr'] = "{$descr} ({$cert['refid']})";
2914
2915
	if (empty($hostname)) {
2916
		/* Find the system hostname, append unique refid */
2917
		$hostname = config_get_path('system/hostname') . "-{$cert['refid']}";
2918
	}
2919
2920
	$dn = [
2921
		'organizationName' => g_get('product_label') . " {$descr} Self-Signed Certificate",
2922
		'commonName' => $hostname,
2923
		'subjectAltName' => "DNS:{$hostname}"
2924
	];
2925
2926
	/* Create the self-signed certificate */
2927
	$old_err_level = error_reporting(0); /* otherwise openssl_ functions throw warnings directly to a page screwing menu tab */
2928
	if (!cert_create($cert, null, 2048, $cert_strict_values['max_server_cert_lifetime'], $dn, "self-signed", "sha256")) {
2929
		while ($log && ($ssl_err = openssl_error_string())) {
2930
			log_error(sprintf(gettext("Error creating self-signed certificate: openssl library returns: %s"), $ssl_err));
2931
		}
2932
		error_reporting($old_err_level);
2933
		return null;
2934
	}
2935
	error_reporting($old_err_level);
2936
2937
	/* Add the new certificate */
2938 63d6bb4f Marcos Mendoza
	config_set_path('cert/', $cert);
2939 f78ae299 jim-p
2940
	return $cert;
2941
}
2942
2943 b89c34aa Ermal
?>