File 2503-ssh-Add-ECPrivateKey-version-compatibility-and-tests.patch of Package erlang
From 8e3c7726ee5124574ebb3b237a0137209d709fb4 Mon Sep 17 00:00:00 2001
From: Jakub Witczak <kuba@erlang.org>
Date: Wed, 14 Jan 2026 12:08:06 +0100
Subject: [PATCH 3/3] ssh: Add ECPrivateKey version compatibility and tests
- Add backward compatibility for ECPrivateKey version field
* Support both integer 1 (OTP 26/27) and atom ecPrivkeyVer1 (OTP 28)
* Fixes pattern match failures after public_key ASN.1 modernization
- Split experimental_openssh_key_v1 typespec into encode/decode variants
* experimental_openssh_key_v1_encode() - for encoding input
* experimental_openssh_key_v1_decode() - for decoding output
* Fixes type asymmetry between encode and decode flows
- Add comprehensive compatibility test
* Tests EdDSA curves: ed25519, ed448
* Tests ECDSA curves: secp256r1, secp384r1
* Verifies both version formats produce identical encoding
- Code cleanup
* Remove outdated comments
* Fix whitespace and formatting
* Update KeyPairs comment to document both tuple formats
Related to commit b50e1482ec (public_key: Add missing macros and definitions)
Fixes #10525, #10526
---
lib/ssh/src/ssh_file.erl | 33 +++++++++++++------------------
lib/ssh/src/ssh_message.erl | 14 ++++++-------
lib/ssh/test/ssh_pubkey_SUITE.erl | 33 +++++++++++++++++++++++++++++--
3 files changed, 52 insertions(+), 28 deletions(-)
diff --git a/lib/ssh/src/ssh_file.erl b/lib/ssh/src/ssh_file.erl
index 42946230e7..4b3de4d94b 100644
--- a/lib/ssh/src/ssh_file.erl
+++ b/lib/ssh/src/ssh_file.erl
@@ -653,16 +657,12 @@ encode(KeyAttrs, Type) when Type==rfc4716_key ;
fun openssh_key_v1_encode/1}
end,
iolist_to_binary(
- [
- [Begin,
+ [[Begin,
[rfc4716_encode_header(H) || H <- proplists:get_value(headers, Attrs, [])],
- split_long_lines( base64:encode( F(Key) ) ),
+ split_long_lines(base64:encode(F(Key))),
"\n",
- End
- ] ||
- {Key,Attrs} <- KeyAttrs
- ]
- );
+ End] ||
+ {Key,Attrs} <- KeyAttrs]);
encode(KeyAttrs, Type) when Type == known_hosts;
Type == auth_keys ;
@@ -1179,16 +1179,14 @@ decode_ssh_file(PrivPub, Algorithm, Pem, Password) ->
{error, key_decode_failed}
end.
-
decode_pem_keys(RawBin, Password) ->
PemLines = split_in_lines(
binary:replace(RawBin, [<<"\\\n">>,<<"\\\r\\\n">>], <<"">>, [global])
),
decode_pem_keys(PemLines, Password, []).
+
decode_pem_keys([], _, Acc) ->
{ok,lists:reverse(Acc)};
-
-
decode_pem_keys(PemLines, Password, Acc) ->
%% Private Key
try get_key_part(PemLines) of
@@ -1386,12 +1384,10 @@ openssh_key_v1_decode(<<"openssh-key-v1",0,
>>, Pwd) ->
openssh_key_v1_decode(Rest, N, Pwd, CipherName, KdfName, KdfOptions, N, []).
-
openssh_key_v1_decode(<<?DEC_BIN(BinKey,_L1), Rest/binary>>, I,
Pwd, CipherName, KdfName, KdfOptions, N, PubKeyAcc) when I>0 ->
PublicKey = ssh_message:ssh2_pubkey_decode(BinKey),
openssh_key_v1_decode(Rest, I-1, Pwd, CipherName, KdfName, KdfOptions, N, [PublicKey|PubKeyAcc]);
-
openssh_key_v1_decode(<<?DEC_BIN(Encrypted,_L)>>,
0, Pwd, CipherName, KdfName, KdfOptions, N, PubKeyAccRev) ->
PubKeys = lists:reverse(PubKeyAccRev),
@@ -1407,7 +1403,6 @@ openssh_key_v1_decode(<<?DEC_BIN(Encrypted,_L)>>,
error({decryption, DecryptError})
end.
-
openssh_key_v1_decode_priv_keys(Bin, I, N, KeyAcc, CmntAcc) when I>0 ->
{PrivKey, <<?DEC_BIN(Comment,_Lc),Rest/binary>>} = ssh_message:ssh2_privkey_decode2(Bin),
openssh_key_v1_decode_priv_keys(Rest, I-1, N, [PrivKey|KeyAcc], [Comment|CmntAcc]);
diff --git a/lib/ssh/src/ssh_message.erl b/lib/ssh/src/ssh_message.erl
index 327ea5cb75..9fc5ff8157 100644
--- a/lib/ssh/src/ssh_message.erl
+++ b/lib/ssh/src/ssh_message.erl
@@ -687,23 +687,23 @@ ssh2_privkey_encode(#'DSAPrivateKey'
>>;
ssh2_privkey_encode(#'ECPrivateKey'
- {version = ecPrivkeyVer1, % Found this in public_key:ec_key/2 ..
+ {version = Version,
parameters = {namedCurve,OID},
privateKey = Priv,
- publicKey = Pub
- }) when OID == ?'id-Ed25519' orelse
- OID == ?'id-Ed448' ->
+ publicKey = Pub})
+ when OID =:= ?'id-Ed25519' orelse OID =:= ?'id-Ed448',
+ Version =:= ecPrivkeyVer1 orelse Version =:= 1 -> % currently used atom and legacy value 1
{CurveName,_} = oid2ssh_curvename(OID),
<<?STRING(CurveName),
?STRING(Pub),
?STRING(Priv)>>;
ssh2_privkey_encode(#'ECPrivateKey'
- {version = ecPrivkeyVer1, % Found this in public_key:ec_key/2 ..
+ {version = Version,
parameters = {namedCurve,OID},
privateKey = Priv,
- publicKey = Q
- }) ->
+ publicKey = Q})
+ when Version =:= ecPrivkeyVer1 orelse Version =:= 1 -> % currently used atom and legacy value 1
{CurveName,_} = oid2ssh_curvename(OID),
<<?STRING(CurveName),
?STRING(CurveName), % SIC!
diff --git a/lib/ssh/test/ssh_pubkey_SUITE.erl b/lib/ssh/test/ssh_pubkey_SUITE.erl
index 03bd97b6e1..2ccf33f6d6 100644
--- a/lib/ssh/test/ssh_pubkey_SUITE.erl
+++ b/lib/ssh/test/ssh_pubkey_SUITE.erl
@@ -88,12 +88,14 @@
ssh_hostkey_fingerprint_list/1,
chk_known_hosts/1,
- ssh_hostkey_pkcs8/1
+ ssh_hostkey_pkcs8/1,
+ ec_private_key_version_compat/1
]).
-include_lib("common_test/include/ct.hrl").
-include_lib("public_key/include/public_key.hrl").
-include("ssh_test_lib.hrl").
+-include_lib("stdlib/include/assert.hrl").
%%%----------------------------------------------------------------
%%% Common Test interface functions -------------------------------
@@ -110,7 +112,8 @@ all() ->
{group, ssh_hostkey_fingerprint},
{group, ssh_public_key_decode_encode},
{group, pkcs8},
- chk_known_hosts
+ chk_known_hosts,
+ ec_private_key_version_compat
].
@@ -796,6 +799,32 @@ ssh_openssh_key_long_header(Config) when is_list(Config) ->
Encoded = ssh_file:encode(Decoded, rfc4716_key),
Decoded = ssh_file:decode(Encoded, rfc4716_key).
+ec_private_key_version_compat(Config) when is_list(Config) ->
+ Keys =
+ [begin
+ try
+ % with OTP 28: version = ecPrivkeyVer1 (atom) not integer
+ {Curve, public_key:generate_key({namedCurve, Curve})}
+ catch Error:Reason:Stacktrace ->
+ ?CT_LOG("SKIP Curve = ~p Error = ~p Reason = ~p~n~p",
+ [Curve, Error, Reason, Stacktrace]),
+ skip
+ end
+ end || Curve <- [ed25519, ed448, secp256r1, secp384r1]],
+ case lists:any(fun(I) -> I /= skip end, Keys) of
+ true ->
+ [begin
+ PrivLegacy = K#'ECPrivateKey'{version = 1}, % OTP-26, OTP-27
+ Encoded = ssh_message:ssh2_privkey_encode(K),
+ EncodedLegacy = ssh_message:ssh2_privkey_encode(PrivLegacy),
+ ?assertEqual(Encoded, EncodedLegacy),
+ ?CT_LOG("Curve = ~p [OK]", [Curve])
+ end || {Curve, K} <- Keys, K /= skip];
+ false ->
+ ct:fail(no_keys)
+ end,
+ ok.
+
%%%----------------------------------------------------------------
%%% Test case helpers
%%%----------------------------------------------------------------
--
2.51.0