%% @author Marc Worrell %% @copyright 2012-2020 Marc Worrell, Maas-Maarten Zeeman %% @doc CA supplied certificate handling %% @end %% Copyright 2012 - 2020 Marc Worrell, Maas-Maarten Zeeman %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. -module(mod_ssl_ca). -moduledoc(" The mod_ssl_ca module adds support for using SSL certificates bought from a Certificate Authority. A free alternative to CA provided tickets is Let’s Encrypt, see [mod_ssl_letsencrypt](/id/doc_module_mod_ssl_letsencrypt). Certificate and key files ------------------------- The certificate and key files are placed into the site sub-directory of the security directory. The subdirectory will be: `sitename/ca/` Where *sitename* must be replaced with the name of your site. The security directory can be found by inspecting the output of: ```erlang bin/zotonic config ``` The Zotonic *security* directory can be in one of the following directories: * The environment variable `ZOTONIC_SECURITY_DIR` * The `~/.zotonic/security` directory * The `/etc/zotonic/security` directory (only on Linux) * The OS specific directory for application data files The OS specific directories are: * On Unix: `~/.config/zotonic/security/` * On macOS: `~/Library/Application Support/zotonic/security/` The default is the OS specific directory. If there is a directory `priv/security/ca` inside your site’s OTP application folder then that directory will be used. The filenames are checked against their extension. When you copy your files to the `ca` directory then you need to ensure that they have the right extensions. The following file extensions are expected: `*.pem` or `*.key` This holds the private key for the encryption. The key must be unlocked and in PKCS#1 format (see below). `*.crt` This is the certificate. Usually it is supplied by the certificate authority where you bought it. It can also be a self signed certificate, see below. `*.ca.crt`, `cabundle.crt` or `bundle.crt` This is the (optional) *CA bundle* that contains root and intermediate certificates for the certificate authority that issued the `.crt` certificate. The certificate authority will supply these. All supplied certificates are concatenated, with the root certificate last. The concatenation is a literal command, like: ```bash cat intermediate.crt root.crt > cabundle.crt ``` Due to caching, it can take up to a minute before the new certificates are used. Format of the private key ------------------------- The Erlang SSL implementation accepts PKCS#1 and PKCS#8 format keys. OpenSSL generates (since 2010) PKCS#8 format keys. A PKCS#1 key starts with: ```none -----BEGIN RSA PRIVATE KEY----- ``` A PKCS#8 key starts with: ```none -----BEGIN PRIVATE KEY----- ``` If there are problems then check if the `.key` or `.pem` file starts with one of the above strings. Using SSL certificates ---------------------- If you order a SSL certificate, the signing authority will ask you which kind of web server you are using and a CSR file. For the web server, select *other*. For the CSR, use the following command: ```bash openssl req -out certificate.csr -new -newkey rsa:2048 -nodes -keyout certificate.key ``` When OpenSSL asks for the *Common Name* then fill in the site’s hostname (e.g. *www.example.com*). From the SSL certificate authority you will receive a signed `.crt` file and maybe a `cabundle.crt` file. See the section *Certificate and key files* above for instructions how to use the `.crt` and `.key` files. Generating a self signed certificate ------------------------------------ There is no need to make your own self signed certificate as Zotonic will generate one for every site. Nevertheless, if you want to use your own self signed certificate, then run the following commmands: ```bash openssl req -x509 -nodes -days 3650 -subj '/CN=www.example.com' -newkey rsa:2048 \\ -keyout certificate.key -out certificate.crt ``` This generates a private key of 2048 bits and a certificate that is valid for 10 years. Accepted Events --------------- This module handles the following notifier callbacks: - `observe_ssl_options`: Return the certificates of this site using `z_depcache:memo`. See also [mod_ssl_letsencrypt](/id/doc_module_mod_ssl_letsencrypt), [Port configurations](/id/doc_developerguide_configuration_port_ssl_configuration#ref-port-ssl-configuration)"). -mod_title("SSL - CA"). -mod_description("Use SSL Certificate from a Certificiate Authority."). -mod_provides([]). -mod_depends([]). -mod_prio(100). -author('Marc Worrell '). -export([ observe_ssl_options/2, ssl_options/1 ]). -include_lib("zotonic_core/include/zotonic.hrl"). -define(SNI_CACHE_TIME, 60). %% @doc Return the certificates of this site. observe_ssl_options(#ssl_options{server_name=_NormalizedHostnameBin}, Context) -> z_depcache:memo(fun() -> ssl_options(Context) end, sni_ssl_ca, ?SNI_CACHE_TIME, Context). ssl_options(Context) -> case cert_files(Context) of {ok, CertFiles} -> PemFile = proplists:get_value(keyfile, CertFiles), case check_keyfile(PemFile) of ok -> {ok, CertFiles}; {error, _} -> undefined end; {error, _} -> undefined end. cert_files(Context) -> SSLDir = cert_dir(Context), Files = scan_cert_dir(SSLDir), files_to_ssl_options(Files). files_to_ssl_options(#{ bundle := CaCrt, crt := Crt, pem := Pem }) -> Options = [ {certfile, Crt}, {keyfile, Pem}, {cacertfile, CaCrt} ] ++ z_ssl_dhfile:dh_options(), {ok, Options}; files_to_ssl_options(#{ crt := Crt, pem := Pem }) -> Options = [ {certfile, Crt}, {keyfile, Pem} ] ++ z_ssl_dhfile:dh_options(), {ok, Options}; files_to_ssl_options(#{ bundle := CaCrt, crt := Crt, key := Key }) -> Options = [ {certfile, Crt}, {keyfile, Key}, {cacertfile, CaCrt} ] ++ z_ssl_dhfile:dh_options(), {ok, Options}; files_to_ssl_options(#{ crt := Crt, key := Key }) -> Options = [ {certfile, Crt}, {keyfile, Key} ] ++ z_ssl_dhfile:dh_options(), {ok, Options}; files_to_ssl_options(_Files) -> {error, notfound}. cert_dir(Context) -> PrivSSLDir = filename:join([z_path:site_dir(Context), "priv", "security", "ca"]), case filelib:is_dir(PrivSSLDir) of true -> PrivSSLDir; false -> {ok, SecurityDir} = z_config_files:security_dir(), filename:join([ SecurityDir, z_context:site(Context), "ca" ]) end. scan_cert_dir( Dir ) -> Files = filelib:wildcard( filename:join( Dir, "*" ) ), lists:foldl( fun (F, Acc) -> case filelib:is_dir(F) of true -> Acc; false -> case filename:basename(F) of "cabundle.crt" -> Acc#{ bundle => F }; "bundle.crt" -> Acc#{ bundle => F }; Basename -> case filename:extension(Basename) of "" -> Acc; ".crt" -> case filename:basename(Basename, ".ca.crt") of Basename -> Acc#{ crt => F }; _ -> Acc#{ bundle => F } end; ".key" -> case classify_key_file(F) of pem -> Acc#{ pem => F }; key -> Acc#{ key => F }; error -> Acc end; ".pem" -> Acc#{ pem => F }; _Ext -> Acc end end end end, #{}, Files). classify_key_file(File) -> case file:read_file(File) of {ok, <<"-----BEGIN PRIVATE KEY", _/binary>>} -> key; {ok, <<"-----BEGIN RSA PRIVATE KEY", _/binary>>} -> pem; _ -> error end. check_keyfile(PemFile) -> case file:read_file(PemFile) of {ok, Bin} -> case public_key:pem_decode(Bin) of [] -> ?LOG_ERROR(#{ text => <<"No private PEM key found">>, in => zotonic_mod_ssl_ca, result => error, reason => no_private_keys_found, filename => PemFile }), {error, no_private_keys_found}; _ -> ok end; {error, Reason} = Error -> ?LOG_ERROR(#{ text => <<"Cannot read key file">>, in => zotonic_mod_ssl_ca, result => error, reason => Reason, filename => PemFile }), Error end.