Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prototype API for fetching os cacerts #5853

Merged
merged 2 commits into from
May 2, 2022

Conversation

dgud
Copy link
Contributor

@dgud dgud commented Apr 1, 2022

API: load/0 load/1 and get/0

Writes to persistent_term to cache the results,
user can re-load cache with load/0 and load/1 functions.

load/1 is made for users with an unsupported OS, or if they
want to load the cache with data from another file.

Currently works on linux (ubuntu) and win32

@github-actions
Copy link
Contributor

github-actions bot commented Apr 1, 2022

CT Test Results

       3 files       66 suites   45m 19s ⏱️
   818 tests    736 ✔️   82 💤 0
3 568 runs  2 772 ✔️ 796 💤 0

Results for commit 5dc34cf.

♻️ This comment has been updated with latest results.

To speed up review, make sure that you have read Contributing to Erlang/OTP and that all checks pass.

See the TESTING and DEVELOPMENT HowTo guides for details about how to run test locally.

Artifacts

// Erlang/OTP Github Action Bot

@dgud dgud added the team:PS Assigned to OTP team PS label Apr 1, 2022
@dgud dgud force-pushed the dgud/pub_key/cacerts/OTP-17798 branch from 0e1a977 to fcb3152 Compare April 1, 2022 15:06
@dgud dgud self-assigned this Apr 4, 2022
Copy link
Contributor

@u3s u3s left a comment

Choose a reason for hiding this comment

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

i've read the code and have no comments.

Copy link
Contributor

@max-au max-au left a comment

Choose a reason for hiding this comment

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

I was thinking of any way to test this, and found nothing else except listing a bunch of actual Root CA thumbprints/names/... in the test case. Given that Root CA changes are extremely rare it might be okay to see a test case failing whenever that happens.

load_darwin() ->
%% Temporary solution, should probably be re-written to use Keychain Access API
KeyChainFile = "/System/Library/Keychains/SystemRootCertificates.keychain",
TmpPemFile = "/tmp/all_certs.pem",
Copy link
Contributor

Choose a reason for hiding this comment

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

Would this be fixed before this change is merged?
Otherwise I might suggest to use some functions returning temporary file name (or temporary file dir)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Probably not, this should be good enough, tm.
Please do, this will remain until someone with Obj-C knowledge decides that it is a much better solution,
and updates the nif, writes a configure file and ..

Copy link
Contributor

Choose a reason for hiding this comment

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

Does the security export command's -o switch allow - as an argument to export to stdout, removing the use of a temp file? I'd check but I don't have access to OS X.

Copy link
Contributor

Choose a reason for hiding this comment

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

Going through Security API would be ideal but in the meantime I think we can improve how we shell out. The default is to write to stdout so going through the temp file should be unnecessary:

> X = os:cmd("security export -t certs -f pemseq -k /System/Library/Keychains/SystemRootCertificates.keychain"), {lists:sublist(X,10), length(X)}.
{"-----BEGIN",249291}

%% Return cacerts
-spec get() -> [public_key:combined_cert()].
get() ->
case persistent_term:get(?MODULE, not_loaded) of
Copy link
Contributor

Choose a reason for hiding this comment

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

If I understand correctly, this persistent_term would stay in memory forever (even after I unload public_key application).
Another observation, there is no automated "reload" operation (compared to SSL certificate cache that will detect changed files on file system). So long-running systems won't detect CA roots changed (and a manual operation would be needed).

One solution I was thinking of was to add an actual process (supervised by public_key, which will now turn from library with no processes to a library with a process). That would also allow to reuse code from SSL application that stores decoded PEM files (in an ETS table).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If I understand correctly, this persistent_term would stay in memory forever

True, but do we really need to handle that case?
I.e. add a delete_cacerts() function to the API that will never be called, or do you see another solution.

There is no automated reload, which is what I wanted:

  1. We don't want change the public_key from a library application

  2. I think I implemented the original cache in ssl and Ingela have been fixing bugs in that since then.
    So I want to keep it simple.
    The user (should) know when the system is updated and can thus call load/[0|1]

