From b8f9db2cbf29d9fc50bef3689b0b4f49c2ccdb6e Mon Sep 17 00:00:00 2001 From: Stefan Rinkes Date: Wed, 10 Jul 2024 19:32:09 +0200 Subject: [PATCH] Add Sign/Verify for SSH-Signatures --- .../SshNet.Keygen.Sample.csproj | 2 +- .../SshNet.Keygen.Tests.csproj | 3 +- SshNet.Keygen.Tests/TestKey.cs | 48 ++++++ SshNet.Keygen.Tests/TestSignatures/file.txt | 1 + .../TestSignatures/file.txt.ECDSA256.sig | 7 + .../TestSignatures/file.txt.ECDSA384.sig | 8 + .../TestSignatures/file.txt.ECDSA521.sig | 10 ++ .../TestSignatures/file.txt.ED25519.sig | 6 + .../TestSignatures/file.txt.RSA2048.sig | 14 ++ .../TestSignatures/file.txt.RSA3072.sig | 19 +++ .../TestSignatures/file.txt.RSA4096.sig | 24 +++ .../TestSignatures/file.txt.RSA8192.sig | 43 +++++ .../TestSignatures/gen_test_sigs.sh | 23 +++ SshNet.Keygen/Extensions/KeyExtension.cs | 2 +- .../Extensions/KeyHostAlgorithmExtension.cs | 22 +++ .../Extensions/PrivateKeyFileExtension.cs | 31 +++- SshNet.Keygen/SshSignature.cs | 160 ++++++++++++++++++ SshNet.Keygen/SshSignatureReader.cs | 32 ++++ 18 files changed, 451 insertions(+), 4 deletions(-) create mode 100644 SshNet.Keygen.Tests/TestSignatures/file.txt create mode 100644 SshNet.Keygen.Tests/TestSignatures/file.txt.ECDSA256.sig create mode 100644 SshNet.Keygen.Tests/TestSignatures/file.txt.ECDSA384.sig create mode 100644 SshNet.Keygen.Tests/TestSignatures/file.txt.ECDSA521.sig create mode 100644 SshNet.Keygen.Tests/TestSignatures/file.txt.ED25519.sig create mode 100644 SshNet.Keygen.Tests/TestSignatures/file.txt.RSA2048.sig create mode 100644 SshNet.Keygen.Tests/TestSignatures/file.txt.RSA3072.sig create mode 100644 SshNet.Keygen.Tests/TestSignatures/file.txt.RSA4096.sig create mode 100644 SshNet.Keygen.Tests/TestSignatures/file.txt.RSA8192.sig create mode 100644 SshNet.Keygen.Tests/TestSignatures/gen_test_sigs.sh create mode 100644 SshNet.Keygen/Extensions/KeyHostAlgorithmExtension.cs create mode 100644 SshNet.Keygen/SshSignature.cs create mode 100644 SshNet.Keygen/SshSignatureReader.cs diff --git a/SshNet.Keygen.Sample/SshNet.Keygen.Sample.csproj b/SshNet.Keygen.Sample/SshNet.Keygen.Sample.csproj index d2c9e12..632a251 100644 --- a/SshNet.Keygen.Sample/SshNet.Keygen.Sample.csproj +++ b/SshNet.Keygen.Sample/SshNet.Keygen.Sample.csproj @@ -8,7 +8,7 @@ - + diff --git a/SshNet.Keygen.Tests/SshNet.Keygen.Tests.csproj b/SshNet.Keygen.Tests/SshNet.Keygen.Tests.csproj index 581acc5..45c71f5 100644 --- a/SshNet.Keygen.Tests/SshNet.Keygen.Tests.csproj +++ b/SshNet.Keygen.Tests/SshNet.Keygen.Tests.csproj @@ -3,7 +3,7 @@ net48;net8.0 net8.0 - 9 + latest false @@ -19,6 +19,7 @@ + \ No newline at end of file diff --git a/SshNet.Keygen.Tests/TestKey.cs b/SshNet.Keygen.Tests/TestKey.cs index 386a3db..ed9c2a6 100644 --- a/SshNet.Keygen.Tests/TestKey.cs +++ b/SshNet.Keygen.Tests/TestKey.cs @@ -209,6 +209,13 @@ private string GetKey(string keyname) return reader.ReadToEnd(); } + private string GetSignatureResource(string keyname) + { + var resourceStream = Assembly.GetExecutingAssembly().GetManifestResourceStream($"SshNet.Keygen.Tests.TestSignatures.{keyname}"); + using var reader = new StreamReader(resourceStream, Encoding.ASCII); + return reader.ReadToEnd().Replace(Environment.NewLine, "\n"); + } + private void TestFormatKey(string keyname, int keyLength, string passphrase = null) { if (!string.IsNullOrEmpty(passphrase)) @@ -302,5 +309,46 @@ public void TestED25519() TestFormatKey("ED25519", 256); TestFormatKey("ED25519", 256, "12345"); } + + [Test] + public void TestVerify() + { + List keys = ["RSA2048", "RSA3072", "RSA4096", "RSA8192", "ECDSA256", "ECDSA384", "ECDSA521", "ED25519"]; + var data = Encoding.UTF8.GetBytes(GetSignatureResource("file.txt")); + + foreach (var key in keys) + { + TestContext.WriteLine($"Testing Key {key}"); + var signature = GetSignatureResource($"file.txt.{key}.sig"); + ClassicAssert.IsTrue(SshSignature.Verify(data, signature)); + } + } + + [Test] + public void TestSign() + { + List keys = ["RSA2048", "RSA3072", "RSA4096", "RSA8192", "ECDSA256", "ECDSA384", "ECDSA521", "ED25519"]; + var data = Encoding.UTF8.GetBytes(GetSignatureResource("file.txt")); + + foreach (var key in keys) + { + TestContext.WriteLine($"Testing Key {key}"); + var expectedSignature = GetSignatureResource($"file.txt.{key}.sig"); + var keyData = GetKey(key); + var keyFile = new PrivateKeyFile(keyData.ToStream()); + var signature = keyFile.Sign(data); + ClassicAssert.IsTrue(SshSignature.Verify(data, signature)); + + // ECDSA Signatures differ on each run + if (!key.StartsWith("ECDSA")) + ClassicAssert.AreEqual(expectedSignature, signature); + + var file = $"file-{key}.txt"; + File.WriteAllText(file, "bla"); + keyFile.SignFile(file); + ClassicAssert.IsTrue(SshSignature.VerifyFile(file, $"{file}.sig")); + + } + } } } \ No newline at end of file diff --git a/SshNet.Keygen.Tests/TestSignatures/file.txt b/SshNet.Keygen.Tests/TestSignatures/file.txt new file mode 100644 index 0000000..a7f8d9e --- /dev/null +++ b/SshNet.Keygen.Tests/TestSignatures/file.txt @@ -0,0 +1 @@ +bla diff --git a/SshNet.Keygen.Tests/TestSignatures/file.txt.ECDSA256.sig b/SshNet.Keygen.Tests/TestSignatures/file.txt.ECDSA256.sig new file mode 100644 index 0000000..a5b0921 --- /dev/null +++ b/SshNet.Keygen.Tests/TestSignatures/file.txt.ECDSA256.sig @@ -0,0 +1,7 @@ +-----BEGIN SSH SIGNATURE----- +U1NIU0lHAAAAAQAAAGgAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAAhuaXN0cDI1NgAAAE +EESopJD2EOZurPTh6aNd6RvfBG4/VIiEYL6RF3xWNuNJ9kD9q/qdNTt3bCTE7QgUiN4LvH +3m2+1W/FdCo1rxVFcQAAAARmaWxlAAAAAAAAAAZzaGE1MTIAAABkAAAAE2VjZHNhLXNoYT +ItbmlzdHAyNTYAAABJAAAAIGQ5GV/wx49Jt1ewe0lGDSdHRK84bxFrqkiaGd0ppF4FAAAA +IQCgeawU+z9lTXJbfDX5jEirNV5EkYzQsWZ+L/T216objQ== +-----END SSH SIGNATURE----- diff --git a/SshNet.Keygen.Tests/TestSignatures/file.txt.ECDSA384.sig b/SshNet.Keygen.Tests/TestSignatures/file.txt.ECDSA384.sig new file mode 100644 index 0000000..e79bd84 --- /dev/null +++ b/SshNet.Keygen.Tests/TestSignatures/file.txt.ECDSA384.sig @@ -0,0 +1,8 @@ +-----BEGIN SSH SIGNATURE----- +U1NIU0lHAAAAAQAAAIgAAAATZWNkc2Etc2hhMi1uaXN0cDM4NAAAAAhuaXN0cDM4NAAAAG +EERxwFxKqyTbx914sfwUEobYBtwuyUqXwOgkAIArdChUzPUyP/SBj8U6SoxtiBHp8CXEzT +pgkjwaYU7qNtypf0/Qy/JR8Bc3VO3iWNufT/t8A/Twc/6DMZIdmxOPWXQwaMAAAABGZpbG +UAAAAAAAAABnNoYTUxMgAAAIMAAAATZWNkc2Etc2hhMi1uaXN0cDM4NAAAAGgAAAAwbnar +SmL/5XlkV8ySShlIHrNNTUzVVioIWxd2AWFYdul7iaGF4DGfK6laBJSTK+eqAAAAMCdjtp +OKFcNpYZ+cauvrBJSvTJ9Pb8EJj0PUi61SrJkp3ZFULesWuglUbtwUttrcSQ== +-----END SSH SIGNATURE----- diff --git a/SshNet.Keygen.Tests/TestSignatures/file.txt.ECDSA521.sig b/SshNet.Keygen.Tests/TestSignatures/file.txt.ECDSA521.sig new file mode 100644 index 0000000..635b8e0 --- /dev/null +++ b/SshNet.Keygen.Tests/TestSignatures/file.txt.ECDSA521.sig @@ -0,0 +1,10 @@ +-----BEGIN SSH SIGNATURE----- +U1NIU0lHAAAAAQAAAKwAAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQAAAI +UEAK5WmW+O0Ut75D9KATr6yEFwYZmBsHJmxUXRRrcyvTbdpKofHoVzpY4WHK4pJg8GHwG1 +NnpDHyjsPAD6oe4dYTiCAO9uYQPGlmxu8KTt+4VOtsX3IkAVaWPb3rsn+k9yz4WP2PEnRD +ILrQXT4Nc5XmoA4lCAcFNIxmmxPu7zVaNq8/hBAAAABGZpbGUAAAAAAAAABnNoYTUxMgAA +AKUAAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAIoAAABBbSiayMz22ln7pRn0byMB57Cq/R +4sg8m0ngeFOmY70XC/kNK7TTA3VTD0UkBsdG2KLAj6s2haUiH8LSiOjJ6mbmAAAABBPpmj +I2mBpTW38WZLGsEf4prYeIO7dO0TY1hzGefm6txqOxFaBmpzZMT8ktYaikU/O8JBaIqiw7 +ZMZNe5ShmuvuM= +-----END SSH SIGNATURE----- diff --git a/SshNet.Keygen.Tests/TestSignatures/file.txt.ED25519.sig b/SshNet.Keygen.Tests/TestSignatures/file.txt.ED25519.sig new file mode 100644 index 0000000..c089d05 --- /dev/null +++ b/SshNet.Keygen.Tests/TestSignatures/file.txt.ED25519.sig @@ -0,0 +1,6 @@ +-----BEGIN SSH SIGNATURE----- +U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgTlboFw2cy42Ati5RrXwX8KLx3M ++p4g78fffefTK/ijoAAAAEZmlsZQAAAAAAAAAGc2hhNTEyAAAAUwAAAAtzc2gtZWQyNTUx +OQAAAED0TUwGrgbbWKVj6BiR07l0OrtJa5uM6vCo5Fe4smzPcTzmJDODkllHIgRuI/E+PD +dC67uGC9N7Jgzc54EoFHIO +-----END SSH SIGNATURE----- diff --git a/SshNet.Keygen.Tests/TestSignatures/file.txt.RSA2048.sig b/SshNet.Keygen.Tests/TestSignatures/file.txt.RSA2048.sig new file mode 100644 index 0000000..29989a9 --- /dev/null +++ b/SshNet.Keygen.Tests/TestSignatures/file.txt.RSA2048.sig @@ -0,0 +1,14 @@ +-----BEGIN SSH SIGNATURE----- +U1NIU0lHAAAAAQAAARcAAAAHc3NoLXJzYQAAAAMBAAEAAAEBAOJpdVVMNyPkABr2ywB2iO +ns3StUJMUNDGuFjqyNzVYvaX3C8rjB6i1EoBCHbp6ZPEEU8e6bOPU6i2hvQTjFWxqmaRvj +3hz7VAu+wMMmQkw1IMZyw2YhKi/+sCz8Yb3vI2xUHR1PZLtZj7K47prVLkbiWtycIiJaCD +n9nI1QYeHX40in+0witV9D6T+tieUbyda/3C31KL1y5Vs4plHssEWayKq/Yi5xqWLAitGO +KGUofEk1N0FEagJrMEzfDiEUxbFOFjedRo2lfgY/KUUzc1gabNYHH927P+gup/60pYLM9s +MpgjBB8v1KJ2F/tCBKMyX0BZ7QYhWvVMFIM4F3SycAAAAEZmlsZQAAAAAAAAAGc2hhNTEy +AAABFAAAAAxyc2Etc2hhMi01MTIAAAEA2WMT5aZ6fJ/ZXF0Gl/Vym8mTtDXEufziwjmt+z +ZSt3MF0GlwNDiYkeHFjyg16zqrJkeddj7yENyQ0Eae0Ew+7iFML6sKTEJKaiYf51/U+Jli +DVawwhH0+i3YZaCGmbiEQeXHfuFtA8deCdyxUkUYbycpdrfd0bx2dZFYu/WgNa9gHu/OVO +NQqqDOAHZUAko2MHN2GZ7wiepbGO9NAjhRtRE2tV6X8l3KI1+PmqvyfOQQMZcVa1V/WaFR +8wx1Z3VBJ+szP0XBdWrrwKY8K7yEE5rm55mx2rGtXuySGgISMbZlUHlJp31YE0Z4jMcEE+ +5m61gVpll8DYFSakNlBk6Xgg== +-----END SSH SIGNATURE----- diff --git a/SshNet.Keygen.Tests/TestSignatures/file.txt.RSA3072.sig b/SshNet.Keygen.Tests/TestSignatures/file.txt.RSA3072.sig new file mode 100644 index 0000000..9c6dd77 --- /dev/null +++ b/SshNet.Keygen.Tests/TestSignatures/file.txt.RSA3072.sig @@ -0,0 +1,19 @@ +-----BEGIN SSH SIGNATURE----- +U1NIU0lHAAAAAQAAAZcAAAAHc3NoLXJzYQAAAAMBAAEAAAGBAKs3fv8jufGNRU/E8SNlcG +Eg3HBy+NXCcF0zzvwQivdQ38GjjZG3AyLJd1IYNX4JmSxLOoy17LMlHlY0Yn9tb9ZVgoiX +Nz+TC/8t1BMvKcokxlcMlJuK30WEYFzcseJ/zrC+KEeHZMlzWa4Hm5R8mvsgF5+9N3RQYL +kekiVn6dCu6Glw0ZGt6QUMhF8VXJx6wDDqbHLgtO/UzauoAE3a87ypj8zpDjsbDee3EJ9p +RkCg4o0FiECOniOkOxRZCVFEKm7kwR8wiPTyjReNZcConEFkU1rb/ZYztkcu7W1psZYUmj +j0TMfZpiimyLuASfAlqKshtL4VZdCOr+YTL6gkoMEQcce8SsT4+38F42ecGsybw43k8tCy +t6D6c72irREqr+yO8qWy1CunMPHcu1zqIgPKISntdjC4ha7XGxqr6kqDlQfkqdqqNaVU5q +x9GzpaB3+cT+auZsZFBgz6ProxxjtCTv3+wuPnAKIMbk24LM1wN21zpzFG4uWf7fYSG9Zb +RQAAAARmaWxlAAAAAAAAAAZzaGE1MTIAAAGUAAAADHJzYS1zaGEyLTUxMgAAAYAC6NRILC +HH/D2iTC+cvr0M2zwjoEDfEOIMCIcIF2BYRobo6UIudUL2ST8eIBriAsxkgdOnvt/+evN/ +iirFMtb7RDC/bysHoaWEJYSkHsM/aklEiPgw0z3+eGDQChRBZQPRPPuAl6GOl8RRuNm0Ea +Wy+Rpj483yvDb4JSsf5+iAIFKhdv692B8T9MaqAfzhgkDLfoKfHPSRs8i3jlCFmIEXdBVu +Gs34jYKaX1uxuDsBvTeC24XsZNcwbRAO2YUrH0PRX6mpqS06xeRTUzIlYd7Iq1an62xGx1 +Alu2cGrh4P1tWHwxF6TC7um6CaqQOVipWP7jAUc12cBdjC205Vw/GCPB+N7Q5Ot7qSl3L5 +ELyd2n1HJVXuL5rFQDp7iELgjuiObn1ZSF3+C1tryBTSVsnfkui4BmZmC2AstbGUfuX2Nn +HDLTpRAt/ocC17HLro43pm6aj/HYa8Y3s8kZoPYVg/YzYOxqPXaBDlPsEyK7P06utKFsq/ +wlTihlGHDyob6n8= +-----END SSH SIGNATURE----- diff --git a/SshNet.Keygen.Tests/TestSignatures/file.txt.RSA4096.sig b/SshNet.Keygen.Tests/TestSignatures/file.txt.RSA4096.sig new file mode 100644 index 0000000..d86c0c5 --- /dev/null +++ b/SshNet.Keygen.Tests/TestSignatures/file.txt.RSA4096.sig @@ -0,0 +1,24 @@ +-----BEGIN SSH SIGNATURE----- +U1NIU0lHAAAAAQAAAhcAAAAHc3NoLXJzYQAAAAMBAAEAAAIBANzKRCvUnxEuXjRGgJbDXZ +1ZsE+25fSozMUSMD4C7ZnhZi+uNiEL5PCOpboywNLplhn7CVs3vC+xY48hV5lfdl7zOrYo +bhkUDfbDSGJiHn1GfeWjHUpXRkkIaERhmSBByj41JrPAupg4tdRO/OCv/iN6Dc2YM9Fe6n +7C/lQ+3X8q6NNRRgHtCZxdA5xcSwpYKP46KqOxRxTVi+drOPTR+wPSJorRMyFgB+1P+28A +wHO3ga2mHSryzQUpuWgh5jcxrYjMFZ3nZLqthDP2MLtJNrMlOJVQjXF3Y35SpKUDp2cJxS +cHPoDm6fnO5O6ukNuwNq2X+fFObyA77CLwpNLnWDp27Mh9IOtjpz0Mu2KeK0t//54tiSK3 +BA5cY11/GFnZgHuz24mnNdIgvNfCvW3lvH7zNkow/01+UMoD76W+hHA9kKJ78avj3MA1V9 +8jMlOxjviBpCYcCbp0ueHJIeei1sWtWHdVbjnVQGmBgmxj0IRHi3Up8DJrBuZlW71o2+a3 +GjJeDMZ2DJwHJan1zJ0rQKDYSxpDwxB72y0hdrRwkJ7XK/ZyMFAVsDloE8gvzsMOyzQB4u +YKKveqr9VTvoIMFFtSuxykrVUT08/z5gbqoUEDZpe628BczFDqW7uY6KrFtcJbWJpff6pi +PUeLR/PVD+f4awg4rGqIeLHbRnUUfO57AAAABGZpbGUAAAAAAAAABnNoYTUxMgAAAhQAAA +AMcnNhLXNoYTItNTEyAAACADH0iWetBeNFVBrYVIIklzW8voMYD9mRUQ7neptnHFcjiZIn +XYk0/HdewS0V8McBUNbBFKhi3Ok2AVkGEXtK6+LqpUr20WgNKNvaLjloiwuPFBzt3ML/Rb +brShEZFX+YwAqExIME7CLhH3CDuax0WwFhWe2KiXz+d+6D2q5+8r4x/pPPWiRh9J39RRsm +014qHT2mon9PpGSHOYmBjuonsFbTy/smRxOY60n6hNQ+QFLLY6L56ZWXBFabueRoB+7G9r +Fk79r6YSMzy80X2U/5GtLTkPbtT7dUtW0TduUyF8mwpz2gRyTwIaD4U/bYcpCjTiT4MT7J +b4mQszNGbt/eDpMHOtZ8OWIHoLjPXCG2h2afJvS+9Xe3e+S3QXPHm+UlF5Cb01lE3Vnq5J +nwnC37DX/Sep1sh49xoS621Atb2qe46lKZPCRCQXY3XJ1LgViljtnDEl/9aSEr9fvl0LnE +qOg/Hh/7yxUcztASaJHN7QBOwUFXrOBSavr1sP58kOpKb7nfKvUeMCq7OUi1blWIJ66nfC +R3LHhgfX6O0ZmK15nVwo4fMSMXo0MrinBRWgY7AofVUw27EEzCKAwVoN/TQzl4Wdws1YSp +RpqSdyCMgtbNQFo2ui2Gfefyz4Y4V9u+pwjFWRfTgA+29XjFLxNM/PoIvrR/Yx6PTOAgiW +9Jzr7i +-----END SSH SIGNATURE----- diff --git a/SshNet.Keygen.Tests/TestSignatures/file.txt.RSA8192.sig b/SshNet.Keygen.Tests/TestSignatures/file.txt.RSA8192.sig new file mode 100644 index 0000000..dd5b8db --- /dev/null +++ b/SshNet.Keygen.Tests/TestSignatures/file.txt.RSA8192.sig @@ -0,0 +1,43 @@ +-----BEGIN SSH SIGNATURE----- +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 +-----END SSH SIGNATURE----- diff --git a/SshNet.Keygen.Tests/TestSignatures/gen_test_sigs.sh b/SshNet.Keygen.Tests/TestSignatures/gen_test_sigs.sh new file mode 100644 index 0000000..2f61e61 --- /dev/null +++ b/SshNet.Keygen.Tests/TestSignatures/gen_test_sigs.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -ex + +function gen_sig { + key=$1 + + rm -f file.txt.sig + cp ../TestKeys/$key key + chmod 0600 key + ssh-keygen -Y sign -f key -n file file.txt + mv file.txt.sig file.txt.$key.sig +} + +# RSA +for b in 2048 3072 4096 8192; do + gen_sig RSA$b +done +# ECDSA +for b in 256 384 521; do + gen_sig ECDSA$b +done +# ED25519 +gen_sig ED25519 diff --git a/SshNet.Keygen/Extensions/KeyExtension.cs b/SshNet.Keygen/Extensions/KeyExtension.cs index bbfc010..60d6aba 100644 --- a/SshNet.Keygen/Extensions/KeyExtension.cs +++ b/SshNet.Keygen/Extensions/KeyExtension.cs @@ -297,7 +297,7 @@ internal static string ToPuttyFormat(this Key key, ISshKeyEncryption encryption, #endregion - private static void PublicKeyData(this Key key, BinaryWriter writer) + internal static void PublicKeyData(this Key key, BinaryWriter writer) { writer.EncodeBinary(key.ToString()); switch (key.ToString()) diff --git a/SshNet.Keygen/Extensions/KeyHostAlgorithmExtension.cs b/SshNet.Keygen/Extensions/KeyHostAlgorithmExtension.cs new file mode 100644 index 0000000..71377ef --- /dev/null +++ b/SshNet.Keygen/Extensions/KeyHostAlgorithmExtension.cs @@ -0,0 +1,22 @@ +using System.Security.Cryptography; +using Renci.SshNet.Security; + +namespace SshNet.Keygen.Extensions +{ + public static class KeyHostAlgorithmExtension + { + #region Sign + + internal static string Signature(this KeyHostAlgorithm keyHostAlgorithm, byte[] data) + { + return SshSignature.Sign(keyHostAlgorithm, data); + } + + internal static void SignatureFile(this KeyHostAlgorithm keyHostAlgorithm, string path) + { + SshSignature.SignFile(keyHostAlgorithm, path); + } + + #endregion + } +} \ No newline at end of file diff --git a/SshNet.Keygen/Extensions/PrivateKeyFileExtension.cs b/SshNet.Keygen/Extensions/PrivateKeyFileExtension.cs index af3b359..1f468a8 100644 --- a/SshNet.Keygen/Extensions/PrivateKeyFileExtension.cs +++ b/SshNet.Keygen/Extensions/PrivateKeyFileExtension.cs @@ -1,6 +1,9 @@ -using System.Linq; +using System; +using System.Linq; +using System.Security.Cryptography; using Renci.SshNet; using Renci.SshNet.Security; +using Renci.SshNet.Security.Cryptography; using SshNet.Keygen.SshKeyEncryption; namespace SshNet.Keygen.Extensions @@ -117,6 +120,32 @@ public static string ToPuttyFormat(this IPrivateKeySource keyFile, ISshKeyEncryp return ((KeyHostAlgorithm) keyFile.HostKeyAlgorithms.First()).Key.ToPuttyFormat(encryption, sshKeyFormat); } + #endregion + + #region Sign + + public static string Sign(this IPrivateKeySource keyFile, byte[] data) + { + return GetSignKeyHostAlgorithm(keyFile).Signature(data); + } + + public static void SignFile(this IPrivateKeySource keyFile, string path) + { + GetSignKeyHostAlgorithm(keyFile).SignatureFile(path); + } + + private static KeyHostAlgorithm GetSignKeyHostAlgorithm(this IPrivateKeySource keyFile) + { + var keyHostAlgorithm = (KeyHostAlgorithm)keyFile.HostKeyAlgorithms.First(); + if (keyHostAlgorithm.Key is RsaKey rsaKey) + { + keyHostAlgorithm = new KeyHostAlgorithm("rsa-sha2-512", keyHostAlgorithm.Key, new RsaDigitalSignature(rsaKey, HashAlgorithmName.SHA512)); + } + + return keyHostAlgorithm; + } + + #endregion } } \ No newline at end of file diff --git a/SshNet.Keygen/SshSignature.cs b/SshNet.Keygen/SshSignature.cs new file mode 100644 index 0000000..c38b400 --- /dev/null +++ b/SshNet.Keygen/SshSignature.cs @@ -0,0 +1,160 @@ +using System; +using System.Data; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; +using Renci.SshNet.Common; +using Renci.SshNet.Security; +using Renci.SshNet.Security.Cryptography; +using SshNet.Keygen.Extensions; + +namespace SshNet.Keygen +{ + public class SshSignature + { + private static readonly Regex SshSignatureRegex = new( + "^-+ *BEGIN SSH SIGNATURE *-+(\\r|\\n)*(?([a-zA-Z0-9/+=]{1,80}(\\r|\\n)+)+)(\\r|\\n)*-+ *END SSH SIGNATURE *-+", + RegexOptions.Compiled | RegexOptions.Multiline); + + private static readonly string Preambel = "SSHSIG"; + private static readonly uint Version = 1; + + public static bool VerifyFile(string path, string signaturePath) + { + return Verify(File.ReadAllBytes(path), File.ReadAllText(signaturePath)); + } + + public static bool Verify(byte[] data, string signature) + { + var signatureMatch = SshSignatureRegex.Match(signature); + if (!signatureMatch.Success) + { + throw new SshException("Invalid SSH signature"); + } + + var signatureData = signatureMatch.Result("${data}"); + var binaryData = Convert.FromBase64String(signatureData); + + var stream = new MemoryStream(binaryData); + var reader = new SshSignatureReader(stream); + + if (Encoding.ASCII.GetString(reader.ReadBytes(6)) != Preambel) + throw new SshException("Wrong preamble"); + + if (reader.ReadUInt32() != Version) + throw new SshException("Wrong version"); + + var pubKeyLength = reader.ReadUInt32(); // pub key length + var pubKeyData = reader.ReadBytes((int)pubKeyLength); // pubkey + + var @namespace = reader.ReadString(); // namespace + reader.ReadString(); // reserved + var hashAlgo = reader.ReadString(); // hash-algo + var hashAlgorithm = HashAlgorithm.Create(hashAlgo); + + if (hashAlgorithm is null) + throw new SshException($"Unknown hash algorithm {hashAlgo}"); + + var encodedSignatureLength = reader.ReadUInt32(); + var encodedSignature = reader.ReadBytes((int)encodedSignatureLength); + var signatureStream = new MemoryStream(encodedSignature); + var signatureReader = new SshSignatureReader(signatureStream); + + var sigAlgo = signatureReader.ReadString(); // sig algo + var sigLength = signatureReader.ReadUInt32(); // sig length + var sigData = signatureReader.ReadBytes((int)sigLength); // sig + + DigitalSignature digitalSignature; + Key key; + + switch (sigAlgo) + { + case "rsa-sha2-512": + key = new RsaKey(new SshKeyData(pubKeyData)); + digitalSignature = new RsaDigitalSignature((RsaKey)key, HashAlgorithmName.SHA512); + break; + case "rsa-sha2-256": + key = new RsaKey(new SshKeyData(pubKeyData)); + digitalSignature = new RsaDigitalSignature((RsaKey)key, HashAlgorithmName.SHA256); + break; + case "ssh-ed25519": + key = new ED25519Key(new SshKeyData(pubKeyData)); + digitalSignature = new ED25519DigitalSignature((ED25519Key)key); + break; + case "ecdsa-sha2-nistp256": + case "ecdsa-sha2-nistp384": + case "ecdsa-sha2-nistp521": + key = new EcdsaKey(new SshKeyData(pubKeyData)); + digitalSignature = new EcdsaDigitalSignature((EcdsaKey)key); + break; + default: + throw new SshException($"Unknown signature algorithm {sigAlgo}"); + } + + var verifyStream = new MemoryStream(); + var verifyWriter = new BinaryWriter(verifyStream); + verifyWriter.Write(Encoding.UTF8.GetBytes(Preambel)); + verifyWriter.EncodeBinary(@namespace); + verifyWriter.EncodeBinary(""); // reserved + verifyWriter.EncodeBinary(hashAlgo); + verifyWriter.EncodeBinary(hashAlgorithm.ComputeHash(data)); + + return digitalSignature.Verify(verifyStream.ToArray(), sigData); + } + + public static void SignFile(KeyHostAlgorithm keyHostAlgorithm, string path) + { + var sigFile = $"{path}.sig"; + File.WriteAllText(sigFile, Sign(keyHostAlgorithm, File.ReadAllBytes(path))); + } + + public static string Sign(KeyHostAlgorithm keyHostAlgorithm, byte[] data) + { + var hashAlgorithmName = HashAlgorithmName.SHA512; + var @namespace = "file"; // ToDo: expose? + + using var pubStream = new MemoryStream(); + using var pubWriter = new BinaryWriter(pubStream); + keyHostAlgorithm.Key.PublicKeyData(pubWriter); + + var hashAlgorithm = HashAlgorithm.Create(hashAlgorithmName.Name); + if (hashAlgorithm is null) + throw new SshException($"Unknown hash algorithm {hashAlgorithmName.Name}"); + + var signStream = new MemoryStream(); + var signWriter = new BinaryWriter(signStream); + signWriter.Write(Encoding.UTF8.GetBytes(Preambel)); + signWriter.EncodeBinary(@namespace); + signWriter.EncodeBinary(""); // reserved + signWriter.EncodeBinary(hashAlgorithmName.Name.ToLower()); + signWriter.EncodeBinary(hashAlgorithm.ComputeHash(data)); + var signed = keyHostAlgorithm.Sign(signStream.ToArray()); + + var stream = new MemoryStream(); + var writer = new BinaryWriter(stream); + + writer.Write(Encoding.UTF8.GetBytes(Preambel)); + writer.EncodeUInt(Version); + writer.EncodeBinary(pubStream.ToArray()); + writer.EncodeBinary(@namespace); + writer.EncodeBinary(""); // reserved + writer.EncodeBinary(hashAlgorithmName.Name.ToLower()); + writer.EncodeBinary(signed); + + var base64 = Convert.ToBase64String(stream.ToArray()).ToCharArray(); + var pem = new StringWriter(); + for (var i = 0; i < base64.Length; i += 70) + { + pem.Write(base64, i, Math.Min(70, base64.Length - i)); + pem.Write("\n"); + } + + var s = new StringWriter(); + s.Write($"-----BEGIN SSH SIGNATURE-----\n"); + s.Write(pem.ToString()); + s.Write("-----END SSH SIGNATURE-----\n"); + return s.ToString(); + } + } +} \ No newline at end of file diff --git a/SshNet.Keygen/SshSignatureReader.cs b/SshNet.Keygen/SshSignatureReader.cs new file mode 100644 index 0000000..1a6b174 --- /dev/null +++ b/SshNet.Keygen/SshSignatureReader.cs @@ -0,0 +1,32 @@ +using System; +using System.IO; +using System.Text; + +namespace SshNet.Keygen +{ + public class SshSignatureReader : BinaryReader + { + public SshSignatureReader(Stream input) : base(input, Encoding.Default, true) + { + } + + public override uint ReadUInt32() + { + var data = base.ReadBytes(4); + if (BitConverter.IsLittleEndian) + Array.Reverse(data); + return BitConverter.ToUInt32(data, 0); + } + + public byte[] ReadStringAsBytes() + { + var len = (int)ReadUInt32(); + return base.ReadBytes(len); + } + + public override string ReadString() + { + return Encoding.UTF8.GetString(ReadStringAsBytes()); + } + } +} \ No newline at end of file