Skip to content

Commit

Permalink
Add support for ed25519 private keys (#212)
Browse files Browse the repository at this point in the history
Adds support for using ed25519 private key in user authentication. As
.NET does not support ED25519 in the BCL it uses
BouncyCastle.Cryptography as a dependency for the key signing tasks.
  • Loading branch information
jborean93 authored Aug 18, 2024
1 parent d72221a commit 280e43b
Show file tree
Hide file tree
Showing 13 changed files with 131 additions and 4 deletions.
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<Project>
<ItemGroup>
<!-- Tmds.Ssh dependencies -->
<PackageVersion Include="BouncyCastle.Cryptography" Version="2.4.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
<!-- Test dependencies -->
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ This section lists the currently supported algorithms. If you would like support

Supported private key formats:
- RSA in `RSA PRIVATE KEY`
- RSA, ECDSA in `OPENSSH PRIVATE KEY` (`openssh-key-v1`)
- RSA, ECDSA, ED25519 in `OPENSSH PRIVATE KEY` (`openssh-key-v1`)

Supported private key encryption cyphers:
- OpenSSH Keys `OPENSSH PRIVATE KEY` (`openssh-key-v1`)
Expand Down
2 changes: 2 additions & 0 deletions src/Tmds.Ssh/AlgorithmNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ static class AlgorithmNames // TODO: rename to KnownNames
public static Name EcdsaSha2Nistp384 => new Name(EcdsaSha2Nistp384Bytes);
private static readonly byte[] EcdsaSha2Nistp521Bytes = "ecdsa-sha2-nistp521"u8.ToArray();
public static Name EcdsaSha2Nistp521 => new Name(EcdsaSha2Nistp521Bytes);
private static readonly byte[] SshEd25519Bytes = "ssh-ed25519"u8.ToArray();
public static Name SshEd25519 => new Name(SshEd25519Bytes);

// Encryption algorithms.
private static readonly byte[] Aes128CbcBytes = "aes128-cbc"u8.ToArray();
Expand Down
63 changes: 63 additions & 0 deletions src/Tmds.Ssh/Ed25519PrivateKey.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// This file is part of Tmds.Ssh which is released under MIT.
// See file LICENSE for full license details.

using System.Buffers;
using Org.BouncyCastle.Math.EC.Rfc8032;

namespace Tmds.Ssh;

sealed class Ed25519PrivateKey : PrivateKey
{
// Contains the private and public key as one block of bytes from the
// serialized OpenSSH key data.
private readonly byte[] _privateKey;
private readonly byte[] _publicKey;

public Ed25519PrivateKey(byte[] privateKey, byte[] publicKey) :
base([AlgorithmNames.SshEd25519])
{
_privateKey = privateKey;
_publicKey = publicKey;
}

public override void Dispose()
{ }

public override void AppendPublicKey(ref SequenceWriter writer)
{
using var innerData = writer.SequencePool.RentSequence();
var innerWriter = new SequenceWriter(innerData);
innerWriter.WriteString(Algorithms[0]);
innerWriter.WriteString(_publicKey);

writer.WriteString(innerData.AsReadOnlySequence());
}

public override void AppendSignature(Name algorithm, ref SequenceWriter writer, ReadOnlySequence<byte> data)
{
if (algorithm != Algorithms[0])
{
ThrowHelper.ThrowProtocolUnexpectedValue();
return;
}

byte[] signature = new byte[Ed25519.SignatureSize];
Ed25519.Sign(
_privateKey,
0,
_publicKey,
0,
data.ToArray(),
0,
(int)data.Length,
signature,
0);

using var innerData = writer.SequencePool.RentSequence();
var innerWriter = new SequenceWriter(innerData);
innerWriter.WriteString(algorithm);
innerWriter.WriteString(signature);

writer.WriteString(innerData.AsReadOnlySequence());
}
}
38 changes: 37 additions & 1 deletion src/Tmds.Ssh/PrivateKeyParser.OpenSsh.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,14 @@ byte padlen % 255
{
return TryParseOpenSshRsaKey(reader, out privateKey, out error);
}
if (keyType.ToString().StartsWith("ecdsa-sha2-"))
else if (keyType.ToString().StartsWith("ecdsa-sha2-"))
{
return TryParseOpenSshEcdsaKey(keyType, reader, out privateKey, out error);
}
else if (keyType == AlgorithmNames.SshEd25519)
{
return TryParseOpenSshEd25519Key(reader, out privateKey, out error);
}
else
{
error = new NotSupportedException($"The key type is unsupported: '{keyType}'.");
Expand Down Expand Up @@ -280,4 +284,36 @@ private static bool TryParseOpenSshEcdsaKey(Name keyIdentifier, SequenceReader r
return false;
}
}

private static bool TryParseOpenSshEd25519Key(SequenceReader reader, [NotNullWhen(true)] out PrivateKey? privateKey, [NotNullWhen(false)] out Exception? error)
{
privateKey = null;

// https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent-14#section-3.2.3
/*
string ENC(A)
string k || ENC(A)
The first value is the EDDSA public key ENC(A). The second value is a
concatenation of the private key k and the public ENC(A) key. Why it is
repeated, I have no idea.
*/

try
{
ReadOnlySequence<byte> publicKey = reader.ReadStringAsBytes();
ReadOnlySequence<byte> keyData = reader.ReadStringAsBytes();

privateKey = new Ed25519PrivateKey(
keyData.Slice(0, keyData.Length - publicKey.Length).ToArray(),
publicKey.ToArray());
error = null;
return true;
}
catch (Exception ex)
{
error = new FormatException($"The data can not be parsed into an ED25519 key.", ex);
return false;
}
}
}
2 changes: 1 addition & 1 deletion src/Tmds.Ssh/SshClientSettings.Defaults.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ partial class SshClientSettings
internal readonly static List<Name> SupportedKeyExchangeAlgorithms = [ AlgorithmNames.EcdhSha2Nistp256, AlgorithmNames.EcdhSha2Nistp384, AlgorithmNames.EcdhSha2Nistp521 ];
internal readonly static List<Name> SupportedServerHostKeyAlgorithms = [ AlgorithmNames.EcdsaSha2Nistp521, AlgorithmNames.EcdsaSha2Nistp384, AlgorithmNames.EcdsaSha2Nistp256, AlgorithmNames.RsaSshSha2_512, AlgorithmNames.RsaSshSha2_256 ];
internal readonly static List<Name> SupportedEncryptionAlgorithms = [ AlgorithmNames.Aes256Gcm, AlgorithmNames.Aes128Gcm ];
internal readonly static List<Name> SupportedPublicKeyAcceptedAlgorithms = [ AlgorithmNames.EcdsaSha2Nistp521, AlgorithmNames.EcdsaSha2Nistp384, AlgorithmNames.EcdsaSha2Nistp256, AlgorithmNames.RsaSshSha2_512, AlgorithmNames.RsaSshSha2_256 ];
internal readonly static List<Name> SupportedPublicKeyAcceptedAlgorithms = [ AlgorithmNames.SshEd25519, AlgorithmNames.EcdsaSha2Nistp521, AlgorithmNames.EcdsaSha2Nistp384, AlgorithmNames.EcdsaSha2Nistp256, AlgorithmNames.RsaSshSha2_512, AlgorithmNames.RsaSshSha2_256 ];
internal readonly static List<Name> SupportedMacAlgorithms = EmptyList;
internal readonly static List<Name> SupportedCompressionAlgorithms = [ AlgorithmNames.None ];
internal readonly static List<Name> DisableCompressionAlgorithms = [ AlgorithmNames.None ];
Expand Down
1 change: 1 addition & 0 deletions src/Tmds.Ssh/Tmds.Ssh.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BouncyCastle.Cryptography" />
<PackageReference Include="Microsoft.Extensions.Logging" />
</ItemGroup>