@dgud
Copy link
Contributor Author

dgud commented Apr 7, 2022

About the testing, yeah that could work, will check some names atleast.

@tsloughter
Copy link
Contributor

This is great to see being added.

My only thought is not really a concern for this specific PR but I think it'd be nice if httpc had an option (or even be the default) to use these instead of having to do what we have had to do before of passing a long list of ssl args including a full list of cacerts. Security WG wrote this up https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/inets

In rebar3 we construct the ssl options for httpc here https://github.com/erlang/rebar3/blob/main/src/rebar_utils.erl#L1080-L1081

We'd still need to override in case the user specifies a specific cacertfile in the configuration, but for general usage it'd be nice to not have to configure this long list of options to make a secure HTTPS request.

@dgud
Copy link
Contributor Author

dgud commented Apr 7, 2022

@max-au Extended the tests after your suggestions and added a cacerts_clear() for completion,
I also changed cacerts_load/1 to take one file as argument instead of a list of tries.

@tsloughter Yeah that was my intention but should be done but outside of this PR.
You could use public_key:cacerts_load(File) before calling httpc and that would automatically use those certs.

@emixa-d
Copy link

emixa-d commented Apr 10, 2022

Not really an Erlang user myself, but I stumbled upon this patch from
https://issues.guix.gnu.org/54796#52
Could the SSL_CERT_FILE environment variable be supported?
That way, non-privileged users could easily override which certificates
to trust.

@wojtekmach
Copy link
Contributor

wojtekmach commented Apr 10, 2022

Could the SSL_CERT_FILE environment variable be supported? That way, non-privileged users could easily override which certificates to trust.

Perhaps worth mentioning that curl supports SSL_CERT_FILE too. (as well as CURL_CA_BUNDLE.)

@max-au
Copy link
Contributor

max-au commented Apr 10, 2022

Thanks @dgud !
Looking more and more at this code, I feel more and more convinced that it belongs ssl application.

  1. There is already a NIF (the one that used openssl crypto). It will save public_key from adding NIF code.
  2. The only way to use Root CA list is actually in "ssl" application.
  3. There is already a PEM cache in SSL application - and it already has (battle-tested) eviction logic.

I also suspect that ssl_pem_cache is where all the certificates end up anyway (unless cache is disabled, but in this case performance degrades too much).

One approach I can think of is adding a ssl_root_ca_store to ssl_admin_sup. This would allow to have this store addressable by the certificate thumbnail or any other mechanism that ssl app needs to find the certificate chain.

@lukebakken
Copy link
Contributor

Supporting environment variables should be out of the scope of providing easy access to the system's trusted root certificates.

@emixa-d
Copy link

emixa-d commented Apr 10, 2022 via email

@lukebakken
Copy link
Contributor

@emixa-d thanks for pointing that out. It seems like it would be useful to investigate the following:

  • How does a common utility like curl determine CA file / directory locations?
  • Are SSL_CERT_FILE and SSL_CERT_DIR "standard" names?
  • What do other VMs do? Java, for instance.
  • What are the standard locations for a few major Linux distros and BSDs?

@dgud
Copy link
Contributor Author

dgud commented Apr 12, 2022

Thanks @dgud ! Looking more and more at this code, I feel more and more convinced that it belongs ssl application.

  1. There is already a NIF (the one that used openssl crypto). It will save public_key from adding NIF code.

There are no nifs in ssl, they are in the crypto application.

  1. The only way to use Root CA list is actually in "ssl" application.

Who knows what people will do?

  1. There is already a PEM cache in SSL application - and it already has (battle-tested) eviction logic.

I also suspect that ssl_pem_cache is where all the certificates end up anyway (unless cache is disabled, but in this case performance degrades too much).

No the cache is only used when the cert files are read by ssl, if you use the cacerts argument nothing is cached,
and there is no need either, the binaries are large enough to not be copied.

