Current section
Files
Jump to
Current section
Files
src/support/z_letsencrypt_ssl.erl
%% 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(z_letsencrypt_ssl).
-author("Guillaume Bour <guillaume@bour.cc>").
-export([private_key/2, cert_request/3, cert_autosigned/3, certificate/3]).
-include_lib("public_key/include/public_key.hrl").
% create key
-spec private_key(undefined|{new, file:filename_all()}|file:filename_all(), file:filename_all()) -> z_letsencrypt:ssl_privatekey().
private_key(undefined, CertsPath) ->
private_key({new, "letsencrypt.key"}, CertsPath);
private_key({new, KeyFile}, CertsPath) ->
FileName = filename:join(CertsPath, KeyFile),
Cmd = "openssl genrsa -out '" ++ z_letsencrypt_utils:str(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 => {
z_letsencrypt_utils:b64encode(binary:encode_unsigned(N)),
z_letsencrypt_utils:b64encode(binary:encode_unsigned(E))
},
file => KeyFile
}.
-spec cert_request(z_letsencrypt:domain(), file:filename_all(), list(z_letsencrypt:domain())) -> z_letsencrypt:ssl_csr().
cert_request(Domain, CertsPath, SANs) ->
DomainStr = z_letsencrypt_utils:str(Domain),
KeyFile = filename:join(CertsPath, DomainStr ++ ".key"),
CertFile = filename:join(CertsPath, DomainStr ++ ".csr"),
{ok, CertFile} = mkcert(request, DomainStr, CertFile, KeyFile, SANs),
case file:read_file(CertFile) of
{ok, RawCsr} ->
[{'CertificationRequest', Csr, not_encrypted}] = public_key:pem_decode(RawCsr),
z_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.
% domain certificate only
certificate(Domain, DomainCert, CertsPath) ->
DomainStr = z_letsencrypt_utils:str(Domain),
FileName = filename:join(CertsPath, DomainStr ++ ".crt"),
file:write_file(FileName, DomainCert),
FileName.
% create temporary (1 day) certificate with subjectAlternativeName
% used for tls-sni-01 challenge
-spec cert_autosigned(z_letsencrypt:domain(), file:filename_all(), list(z_letsencrypt:domain())) -> {ok, file:filename_all()}.
cert_autosigned(Domain, KeyFile, SANs) ->
DomainStr = z_letsencrypt_utils:str(Domain),
KeyDir = filename:dirname(KeyFile),
CertFile = filename:join(KeyDir, DomainStr ++ "-tlssni-autosigned.pem"),
mkcert(autosigned, Domain, CertFile, KeyFile, SANs).
-spec mkcert(request|autosigned, z_letsencrypt:domain(), file:filename_all(), file:filename_all(), list(z_letsencrypt:domain())) -> {ok, file:filename_all()}.
mkcert(Type, Domain, OutName, Keyfile, SANs) ->
Names = [ Domain | SANs ],
NamesNr = lists:zip(Names, lists:seq(1,length(Names))),
Cnf = [
"[req]\n",
"distinguished_name = req_distinguished_name\n",
"x509_extensions = v3_req\n",
"prompt = no\n",
"[req_distinguished_name]\n",
"CN = ", Domain, "\n",
"[v3_req]\n",
"subjectAltName = @alt_names\n",
"[alt_names]\n"
] ++ [
[ "DNS.", integer_to_list(Nr), " = ", Name, "\n" ] || {Name, Nr} <- NamesNr
],
ConfDir = filename:dirname(OutName),
ConfFile = filename:join(ConfDir, "letsencrypt_san_openssl." ++ z_letsencrypt_utils:str(Domain) ++ ".cnf"),
ok = file:write_file(ConfFile, Cnf),
Cmd = io_lib:format("openssl req -new -key '~s' -sha256 -out '~s' -config '~s'",
[Keyfile, OutName, ConfFile]),
Cmd1 = case Type of
request -> [Cmd | " -reqexts v3_req" ];
autosigned -> [Cmd | " -extensions v3_req -x509 -days 1" ]
end,
_Status = os:cmd(Cmd1),
file:delete(ConfFile),
{ok, OutName}.