From fe036bafe638fd01465111ac2b5bc58e61ef914f 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 | 1 + SshNet.Keygen.Tests/TestKey.cs | 28 +++ SshNet.Keygen.Tests/TestSignatures/file.txt | 1 + .../TestSignatures/file.txt.sig | 14 ++ SshNet.Keygen/Extensions/KeyExtension.cs | 2 +- .../Extensions/KeyHostAlgorithmExtension.cs | 22 +++ .../Extensions/PrivateKeyFileExtension.cs | 31 +++- SshNet.Keygen/SshSignature.cs | 164 ++++++++++++++++++ SshNet.Keygen/SshSignatureReader.cs | 32 ++++ 10 files changed, 294 insertions(+), 3 deletions(-) create mode 100644 SshNet.Keygen.Tests/TestSignatures/file.txt create mode 100644 SshNet.Keygen.Tests/TestSignatures/file.txt.sig 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..27e753f 100644 --- a/SshNet.Keygen.Tests/SshNet.Keygen.Tests.csproj +++ b/SshNet.Keygen.Tests/SshNet.Keygen.Tests.csproj @@ -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..c371577 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(); + } + private void TestFormatKey(string keyname, int keyLength, string passphrase = null) { if (!string.IsNullOrEmpty(passphrase)) @@ -302,5 +309,26 @@ public void TestED25519() TestFormatKey("ED25519", 256); TestFormatKey("ED25519", 256, "12345"); } + + [Test] + public void TestVerify() + { + var data = Encoding.ASCII.GetBytes(GetSignatureResource("file.txt")); + var signature = GetSignatureResource("file.txt.sig"); + ClassicAssert.IsTrue(SshSignature.Verify(data, signature)); + } + + [Test] + public void TestSign() + { + var data = Encoding.ASCII.GetBytes(GetSignatureResource("file.txt")); + var expectedSignature = GetSignatureResource("file.txt.sig"); + var keydata = GetKey("RSA2048"); + var keyFile = new PrivateKeyFile(keydata.ToStream()); + var signature = keyFile.Sign(data, HashAlgorithmName.SHA512); + + SshSignature.Verify(data, signature); + ClassicAssert.AreEqual(expectedSignature, signature); + } } } \ 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.sig b/SshNet.Keygen.Tests/TestSignatures/file.txt.sig new file mode 100644 index 0000000..a9822e8 --- /dev/null +++ b/SshNet.Keygen.Tests/TestSignatures/file.txt.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----- \ No newline at end of file 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..318ff64 --- /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 Sign(this KeyHostAlgorithm keyHostAlgorithm, byte[] data, HashAlgorithmName hashAlgorithmName) + { + return SshSignature.Sign(keyHostAlgorithm, data, hashAlgorithmName); + } + + internal static void SignFile(this KeyHostAlgorithm keyHostAlgorithm, string path, HashAlgorithmName hashAlgorithmName) + { + SshSignature.SignFile(keyHostAlgorithm, path, hashAlgorithmName); + } + + #endregion + } +} \ No newline at end of file diff --git a/SshNet.Keygen/Extensions/PrivateKeyFileExtension.cs b/SshNet.Keygen/Extensions/PrivateKeyFileExtension.cs index af3b359..3f1c017 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 @@ -118,5 +121,31 @@ public static string ToPuttyFormat(this IPrivateKeySource keyFile, ISshKeyEncryp } #endregion + + #region Sign + + public static string Sign(this IPrivateKeySource keyFile, byte[] data, HashAlgorithmName hashAlgorithmName) + { + return keyFile.GetSignKeyHostAlgorithm(hashAlgorithmName).Sign(data, hashAlgorithmName); + } + + public static void SignFile(this IPrivateKeySource keyFile, string path, HashAlgorithmName hashAlgorithmName) + { + keyFile.GetSignKeyHostAlgorithm(hashAlgorithmName).SignFile(path, hashAlgorithmName); + } + + private static KeyHostAlgorithm GetSignKeyHostAlgorithm(this IPrivateKeySource keyFile, HashAlgorithmName hashAlgorithmName) + { + var keyHostAlgorithm = (KeyHostAlgorithm)keyFile.HostKeyAlgorithms.First(); + if (keyHostAlgorithm.Key is RsaKey rsaKey) + { + keyHostAlgorithm = hashAlgorithmName == HashAlgorithmName.SHA512 + ? new KeyHostAlgorithm("rsa-sha2-512", keyHostAlgorithm.Key, new RsaDigitalSignature(rsaKey, HashAlgorithmName.SHA512)) + : new KeyHostAlgorithm("rsa-sha2-256", keyHostAlgorithm.Key, new RsaDigitalSignature(rsaKey, HashAlgorithmName.SHA256)); + } + + 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..41d44fa --- /dev/null +++ b/SshNet.Keygen/SshSignature.cs @@ -0,0 +1,164 @@ +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.ASCII.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, HashAlgorithmName hashAlgorithmName) + { + var sigFile = $"{path}.sig"; + File.WriteAllText(sigFile, Sign(keyHostAlgorithm, File.ReadAllBytes(path), hashAlgorithmName)); + } + + public static string Sign(KeyHostAlgorithm keyHostAlgorithm, byte[] data, HashAlgorithmName hashAlgorithmName) + { + 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.ASCII.GetBytes(Preambel)); + signWriter.EncodeBinary(@namespace); + signWriter.EncodeBinary(""); // reserved + signWriter.EncodeBinary(hashAlgorithmName.Name.ToLower()); + signWriter.EncodeBinary(hashAlgorithm.ComputeHash(data)); + var signed = keyHostAlgorithm.DigitalSignature.Sign(signStream.ToArray()); + + var signatureStream = new MemoryStream(); + var signatureWriter = new BinaryWriter(signatureStream); + signatureWriter.EncodeBinary(keyHostAlgorithm.Name); + signatureWriter.EncodeBinary(signed); + + var stream = new MemoryStream(); + var writer = new BinaryWriter(stream); + + writer.Write(Encoding.ASCII.GetBytes(Preambel)); + writer.EncodeUInt(Version); + writer.EncodeBinary(pubStream.ToArray()); + writer.EncodeBinary(@namespace); + writer.EncodeBinary(""); // reserved + writer.EncodeBinary(hashAlgorithmName.Name.ToLower()); + writer.EncodeBinary(signatureStream.ToArray()); + + 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(Environment.NewLine); + } + + var s = new StringWriter(); + s.Write($"-----BEGIN SSH SIGNATURE-----{Environment.NewLine}"); + s.Write(pem.ToString()); + s.Write("-----END SSH SIGNATURE-----"); + 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..9d218b7 --- /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.ASCII.GetString(ReadStringAsBytes()); + } + } +} \ No newline at end of file