One approach I can think of is adding a ssl_root_ca_store to ssl_admin_sup. This would allow to have this store addressable by the certificate thumbnail or any other mechanism that ssl app needs to find the certificate chain.

@dgud
Copy link
Contributor Author

dgud commented Apr 12, 2022

Not really an Erlang user myself, but I stumbled upon this patch from
https://issues.guix.gnu.org/54796#52
Could the SSL_CERT_FILE environment variable be supported?
That way, non-privileged users could easily override which certificates
to trust.

I do like that from a useability perspective, but we have had complaints before for that an erlang system
default loads an '.erlang' file and other things like that and that that can be an security issue that the user
it allowed to change the behavior by default.

The load/1 function can be used for loading an arbitrary file, so the user/application can easily read the environment
variable and load the file.

So for now I don't think we should read it by default.

@emixa-d
Copy link

emixa-d commented Apr 12, 2022 via email

@emixa-d
Copy link

emixa-d commented Apr 12, 2022

(GitHub seems to have lost this response)

Luke Bakken schreef op zo 10-04-2022 om 15:48 [-0700]:

@emixa-d thanks for pointing that out. It seems like it would be useful
to investigate the following:

  • How does a common utility like curl determine CA file / directory
    locations?

The utility curl looks in CURL_CA_BUNDLE (see
https://curl.se/docs/sslcerts.html). I'd assume it fallbacks
to /etc/ssl/certs but I don't actually know that for a fact.

AFAIK, the library curl does not look in any environemn variables by
default. However, in Guix (see gnu/packages/patches/curl-use-ssl-cert-
env.patch), libcurl is patched to look in SSL_CERT_DIR/SSL_CERT_FILE by
default.

  • Are SSL_CERT_FILE and SSL_CERT_DIR "standard" names?

SSL_CERT_DIR / SSL_CERT_FILE are used by OpenSSL.
GNUtls currently does not support any environment variables.
However, there is a patch adding support for SSL_CERT_DIR /
SSL_CERT_FILE:
https://gitlab.com/gnutls/gnutls/-/merge_requests/1541.

Guix (as in, some of the support libraries like guix/git.scm and
guix/build/download.scm, not the set of packages) looks for
certificates in SSL_CERT_FILE/SSL_CERT_DIR, with a fallback to
/etc/ssl/certs/ca-certificates.crt and /etc/ssl/certs/ca-bundle.crt.

I am not aware of any ‘competing’ standards.

Git has its own environment variable GIT_SSL_CAINFO.

'nss' has its own mechanism (some database and not directly files --
probably not relevant to
erlang/otp?)(https://www.happyassassin.net/posts/2015/01/12/a-note-about-ssltls-trusted-certificate-stores-and-platforms/).

  • What do other VMs do? Java, for instance.

If Java recognises any environment variables, I haven't found them.
According to
https://stackoverflow.com/questions/5871279/ssl-and-cert-keystore#,
it looks in $JAVA_HOME/lib/security/jssecacerts or
$JAVA_HOME/lib/security/cacerts which can be overriden by setting
‘javax.net.ssl.trustStore.’

Firefox does not use SSL_CERT_FILE/SSL_CERT_DIR. It has its own
certificate database, named 'nss'.

I'm out of VMs, unless QEMU counts here.

  • What are the standard locations for a few major Linux distros and
    BSDs?

On Guix System, it's /etc/ssl/certs, but only if not running in some
container made with "guix shell --container, and only if the system
administrator installed a certificate globally (or, in Guix
terminology, installed a certificate package in the system profile).
Guix System also sets SSL_CERT_DIR/SSL_CERT_FILE.

On Debian systems, it's /etc/ssl/certs.

Apparently, on at least one ArchLinux version, it is /etc/ssl/cert.pem
(NixOS/nixpkgs#8247 (comment))
They also mention /etc/ssl/certs/ca-certificates.crt,
/etc/ssl/certs/ca-bundle.crt, /etc/pki/tls/certs/ca-bundle.crt.
Apparently these files are created by NixOS?

OpenSUSE puts it in yet another location: /var/lib/ca-
certificates/bundle.pem
(https://www.happyassassin.net/posts/2015/01/12/a-note-about-ssltls-trusted-certificate-stores-and-platforms/).
There's also another location but it's broken or something? That blog
post recommends just using SSL_CTX_set_default_verify_paths() (assuming
you are using openssl)

Apparently FreeBSD puts things in /etc/ssl/cert.pem:
https://www.freshports.org/security/ca_root_nss.

@dgud
Copy link
Contributor Author

dgud commented Apr 12, 2022

@emixa-d I believe the discussion about security comes from that if you download a binary and run it
it should do what you expect, e.g. in this case only fetch data from servers that can be verified by OS cacerts,
not the certs the user set or a hacker on the user system.

@lukebakken
Copy link
Contributor

The load/1 function can be used for loading an arbitrary file, so the user/application can easily read the environment
variable and load the file.

@dgud thanks. I agree, environment variable support should be left up to application developers.

@dgud
Copy link
Contributor Author

dgud commented Apr 14, 2022

@max-au @lukebakken @wojtekmach Removed the temporary file and read from stdin instead.

@wojtekmach
Copy link
Contributor

Removed the temporary file and read from stdin instead.

Works great!

@potatosalad
Copy link
Contributor

I feel more and more convinced that it belongs ssl application.

@max-au As a counter-argument: I think the scope of the public_key library is all-things-PKI-related, which is where I would expect to find things like the OS Root Certs.

@dgud Out of curiosity, why add a new NIF to public_key versus adding a new NIF function to the existing crypto NIF while keeping the nice-to-use API in public_key? That seems to be the pattern for things like crypto:sign/4,5 and public_key:sign/3,4.

@dgud
Copy link
Contributor Author

dgud commented Apr 20, 2022

@potatosalad Because crypto is "thin" wrapper towards "SSLs crypto-lib" and unrelated to this.
It would have been simpler but kind of a hack, IMO.

@emixa-d
Copy link

emixa-d commented Apr 20, 2022

(I previously sent this response GitHub by e-mail but it seems to have lost this response)

Dan Gudmundsson schreef op di 12-04-2022 om 13:34 [+0000]:

@emixa-d I believe the discussion about security comes from that if
you download a binary and run it
it should do what you expect, e.g. in this case only fetch data from
servers that can be verified by OS cacerts,
not the certs the user set or a hacker on the user system.

If I, as user, download some binaries and run them, I expect them to
use the cacerts I specified as trusted via the environment variables,
instead of the OS' ~128 fallback root CAs (in case of Mozilla's set,
about 100 points of failure!) I don't actually trust but have to use to
actually browse the web.

If some attacker has compromised my shell process to set some
environment variables then I have bigger problems to worry about.
Also, the attacker could just set LD_LIBRARY_PATH or LD_PRELOAD or just directly run some malicious binary, why would they bother with SSL_CERT_FILE and the like?

Anyway, OK, but I expect it to be eventually patched downstream because
...

The load/1 function can be used for loading an arbitrary file, so the
user/application can easily read the environment
variable and load the file.

@dgud thanks. I agree, environment variable support should be left up
to application developers.

... AFAICT, often applications don't bother with opting-into
environment variables if by default things use the global certificate
store (/etc/ssl/certs or the like). Or maybe I'll be pleasantly
surprised.

API: load/0 load/1 and get/0 clear/0

Writes to persistent_term to cache the results,
user can re-load cache with load/0 and load/1 functions.

load/1 is made for users with an unsupported OS, or if they
want to load the cache with data from another file.
@dgud dgud force-pushed the dgud/pub_key/cacerts/OTP-17798 branch from 7fead27 to 5dc34cf Compare April 28, 2022 12:57
@dgud dgud merged commit e9b8395 into erlang:master May 2, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
team:PS Assigned to OTP team PS
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants