Skip to content

Commit

Permalink
Add 'sign_fun' entry in 'key' config option
Browse files Browse the repository at this point in the history
This option allows the user to define a function tasked to sign an ssl message.
This gives total freedom around key handling.

Users are free to program such function the way they think is best,
allowing them to support any private key storage
or to delegate signature to any external service or device.

Most notably: this allows to implement custom access to any HSM device.
  • Loading branch information
ziopio authored and IngelaAndin committed Nov 23, 2023
1 parent 541191d commit aae1011
Show file tree
Hide file tree
Showing 8 changed files with 308 additions and 114 deletions.
39 changes: 30 additions & 9 deletions lib/public_key/src/public_key.erl
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,18 @@
test_root_cert/0
]).

-type public_key() :: rsa_public_key() | rsa_pss_public_key() | dsa_public_key() | ec_public_key() | ed_public_key() .
-type private_key() :: rsa_private_key() | rsa_pss_private_key() | dsa_private_key() | ec_private_key() | ed_private_key() .
-type public_key() :: rsa_public_key() |
rsa_pss_public_key() |
dsa_public_key() |
ec_public_key() |
ed_public_key() .
-type private_key() :: rsa_private_key() |
rsa_pss_private_key() |
dsa_private_key() |
ec_private_key() |
ed_private_key() |
#{algorithm := eddsa | rsa_pss_pss | ecdsa | rsa | dsa,
sign_fun => fun(), encrypt_fun => fun()} .
-type rsa_public_key() :: #'RSAPublicKey'{}.
-type rsa_private_key() :: #'RSAPrivateKey'{}.
-type dss_public_key() :: integer().
Expand Down Expand Up @@ -634,17 +644,17 @@ encrypt_private(PlainText, Key) ->
-spec encrypt_private(PlainText, Key, Options) ->
CipherText
when PlainText :: binary(),
Key :: rsa_private_key(),
Key :: rsa_private_key() | #{encrypt_fun := fun()},
Options :: crypto:pk_encrypt_decrypt_opts(),
CipherText :: binary() .
encrypt_private(PlainText,
#'RSAPrivateKey'{modulus = N, publicExponent = E,
privateExponent = D} = Key,
Options)
encrypt_private(PlainText, Key, Options)
when is_binary(PlainText),
is_integer(N), is_integer(E), is_integer(D),
is_list(Options) ->
crypto:private_encrypt(rsa, PlainText, format_rsa_private_key(Key), default_options(Options)).
Opts = default_options(Options),
case format_sign_key(Key) of
{extern, Fun} -> Fun(PlainText, Opts);
{rsa, CryptoKey} -> crypto:private_encrypt(rsa, PlainText, CryptoKey, Opts)
end.