Expand Down
14 changes: 14 additions & 0 deletions test/Tmds.Ssh.Tests/PrivateKeyCredentialTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,20 @@ await RunWithKeyConversion(_sshServer.TestUserIdentityFileEcdsa521, async (strin
}, async (c) => await c.ConnectAsync());
}

[Theory]
[InlineData(null)]
[InlineData("aes256-ctr")]
public async Task OpenSshEd25519Key(string? cipher)
{
await RunWithKeyConversion(_sshServer.TestUserIdentityFileEd25519, async (string localKey) =>
{
string? keyPass = string.IsNullOrWhiteSpace(cipher) ? null : TestPassword;
await EncryptSshKey(localKey, "RFC4716", keyPass, cipher);
return new PrivateKeyCredential(localKey, keyPass);
}, async (c) => await c.ConnectAsync());
}

[Fact]
public async Task OpenSshKeyPromptNotCalledForPlaintextKey()
{
Expand Down
2 changes: 1 addition & 1 deletion test/Tmds.Ssh.Tests/SshClientSettingsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public void Defaults()
Assert.Null(settings.HostAuthentication);
Assert.Equal(new[] { new Name("ecdh-sha2-nistp256"), new Name("ecdh-sha2-nistp384"), new Name("ecdh-sha2-nistp521") }, settings.KeyExchangeAlgorithms);
Assert.Equal(new[] { new Name("ecdsa-sha2-nistp521"), new Name("ecdsa-sha2-nistp384"), new Name("ecdsa-sha2-nistp256"), new Name("rsa-sha2-512"), new Name("rsa-sha2-256") }, settings.ServerHostKeyAlgorithms);
Assert.Equal(new[] { new Name("ecdsa-sha2-nistp521"), new Name("ecdsa-sha2-nistp384"), new Name("ecdsa-sha2-nistp256"), new Name("rsa-sha2-512"), new Name("rsa-sha2-256") }, settings.PublicKeyAcceptedAlgorithms);
Assert.Equal(new[] { new Name("ssh-ed25519"), new Name("ecdsa-sha2-nistp521"), new Name("ecdsa-sha2-nistp384"), new Name("ecdsa-sha2-nistp256"), new Name("rsa-sha2-512"), new Name("rsa-sha2-256") }, settings.PublicKeyAcceptedAlgorithms);
Assert.Equal(new[] { new Name("[email protected]"), new Name("[email protected]") }, settings.EncryptionAlgorithmsClientToServer);
Assert.Equal(new[] { new Name("[email protected]"), new Name("[email protected]") }, settings.EncryptionAlgorithmsServerToClient);
Assert.Equal(Array.Empty<Name>(), settings.MacAlgorithmsClientToServer);
Expand Down
1 change: 1 addition & 0 deletions test/Tmds.Ssh.Tests/SshServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public class SshServer : IDisposable
public string TestUserIdentityFileEcdsa256 => $"{ContainerBuildContext}/user_key_ecdsa_256";
public string TestUserIdentityFileEcdsa384 => $"{ContainerBuildContext}/user_key_ecdsa_384";
public string TestUserIdentityFileEcdsa521 => $"{ContainerBuildContext}/user_key_ecdsa_521";
public string TestUserIdentityFileEd25519 => $"{ContainerBuildContext}/user_key_ed25519";
public string TestSubsystem = "test_subsystem";
public string ServerHost => _host;
public int ServerPort => _port;
Expand Down
1 change: 1 addition & 0 deletions test/Tmds.Ssh.Tests/sshd_container/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ COPY user_key_rsa.pub /home/testuser/.ssh/user_key_rsa.pub
COPY user_key_ecdsa_256.pub /home/testuser/.ssh/user_key_ecdsa_256.pub
COPY user_key_ecdsa_384.pub /home/testuser/.ssh/user_key_ecdsa_384.pub
COPY user_key_ecdsa_521.pub /home/testuser/.ssh/user_key_ecdsa_521.pub
COPY user_key_ed25519.pub /home/testuser/.ssh/user_key_ed25519.pub
RUN cat /home/testuser/.ssh/user_key_*.pub > /home/testuser/.ssh/authorized_keys
RUN chown -R testuser:testuser /home/testuser/.ssh
RUN chmod 600 /home/testuser/.ssh/authorized_keys
Expand Down
7 changes: 7 additions & 0 deletions test/Tmds.Ssh.Tests/sshd_container/user_key_ed25519
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDBGs/6MWZXV20KtacQson8/EyafCK7KDCh0ZH8LBgvYQAAAKA1mv4HNZr+
BwAAAAtzc2gtZWQyNTUxOQAAACDBGs/6MWZXV20KtacQson8/EyafCK7KDCh0ZH8LBgvYQ
AAAEAzz1majY8Z1JKFhHlfyGiApNGarWquj0JNEG7TQynbCcEaz/oxZldXbQq1pxCyifz8
TJp8IrsoMKHRkfwsGC9hAAAAGnRtZHNAbG9jYWxob3N0LmxvY2FsZG9tYWluAQID
-----END OPENSSH PRIVATE KEY-----
1 change: 1 addition & 0 deletions test/Tmds.Ssh.Tests/sshd_container/user_key_ed25519.pub
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMEaz/oxZldXbQq1pxCyifz8TJp8IrsoMKHRkfwsGC9h [email protected]

0 comments on commit 280e43b

Please sign in to comment.