Skip to content

Commit

Permalink
Add Sign/Verify for SSH-Signatures
Browse files Browse the repository at this point in the history
  • Loading branch information
darinkes committed Jul 11, 2024
1 parent b230c43 commit 44df768
Show file tree
Hide file tree
Showing 10 changed files with 294 additions and 3 deletions.
2 changes: 1 addition & 1 deletion SshNet.Keygen.Sample/SshNet.Keygen.Sample.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="SSH.NET" Version="2024.0.0" />
<PackageReference Include="SSH.NET" Version="2024.1.0" />
<ProjectReference Include="..\SshNet.Keygen\SshNet.Keygen.csproj" />
</ItemGroup>

Expand Down
1 change: 1 addition & 0 deletions SshNet.Keygen.Tests/SshNet.Keygen.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

<ItemGroup>
<EmbeddedResource Include="TestKeys\*" />
<EmbeddedResource Include="TestSignatures\*" />
</ItemGroup>

</Project>
28 changes: 28 additions & 0 deletions SshNet.Keygen.Tests/TestKey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(string keyname, int keyLength, string passphrase = null)
{
if (!string.IsNullOrEmpty(passphrase))
Expand Down Expand Up @@ -302,5 +309,26 @@ public void TestED25519()
TestFormatKey<ED25519Key>("ED25519", 256);
TestFormatKey<ED25519Key>("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);
}
}
}
1 change: 1 addition & 0 deletions SshNet.Keygen.Tests/TestSignatures/file.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bla
14 changes: 14 additions & 0 deletions SshNet.Keygen.Tests/TestSignatures/file.txt.sig
Original file line number Diff line number Diff line change
@@ -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-----
2 changes: 1 addition & 1 deletion SshNet.Keygen/Extensions/KeyExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
22 changes: 22 additions & 0 deletions SshNet.Keygen/Extensions/KeyHostAlgorithmExtension.cs
Original file line number Diff line number Diff line change
@@ -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
}
}
31 changes: 30 additions & 1 deletion SshNet.Keygen/Extensions/PrivateKeyFileExtension.cs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
}
}
164 changes: 164 additions & 0 deletions SshNet.Keygen/SshSignature.cs
Original file line number Diff line number Diff line change
@@ -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)*(?<data>([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("\n");
}

var s = new StringWriter();
s.Write($"-----BEGIN SSH SIGNATURE-----\n");
s.Write(pem.ToString());
s.Write("-----END SSH SIGNATURE-----");
return s.ToString();
}
}
}
32 changes: 32 additions & 0 deletions SshNet.Keygen/SshSignatureReader.cs
Original file line number Diff line number Diff line change
@@ -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());
}
}
}

0 comments on commit 44df768

Please sign in to comment.