%% Copyright 2015-2020 Guillaume Bour %% %% 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(letsencrypt_ssl). -author("Guillaume Bour "). -export([private_key/2, cert_request/3, cert_autosigned/3, certificate/3]). -include_lib("public_key/include/public_key.hrl"). -import(letsencrypt_utils, [bin/1]). % create key -spec private_key(undefined|{new, string()}|string(), string()) -> letsencrypt:ssl_privatekey(). private_key(undefined, CertsPath) -> private_key({new, "letsencrypt.key"}, CertsPath); private_key({new, KeyFile}, CertsPath) -> FileName = CertsPath++"/"++KeyFile, Cmd = "openssl genrsa -out '"++FileName++"' 2048", _R = os:cmd(Cmd), private_key(FileName, CertsPath); private_key(KeyFile, _) -> {ok, Pem} = file:read_file(KeyFile), [Key] = public_key:pem_decode(Pem), #'RSAPrivateKey'{modulus=N, publicExponent=E, privateExponent=D} = public_key:pem_entry_decode(Key), #{ raw => [E,N,D], b64 => { letsencrypt_utils:b64encode(binary:encode_unsigned(N)), letsencrypt_utils:b64encode(binary:encode_unsigned(E)) }, file => KeyFile }. -spec cert_request(string(), string(), list(string())) -> letsencrypt:ssl_csr(). cert_request(Domain, CertsPath, SANs) -> KeyFile = CertsPath ++ "/" ++ Domain ++ ".key", CertFile = CertsPath ++ "/" ++ Domain ++ ".csr", {ok, CertFile} = mkcert(request, Domain, CertFile, KeyFile, SANs), %io:format("CSR ~p~n", [CertFile]), case file:read_file(CertFile) of {ok, RawCsr} -> [{'CertificationRequest', Csr, not_encrypted}] = public_key:pem_decode(RawCsr), %io:format("csr= ~p~n", [Csr]), letsencrypt_utils:b64encode(Csr); {error, enoent} -> io:format("cert_request: cert file ~p not found~n", [CertFile]), throw(file_not_found); {error, Err} -> io:format("cert_request: unknown error ~p~n", [Err]), throw(unknown_error) end. % create temporary (1 day) certificate with subjectAlternativeName % used for tls-sni-01 challenge -spec cert_autosigned(string(), string(), list(string())) -> {ok, string()}. cert_autosigned(Domain, KeyFile, SANs) -> CertFile = "/tmp/"++Domain++"-tlssni-autosigned.pem", mkcert(request, Domain, CertFile, KeyFile, SANs). -spec mkcert(request|autosigned, string(), string(), string(), list(string())) -> {ok, string()}. mkcert(request, Domain, OutName, Keyfile, SANs) -> AltNames = lists:foldl(fun(San, Acc) -> <> end, <<"subjectAltName=DNS:", (bin(Domain))/binary>>, SANs), Cmd = io_lib:format("openssl req -new -key '~s' -out '~s' -subj '/CN=~s' -addext '~s'", [Keyfile, OutName, Domain, AltNames]), _Status = os:cmd(Cmd), %io:format("mkcert(request):~p => ~p~n", [lists:flatten(Cmd), _Status]), {ok, OutName}. % domain certificate only certificate(Domain, DomainCert, CertsPath) -> FileName = CertsPath++"/"++Domain++".crt", %io:format("domain cert: ~p~nintermediate: ~p~n", [DomainCert, IntermediateCert]), %io:format("writing final certificate to ~p~n", [FileName]), file:write_file(FileName, DomainCert), FileName.