%%--------------------------------------------------------------------
%% Description: List available group sizes among the pre-computed dh groups
Expand Down Expand Up @@ -840,6 +850,8 @@ sign(DigestOrPlainText, DigestType, Key, Options) ->
case format_sign_key(Key) of
badarg ->
erlang:error(badarg, [DigestOrPlainText, DigestType, Key, Options]);
{extern, Fun} when is_function(Fun) ->
Fun(DigestOrPlainText, DigestType, Options);
{Algorithm, CryptoKey} ->
try crypto:sign(Algorithm, DigestType, DigestOrPlainText, CryptoKey, Options)
catch %% Compatible with old error schema
Expand Down Expand Up @@ -1505,8 +1517,17 @@ format_pkix_sign_key({#'RSAPrivateKey'{} = Key, _}) ->
Key;
format_pkix_sign_key(Key) ->
Key.

format_sign_key(#{encrypt_fun := KeyFun}) ->
{extern, KeyFun};
format_sign_key(#{sign_fun := KeyFun}) ->
{extern, KeyFun};
format_sign_key(Key = #'RSAPrivateKey'{}) ->
{rsa, format_rsa_private_key(Key)};
format_sign_key({#'RSAPrivateKey'{} = Key, _}) ->
%% Params are handled in options arg
%% provided by caller.
{rsa, format_rsa_private_key(Key)};
format_sign_key(#'DSAPrivateKey'{p = P, q = Q, g = G, x = X}) ->
{dss, [P, Q, G, X]};
format_sign_key(#'ECPrivateKey'{privateKey = PrivKey, parameters = {namedCurve, Curve} = Param})
Expand Down
45 changes: 44 additions & 1 deletion lib/public_key/test/public_key_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,16 @@
cert_pem/1,
encrypt_decrypt/0,
encrypt_decrypt/1,
encrypt_decrypt_sign_fun/0,
encrypt_decrypt_sign_fun/1,
rsa_sign_verify/0,
rsa_sign_verify/1,
rsa_pss_sign_verify/0,
rsa_pss_sign_verify/1,
dsa_sign_verify/0,
dsa_sign_verify/1,
custom_sign_fun_verify/0,
custom_sign_fun_verify/1,
pkix/0,
pkix/1,
pkix_countryname/0,
Expand Down Expand Up @@ -154,6 +158,7 @@ all() ->
appup,
{group, pem_decode_encode},
encrypt_decrypt,
encrypt_decrypt_sign_fun,
{group, sign_verify},
pkix,
pkix_countryname,
Expand Down Expand Up @@ -191,7 +196,7 @@ groups() ->
ec_pem_encode_generated, gen_ec_param_prime_field,
gen_ec_param_char_2_field]},
{sign_verify, [], [rsa_sign_verify, rsa_pss_sign_verify, dsa_sign_verify,
eddsa_sign_verify_24_compat]}
eddsa_sign_verify_24_compat, custom_sign_fun_verify]}
].
%%-------------------------------------------------------------------
init_per_suite(Config) ->
Expand Down Expand Up @@ -656,6 +661,22 @@ encrypt_decrypt(Config) when is_list(Config) ->
RsaEncrypted2 = public_key:encrypt_public(Msg, PublicKey),
Msg = public_key:decrypt_private(RsaEncrypted2, PrivateKey),
ok.

%%--------------------------------------------------------------------
encrypt_decrypt_sign_fun() ->
[{doc, "Test public_key:encrypt_private with user provided sign_fun"}].
encrypt_decrypt_sign_fun(Config) when is_list(Config) ->
{PrivateKey, _DerKey} = erl_make_certs:gen_rsa(64),
#'RSAPrivateKey'{modulus=Mod, publicExponent=Exp} = PrivateKey,
EncryptFun = fun (PlainText, Options) ->
public_key:encrypt_private(PlainText, PrivateKey, Options)
end,
CustomPrivKey = #{sign_fun => EncryptFun},
PublicKey = #'RSAPublicKey'{modulus=Mod, publicExponent=Exp},
Msg = list_to_binary(lists:duplicate(5, "Foo bar 100")),
RsaEncrypted = public_key:encrypt_private(Msg, CustomPrivKey),
Msg = public_key:decrypt_public(RsaEncrypted, PublicKey),
ok.

%%--------------------------------------------------------------------
rsa_sign_verify() ->
Expand Down Expand Up @@ -732,6 +753,28 @@ dsa_sign_verify(Config) when is_list(Config) ->
{DSAPublicKey, DSAParams}),
false = public_key:verify(Digest, none, <<1:8, DigestSign/binary>>,
{DSAPublicKey, DSAParams}).
%%--------------------------------------------------------------------

custom_sign_fun_verify() ->
[{doc, "Checks that public_key:sign correctly calls the `sign_fun`"}].
custom_sign_fun_verify(Config) when is_list(Config) ->
{_, CaKey} = erl_make_certs:make_cert([{key, rsa}]),
PrivateRSA = public_key:pem_entry_decode(CaKey),
#'RSAPrivateKey'{modulus=Mod, publicExponent=Exp} = PrivateRSA,
PublicRSA = #'RSAPublicKey'{modulus=Mod, publicExponent=Exp},
SignFun = fun (Msg, HashAlgo, Options) ->
public_key:sign(Msg, HashAlgo, PrivateRSA, Options)
end,
CustomKey = #{algorithm => rsa, sign_fun => SignFun},

Msg = list_to_binary(lists:duplicate(5, "Foo bar 100")),
RSASign = public_key:sign(Msg, sha, CustomKey),
true = public_key:verify(Msg, sha, RSASign, PublicRSA),
false = public_key:verify(<<1:8, Msg/binary>>, sha, RSASign, PublicRSA),
false = public_key:verify(Msg, sha, <<1:8, RSASign/binary>>, PublicRSA),

RSASign1 = public_key:sign(Msg, md5, CustomKey),
true = public_key:verify(Msg, md5, RSASign1, PublicRSA).

%%--------------------------------------------------------------------
pkix() ->
Expand Down
24 changes: 18 additions & 6 deletions lib/ssl/doc/src/ssl.xml
Original file line number Diff line number Diff line change
Expand Up @@ -343,11 +343,23 @@
<datatype>
<name name="key"/>
<desc>
<p>The DER-encoded user's private key or a map referring to a crypto
engine and its key reference that optionally can be password protected,
see also <seemfa marker="crypto:crypto#engine_load/3"> crypto:engine_load/3
</seemfa> and <seeguide marker="crypto:engine_load"> Crypto's Users Guide</seeguide>. If this option
is supplied, it overrides option <c>keyfile</c>.</p>

<p>The DER-encoded user's private key or a map referring to a
crypto engine or Erlang fun together with key reference
information, that both can be used for customized signing with
for instance hardware security modules (HSM) or trusted
platform modules (TPM). An engine key can optionally be
password protected, see also <seemfa
marker="crypto:crypto#engine_load/3"> crypto:engine_load/3
</seemfa> and <seeguide marker="crypto:engine_load"> Crypto's
Users Guide</seeguide>. A fun option should include a fun
that mimics <seemfa
marker="public_key:public_key#sign/4">public_key:sign/4</seemfa>
and possibly <seemfa
marker="public_key:public_key#private_encrypt/4">public_key:private_encrypt/4</seemfa>
if legacy versions TLS-1.0 and TLS-1.1 should be supported. If
this option is supplied, it overrides option <c>keyfile</c>.
</p>
</desc>
</datatype>

Expand Down Expand Up @@ -616,7 +628,7 @@ version.
ROOT-CA, and so on. The default value is 10.</p>
</desc>
</datatype>

<datatype>
<name name="custom_verify"/>
<desc>
Expand Down
Loading

0 comments on commit aae1011

Please sign in to comment.