这是indexloc提供的服务,不要输入任何密码
Skip to content

Conversation

@overhacked
Copy link
Contributor

pfSense can be configured to use a proxy server, but if it is a man-in-the-middle SSL proxy, there has been no GUI way to add trusted root certificates for HTTPS client actions like updates, URL alias downloads, etc.

This patch adds a "Trusted" checkbox (default False) to CAs in the GUI that will add the certificate to /etc/ssl/certs. An environment variable is set at boot-time that causes libfetch (eventually libssl) to look for root certificates in that directory.

At each boot, /etc/ssl/certs is created and populated with all
CA certificates configured in the web GUI.
Is checked when regenerating trusted CAs.
Instead of removing specific untrusted certs, delete
all files in /etc/ssl/certs BUT leave symlinks in place
for power users or for testing.
Moved out of system.inc so it can be accessed by
system_camanager.php to refresh /etc/ssl/certs on changes
to the CA list.
Call ca_trustedcerts_generate() after write_config()
in system_camanager.php
Previously was cleaning out directory only if
CAs were configured, so deleting last CA left
an orphan, trusted certificate in /etc/ssl/certs
Prevents any possible hash name collision between symlinked
certs and pfSense-created cert files
echo "ca_trustedcerts_generate() being called $mt\n";
}

mwexec("/bin/mkdir -p {$g['etc_path']}/ssl/certs");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not use a shell command for this, we have functions to make directories.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do.


mwexec("/bin/mkdir -p {$g['etc_path']}/ssl/certs");
/* Remove all certs in the directory (but leave symlinks in place to allow power users to add trusted certs outside the Certificate Manager) */
mwexec("/usr/bin/find {$g['etc_path']}/ssl/certs -type f -exec /bin/rm '{}' ';'");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not use a shell command for this, use a function similar to this: https://github.com/pfsense/FreeBSD-ports/blob/devel/net-mgmt/pfSense-pkg-net-snmp/files/usr/local/pkg/net-snmp.inc#L223
If you want to exclude something, then write your files with a unique prefix and then glob using that prefix,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with this and the previous comment that I should be using pfSense library functions over shell commands. The mkdir code is easy to change; I had aped other code in certs.inc that did the same thing, so my next pull request may be a few patches to clean up that file.

I will refactor to eliminate the call to /usr/bin/find and use is_link and unlink instead. There's no way to use prefixes in /etc/ssl/certs, unfortunately, because OpenSSL requires that the files be named {cert_hash}.0, and OpenSSL doesn't support certificates in subdirectories of cert_path, so I can't do that either. Symlinks seemed a reasonable compromise to not completely obliterate the ability of power users and developers to manually place certificates in the directory.

}

