Skip to content

Commit

Permalink
Merge pull request erlang#7898 from IngelaAndin/ingela/ssl/custom_sig…
Browse files Browse the repository at this point in the history
…n_fun

Ingela/ssl/custom sign fun

OTP-18876
  • Loading branch information
IngelaAndin authored Dec 12, 2023
2 parents b957dbe + ac3a3b0 commit 4836605
Show file tree
Hide file tree
Showing 11 changed files with 400 additions and 141 deletions.
25 changes: 21 additions & 4 deletions lib/public_key/doc/src/public_key.xml
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,14 @@
</desc>
</datatype>

<datatype>
<name name="custom_key_opts"/>
<desc>
<p>Can be provided together with a custom private key, that specifies a key fun,
to provide additional options understood by the fun.</p>
</desc>
</datatype>

<datatype>
<name name="ed_oid_name"/>
<desc>
Expand Down Expand Up @@ -390,9 +398,14 @@
<name name="encrypt_private" arity="3" since="OTP 21.1"/>
<fsummary>Public-key encryption using the private key.</fsummary>
<desc>
<p>Public-key encryption using the private key.
See also <seemfa
marker="crypto:crypto#private_encrypt/4">crypto:private_encrypt/4</seemfa>.</p>
<p>Public-key encryption using the private key. See also <seemfa
marker="crypto:crypto#private_encrypt/4">crypto:private_encrypt/4</seemfa>.
The key, can besides a standard RSA key, be a map specifing the
key algorithm <c>rsa</c> and a fun to handle the encryption
operation. This may be used for customized the encryption
operation with for instance hardware security modules (HSM) or
trusted platform modules (TPM).
</p>
</desc>
</func>

Expand Down Expand Up @@ -1006,7 +1019,11 @@ end
<p>Creates a digital signature.</p>
<p>The <c>Msg</c> is either the binary "plain text" data to be
signed or it is the hashed value of "plain text", that is, the
digest.</p>
digest. The key, can besides a standard key, be a map specifing
a key algorithm and a fun that should handle the signing. This may
be used for customized signing with for instance hardware security
modules (HSM) or trusted platform modules (TPM).
</p>
</desc>
</func>

Expand Down
45 changes: 34 additions & 11 deletions lib/public_key/src/public_key.erl
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,22 @@
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()} .
-type custom_key_opts() :: [term()].
-type rsa_public_key() :: #'RSAPublicKey'{}.
-type rsa_private_key() :: #'RSAPrivateKey'{}.
-type rsa_private_key() :: #'RSAPrivateKey'{} | #{algorithm := rsa,
encrypt_fun => fun()}.
-type dss_public_key() :: integer().
-type rsa_pss_public_key() :: {rsa_pss_public_key(), #'RSASSA-PSS-params'{}}.
-type rsa_pss_private_key() :: { #'RSAPrivateKey'{}, #'RSASSA-PSS-params'{}}.
Expand Down Expand Up @@ -635,16 +647,16 @@ encrypt_private(PlainText, Key) ->
CipherText
when PlainText :: binary(),
Key :: rsa_private_key(),
Options :: crypto:pk_encrypt_decrypt_opts(),
Options :: crypto:pk_encrypt_decrypt_opts() | custom_key_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 @@ -831,7 +843,7 @@ sign(DigestOrPlainText, DigestType, Key) ->
Signature when Msg :: binary() | {digest,binary()},
DigestType :: digest_type(),
Key :: private_key(),
Options :: crypto:pk_sign_verify_opts(),
Options :: crypto:pk_sign_verify_opts() | custom_key_opts(),
Signature :: binary() .
sign(Digest, none, Key = #'DSAPrivateKey'{}, Options) when is_binary(Digest) ->
%% Backwards compatible
Expand All @@ -840,6 +852,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 +1519,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 @@ -77,12 +77,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 @@ -153,6 +157,7 @@ all() ->
appup,
{group, pem_decode_encode},
encrypt_decrypt,
encrypt_decrypt_sign_fun,
{group, sign_verify},
pkix,
pkix_countryname,
Expand Down Expand Up @@ -190,7 +195,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 @@ -655,6 +660,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 = #{encrypt_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 @@ -731,6 +752,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
36 changes: 30 additions & 6 deletions lib/ssl/doc/src/ssl.xml
Original file line number Diff line number Diff line change
Expand Up @@ -343,11 +343,35 @@
<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 user's private key. Either the key can be provided
directly as DER encoded entity, or indirectly using a crypto
engine/provider (with key reference information) or an Erlang
fun (with possible custom options). The latter two options
can both be used for customized signing with for instance
hardware security modules (HSM) or trusted platform modules
(TPM). </p>

<list>
<item><p>A DER encoded key will need to specify the ASN-1 type used to
create the encoding.</p></item>

<item><p>An engine/provider needs to specify specific
information to support this concept and 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>. </p></item>

<item><p>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#encrypt_private/3">public_key:private_encrypt/4</seemfa>
if legacy versions TLS-1.0 and TLS-1.1 should be supported. </p></item>
</list>

<p>If this option is supplied, it overrides option <c>keyfile</c>.
</p>
</desc>
</datatype>

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

<datatype>
<name name="custom_verify"/>
<desc>
Expand Down
2 changes: 1 addition & 1 deletion lib/ssl/src/ssl.app.src
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,6 @@
{applications, [crypto, public_key, kernel, stdlib]},
{env, []},
{mod, {ssl_app, []}},
{runtime_dependencies, ["stdlib-4.1","public_key-1.11.3","kernel-9.0",
{runtime_dependencies, ["stdlib-4.1","public_key-@OTP-18876@","kernel-9.0",
"erts-14.0","crypto-5.0", "inets-5.10.7",
"runtime_tools-1.15.1"]}]}.
Loading

0 comments on commit 4836605

Please sign in to comment.