function cert_get_hash($crt) {
$str_crt = base64_decode($crt);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to make this variable for a single use, combine the two lines openssl_x509_parse(base64_decode($crt));


/* add configured trusted CA certificates to /etc/ssl/certs */
ca_trustedcerts_generate();

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do trusted certs need to be re/generated during boot up? /etc/ssl/certs is a static dir is it not? The certs should still be there across reboots. Only reason I think of is restoring/swapping a config. But the trusted certs re/generation for that scenario should and could be done by other means. Such as part of the config restore process. Or a trusted certs regeneration button in ca manager for those times when manually swapping a config.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's safer to do it at boot time to ensure it's consistent. It's not like it's a huge amount of work or writes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And completely unnecessary. It's not like people are restoring or swapping configs in prod every other time they reboot. Also not friendly to write cycle sensitive media.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless you can check to be sure the state is consistent and update as needed, it is necessary. You don't know what someone might have done, or what might have happened on upgrade, etc. It's not like it's happening every few minutes, the number of writes overall is minimal. Better safe than sorry.

Copy link
Contributor

@NOYB NOYB Feb 20, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless you can check to be sure the state is consistent and update as needed, it is necessary.

Which is how it should be done. Verify what is there against what config says should be there and add/remove accordingly. Also include a "synchronize" button on the CA manager page for user to be able to manually initiated the same thing.

$name = $a_ca[$id]['descr'];
unset($a_ca[$id]);
write_config();
ca_trustedcerts_generate();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would rather trusted certs be generated/removed on a per cert add/edit basis instead of regenerating all of them every time a change is made. Even changes that do not make changes to trusted certs settings.

@NOYB
Copy link
Contributor

NOYB commented Feb 19, 2017

Suggest squash some of these commits to create a cleaner history? Especially those that are just changes to the changes.


if (!$input_errors) {
write_config();
ca_trustedcerts_generate();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would rather trusted certs be generated/removed on a per cert add/edit basis instead of regenerating all of them every time a change is made. Even changes that do not make changes to trusted certs settings.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure it is a good idea to trust that files named *hashvalue*.0 have the correct certificate as their contents. I could certainly read in the existing file and check if it contains the correct certificate, but that seems like a lot of overhead compared to rewriting the certfiles during change/reboot. Thoughts on security implications of blindly trusting the file contents?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some more comments on this up higher. I'd rather see them rewritten each boot to be absolutely sure. I don't see saving 2-3 writes per reboot as being worth all the extra complexity. You could read the files in and compare, or compute and compare hashes without reading directly, but it's a lot of overhead for minimal benefit.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deleting the entire set of cert files and rewriting them provides no additional safety.

  1. If the contained cert does not match the hash name then it will fail for the site. The certs here are found for the site by their hash name. They are not all read until a match is found. That's why they are required to be hash named.

  2. Symlinks are not being destroyed. So anyone who wishes to tamper can do so simply by creating a link to a cert located anywhere on the system they want.

  3. Anyone wishing to tamper can simply put a cert of their choosing in the CA bundle (/usr/local/share/certs/ca-root-nss.crt). It is not purged and reloaded at boot either.

This argument of safety and trust is nonsensical.

No need to read the files in and compare. Just create an array of file names config says should be there and add/remove accordingly. Not much work at all really, and far less work than writing files that don't need to be written. Deleting and rewriting the files is much more work for no benefit.

// Get list of trust files for CA certs from config.
foreach($config['ca'] as $ca) {
	$ca_hash = cert_get_hash($ca['crt']);
	$ca_certfiles[] = "{$g['etc_path']}/ssl/certs/{$ca_hash}.0";
}

// Remove cert trust files that have no corresponding CA cert in config.
foreach (glob("{$g['etc_path']}/ssl/certs/*") as $cert_file) {
	if (!in_array($cert_file, $ca_certfiles)) {
		unlink_if_exists($cert_file);
	}
}

// Add missing cert trust files for CA certs in config.  Remove cert trust files of CA certs in config that are not trusted or are revoked.
foreach ($a_ca as $id => $value) {
	ca_trustedcert_add($id);
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wanting to be sure the firewall has correct contents in the files is hardly "nonsensical". When I say "safe" I mean "Ensure the contents of the files are correct and that all are present", not necessarily "safe" from a security standpoint (which is also important).

If a file is named properly but contains the wrong contents (0-byte file, truncated, overwritten, etc) it would be completely missed unless the contents of the files are checked/verified. There would be no way to correct that unless the file is overwritten or removed and rewritten. The use of the CA would fail but there's no way to correct that, and it wouldn't be obvious to the user that they might have to trigger a forced rewrite by edit/save of the CA if it was fixed to force an update then.

You're still jumping through a lot of hoops to avoid something that isn't even a sizable problem. A few writes per reboot isn't going to kill a disk. We need to be sure the files are in a consistent state with correct contents, making no assumptions and taking no shortcuts.

Copy link
Contributor

@NOYB NOYB Feb 21, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just because it's the way you what pfSense to behave does not make it good or even beneficial. Microsoft does things because that's they way they want it to be too. Doesn't necessarily make it better. Doing that way just because that's the way we want it to be is not sound design. Any good engineer will tell you that the more you "touch" something increases the probability of introducing an error/failure/etc. The file is there, it's good. Leave it alone and let it be. Just the like CA bundle file.

If customers are willing to accept your inconsistently applied bloatware method of file integrity assurance. That is between them and you. But yes I prefer it be left alone once there. And that's how it will be done on my systems.

If it really is that critically important to you/pfSense then the rewriting on boot up is still not the "right"/"best" way of ensuring the content is correct, and has much more overhead than doing it in a well engineered fashion that could also be applied to any file deemed to be critical.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You disagree, I get it. Let it go. No need for insults or name-calling. Your way is not better, it's riskier. You don't know the file is good until you check it to know it's good. You want to allow for the possibility that it's not right -- that's on you, but that isn't how we operate. Overwrite or read/verify checksum, either way is fine since you end up with a file in a known-good state.

Leaving unverified data in place because the filename happens to be correct isn't "well engineered".

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. No need for insults or name-calling.

Where did I insult you? Where did I call you a name?

You certainly hurled some insults at me though.

My way is no more riskier than how the CA bundle is currently being handled. And like I stated. Rewriting files at boot up is still not the "right" way to ensure file integrity. There are better, more reliable methods with far less overhead.

Leaving unverified data in place because the filename happens to be correct isn't "well engineered".

But you and pfSense are okay with that as long is it's other just as critical files. Yes. That logic is well reasoned out. Not.

I have never said they shouldn't be verified. But if they are going to be then do it "right". With a well engineer method and consistently apply it to critical files.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"inconsistently applied bloatware", for example. Uncalled for. You keep arguing in circles ignoring my explanations, so I'm done.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Talking down to someone with statements like; let me boil it down to basic, so you can get it and such. Yes those kinds of statements that you made are insults. I wasn't born yesterday. I know what those statements are about. They are intended to belittle.

I'm not the one arguing in circles and ignoring your explanations. I'm debunking them and the reasoning that's being given for them.

if (!empty($a_ca[$id]['prv'])) {
$pconfig['key'] = base64_decode($a_ca[$id]['prv']);
}
$pconfig['trusted'] = $a_ca[$id]['trusted'];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Think this is what you really want to do here: $pconfig['trusted'] = isset($a_ca[$id]['trusted']);

$pconfig['digest_alg'] = "sha256";
$pconfig['lifetime'] = "3650";
$pconfig['dn_commonname'] = "internal-ca";
$pconfig['trusted'] = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't need this as it will not be set by default.

} else {
$ca['trusted'] = $pconfig['trusted'];
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be all that it needed here: $ca['trusted'] = isset($pconfig['trusted']);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about this one and subsequent comment that suggests deleting && $ca['trusted']. isset() will return TRUE even if the variable is set FALSE, right? Is it completely safe to assume that a boolean FALSE never appears in global $config?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Option set/not set rather that true/false is the way it is done throughout pfSense. Yes using isset instead of true/false works, is cleaner/clearer and is the consistent boolean option technique in pfSense config.

<tr>
<td><?=$name?></td>
<td><i class="fa fa-<?= (!empty($ca['prv'])) ? "check" : "times" ; ?>"></i></td>
<td><i class="fa fa-<?= (isset($ca['trusted']) && $ca['trusted']) ? "check" : "times" ; ?>"></i></td>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't need the && $ca['trusted'] here.

@NOYB
Copy link
Contributor

NOYB commented Feb 19, 2017

Hey Overhacked. Thanks for doing this. Really appreciate it. This has been a thorn in my side for a long time. I've given quite a few suggestions here. Most of them stem from what I've been doing for my own config to trust self signed certs. Please take them as constructive input. Again thanks.

@NOYB
Copy link
Contributor

NOYB commented Feb 20, 2017

Let me toss another thing your direction to consider. Removing revoked CA certs.


if (is_array($config['ca'])) {
foreach ($config['ca'] as & $ca) {
if ($ca['trusted']) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And along with the other issets this would be an isset test also. if (isset($ca['trusted']))

@overhacked
Copy link
Contributor Author

Re @NOYB commenting above about removing revoked CA certs:

Isn't that something that OpenSSL takes care of when it is properly configured with a CRL? If a user configures one in the GUI, won't or couldn't it already be loaded into the validation via:

/* Load CRLs into the `X509_STORE` */

X509_STORE *x509_store = SSL_CTX_get_cert_store(ctx);
X509_STORE_add_crl(x509_store, crl);

/* Enable CRL checking */
X509_VERIFY_PARAM *param = X509_VERIFY_PARAM_new();
X509_VERIFY_PARAM_set_flags(param, X509_V_FLAG_CRL_CHECK);
SSL_CTX_set1_param(ctx, param);
X509_VERIFY_PARAM_free(param);

(via https://stackoverflow.com/questions/4389954/does-openssl-automatically-handle-crls-certificate-revocation-lists-now)

@NOYB
Copy link
Contributor

NOYB commented Feb 21, 2017

Haven't study out how pfSense clients will handle that. But if the cert is not there then it won't be trusted. If you, or someone, can verify that all pfSense clients, cURL, SMTP, etc. will utilize the CRL then yeah it wouldn't matter then.

Handling isset() more consistently
Removed calls to shell commands in place of PHP functions
@overhacked
Copy link
Contributor Author

Just pushed fixes to changes directly requested above, except:

  • No handling of CRL's in this code. Should I trust OpenSSL (aka other clients) to check CRLs, or should I check CA certificate validity here?

@NOYB
Copy link
Contributor

NOYB commented Feb 21, 2017

So now that you've made those changes. Thank you. Here is another thing to look into.

Have only played with this on 2.3 (not 2.4) but are you sure that PHP's curl_exec will look in /etc/ssl/certs for certs?

Maybe the PHP config is different in 2.4 but in 2.3 it does not. Have to add the CURLOPT_CAPATH option. Among things that could be affected are URL table downloads and check ip for custom DynDNS and RFC 2136 clients.

Again thanks for doing this.

@overhacked
Copy link
Contributor Author

Sadly, you're right about curl_exec(), @NOYB. CURLOPT_CAPATH has to be set for it to look at the certs directory at all, and worse, there is no php.ini variable to configure it. Going to sleep on this problem and see if I can come up with a solution. I had so hoped that all libssl-using parts of PHP would honor the environment variables.

@NOYB
Copy link
Contributor

NOYB commented Feb 22, 2017

Fortunately I think there are only a few places the CURLOPT_CAPATH is needed. I'll provide a list of the places I found below.

Look into openssl.capath/cainfo it may be usable for this but I don't know.

Another thing I was thinking is maybe create a PHP var with the same path as being set in the environment variable. Then CURLOPT_CAPATH where needed could be set from the var instead of a hard coded string everywhere like I did for testing. It would probably have to be a global var.

Maybe someone knows an even cleaner way. If so that would be great.

There are some other places than these below where verify host/peer is used but they are for services that should be in the CA bundle already.

diff --git a/src/etc/inc/dyndns.class b/src/etc/inc/dyndns.class
index c88fecacdd..1203ac6697 100644
--- a/src/etc/inc/dyndns.class
+++ b/src/etc/inc/dyndns.class
@@ -674,6 +674,7 @@
 						}
 						if ($this->_curlSslVerifypeer) {
 							curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, TRUE);
+							curl_setopt($ch, CURLOPT_CAPATH, '/etc/ssl/certs');
 						} else {
 							curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
 						}



diff --git a/src/etc/inc/pfsense-utils.inc b/src/etc/inc/pfsense-utils.inc
index 075306f01d..d5da2cf9b2 100644
--- a/src/etc/inc/pfsense-utils.inc
+++ b/src/etc/inc/pfsense-utils.inc
@@ -1823,6 +1823,7 @@ function download_file($url, $destination, $verify_ssl = true, $connect_timeout
 	curl_setopt($ch, CURLOPT_URL, $url);
 	curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $verify_ssl);
 	curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $verify_ssl);
+	curl_setopt($ch, CURLOPT_CAPATH, '/etc/ssl/certs');
 	curl_setopt($ch, CURLOPT_FILE, $fp);
 	curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $connect_timeout);
 	curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
@@ -1878,6 +1879,7 @@ function download_file_with_progress_bar($url, $destination, $verify_ssl = true,
 	curl_setopt($ch, CURLOPT_URL, $url);
 	curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $verify_ssl);
 	curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $verify_ssl);
+	curl_setopt($ch, CURLOPT_CAPATH, '/etc/ssl/certs');
 	curl_setopt($ch, CURLOPT_HEADERFUNCTION, 'read_header');
 	curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
 	curl_setopt($ch, CURLOPT_WRITEFUNCTION, $readbody);



diff --git a/src/etc/inc/services.inc b/src/etc/inc/services.inc
index 021fb7e562..6a53378681 100644
--- a/src/etc/inc/services.inc
+++ b/src/etc/inc/services.inc
@@ -2035,6 +2035,7 @@ function dyndnsCheckIP($int) {
 		$ip_ch = curl_init($hosttocheck);
 		curl_setopt($ip_ch, CURLOPT_RETURNTRANSFER, 1);
 		curl_setopt($ip_ch, CURLOPT_SSL_VERIFYPEER, $verifysslpeer);
+		curl_setopt($ip_ch, CURLOPT_CAPATH, '/etc/ssl/certs');
 		curl_setopt($ip_ch, CURLOPT_INTERFACE, 'host!' . $ip_address);
 		curl_setopt($ip_ch, CURLOPT_CONNECTTIMEOUT, '30');
 		curl_setopt($ip_ch, CURLOPT_TIMEOUT, 120);

Liking what you are doing. Keep it up.


safe_mkdir("{$g['etc_path']}/ssl/certs");
/* Remove all certs in the directory (but leave symlinks in place to allow power users to add trusted certs outside the Certificate Manager) */
array_map('unlink', array_filter('is_file',glob("{$g['etc_path']}/ssl/certs/*")));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Believe array_filter syntax is incorrect. The callback function should be the second parameter.
Even so in my testing is_file was true for symlinks too. So don't think this will do what is expected.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was tired and mimicked array_map() syntax. I'll roll this into my next push. Speaking of, regarding your previous comment about squashing, won't a project maintainer be merging all this activity as a squash commit, anyway?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The merge should have them all squashed together. But I know that on mine when I look at a file history it shows all the individual commits.

Refactored calls to curl_setopt() in various modules
to a common function in pfsense-utils.inc. Did not modify
curl_* calls in simplepie.inc, as it is an upstream library.
@overhacked
Copy link
Contributor Author

Most recent commits pushed address this issue in curl_*() calls in the pfSense code base. All future code wanting to use the PHP cURL extension should get their cURL handle from get_curl_handle(…) in pfsense-utils.inc instead of calling curl_init() directly.

I deliberately omitted simplepie.inc from these changes, as it is an external library.

@overhacked
Copy link
Contributor Author

FYI, I submitted a PR to the PHP project that would address this issue in php.ini: php/php-src#2399

@overhacked
Copy link
Contributor Author

Thanks all for the feedback. I'm going to close this PR and submit another. (I will mention commenters above in the new PR.)

I've discovered after doing further testing that the way webConfigurator spawns pkg, curl, or calls curl_* functions varies too widely for any of the above solutions to address. The hardest obstacle to overcome is that nginx fastcgi sanitizes the environment variables. It is possible to set fastcgi_param in /var/etc/nginx-webConfigurator.conf (generated by /etc/inc/system.inc), but those environment variables don't seem to make it all the way through to the exec() call that spawns pkg (or curl).

If you have any suggestions about whether it is worth implementing what amounts to kludges for each different way that pfSense spawns TLS-using processes, please chime in. Otherwise, my plan is to use the GUI code I've written and submit a PR that appends to the existing /etc/ssl/cert.pem while retaining the original /usr/local/share/certs/ca-root-nss.crt to use as the base file every time new CAs are added or removed.

@overhacked overhacked closed this Mar 21, 2017
netgate-git-updates pushed a commit that referenced this pull request Oct 31, 2019
Similar to closed PR #3558 from overhacked, but with a number of
changes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants