diff --git a/docfx/examples.md b/docfx/examples.md
index b4c7e2588..fd2ab53b1 100644
--- a/docfx/examples.md
+++ b/docfx/examples.md
@@ -59,12 +59,39 @@ using (var client = new SshClient("sftp.foo.com", "guest", "pwd"))
{
client.HostKeyReceived += (sender, e) =>
{
- e.CanTrust = expectedFingerPrint.Equals(e.FingerPrintSHA256);
+ e.CanTrust = e.FingerPrintSHA256 == expectedFingerPrint;
};
client.Connect();
}
```
+When expecting the server to present a certificate signed by a trusted certificate authority:
+
+```cs
+string expectedCAFingerPrint = "tF3DRTUXtYFZ5Yz0SBOrEbixHaCifHmNVK6FtptXZVM";
+
+using (var client = new SshClient("sftp.foo.com", "guest", "pwd"))
+{
+ client.HostKeyReceived += (sender, e) =>
+ {
+ e.CanTrust = e.Certificate?.CertificateAuthorityKeyFingerPrint == expectedCAFingerPrint;
+ };
+ client.Connect();
+}
+```
+
+### Authenticating with a user certificate
+
+When you have a certificate for your key which is signed by a certificate authority that the server trusts:
+
+```cs
+using (var privateKeyFile = new PrivateKeyFile("path/to/my/key", passPhrase: null, "path/to/my/certificate.pub"))
+using (var client = new SshClient("sftp.foo.com", "guest", privateKeyFile))
+{
+ client.Connect();
+}
+```
+
### Open a Shell
```cs
diff --git a/src/Renci.SshNet/Common/HostKeyEventArgs.cs b/src/Renci.SshNet/Common/HostKeyEventArgs.cs
index a41a675c6..b18d0cb4d 100644
--- a/src/Renci.SshNet/Common/HostKeyEventArgs.cs
+++ b/src/Renci.SshNet/Common/HostKeyEventArgs.cs
@@ -1,4 +1,5 @@
-using System;
+#nullable enable
+using System;
using Renci.SshNet.Abstractions;
using Renci.SshNet.Security;
@@ -83,6 +84,12 @@ public string FingerPrintMD5
///
public int KeyLength { get; private set; }
+ ///
+ /// Gets the certificate presented by the host, or if the host
+ /// did not present a certificate.
+ ///
+ public Certificate? Certificate { get; }
+
///
/// Initializes a new instance of the class.
///
@@ -93,7 +100,7 @@ public HostKeyEventArgs(KeyHostAlgorithm host)
ThrowHelper.ThrowIfNull(host);
CanTrust = true;
- HostKey = host.Data;
+ HostKey = host.KeyData.GetBytes();
HostKeyName = host.Name;
KeyLength = host.Key.KeyLength;
@@ -107,6 +114,11 @@ public HostKeyEventArgs(KeyHostAlgorithm host)
return BitConverter.ToString(FingerPrint).Replace('-', ':').ToLowerInvariant();
#pragma warning restore CA1308 // Normalize strings to uppercase
});
+
+ if (host is CertificateHostAlgorithm certificateAlg)
+ {
+ Certificate = certificateAlg.Certificate;
+ }
}
}
}
diff --git a/src/Renci.SshNet/ConnectionInfo.cs b/src/Renci.SshNet/ConnectionInfo.cs
index f99dbe613..52f614554 100644
--- a/src/Renci.SshNet/ConnectionInfo.cs
+++ b/src/Renci.SshNet/ConnectionInfo.cs
@@ -387,19 +387,26 @@ public ConnectionInfo(string host, int port, string username, ProxyTypes proxyTy
{ "hmac-sha1-etm@openssh.com", new HashInfo(20*8, key => new HMACSHA1(key), isEncryptThenMAC: true) },
};
- HostKeyAlgorithms = new Dictionary>
- {
- { "ssh-ed25519", data => new KeyHostAlgorithm("ssh-ed25519", new ED25519Key(new SshKeyData(data))) },
- { "ecdsa-sha2-nistp256", data => new KeyHostAlgorithm("ecdsa-sha2-nistp256", new EcdsaKey(new SshKeyData(data))) },
- { "ecdsa-sha2-nistp384", data => new KeyHostAlgorithm("ecdsa-sha2-nistp384", new EcdsaKey(new SshKeyData(data))) },
- { "ecdsa-sha2-nistp521", data => new KeyHostAlgorithm("ecdsa-sha2-nistp521", new EcdsaKey(new SshKeyData(data))) },
#pragma warning disable SA1107 // Code should not contain multiple statements on one line
- { "rsa-sha2-512", data => { var key = new RsaKey(new SshKeyData(data)); return new KeyHostAlgorithm("rsa-sha2-512", key, new RsaDigitalSignature(key, HashAlgorithmName.SHA512)); } },
- { "rsa-sha2-256", data => { var key = new RsaKey(new SshKeyData(data)); return new KeyHostAlgorithm("rsa-sha2-256", key, new RsaDigitalSignature(key, HashAlgorithmName.SHA256)); } },
+ var hostAlgs = new Dictionary>();
+ hostAlgs.Add("ssh-ed25519-cert-v01@openssh.com", data => { var cert = new Certificate(data); return new CertificateHostAlgorithm("ssh-ed25519-cert-v01@openssh.com", cert, hostAlgs); });
+ hostAlgs.Add("ecdsa-sha2-nistp256-cert-v01@openssh.com", data => { var cert = new Certificate(data); return new CertificateHostAlgorithm("ecdsa-sha2-nistp256-cert-v01@openssh.com", cert, hostAlgs); });
+ hostAlgs.Add("ecdsa-sha2-nistp384-cert-v01@openssh.com", data => { var cert = new Certificate(data); return new CertificateHostAlgorithm("ecdsa-sha2-nistp384-cert-v01@openssh.com", cert, hostAlgs); });
+ hostAlgs.Add("ecdsa-sha2-nistp521-cert-v01@openssh.com", data => { var cert = new Certificate(data); return new CertificateHostAlgorithm("ecdsa-sha2-nistp521-cert-v01@openssh.com", cert, hostAlgs); });
+ hostAlgs.Add("rsa-sha2-512-cert-v01@openssh.com", data => { var cert = new Certificate(data); return new CertificateHostAlgorithm("rsa-sha2-512-cert-v01@openssh.com", cert, new RsaDigitalSignature((RsaKey)cert.Key, HashAlgorithmName.SHA512), hostAlgs); });
+ hostAlgs.Add("rsa-sha2-256-cert-v01@openssh.com", data => { var cert = new Certificate(data); return new CertificateHostAlgorithm("rsa-sha2-256-cert-v01@openssh.com", cert, new RsaDigitalSignature((RsaKey)cert.Key, HashAlgorithmName.SHA256), hostAlgs); });
+ hostAlgs.Add("ssh-rsa-cert-v01@openssh.com", data => { var cert = new Certificate(data); return new CertificateHostAlgorithm("ssh-rsa-cert-v01@openssh.com", cert, hostAlgs); });
+ hostAlgs.Add("ssh-dss-cert-v01@openssh.com", data => { var cert = new Certificate(data); return new CertificateHostAlgorithm("ssh-dss-cert-v01@openssh.com", cert, hostAlgs); });
+ hostAlgs.Add("ssh-ed25519", data => new KeyHostAlgorithm("ssh-ed25519", new ED25519Key(new SshKeyData(data))));
+ hostAlgs.Add("ecdsa-sha2-nistp256", data => new KeyHostAlgorithm("ecdsa-sha2-nistp256", new EcdsaKey(new SshKeyData(data))));
+ hostAlgs.Add("ecdsa-sha2-nistp384", data => new KeyHostAlgorithm("ecdsa-sha2-nistp384", new EcdsaKey(new SshKeyData(data))));
+ hostAlgs.Add("ecdsa-sha2-nistp521", data => new KeyHostAlgorithm("ecdsa-sha2-nistp521", new EcdsaKey(new SshKeyData(data))));
+ hostAlgs.Add("rsa-sha2-512", data => { var key = new RsaKey(new SshKeyData(data)); return new KeyHostAlgorithm("rsa-sha2-512", key, new RsaDigitalSignature(key, HashAlgorithmName.SHA512)); });
+ hostAlgs.Add("rsa-sha2-256", data => { var key = new RsaKey(new SshKeyData(data)); return new KeyHostAlgorithm("rsa-sha2-256", key, new RsaDigitalSignature(key, HashAlgorithmName.SHA256)); });
+ hostAlgs.Add("ssh-rsa", data => new KeyHostAlgorithm("ssh-rsa", new RsaKey(new SshKeyData(data))));
+ hostAlgs.Add("ssh-dss", data => new KeyHostAlgorithm("ssh-dss", new DsaKey(new SshKeyData(data))));
#pragma warning restore SA1107 // Code should not contain multiple statements on one line
- { "ssh-rsa", data => new KeyHostAlgorithm("ssh-rsa", new RsaKey(new SshKeyData(data))) },
- { "ssh-dss", data => new KeyHostAlgorithm("ssh-dss", new DsaKey(new SshKeyData(data))) },
- };
+ HostKeyAlgorithms = hostAlgs;
CompressionAlgorithms = new Dictionary>
{
diff --git a/src/Renci.SshNet/PrivateKeyFile.cs b/src/Renci.SshNet/PrivateKeyFile.cs
index 341a429d5..414bcfcd7 100644
--- a/src/Renci.SshNet/PrivateKeyFile.cs
+++ b/src/Renci.SshNet/PrivateKeyFile.cs
@@ -6,6 +6,7 @@
using System.Formats.Asn1;
using System.Globalization;
using System.IO;
+using System.Linq;
using System.Numerics;
using System.Security.Cryptography;
using System.Text;
@@ -119,15 +120,20 @@ namespace Renci.SshNet
public partial class PrivateKeyFile : IPrivateKeySource, IDisposable
{
private const string PrivateKeyPattern = @"^-+ *BEGIN (?\w+( \w+)*) *-+\r?\n((Proc-Type: 4,ENCRYPTED\r?\nDEK-Info: (?[A-Z0-9-]+),(?[A-F0-9]+)\r?\n\r?\n)|(Comment: ""?[^\r\n]*""?\r?\n))?(?([a-zA-Z0-9/+=]{1,80}\r?\n)+)(\r?\n)?-+ *END \k *-+";
+ private const string CertificatePattern = @"(?[-\w]+@openssh\.com)\s(?[a-zA-Z0-9\/+=]*)(\s+(?.*))?";
#if NET7_0_OR_GREATER
private static readonly Regex PrivateKeyRegex = GetPrivateKeyRegex();
+ private static readonly Regex CertificateRegex = GetCertificateRegex();
[GeneratedRegex(PrivateKeyPattern, RegexOptions.Multiline | RegexOptions.ExplicitCapture)]
private static partial Regex GetPrivateKeyRegex();
+
+ [GeneratedRegex(CertificatePattern, RegexOptions.ExplicitCapture)]
+ private static partial Regex GetCertificateRegex();
#else
- private static readonly Regex PrivateKeyRegex = new Regex(PrivateKeyPattern,
- RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.ExplicitCapture);
+ private static readonly Regex PrivateKeyRegex = new Regex(PrivateKeyPattern, RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.ExplicitCapture);
+ private static readonly Regex CertificateRegex = new Regex(CertificatePattern, RegexOptions.Compiled | RegexOptions.ExplicitCapture);
#endif
private readonly List _hostAlgorithms = new List();
@@ -156,6 +162,13 @@ public Key Key
}
}
+ ///
+ /// Gets the public key certificate associated with this key,
+ /// or if no certificate data
+ /// has been passed to the constructor.
+ ///
+ public Certificate? Certificate { get; private set; }
+
///
/// Initializes a new instance of the class.
///
@@ -173,7 +186,7 @@ public PrivateKeyFile(Key key)
///
/// The private key.
public PrivateKeyFile(Stream privateKey)
- : this(privateKey, passPhrase: null)
+ : this(privateKey, passPhrase: null, certificate: null)
{
}
@@ -186,7 +199,7 @@ public PrivateKeyFile(Stream privateKey)
/// This method calls internally, this method does not catch exceptions from .
///
public PrivateKeyFile(string fileName)
- : this(fileName, passPhrase: null)
+ : this(fileName, passPhrase: null, certificateFileName: null)
{
}
@@ -200,6 +213,18 @@ public PrivateKeyFile(string fileName)
/// This method calls internally, this method does not catch exceptions from .
///
public PrivateKeyFile(string fileName, string? passPhrase)
+ : this(fileName, passPhrase, certificateFileName: null)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The path of the private key file.
+ /// The pass phrase for the private key.
+ /// The path of a certificate file which certifies the private key.
+ /// is .
+ public PrivateKeyFile(string fileName, string? passPhrase, string? certificateFileName)
{
ThrowHelper.ThrowIfNull(fileName);
@@ -208,6 +233,16 @@ public PrivateKeyFile(string fileName, string? passPhrase)
Open(keyFile, passPhrase);
}
+ if (certificateFileName is not null)
+ {
+ using (var certificateFile = File.OpenRead(certificateFileName))
+ {
+ OpenCertificate(certificateFile);
+ }
+
+ Debug.Assert(Certificate is not null, $"{nameof(Certificate)} is null.");
+ }
+
Debug.Assert(Key is not null, $"{nameof(Key)} is null.");
Debug.Assert(HostKeyAlgorithms.Count > 0, $"{nameof(HostKeyAlgorithms)} is not set.");
}
@@ -219,11 +254,29 @@ public PrivateKeyFile(string fileName, string? passPhrase)
/// The pass phrase.
/// is .
public PrivateKeyFile(Stream privateKey, string? passPhrase)
+ : this(privateKey, passPhrase, certificate: null)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The private key.
+ /// The pass phrase for the private key.
+ /// A certificate which certifies the private key.
+ public PrivateKeyFile(Stream privateKey, string? passPhrase, Stream? certificate)
{
ThrowHelper.ThrowIfNull(privateKey);
Open(privateKey, passPhrase);
+ if (certificate is not null)
+ {
+ OpenCertificate(certificate);
+
+ Debug.Assert(Certificate is not null, $"{nameof(Certificate)} is null.");
+ }
+
Debug.Assert(Key is not null, $"{nameof(Key)} is null.");
Debug.Assert(HostKeyAlgorithms.Count > 0, $"{nameof(HostKeyAlgorithms)} is not set.");
}
@@ -854,6 +907,65 @@ private static Key ParseOpenSslPkcs8PrivateKey(PrivateKeyInfo privateKeyInfo)
throw new SshException(string.Format(CultureInfo.InvariantCulture, "Private key algorithm \"{0}\" is not supported.", algorithmOid));
}
+ ///
+ /// Opens the specified certificate.
+ ///
+ /// The certificate.
+ private void OpenCertificate(Stream certificate)
+ {
+ Debug.Assert(certificate is not null, "Should have validated not-null in the constructor.");
+
+ Match certificateMatch;
+
+ using (var sr = new StreamReader(certificate))
+ {
+ var text = sr.ReadToEnd();
+ certificateMatch = CertificateRegex.Match(text);
+ }
+
+ if (!certificateMatch.Success)
+ {
+ throw new SshException("Invalid certificate file.");
+ }
+
+ var data = certificateMatch.Result("${data}");
+
+ Certificate = new Certificate(Convert.FromBase64String(data));
+
+ Debug.Assert(Key is not null, $"{nameof(Key)} should have been initialised already.");
+
+ if (!Certificate.Key.Public.SequenceEqual(Key.Public))
+ {
+ throw new ArgumentException("The supplied certificate does not certify the supplied key.");
+ }
+
+ if (Key is RsaKey rsaKey)
+ {
+ Debug.Assert(Certificate.Key is RsaKey,
+ $"Expected {nameof(Certificate)}.{nameof(Certificate.Key)} to be {nameof(RsaKey)} but was {Certificate.Key?.GetType()}");
+
+ _hostAlgorithms.Insert(0, new CertificateHostAlgorithm("ssh-rsa-cert-v01@openssh.com", Key, Certificate));
+
+#pragma warning disable CA2000 // Dispose objects before losing scope
+ _hostAlgorithms.Insert(0, new CertificateHostAlgorithm(
+ "rsa-sha2-256-cert-v01@openssh.com",
+ Key,
+ Certificate,
+ new RsaDigitalSignature(rsaKey, HashAlgorithmName.SHA256)));
+
+ _hostAlgorithms.Insert(0, new CertificateHostAlgorithm(
+ "rsa-sha2-512-cert-v01@openssh.com",
+ Key,
+ Certificate,
+ new RsaDigitalSignature(rsaKey, HashAlgorithmName.SHA512)));
+#pragma warning restore CA2000 // Dispose objects before losing scope
+ }
+ else
+ {
+ _hostAlgorithms.Insert(0, new CertificateHostAlgorithm(Certificate.Name, Key, Certificate));
+ }
+ }
+
///
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
///
diff --git a/src/Renci.SshNet/Security/Certificate.cs b/src/Renci.SshNet/Security/Certificate.cs
new file mode 100644
index 000000000..788239c4c
--- /dev/null
+++ b/src/Renci.SshNet/Security/Certificate.cs
@@ -0,0 +1,432 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+
+using Renci.SshNet.Abstractions;
+using Renci.SshNet.Common;
+
+namespace Renci.SshNet.Security
+{
+ ///
+ /// Represents an OpenSSH certificate as described in
+ /// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.certkeys.
+ ///
+ // The xmldoc comments in the class are mostly lifted from the linked document.
+#pragma warning disable SA1623 // Property summary documentation should match accessors; for the above reason
+ public class Certificate
+ {
+ ///
+ /// The type identifier of the certificate.
+ ///
+ ///
+ /// The value is one of the following:
+ ///
+ /// - ssh-rsa-cert-v01@openssh.com
+ /// - ssh-dss-cert-v01@openssh.com
+ /// - ecdsa-sha2-nistp256-cert-v01@openssh.com
+ /// - ecdsa-sha2-nistp384-cert-v01@openssh.com
+ /// - ecdsa-sha2-nistp521-cert-v01@openssh.com
+ /// - ssh-ed25519-cert-v01@openssh.com
+ ///
+ ///
+ public string Name
+ {
+ get
+ {
+ return _data.Name;
+ }
+ }
+
+ ///
+ /// A CA-provided random bitstring of arbitrary length
+ /// (but typically 16 or 32 bytes) included to make attacks that depend on
+ /// inducing collisions in the signature hash infeasible.
+ ///
+ public byte[] Nonce
+ {
+ get
+ {
+ return _data.Nonce;
+ }
+ }
+
+ ///
+ /// The public key that has been certified by the certificate authority.
+ ///
+ public Key Key
+ {
+ get
+ {
+ return _data.Key;
+ }
+ }
+
+ internal SshKeyData KeyData
+ {
+ get
+ {
+ return _data.KeyData;
+ }
+ }
+
+ ///
+ /// An optional certificate serial number set by the CA to
+ /// provide an abbreviated way to refer to certificates from that CA.
+ /// If a CA does not wish to number its certificates, it must set this
+ /// field to zero.
+ ///
+ public ulong Serial
+ {
+ get
+ {
+ return _data.Serial;
+ }
+ }
+
+ ///
+ /// Specifies whether this certificate is for identification of a user
+ /// or a host.
+ ///
+ public CertificateType Type
+ {
+ get
+ {
+ return (CertificateType)_data.Type;
+ }
+ }
+
+ ///
+ /// A free-form text field that is filled in by the CA at the time
+ /// of signing; the intention is that the contents of this field are used to
+ /// identify the identity principal in log messages.
+ ///
+ public string KeyId
+ {
+ get
+ {
+ return _data.KeyId;
+ }
+ }
+
+ ///
+ /// The names for which this certificate is valid;
+ /// hostnames for SSH_CERT_TYPE_HOST certificates and
+ /// usernames for SSH_CERT_TYPE_USER certificates. As a special case, a
+ /// zero-length "valid principals" field means the certificate is valid for
+ /// any principal of the specified type.
+ ///
+ public IList ValidPrincipals
+ {
+ get
+ {
+ return _data.ValidPrincipals;
+ }
+ }
+
+ ///
+ /// The beginning of the validity period of the certificate, as the number
+ /// of seconds elapsed since 1970-01-01T00:00:00Z.
+ ///
+ ///
+ public ulong ValidAfterUnixSeconds
+ {
+ get
+ {
+ return _data.ValidAfter;
+ }
+ }
+
+ ///
+ /// The beginning of the validity period of the certificate.
+ ///
+ public DateTimeOffset ValidAfter
+ {
+ get
+ {
+ return DateTimeOffset.FromUnixTimeSeconds((long)_data.ValidAfter);
+ }
+ }
+
+ ///
+ /// The end of the validity period of the certificate, as the number
+ /// of seconds elapsed since 1970-01-01T00:00:00Z.
+ ///
+ public ulong ValidBeforeUnixSeconds
+ {
+ get
+ {
+ return _data.ValidBefore;
+ }
+ }
+
+ ///
+ /// The end of the validity period of the certificate.
+ ///
+ public DateTimeOffset ValidBefore
+ {
+ get
+ {
+ return _data.ValidBefore == ulong.MaxValue
+ ? DateTimeOffset.MaxValue
+ : DateTimeOffset.FromUnixTimeSeconds((long)_data.ValidBefore);
+ }
+ }
+
+ ///
+ /// A set of zero or more options on the certificate's validity.
+ /// The key identifies the option and the value encodes
+ /// option-specific information.
+ /// All such options are "critical" in the sense that an implementation
+ /// must refuse to authorise a key that has an unrecognised option.
+ ///
+ public IDictionary CriticalOptions
+ {
+ get
+ {
+ return _data.CriticalOptions;
+ }
+ }
+
+ ///
+ /// A set of zero or more optional extensions. These extensions
+ /// are not critical, and an implementation that encounters one that it does
+ /// not recognise may safely ignore it.
+ ///
+ public IDictionary Extensions
+ {
+ get
+ {
+ return _data.Extensions;
+ }
+ }
+
+ ///
+ /// The CA key used to sign the certificate.
+ /// The valid key types for CA keys are ssh-rsa,
+ /// ssh-dss, ssh-ed25519 and the ECDSA types ecdsa-sha2-nistp256,
+ /// ecdsa-sha2-nistp384, ecdsa-sha2-nistp521. "Chained" certificates, where
+ /// the signature key type is a certificate type itself are NOT supported.
+ /// Note that it is possible for a RSA certificate key to be signed by a
+ /// Ed25519 or ECDSA CA key and vice-versa.
+ ///
+ public byte[] CertificateAuthorityKey
+ {
+ get
+ {
+ return _data.SignatureKey;
+ }
+ }
+
+ ///
+ /// Gets the SHA256 fingerprint of the certificate authority key in the same format
+ /// as the ssh command, i.e. non-padded base64, but without the SHA256: prefix.
+ ///
+ /// ohD8VZEXGWo6Ez8GSEJQ9WpafgLFsOfLOtGGQCQo6Og.
+ ///
+ /// Base64 encoded SHA256 fingerprint with padding (equals sign) removed.
+ ///
+ public string CertificateAuthorityKeyFingerPrint
+ {
+ get
+ {
+ return Convert.ToBase64String(CryptoAbstraction.HashSHA256(CertificateAuthorityKey)).TrimEnd('=');
+ }
+ }
+
+ ///
+ /// The signature computed over all preceding fields from the initial string
+ /// up to, and including the signature key. Signatures are computed and
+ /// encoded according to the rules defined for the CA's public key algorithm
+ /// (RFC4253 section 6.6 for ssh-rsa and ssh-dss, RFC5656 for the ECDSA
+ /// types, and RFC8032 for Ed25519).
+ ///
+ public byte[] Signature
+ {
+ get
+ {
+ return _data.Signature;
+ }
+ }
+
+ ///
+ /// The encoded certificate bytes.
+ ///
+ internal byte[] Bytes { get; }
+
+ ///
+ /// The encoded bytes of the certificate which are used
+ /// to calculate .
+ /// This consists of all of the fields before (i.e. except from)
+ /// .
+ ///
+ internal byte[] BytesForSignature
+ {
+ get
+ {
+ return Bytes.Take((int)_data.ByteCountBeforeSignature);
+ }
+ }
+
+ private readonly CertificateData _data;
+
+ ///
+ /// Initializes a new instance of the
+ /// class based on the data encoded in .
+ ///
+ /// The encoded public-key certificate data.
+ public Certificate(byte[] data)
+ {
+ Bytes = data;
+ _data = new CertificateData();
+ _data.Load(Bytes);
+ }
+
+ private sealed class CertificateData : SshData
+ {
+ public string Name { get; private set; }
+
+ public byte[] Nonce { get; private set; }
+
+ public Key Key { get; private set; }
+
+ public SshKeyData KeyData { get; private set; }
+
+ public ulong Serial { get; private set; }
+
+ public uint Type { get; private set; }
+
+ public string KeyId { get; private set; }
+
+ public List ValidPrincipals { get; private set; }
+
+ public ulong ValidAfter { get; private set; }
+
+ public ulong ValidBefore { get; private set; }
+
+ public Dictionary CriticalOptions { get; private set; }
+
+ public Dictionary Extensions { get; private set; }
+
+ public byte[] SignatureKey { get; private set; }
+
+ ///
+ /// Returns the number of bytes in the encoded certificate data
+ /// up to and including .
+ /// Used for verifying which is calculated
+ /// from those bytes.
+ ///
+ public long ByteCountBeforeSignature { get; private set; }
+
+ public byte[] Signature { get; private set; }
+
+ protected override void LoadData()
+ {
+ Name = ReadString();
+ Nonce = ReadBinary();
+ Key = ReadPublicKey(out var keyData);
+ KeyData = keyData;
+ Serial = ReadUInt64();
+ Type = ReadUInt32();
+ KeyId = ReadString();
+ ValidPrincipals = ReadValidPrincipals(ReadBinary());
+ ValidAfter = ReadUInt64();
+ ValidBefore = ReadUInt64();
+ CriticalOptions = ReadExtensionPair(ReadBinary());
+ Extensions = ReadExtensionPair(ReadBinary());
+ _ = ReadBinary(); // Unused reserved field
+ SignatureKey = ReadBinary();
+
+ ByteCountBeforeSignature = DataStream.Position;
+
+ Signature = ReadBinary();
+ }
+
+ private Key ReadPublicKey(out SshKeyData keyData)
+ {
+ switch (Name)
+ {
+ case "ssh-rsa-cert-v01@openssh.com":
+ keyData = new SshKeyData("ssh-rsa", LoadPublicKeys(2));
+ return new RsaKey(keyData);
+ case "ssh-dss-cert-v01@openssh.com":
+ keyData = new SshKeyData("ssh-dss", LoadPublicKeys(4));
+ return new DsaKey(keyData);
+ case "ecdsa-sha2-nistp256-cert-v01@openssh.com":
+ case "ecdsa-sha2-nistp384-cert-v01@openssh.com":
+ case "ecdsa-sha2-nistp521-cert-v01@openssh.com":
+ keyData = new SshKeyData(Name.Substring(0, 19), LoadPublicKeys(2));
+ return new EcdsaKey(keyData);
+ case "ssh-ed25519-cert-v01@openssh.com":
+ keyData = new SshKeyData("ssh-ed25519", LoadPublicKeys(1));
+ return new ED25519Key(keyData);
+ default:
+ throw new NotSupportedException($"Certificate type '{Name}'.");
+ }
+
+ BigInteger[] LoadPublicKeys(int numPublicKeyFields)
+ {
+ var keys = new BigInteger[numPublicKeyFields];
+
+ for (var i = 0; i < numPublicKeyFields; i++)
+ {
+ keys[i] = ReadBinary().ToBigInteger();
+ }
+
+ return keys;
+ }
+ }
+
+ private static Dictionary ReadExtensionPair(byte[] data)
+ {
+ var result = new Dictionary();
+ using var reader = new SshDataStream(data);
+
+ while (!reader.IsEndOfData)
+ {
+ var extensionName = reader.ReadString();
+ var extensionData = reader.ReadString();
+ result.Add(extensionName, extensionData);
+ }
+
+ return result;
+ }
+
+ private static List ReadValidPrincipals(byte[] data)
+ {
+ var result = new List();
+ using var reader = new SshDataStream(data);
+
+ while (!reader.IsEndOfData)
+ {
+ result.Add(reader.ReadString());
+ }
+
+ return result;
+ }
+
+ protected override void SaveData()
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ ///
+ /// Used to specify whether a certificate is for identification of a user
+ /// or a host.
+ ///
+#pragma warning disable CA1028 // Enum Storage should be Int32; match the type specified in PROTOCOL.certkeys
+ public enum CertificateType : uint
+#pragma warning restore CA1028 // Enum Storage should be Int32
+ {
+ ///
+ /// The certificate is for identification of a user (SSH_CERT_TYPE_USER).
+ ///
+ User = 1,
+
+ ///
+ /// The certificate is for identification of a host (SSH_CERT_TYPE_HOST).
+ ///
+ Host = 2
+ }
+ }
+}
diff --git a/src/Renci.SshNet/Security/CertificateHostAlgorithm.cs b/src/Renci.SshNet/Security/CertificateHostAlgorithm.cs
index 41d5f4103..bd945b609 100644
--- a/src/Renci.SshNet/Security/CertificateHostAlgorithm.cs
+++ b/src/Renci.SshNet/Security/CertificateHostAlgorithm.cs
@@ -1,50 +1,152 @@
-using System;
+#nullable enable
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+
+using Renci.SshNet.Security.Cryptography;
namespace Renci.SshNet.Security
{
///
/// Implements certificate support for host algorithm.
///
- public class CertificateHostAlgorithm : HostAlgorithm
+ public class CertificateHostAlgorithm : KeyHostAlgorithm
{
///
- /// Gets the host key data.
+ /// The factories which may be used in order to verify
+ /// the signature within the certificate.
+ ///
+ private readonly IReadOnlyDictionary>? _keyAlgorithms;
+
+ ///
+ /// Gets certificate used in this host key algorithm.
+ ///
+ public Certificate Certificate { get; }
+
+ ///
+ internal override SshKeyData KeyData
+ {
+ get
+ {
+ return Certificate.KeyData;
+ }
+ }
+
+ ///
+ /// Gets the encoded bytes of the certificate.
///
public override byte[] Data
{
- get { throw new NotImplementedException(); }
+ get { return Certificate.Bytes; }
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The algorithm identifier.
+ /// The private key used for this host algorithm.
+ /// The certificate which certifies .
+ public CertificateHostAlgorithm(string name, Key privateKey, Certificate certificate)
+ : base(name, privateKey)
+ {
+ Certificate = certificate;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The algorithm identifier.
+ /// The private key used for this host algorithm.
+ /// The certificate which certifies .
+ ///
+ public CertificateHostAlgorithm(string name, Key privateKey, Certificate certificate, DigitalSignature digitalSignature)
+ : base(name, privateKey, digitalSignature)
+ {
+ Certificate = certificate;
}
///
/// Initializes a new instance of the class.
///
- /// The host key name.
- public CertificateHostAlgorithm(string name)
- : base(name)
+ /// The algorithm identifier.
+ /// The certificate.
+ ///
+ public CertificateHostAlgorithm(string name, Certificate certificate, IReadOnlyDictionary> keyAlgorithms)
+ : base(name, certificate.Key)
{
+ Certificate = certificate;
+ _keyAlgorithms = keyAlgorithms;
}
///
- /// Signs the specified data.
+ /// Initializes a new instance of the class.
///
- /// The data.
- /// Signed data.
- /// Always.
+ /// The algorithm identifier.
+ /// The certificate.
+ ///
+ ///
+ public CertificateHostAlgorithm(string name, Certificate certificate, DigitalSignature digitalSignature, IReadOnlyDictionary> keyAlgorithms)
+ : base(name, certificate.Key, digitalSignature)
+ {
+ Certificate = certificate;
+ _keyAlgorithms = keyAlgorithms;
+ }
+
+ ///
public override byte[] Sign(byte[] data)
{
- throw new NotImplementedException();
+ Debug.Assert("-cert-v01@openssh.com".Length == 21);
+
+ var signatureFormatIdentifier = Name.EndsWith("-cert-v01@openssh.com", StringComparison.Ordinal)
+ ? Name.Substring(0, Name.Length - 21)
+ : Name;
+
+ return new SignatureKeyData(signatureFormatIdentifier, DigitalSignature.Sign(data)).GetBytes();
}
///
/// Verifies the signature.
///
- /// The data.
- /// The signature.
- /// if signature was successfully verified; otherwise .
- /// Always.
- public override bool VerifySignature(byte[] data, byte[] signature)
+ /// The data to verify the signature against.
+ /// The signature blob in format specific encoding.
+ ///
+ /// if is the result of signing
+ /// with the corresponding private key to ,
+ /// and is valid with respect to its validity period and to its
+ /// signature therein as signed by the certificate authority.
+ ///
+ internal override bool VerifySignatureBlob(byte[] data, byte[] signatureBlob)
{
- throw new NotImplementedException();
+ // Validate the session signature against the public key as normal.
+
+ if (!base.VerifySignatureBlob(data, signatureBlob))
+ {
+ return false;
+ }
+
+ // Validate the validity period of the certificate.
+
+ var unixNow = (ulong)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
+
+ if (unixNow < Certificate.ValidAfterUnixSeconds || unixNow > Certificate.ValidBeforeUnixSeconds)
+ {
+ return false;
+ }
+
+ // Validate the certificate (i.e. the signature contained within) against
+ // the CA public key (also contained in the certificate).
+
+ var certSignatureData = new SignatureKeyData();
+ certSignatureData.Load(Certificate.Signature);
+
+ if (_keyAlgorithms is null)
+ {
+ throw new InvalidOperationException($"Invalid usage of {nameof(CertificateHostAlgorithm)}.{nameof(VerifySignature)}. " +
+ $"Use a constructor which passes key algorithms.");
+ }
+
+ return _keyAlgorithms.TryGetValue(certSignatureData.AlgorithmName, out var keyAlgFactory) &&
+ keyAlgFactory(Certificate.CertificateAuthorityKey).VerifySignatureBlob(Certificate.BytesForSignature, certSignatureData.Signature);
}
}
}
diff --git a/src/Renci.SshNet/Security/KeyExchange.cs b/src/Renci.SshNet/Security/KeyExchange.cs
index f5c3d5c7f..96eb326ca 100644
--- a/src/Renci.SshNet/Security/KeyExchange.cs
+++ b/src/Renci.SshNet/Security/KeyExchange.cs
@@ -368,22 +368,40 @@ private protected bool ValidateExchangeHash(byte[] encodedKey, byte[] encodedSig
{
var exchangeHash = CalculateHash();
+ // We need to inspect both the key and signature format identifers to find the correct
+ // HostAlgorithm instance. Example cases:
+
+ // Key identifier Signature identifier | Algorithm name
+ // ssh-rsa ssh-rsa | ssh-rsa
+ // ssh-rsa rsa-sha2-256 | rsa-sha2-256
+ // ssh-rsa-cert-v01@openssh.com ssh-rsa | ssh-rsa-cert-v01@openssh.com
+ // ssh-rsa-cert-v01@openssh.com rsa-sha2-256 | rsa-sha2-256-cert-v01@openssh.com
+
var signatureData = new KeyHostAlgorithm.SignatureKeyData();
signatureData.Load(encodedSignature);
- var keyAlgorithm = Session.ConnectionInfo.HostKeyAlgorithms[signatureData.AlgorithmName](encodedKey);
+ string keyName;
+ using (var keyReader = new SshDataStream(encodedKey))
+ {
+ keyName = keyReader.ReadString();
+ }
- Session.ConnectionInfo.CurrentHostKeyAlgorithm = signatureData.AlgorithmName;
+ string algorithmName;
- if (CanTrustHostKey(keyAlgorithm))
+ if (signatureData.AlgorithmName.StartsWith("rsa-sha2", StringComparison.Ordinal))
{
- // keyAlgorithm.VerifySignature decodes the signature data before verifying.
- // But as we have already decoded the data to find the signature algorithm,
- // we just verify the decoded data directly through the DigitalSignature.
- return keyAlgorithm.DigitalSignature.Verify(exchangeHash, signatureData.Signature);
+ algorithmName = keyName.Replace("ssh-rsa", signatureData.AlgorithmName);
}
+ else
+ {
+ algorithmName = keyName;
+ }
+
+ var keyAlgorithm = Session.ConnectionInfo.HostKeyAlgorithms[algorithmName](encodedKey);
+
+ Session.ConnectionInfo.CurrentHostKeyAlgorithm = algorithmName;
- return false;
+ return keyAlgorithm.VerifySignatureBlob(exchangeHash, signatureData.Signature) && CanTrustHostKey(keyAlgorithm);
}
///
diff --git a/src/Renci.SshNet/Security/KeyHostAlgorithm.cs b/src/Renci.SshNet/Security/KeyHostAlgorithm.cs
index 5611c9713..c69005c1c 100644
--- a/src/Renci.SshNet/Security/KeyHostAlgorithm.cs
+++ b/src/Renci.SshNet/Security/KeyHostAlgorithm.cs
@@ -20,6 +20,18 @@ public class KeyHostAlgorithm : HostAlgorithm
///
public DigitalSignature DigitalSignature { get; private set; }
+ ///
+ /// Gets the encoded public key data.
+ ///
+ internal virtual SshKeyData KeyData
+ {
+ get
+ {
+ var keyFormatIdentifier = Key is RsaKey ? "ssh-rsa" : Name;
+ return new SshKeyData(keyFormatIdentifier, Key.Public);
+ }
+ }
+
///
/// Gets the encoded public key data.
///
@@ -30,8 +42,7 @@ public override byte[] Data
{
get
{
- var keyFormatIdentifier = Key is RsaKey ? "ssh-rsa" : Name;
- return new SshKeyData(keyFormatIdentifier, Key.Public).GetBytes();
+ return KeyData.GetBytes();
}
}
@@ -77,20 +88,37 @@ public override byte[] Sign(byte[] data)
}
///
- /// Verifies the signature.
+ /// Verifies the encoded signature.
///
/// The data to verify the signature against.
- /// The encoded signature data.
+ ///
+ /// The encoded signature data, as the signature format identifier followed by the signature blob.
+ ///
///
- /// if is the result of signing
- /// with the corresponding private key to .
+ /// if is the result of signing and encoding
+ /// with the corresponding private key to .
///
+ /// See .
public override bool VerifySignature(byte[] data, byte[] signature)
{
var signatureData = new SignatureKeyData();
signatureData.Load(signature);
- return DigitalSignature.Verify(data, signatureData.Signature);
+ return VerifySignatureBlob(data, signatureData.Signature);
+ }
+
+ ///
+ /// Verifies the signature.
+ ///
+ /// The data to verify the signature against.
+ /// The signature blob in format specific encoding.
+ ///
+ /// if is the result of signing
+ /// with the corresponding private key to .
+ ///
+ internal virtual bool VerifySignatureBlob(byte[] data, byte[] signatureBlob)
+ {
+ return DigitalSignature.Verify(data, signatureBlob);
}
internal sealed class SignatureKeyData : SshData
diff --git a/test/Data/Key.OPENSSH.ECDSA-cert.pub b/test/Data/Key.OPENSSH.ECDSA-cert.pub
new file mode 100644
index 000000000..23555b39c
--- /dev/null
+++ b/test/Data/Key.OPENSSH.ECDSA-cert.pub
@@ -0,0 +1 @@
+ecdsa-sha2-nistp256-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb20AAAAg7W7ctYMMIVaWbaLUmo28K5Sl4CcOExOY+rILg1Wum60AAAAIbmlzdHAyNTYAAABBBI/dlNvfssW9KYrB67TcDmz9zBzDf7eMvUupAroP3b3FjUnYnpL3Utc4GkF/PiX7w2DuxaG70/+EX/CYHZBHKCsAAAAAAAAAAAAAAAEAAAAVZWNkc2EyNTZjZXJ0UnNhU2hhNTEyAAAACgAAAAZzc2huZXQAAAAAAAAAAP//////////AAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAAhcAAAAHc3NoLXJzYQAAAAMBAAEAAAIBAK9YEVSlF1erJEAcsiobagUVqpdZnVg5GIjHSXt+I7XIbQGfjVJpBpafMGsJac8YmRYOcJv13dvSjzWLqCCJvBNg8cXrt6InLJo0ISdgwk5y+i9+YQHyr8DRl836ipI5cFShRmG+p04bgUXNsj99pNWyIzwcfymJkfV4dxEryLZSwrM0DKQrNjGZ1ggWsc/oxyBGu1rRNjqzxdZY2rCiOsfxQ+dRLE5gJfz14OzfA8rPKvKw343+mugsiobTE8uPKUpM7NzUyDZzLzg/x8s2KWJkvWtYbo5To/I8Yl53B/72sWMCLtk5GcASMnnMCfsrnWx5+j3BfXb910hEaKfTb/JKu9zbz4CoEcDaB3e6y+w2+VB0s9Ubi3fr4qq20WLqdoV3gzi/6hjaXCjD6S9xbPE+xkkBR8AMaGs+chJyvj2b0TTh2mtvY0MLDnQxFMQW9gTvUvSM69EH/qzZkE5cDk+J7HEQHlGiRGIeug6bCjCF3BjslDGpEqX4NuUwNzgQ1RlDSXc/mKNdZkYjOnwttQrY3WcNzy6pABozWRJRORQRkjlpRNyp1o6Xn/XhceRayQGnT3Mk2wjpWhXXsLUT9J3DOctsIM9/xaAm1g0qHmrc5HlkUGIGxHY9gpmD2yT/CuuWe7xV+BVhDmjgfOBmDy9jHv1px+P0miwudUZzIDkTAAACFAAAAAxyc2Etc2hhMi01MTIAAAIAH+pygyvTZfq+NamjGaL7fAYBc+a0Yz7anU997Kye0EHiK8HFCq855qN4q5EBqGglmBZ/o+1aBWrK5e6imj7s63cU9G2yMy+Rr0oGCDGNWKHJVa2qeU8beIAeT82TYrusiHR9viEzc4XKgQE4k3lxUZth8Mdm8I6lkkT4fJiqrAEpCbJArCAwohrDaTP1v0DwoAd3Svqz7dLz0/I1Yv8wJqlg4bOiei3EAbRiHGFmZggi4JRV9ZWlOFLdrVbtHbbv3N68QZVFj7n4pmyTEFDxLHIxMMjOOiWn8ZIyQCz5+Ahy4Liq/gYuQAlmm2P775BCLydzcexUKLkeNEhp6VT/QR85S+qOpzvpFVqP5Qu8OL55P9IKFUg+Z0r1G9boq5rkSRc3zMPRCm7SQkk+caJq3pNDDwM8r5uRPNJwEAG/7Od5mcAVgRsXO8ccMlF6QLyGFJu8mL6avGJlEMdgBvdGMvmOQCY+zHPP0PB8dK47KoH4uopi7vXfSGK3JUB+HbBFgOUq+2lD3X2WyZHh5GDHL+c4zwmwCYyom5IoQ+DP5BWIeGKAAT1THZiIGYC/exwvbWBvPBsmOZaFAmIEaqz9oooidE9iq8AqgB6C6W5F/j1Y0QsE2h2N6cvzsgT8o++oARM1w3ZuqD4gvGCF++9I8NdGnmDYo/2eE0HUgSBuVlw= (null)
diff --git a/test/Data/Key.OPENSSH.ECDSA.Encrypted.Aes.128.CTR-cert.pub b/test/Data/Key.OPENSSH.ECDSA.Encrypted.Aes.128.CTR-cert.pub
new file mode 100644
index 000000000..46f838842
--- /dev/null
+++ b/test/Data/Key.OPENSSH.ECDSA.Encrypted.Aes.128.CTR-cert.pub
@@ -0,0 +1 @@
+ecdsa-sha2-nistp256-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgwHAnH6bfXU1eV2weJ6Xw5wIG0N7dUfF83qQdwzDWdmcAAAAIbmlzdHAyNTYAAABBBP05AsaI5tCqEDeC6upJ6C8mzlEg79coH3HAj+yMkCbSOyarh323fQLri3W7i5wkBzoCehiLQcTqm25dCfkDbz0AAAAAAAAAAAAAAAEAAAASZWNkc2EyNTZDZXJ0UnNhNTEyAAAACgAAAAZzc2huZXQAAAAAAAAAAP//////////AAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAAhcAAAAHc3NoLXJzYQAAAAMBAAEAAAIBAK9YEVSlF1erJEAcsiobagUVqpdZnVg5GIjHSXt+I7XIbQGfjVJpBpafMGsJac8YmRYOcJv13dvSjzWLqCCJvBNg8cXrt6InLJo0ISdgwk5y+i9+YQHyr8DRl836ipI5cFShRmG+p04bgUXNsj99pNWyIzwcfymJkfV4dxEryLZSwrM0DKQrNjGZ1ggWsc/oxyBGu1rRNjqzxdZY2rCiOsfxQ+dRLE5gJfz14OzfA8rPKvKw343+mugsiobTE8uPKUpM7NzUyDZzLzg/x8s2KWJkvWtYbo5To/I8Yl53B/72sWMCLtk5GcASMnnMCfsrnWx5+j3BfXb910hEaKfTb/JKu9zbz4CoEcDaB3e6y+w2+VB0s9Ubi3fr4qq20WLqdoV3gzi/6hjaXCjD6S9xbPE+xkkBR8AMaGs+chJyvj2b0TTh2mtvY0MLDnQxFMQW9gTvUvSM69EH/qzZkE5cDk+J7HEQHlGiRGIeug6bCjCF3BjslDGpEqX4NuUwNzgQ1RlDSXc/mKNdZkYjOnwttQrY3WcNzy6pABozWRJRORQRkjlpRNyp1o6Xn/XhceRayQGnT3Mk2wjpWhXXsLUT9J3DOctsIM9/xaAm1g0qHmrc5HlkUGIGxHY9gpmD2yT/CuuWe7xV+BVhDmjgfOBmDy9jHv1px+P0miwudUZzIDkTAAACFAAAAAxyc2Etc2hhMi01MTIAAAIAQCrigzdgDsE1VUj2Ii0xwIM9kjYzIX7irCVQn6pq6vluH9e43/4AfMIE3GmHubfwUGr+z3kg2ykvy7DGDpUr6rBT62zUsr/52djAo6t8iVW7misEBeGe0TAsuCt+D6td81Kzl/MQ/DBW03q9tscGNN5DQmZwQYLjg2qydP4BooUaLlQRyg4W8J0wb2ek9lMHWCqqJZPAZu38nagBoyeyFOggnQ41cU3+vYoei/GRTmKoNk8t5wtEqJmsF7f0zGFULRFPhy0v/pBWjBC+DpwTGtf5+9SMF3qm943iSxwHFr7mPUwVg23CP4DHXVImZdxEh8VC9GcZkq0OR7JfnBvMAfPG20pqMZUIJl5qR88tKibyIp2EY2vy3IQ+iYS6fRpmPCgHck8qlHjZaaqBEeV2E0t2YJMDs5CrgLkCOClsjBeNm1wp7xL7VV07ZLNYIVAW1Bx7YTyPT/18Y/PNrnBd2Z8XCTX0oI2EXh7t8pjHSMk2HVq/i6Pmnmndvm8Wym+IDScBGeJJB4jiiAs6OaDZTOjPy0wudo7NapoeAEfDAWkPMavARdl6zz1q8IGxJ3ldxvcTU4Og83x3NmvZXPtzfoMOJJbA6H66/9DylClEjflJU4Rvf7pG6F4UEUACZu/ocKAasRIfatSo4QGCuV+Fq/geHrC/oiEELo2+Erj7pRM= robert@VMWKS-015
diff --git a/test/Data/Key.OPENSSH.ECDSA384-cert.pub b/test/Data/Key.OPENSSH.ECDSA384-cert.pub
new file mode 100644
index 000000000..a742803ba
--- /dev/null
+++ b/test/Data/Key.OPENSSH.ECDSA384-cert.pub
@@ -0,0 +1 @@
+ecdsa-sha2-nistp384-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHAzODQtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgmWC208kRFGQSG7AG3NFtkSeeGKVuKlby6CpjhD5bZpIAAAAIbmlzdHAzODQAAABhBFM/UMxegeBb5Ff5L83FQQSWi7VyYsPoISJH7OnNoYbqbOXouFRj5nd/Yze7i7u1wzxOAH+OIducj1Np43lArgdfUP0NeQflGF+ct+ubeQJM2gIUp3RZr9AC8quU0qJGLwAAAAAAAAAAAAAAAQAAABJlY2RzYTM4NGNlcnRSc2EyNTYAAAAKAAAABnNzaG5ldAAAAAAAAAAA//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAACFwAAAAdzc2gtcnNhAAAAAwEAAQAAAgEAr1gRVKUXV6skQByyKhtqBRWql1mdWDkYiMdJe34jtchtAZ+NUmkGlp8wawlpzxiZFg5wm/Xd29KPNYuoIIm8E2Dxxeu3oicsmjQhJ2DCTnL6L35hAfKvwNGXzfqKkjlwVKFGYb6nThuBRc2yP32k1bIjPBx/KYmR9Xh3ESvItlLCszQMpCs2MZnWCBaxz+jHIEa7WtE2OrPF1ljasKI6x/FD51EsTmAl/PXg7N8Dys8q8rDfjf6a6CyKhtMTy48pSkzs3NTINnMvOD/HyzYpYmS9a1hujlOj8jxiXncH/vaxYwIu2TkZwBIyecwJ+yudbHn6PcF9dv3XSERop9Nv8kq73NvPgKgRwNoHd7rL7Db5UHSz1RuLd+viqrbRYup2hXeDOL/qGNpcKMPpL3Fs8T7GSQFHwAxoaz5yEnK+PZvRNOHaa29jQwsOdDEUxBb2BO9S9Izr0Qf+rNmQTlwOT4nscRAeUaJEYh66DpsKMIXcGOyUMakSpfg25TA3OBDVGUNJdz+Yo11mRiM6fC21CtjdZw3PLqkAGjNZElE5FBGSOWlE3KnWjpef9eFx5FrJAadPcyTbCOlaFdewtRP0ncM5y2wgz3/FoCbWDSoeatzkeWRQYgbEdj2CmYPbJP8K65Z7vFX4FWEOaOB84GYPL2Me/WnH4/SaLC51RnMgORMAAAIUAAAADHJzYS1zaGEyLTI1NgAAAgB6DjB06g/oN5cMdyHzSrA3AjZXkfmmrxDLos2OvgkVOwsRdThj5G7r+c3wVH5cvZ0QdSl9S4CqO/aCaPeYcavqSFKYTbmDE0phQRQB8dsH8PG552b6L8ZvxRNGNFAxvBAm1G8VM3qLK04FREkfBhfxArYhtiKIT0BAXZPwkf6NSaEM5oMZ0ypC9FeKbs/05/3P01h9AZPvBIA5mqdIq6iGpzdmm13yjotsaideFnuBePmLaHrKybZb+qtvpT4/RKKp6GiM3Kbz+MpDidwcn+ltcNU4sxzIEUWm51Ig0fgN0QGkgggw66ala4Z7lwGImLHGZ7tPk8XuWANPv92hWGBAV5EXT42AjYEkdEZl7cwRR6qz3bwgBDqRWfrMkB68g2qDhPoZlF5UwNFVljefl3S/AqNjo8RjLWNLwNRkUxiu9u/m5uEZX8TqjAXphYU5JGpIR45hoscxv5glTtidtC6Lzj9hX44eroYs48HZ6yktDjILgcfSHyxTLAjjcWIUtWTmXNXsqJM2Afmt5iND1ZC86gGhCnMhLcQQ8+FC0tOw0WX1ksX/J5sA7qqp3ztxuPQeVY7k7htUVpb78TQZzPxjYLxRV/NYtZKlrvK2u2zM+eExNxCTPOt/ZlS9T2bxolwVZL1PzHCGkhWr76FNhitGVVxc06TcL2G9YPLznYdLVw== (null)
diff --git a/test/Data/Key.OPENSSH.ECDSA384.Encrypted.Aes.256.GCM-cert.pub b/test/Data/Key.OPENSSH.ECDSA384.Encrypted.Aes.256.GCM-cert.pub
new file mode 100644
index 000000000..f7859ca79
--- /dev/null
+++ b/test/Data/Key.OPENSSH.ECDSA384.Encrypted.Aes.256.GCM-cert.pub
@@ -0,0 +1 @@
+ecdsa-sha2-nistp384-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHAzODQtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgCqaZ+v4X2FFSge4hPDeBFqdFKDHpvqG7CkrXBmViLiUAAAAIbmlzdHAzODQAAABhBIoFo5PTu8NHANpVbFmZLpH3wvvegkt14r6cJF3WeUYAQLuhfxfujxli6+nHdgkuEZXVt3EVDZaosap9K8ZdiPxdA7fHj41Dc5CyyMIeNCdCTkO0+BBDCygLHCw6k0a7wgAAAAAAAAAAAAAAAQAAABJlY2RzYTM4NENlcnRSc2EyNTYAAAAKAAAABnNzaG5ldAAAAAAAAAAA//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAACFwAAAAdzc2gtcnNhAAAAAwEAAQAAAgEAr1gRVKUXV6skQByyKhtqBRWql1mdWDkYiMdJe34jtchtAZ+NUmkGlp8wawlpzxiZFg5wm/Xd29KPNYuoIIm8E2Dxxeu3oicsmjQhJ2DCTnL6L35hAfKvwNGXzfqKkjlwVKFGYb6nThuBRc2yP32k1bIjPBx/KYmR9Xh3ESvItlLCszQMpCs2MZnWCBaxz+jHIEa7WtE2OrPF1ljasKI6x/FD51EsTmAl/PXg7N8Dys8q8rDfjf6a6CyKhtMTy48pSkzs3NTINnMvOD/HyzYpYmS9a1hujlOj8jxiXncH/vaxYwIu2TkZwBIyecwJ+yudbHn6PcF9dv3XSERop9Nv8kq73NvPgKgRwNoHd7rL7Db5UHSz1RuLd+viqrbRYup2hXeDOL/qGNpcKMPpL3Fs8T7GSQFHwAxoaz5yEnK+PZvRNOHaa29jQwsOdDEUxBb2BO9S9Izr0Qf+rNmQTlwOT4nscRAeUaJEYh66DpsKMIXcGOyUMakSpfg25TA3OBDVGUNJdz+Yo11mRiM6fC21CtjdZw3PLqkAGjNZElE5FBGSOWlE3KnWjpef9eFx5FrJAadPcyTbCOlaFdewtRP0ncM5y2wgz3/FoCbWDSoeatzkeWRQYgbEdj2CmYPbJP8K65Z7vFX4FWEOaOB84GYPL2Me/WnH4/SaLC51RnMgORMAAAIUAAAADHJzYS1zaGEyLTI1NgAAAgBKYSthoC0jBYvlWqbHiUfk/dEfnIthoJsnvsQwWHCY5YIv6EqwO0n1j+Oc6942NDdeJYG3FoA/8hvDvi/QRMEWN8/Opoj3d0mWJdyCjZAt+1o6gq2oNsuXywJEW9rnC9bmdsMSbQgDSpc+DY7asp/DSLXZDb54JALf6rcZ8kfPXQ17gdNfw2cOg4X1n3cvGvOMDLdRtwSNRYn/RfD9idf9acfycSJIRNvBFwcx0kPWlc5NWsrIbP7pMsh0Rdr7Cc/7cO2TFy0qvrR6jB+WXWhkQFlPbg9EvOrPeuxtppetqRl7e0jc3CsdL8yiVasd57Uyfc7z+HX9dCnk71L+yGmOsJM42M8p808WBV3IXOGne8HScOTb/6rjFDChV3GcsXO/fdZdG1WEdRsYoYqx5iadTKTXt1+RB4Hygm+TTYm3P34Mkq+mAu5xTwsWgV716h3vcyAE2+bA6fo9RDa+Rrizo70bOd8pDDLpbUInpISYRYFuAuvgceHRt19i45McANlTXarOl3Dt1THuOTWcCzlWZbadES+hBGPzDhhrQAEV14bgMvcJbKGeG3UsdZY8Rhy7tERDta/hGffwFAO/jnjEvTO28xqucCBowIutDFlq6fG8JX9urwNUMKarGIFHR7a1EEsK8+cuypxXDm978/EFzp0mVeg7eFBCrK/qBKKFJw== SSH.NET
diff --git a/test/Data/Key.OPENSSH.ECDSA521-cert.pub b/test/Data/Key.OPENSSH.ECDSA521-cert.pub
new file mode 100644
index 000000000..bdba9f4f9
--- /dev/null
+++ b/test/Data/Key.OPENSSH.ECDSA521-cert.pub
@@ -0,0 +1 @@
+ecdsa-sha2-nistp521-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHA1MjEtY2VydC12MDFAb3BlbnNzaC5jb20AAAAggRfJnc8gqLYnXn7VgT8GoB1ko5INrxXhDD9Ap+MBZqMAAAAIbmlzdHA1MjEAAACFBABrunhZWBr7Tyq7XrQGt3MrJE0kxAJ4aEWW412rvf+5pbeqWqgSJo21zm4HscfKMJZBOZ/OtJEtFntgHBRqdzDKHgCrqAGAaxdXPA29jeTFEOUatJ8yaweVfPjV2DD3CbV8Fx/3ueJ7FFD/EaWGTJ/shiVD+zkGlcXaVL2XQfmEGKmlGAAAAAAAAAAAAAAAAQAAABFlY2RzYTUyMWNlcnRFY2RzYQAAAAoAAAAGc3NobmV0AAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAABoAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLvxUyJKR9Jxxutu2sMEoZ1MsIxUdMNqYcpmbUX8Yu4kdGx+Wr4ktOXv8CseoXSkY2W2/lh8g/roE6N4H6cQWk8AAABlAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAABKAAAAIQCUzZ6Z537DWC4RrgxWmGSY3/GpY9ZySZx+lSvNEj7NQgAAACEAuI4WpushiyWuFq+qgUbzRzAlURAcVTP0TH6QVFTIKfA= (null)
diff --git a/test/Data/Key.OPENSSH.ECDSA521.Encrypted.Aes.192.CBC-cert.pub b/test/Data/Key.OPENSSH.ECDSA521.Encrypted.Aes.192.CBC-cert.pub
new file mode 100644
index 000000000..64a1db483
--- /dev/null
+++ b/test/Data/Key.OPENSSH.ECDSA521.Encrypted.Aes.192.CBC-cert.pub
@@ -0,0 +1 @@
+ecdsa-sha2-nistp521-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHA1MjEtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgvnIWATz6TOlL4gCr4t6yk2PIxN2eHym+YSDgJs0wMukAAAAIbmlzdHA1MjEAAACFBAGdkuygP8JJOCTbo3G6pxSBXIODWBNkYaidj4Z47o+r4OiAiAcEgpZSsQn1qhXMOtzeqBMvRe7IphOWFXBae2viVABR5JNwUB08HtxRG6zgr6jv5vrDqggHGNAcShOcluRrWu87nIQgIw2N5unSX6HhdOQ3VYRTUI3kAAfx/8WI6GSfvQAAAAAAAAAAAAAAAQAAABFlY2RzYTUyMUNlcnRFY2RzYQAAAAoAAAAGc3NobmV0AAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAABoAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLvxUyJKR9Jxxutu2sMEoZ1MsIxUdMNqYcpmbUX8Yu4kdGx+Wr4ktOXv8CseoXSkY2W2/lh8g/roE6N4H6cQWk8AAABjAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAABIAAAAIFnvTwo0GBTn91FYop1TCjS1KSNH2Y9OrKAQJwA8eJI4AAAAIAZ1vRormvCd1lOlBx52MmWZOlitfR6DN/tUe5cyXTMF SSH.NET
diff --git a/test/Data/Key.OPENSSH.ED25519-cert.pub b/test/Data/Key.OPENSSH.ED25519-cert.pub
new file mode 100644
index 000000000..4afbe2f91
--- /dev/null
+++ b/test/Data/Key.OPENSSH.ED25519-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIDZqX9y0awmstGNnKnsJk+rljDcgUKP+lPg7L3DX1zm3AAAAIA0JZnDQrxQZcNALfZYG7LPAW1MYEGvVW5nje7OlMGMiAAAAAAAAAAAAAAABAAAAEmVkMjU1MTktY2VydC1lY2RzYQAAAAoAAAAGc3NobmV0AAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAABoAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLvxUyJKR9Jxxutu2sMEoZ1MsIxUdMNqYcpmbUX8Yu4kdGx+Wr4ktOXv8CseoXSkY2W2/lh8g/roE6N4H6cQWk8AAABkAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAABJAAAAIQD0JteEHgKpsM+xWqLsSl5CXj8pC8VlSTBKaIHDbQ0mhwAAACB+RIVgmktyCbm5/D1geigyzLnB2pHPrPIlNtTo+0vtLQ== Key.OPENSSH.ED25519
diff --git a/test/Data/Key.OPENSSH.ED25519.Encrypted.ChaCha20.Poly1305-cert.pub b/test/Data/Key.OPENSSH.ED25519.Encrypted.ChaCha20.Poly1305-cert.pub
new file mode 100644
index 000000000..71384f52a
--- /dev/null
+++ b/test/Data/Key.OPENSSH.ED25519.Encrypted.ChaCha20.Poly1305-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIO17UOT7XYGpB0kekNKXWNafEvDaZzmYz40s5X0h7mT1AAAAIBquS79wnun0ksZv6JJMgHzoZhzqH6Lkft+1sHTHFOjYAAAAAAAAAAAAAAABAAAAEGVkMjU1MTlDZXJ0RWNkc2EAAAAKAAAABnNzaG5ldAAAAAAAAAAA//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAaAAAABNlY2RzYS1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQS78VMiSkfSccbrbtrDBKGdTLCMVHTDamHKZm1F/GLuJHRsflq+JLTl7/ArHqF0pGNltv5YfIP66BOjeB+nEFpPAAAAYwAAABNlY2RzYS1zaGEyLW5pc3RwMjU2AAAASAAAACBeyEF9YGumQTDU0WIMG44g7p5xmtDQ9iwCgSazPRQ6UQAAACBbuMtgLhGK1uaqNYTrVpFvQr2kKU2jB4F8S/1Oopx5/Q== (null)
diff --git a/test/Data/Key.OPENSSH.RSA-cert.pub b/test/Data/Key.OPENSSH.RSA-cert.pub
new file mode 100644
index 000000000..210f449dc
--- /dev/null
+++ b/test/Data/Key.OPENSSH.RSA-cert.pub
@@ -0,0 +1 @@
+ssh-rsa-cert-v01@openssh.com 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 Key.OPENSSH.RSA
diff --git a/test/Data/Key.OPENSSH.RSA.Encrypted.Aes.192.CTR-cert.pub b/test/Data/Key.OPENSSH.RSA.Encrypted.Aes.192.CTR-cert.pub
new file mode 100644
index 000000000..abdd0050a
--- /dev/null
+++ b/test/Data/Key.OPENSSH.RSA.Encrypted.Aes.192.CTR-cert.pub
@@ -0,0 +1 @@
+ssh-rsa-cert-v01@openssh.com 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 SSH.NET
diff --git a/test/Renci.SshNet.IntegrationTests/Common/RemoteSshdConfigExtensions.cs b/test/Renci.SshNet.IntegrationTests/Common/RemoteSshdConfigExtensions.cs
index 865154bfb..2a2ccaa4f 100644
--- a/test/Renci.SshNet.IntegrationTests/Common/RemoteSshdConfigExtensions.cs
+++ b/test/Renci.SshNet.IntegrationTests/Common/RemoteSshdConfigExtensions.cs
@@ -15,6 +15,7 @@ public static void Reset(this RemoteSshdConfig remoteSshdConfig)
.WithLogLevel(LogLevel.Debug3)
.ClearHostKeyFiles()
.AddHostKeyFile(HostKeyFile.Rsa.FilePath)
+ .WithHostKeyCertificate(null)
.ClearSubsystems()
.AddSubsystem(new Subsystem("sftp", "/usr/lib/ssh/sftp-server"))
.ClearCiphers()
diff --git a/test/Renci.SshNet.IntegrationTests/Dockerfile.TestServer b/test/Renci.SshNet.IntegrationTests/Dockerfile.TestServer
index 628f1a88c..548f76dab 100644
--- a/test/Renci.SshNet.IntegrationTests/Dockerfile.TestServer
+++ b/test/Renci.SshNet.IntegrationTests/Dockerfile.TestServer
@@ -17,6 +17,7 @@ RUN apk update && apk upgrade --no-cache && \
sed -i 's/#LogLevel\s*INFO/LogLevel DEBUG3/' /etc/ssh/sshd_config && \
# Set the default RSA key
echo 'HostKey /etc/ssh/ssh_host_rsa_key' >> /etc/ssh/sshd_config && \
+ echo 'TrustedUserCAKeys /etc/ssh/user-ca.pub' >> /etc/ssh/sshd_config && \
chmod 646 /etc/ssh/sshd_config && \
# install and configure sudo
apk add --no-cache sudo && \
diff --git a/test/Renci.SshNet.IntegrationTests/HostCertificateFile.cs b/test/Renci.SshNet.IntegrationTests/HostCertificateFile.cs
new file mode 100644
index 000000000..2526aac82
--- /dev/null
+++ b/test/Renci.SshNet.IntegrationTests/HostCertificateFile.cs
@@ -0,0 +1,24 @@
+namespace Renci.SshNet.IntegrationTests
+{
+ public sealed class HostCertificateFile
+ {
+ public static readonly HostCertificateFile RsaCertRsa = new HostCertificateFile("ssh-rsa-cert-v01@openssh.com", "/etc/ssh/ssh_host_rsa_key-cert_rsa.pub", HostKeyFile.Rsa, "x0vVk+h7SGE7bNN0wAA2vsA9Mg9qLOZPqhq2Dj/rqfM");
+ public static readonly HostCertificateFile Ed25519CertEcdsa = new HostCertificateFile("ssh-ed25519-cert-v01@openssh.com", "/etc/ssh/ssh_host_ed25519_key-cert_ecdsa", HostKeyFile.Ed25519, "Z2diHpknyvJpetRw47iIjqt9OUzm6cAVOe4FM5FbDQw");
+ public static readonly HostCertificateFile Ecdsa256CertRsa = new HostCertificateFile("ecdsa-sha2-nistp256-cert-v01@openssh.com", "/etc/ssh/ssh_host_ecdsa256_key-cert_rsa", HostKeyFile.Ecdsa256, "x0vVk+h7SGE7bNN0wAA2vsA9Mg9qLOZPqhq2Dj/rqfM");
+ public static readonly HostCertificateFile Ecdsa384CertEcdsa = new HostCertificateFile("ecdsa-sha2-nistp384-cert-v01@openssh.com", "/etc/ssh/ssh_host_ecdsa384_key-cert_ecdsa", HostKeyFile.Ecdsa384, "Z2diHpknyvJpetRw47iIjqt9OUzm6cAVOe4FM5FbDQw");
+ public static readonly HostCertificateFile Ecdsa521CertEd25519 = new HostCertificateFile("ecdsa-sha2-nistp521-cert-v01@openssh.com", "/etc/ssh/ssh_host_ecdsa521_key-cert_ed25519", HostKeyFile.Ecdsa521, "tF3DRTUXtYFZ5Yz0SBOrEbixHaCifHmNVK6FtptXZVM");
+
+ private HostCertificateFile(string certificateName, string filePath, HostKeyFile hostKeyFile, string caFingerPrint)
+ {
+ CertificateName = certificateName;
+ FilePath = filePath;
+ HostKeyFile = hostKeyFile;
+ CAFingerPrint = caFingerPrint;
+ }
+
+ public string CertificateName { get; }
+ public string FilePath { get; }
+ public HostKeyFile HostKeyFile { get; }
+ public string CAFingerPrint { get; }
+ }
+}
diff --git a/test/Renci.SshNet.IntegrationTests/HostKeyAlgorithmTests.cs b/test/Renci.SshNet.IntegrationTests/HostKeyAlgorithmTests.cs
index ccb153f49..40049938f 100644
--- a/test/Renci.SshNet.IntegrationTests/HostKeyAlgorithmTests.cs
+++ b/test/Renci.SshNet.IntegrationTests/HostKeyAlgorithmTests.cs
@@ -1,5 +1,6 @@
using Renci.SshNet.Common;
using Renci.SshNet.IntegrationTests.Common;
+using Renci.SshNet.Security;
using Renci.SshNet.TestTools.OpenSSH;
namespace Renci.SshNet.IntegrationTests
@@ -71,12 +72,49 @@ public void Ecdsa521()
DoTest(HostKeyAlgorithm.EcdsaSha2Nistp521, HostKeyFile.Ecdsa521);
}
- private void DoTest(HostKeyAlgorithm hostKeyAlgorithm, HostKeyFile hostKeyFile)
+ [TestMethod]
+ public void SshRsaCertificate()
+ {
+ DoTest(HostKeyAlgorithm.SshRsaCertV01OpenSSH, HostCertificateFile.RsaCertRsa);
+ }
+
+ [TestMethod]
+ public void SshRsaSha256Certificate()
+ {
+ DoTest(HostKeyAlgorithm.RsaSha2256CertV01OpenSSH, HostCertificateFile.RsaCertRsa);
+ }
+
+ [TestMethod]
+ public void Ecdsa256Certificate()
+ {
+ DoTest(HostKeyAlgorithm.EcdsaSha2Nistp256CertV01OpenSSH, HostCertificateFile.Ecdsa256CertRsa);
+ }
+
+ [TestMethod]
+ public void Ecdsa384Certificate()
+ {
+ DoTest(HostKeyAlgorithm.EcdsaSha2Nistp384CertV01OpenSSH, HostCertificateFile.Ecdsa384CertEcdsa);
+ }
+
+ [TestMethod]
+ public void Ecdsa521Certificate()
+ {
+ DoTest(HostKeyAlgorithm.EcdsaSha2Nistp521CertV01OpenSSH, HostCertificateFile.Ecdsa521CertEd25519);
+ }
+
+ [TestMethod]
+ public void Ed25519Certificate()
+ {
+ DoTest(HostKeyAlgorithm.SshEd25519CertV01OpenSSH, HostCertificateFile.Ed25519CertEcdsa);
+ }
+
+ private void DoTest(HostKeyAlgorithm hostKeyAlgorithm, HostKeyFile hostKeyFile, HostCertificateFile hostCertificateFile = null)
{
_remoteSshdConfig.ClearHostKeyAlgorithms()
.AddHostKeyAlgorithm(hostKeyAlgorithm)
.ClearHostKeyFiles()
.AddHostKeyFile(hostKeyFile.FilePath)
+ .WithHostKeyCertificate(hostCertificateFile?.FilePath)
.Update()
.Restart();
@@ -93,6 +131,22 @@ private void DoTest(HostKeyAlgorithm hostKeyAlgorithm, HostKeyFile hostKeyFile)
Assert.AreEqual(hostKeyAlgorithm.Name, hostKeyEventsArgs.HostKeyName);
Assert.AreEqual(hostKeyFile.KeyLength, hostKeyEventsArgs.KeyLength);
CollectionAssert.AreEqual(hostKeyFile.FingerPrint, hostKeyEventsArgs.FingerPrint);
+
+ if (hostCertificateFile is not null)
+ {
+ Assert.IsNotNull(hostKeyEventsArgs.Certificate);
+ Assert.AreEqual(Certificate.CertificateType.Host, hostKeyEventsArgs.Certificate.Type);
+ Assert.AreEqual(hostCertificateFile.CAFingerPrint, hostKeyEventsArgs.Certificate.CertificateAuthorityKeyFingerPrint);
+ }
+ else
+ {
+ Assert.IsNull(hostKeyEventsArgs.Certificate);
+ }
+ }
+
+ private void DoTest(HostKeyAlgorithm hostKeyAlgorithm, HostCertificateFile hostCertificateFile)
+ {
+ DoTest(hostKeyAlgorithm, hostCertificateFile.HostKeyFile, hostCertificateFile);
}
}
}
diff --git a/test/Renci.SshNet.IntegrationTests/PrivateKeyAuthenticationTests.cs b/test/Renci.SshNet.IntegrationTests/PrivateKeyAuthenticationTests.cs
index 19414999e..6a4a37cdc 100644
--- a/test/Renci.SshNet.IntegrationTests/PrivateKeyAuthenticationTests.cs
+++ b/test/Renci.SshNet.IntegrationTests/PrivateKeyAuthenticationTests.cs
@@ -70,14 +70,74 @@ public void Ed25519()
DoTest(PublicKeyAlgorithm.SshEd25519, "Data.Key.OPENSSH.ED25519.Encrypted.txt", "12345");
}
- private void DoTest(PublicKeyAlgorithm publicKeyAlgorithm, string keyResource, string passPhrase = null)
+ // The private keys used for the certificate tests below should stay out of authorized_keys for a proper test.
+
+ [TestMethod]
+ public void SshRsaCertificate()
+ {
+ // ssh-keygen -L -f Key.OPENSSH.RSA.Encrypted.Aes.192.CTR-cert.pub
+ // Type: ssh-rsa-cert-v01@openssh.com user certificate
+ // Public key: RSA-CERT SHA256:MMIzDVhQHqU9SAZ8p3x2wo6JpXixCWO/7qf6h0l8DJA
+ // Signing CA: RSA SHA256:NqLEgdYti0XjUkYjGyQv2Ddy1O5v2NZDZFRtlfESLIA (using rsa-sha2-512)
+ // And we will authenticate (sign) with ssh-rsa (SHA-1)
+ DoTest(PublicKeyAlgorithm.SshRsaCertV01OpenSSH, "Data.Key.OPENSSH.RSA.Encrypted.Aes.192.CTR.txt", "12345", "Data.Key.OPENSSH.RSA.Encrypted.Aes.192.CTR-cert.pub");
+ }
+
+ [TestMethod]
+ public void SshRsaSha256Certificate()
+ {
+ // As above, but we will authenticate (sign) with rsa-sha2-256
+ DoTest(PublicKeyAlgorithm.SshRsaCertV01OpenSSH, "Data.Key.OPENSSH.RSA.Encrypted.Aes.192.CTR.txt", "12345", "Data.Key.OPENSSH.RSA.Encrypted.Aes.192.CTR-cert.pub");
+ }
+
+ [TestMethod]
+ public void Ecdsa256Certificate()
+ {
+ // ssh-keygen -L -f Key.OPENSSH.ECDSA.Encrypted.Aes.128.CTR-cert.pub
+ // Type: ecdsa-sha2-nistp256-cert-v01@openssh.com user certificate
+ // Public key: ECDSA-CERT SHA256:ufAaMwjTmKrjvt4CQiLPal1/HrmB2D7oL+H2lh/Om8c
+ // Signing CA: RSA SHA256:NqLEgdYti0XjUkYjGyQv2Ddy1O5v2NZDZFRtlfESLIA (using rsa-sha2-512)
+ DoTest(PublicKeyAlgorithm.EcdsaSha2Nistp256CertV01OpenSSH, "Data.Key.OPENSSH.ECDSA.Encrypted.Aes.128.CTR.txt", "12345", "Data.Key.OPENSSH.ECDSA.Encrypted.Aes.128.CTR-cert.pub");
+ }
+
+ [TestMethod]
+ public void Ecdsa384Certificate()
+ {
+ // ssh-keygen -L -f Key.OPENSSH.ECDSA384.Encrypted.Aes.256.GCM-cert.pub
+ // Type: ecdsa-sha2-nistp384-cert-v01@openssh.com user certificate
+ // Public key: ECDSA-CERT SHA256:wy4X47uddqD8nggcsGHG7Rcs0qcnh4r6NrdBGdh/8us
+ // Signing CA: RSA SHA256:NqLEgdYti0XjUkYjGyQv2Ddy1O5v2NZDZFRtlfESLIA (using rsa-sha2-256)
+ DoTest(PublicKeyAlgorithm.EcdsaSha2Nistp384CertV01OpenSSH, "Data.Key.OPENSSH.ECDSA384.Encrypted.Aes.256.GCM.txt", "12345", "Data.Key.OPENSSH.ECDSA384.Encrypted.Aes.256.GCM-cert.pub");
+ }
+
+ [TestMethod]
+ public void Ecdsa521Certificate()
+ {
+ // ssh-keygen -L -f Key.OPENSSH.ECDSA521.Encrypted.Aes.192.CBC-cert.pub
+ // Type: ecdsa-sha2-nistp521-cert-v01@openssh.com user certificate
+ // Public key: ECDSA-CERT SHA256:U3wBX0sSPYxso31gi1QPz7O+1eMOTb0LoOSOjWRwyYE
+ // Signing CA: ECDSA SHA256:r/t6I+bZQzN5BhSuntFSHDHlrnNHVM2lAo6gbvynG/4 (using ecdsa-sha2-nistp256)
+ DoTest(PublicKeyAlgorithm.EcdsaSha2Nistp521CertV01OpenSSH, "Data.Key.OPENSSH.ECDSA521.Encrypted.Aes.192.CBC.txt", "12345", "Data.Key.OPENSSH.ECDSA521.Encrypted.Aes.192.CBC-cert.pub");
+ }
+
+ [TestMethod]
+ public void Ed25519Certificate()
+ {
+ // ssh-keygen -L -f Key.OPENSSH.ED25519.Encrypted.ChaCha20.Poly1305-cert.pub
+ // Type: ssh-ed25519-cert-v01@openssh.com user certificate
+ // Public key: ED25519-CERT SHA256:gwO3eBcuPqChqg9B/kHsQo1/bYTAjaEZCanA7hqSuEg
+ // Signing CA: ECDSA SHA256:r/t6I+bZQzN5BhSuntFSHDHlrnNHVM2lAo6gbvynG/4 (using ecdsa-sha2-nistp256)
+ DoTest(PublicKeyAlgorithm.SshEd25519CertV01OpenSSH, "Data.Key.OPENSSH.ED25519.Encrypted.ChaCha20.Poly1305.txt", "12345", "Data.Key.OPENSSH.ED25519.Encrypted.ChaCha20.Poly1305-cert.pub");
+ }
+
+ private void DoTest(PublicKeyAlgorithm publicKeyAlgorithm, string keyResource, string passPhrase = null, string certificateResource = null)
{
_remoteSshdConfig.ClearPublicKeyAcceptedAlgorithms()
.AddPublicKeyAcceptedAlgorithm(publicKeyAlgorithm)
.Update()
.Restart();
- var connectionInfo = _connectionInfoFactory.Create(CreatePrivateKeyAuthenticationMethod(keyResource, passPhrase));
+ var connectionInfo = _connectionInfoFactory.Create(CreatePrivateKeyAuthenticationMethod(keyResource, passPhrase, certificateResource));
using (var client = new SshClient(connectionInfo))
{
@@ -85,12 +145,26 @@ private void DoTest(PublicKeyAlgorithm publicKeyAlgorithm, string keyResource, s
}
}
- private static PrivateKeyAuthenticationMethod CreatePrivateKeyAuthenticationMethod(string keyResource, string passPhrase)
+ private static PrivateKeyAuthenticationMethod CreatePrivateKeyAuthenticationMethod(string keyResource, string passPhrase, string certificateResource)
{
- using (var stream = GetData(keyResource))
+ PrivateKeyFile privateKey;
+
+ using (var keyStream = GetData(keyResource))
{
- return new PrivateKeyAuthenticationMethod(Users.Regular.UserName, new PrivateKeyFile(stream, passPhrase));
+ if (certificateResource is not null)
+ {
+ using (var certificateStream = GetData(certificateResource))
+ {
+ privateKey = new PrivateKeyFile(keyStream, passPhrase, certificateStream);
+ }
+ }
+ else
+ {
+ privateKey = new PrivateKeyFile(keyStream, passPhrase);
+ }
}
+
+ return new PrivateKeyAuthenticationMethod(Users.Regular.UserName, privateKey);
}
}
}
diff --git a/test/Renci.SshNet.IntegrationTests/RemoteSshdConfig.cs b/test/Renci.SshNet.IntegrationTests/RemoteSshdConfig.cs
index 74487fdae..3594af66a 100644
--- a/test/Renci.SshNet.IntegrationTests/RemoteSshdConfig.cs
+++ b/test/Renci.SshNet.IntegrationTests/RemoteSshdConfig.cs
@@ -205,6 +205,12 @@ public RemoteSshdConfig AddHostKeyFile(string hostKeyFile)
return this;
}
+ public RemoteSshdConfig WithHostKeyCertificate(string hostKeyCertificate)
+ {
+ _config.HostCertificate = hostKeyCertificate;
+ return this;
+ }
+
public RemoteSshd Update()
{
using (var client = new ScpClient(_connectionInfoFactory.Create()))
diff --git a/test/Renci.SshNet.IntegrationTests/TestsFixtures/InfrastructureFixture.cs b/test/Renci.SshNet.IntegrationTests/TestsFixtures/InfrastructureFixture.cs
index 3b598bf25..4be520af6 100644
--- a/test/Renci.SshNet.IntegrationTests/TestsFixtures/InfrastructureFixture.cs
+++ b/test/Renci.SshNet.IntegrationTests/TestsFixtures/InfrastructureFixture.cs
@@ -2,10 +2,6 @@
using DotNet.Testcontainers.Containers;
using DotNet.Testcontainers.Images;
-#if !NET && !NETSTANDARD2_1_OR_GREATER
-using Renci.SshNet.Abstractions;
-#endif
-
namespace Renci.SshNet.IntegrationTests.TestsFixtures
{
public sealed class InfrastructureFixture : IDisposable
@@ -36,10 +32,6 @@ public static InfrastructureFixture Instance
public SshUser User = new SshUser("sshnet", "ssh4ever");
- // To get the sshd logs (also uncomment WithOutputConsumer below)
- private readonly Stream _fsOut = Stream.Null; // File.Create("fsout.txt");
- private readonly Stream _fsErr = Stream.Null; // File.Create("fserr.txt");
-
public async Task InitializeAsync()
{
_sshServerImage = new ImageFromDockerfileBuilder()
@@ -55,7 +47,6 @@ public async Task InitializeAsync()
.WithHostname("renci-ssh-tests-server")
.WithImage(_sshServerImage)
.WithPortBinding(22, true)
- //.WithOutputConsumer(Consume.RedirectStdoutAndStderrToStream(_fsOut, _fsErr))
.Build();
await _sshServer.StartAsync();
@@ -76,6 +67,15 @@ public async Task DisposeAsync()
{
if (_sshServer != null)
{
+ //try
+ //{
+ // File.WriteAllBytes(@"C:\tmp\auth.log", await _sshServer.ReadFileAsync("/var/log/auth.log"));
+ //}
+ //catch (Exception ex)
+ //{
+ // Console.Error.WriteLine(ex.ToString());
+ //}
+
await _sshServer.DisposeAsync();
}
@@ -83,9 +83,6 @@ public async Task DisposeAsync()
{
await _sshServerImage.DisposeAsync();
}
-
- await _fsOut.DisposeAsync();
- await _fsErr.DisposeAsync();
}
public void Dispose()
diff --git a/test/Renci.SshNet.IntegrationTests/server/ca/host_ca_ecdsa_key b/test/Renci.SshNet.IntegrationTests/server/ca/host_ca_ecdsa_key
new file mode 100644
index 000000000..9ff448a4e
--- /dev/null
+++ b/test/Renci.SshNet.IntegrationTests/server/ca/host_ca_ecdsa_key
@@ -0,0 +1,12 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAArAAAABNlY2RzYS
+1zaGEyLW5pc3RwNTIxAAAACG5pc3RwNTIxAAAAhQQBOm4mqGkmRjg+N/BstCoV+3OYDp5Q
+q/s3Lp/CLCNjAHsyEKQJ7U0gHTybdXskhPZwPEQpTZRyXmNyyZUxvRGu6jIB9njA8bq6m8
+tOrbJz3SY26gcJPtVQgPzz7RkGHLJIvopkzAdKYLtf/vYpiCAP8SU0aZLL8j/WaQQRoZ36
+QJr2Rp8AAAEIzunYnM7p2JwAAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQ
+AAAIUEATpuJqhpJkY4PjfwbLQqFftzmA6eUKv7Ny6fwiwjYwB7MhCkCe1NIB08m3V7JIT2
+cDxEKU2Ucl5jcsmVMb0RruoyAfZ4wPG6upvLTq2yc90mNuoHCT7VUID88+0ZBhyySL6KZM
+wHSmC7X/72KYggD/ElNGmSy/I/1mkEEaGd+kCa9kafAAAAQgDOhCBRxYWebDsbGhHbzEbg
+BcrczdS+3Jkf2eq1SIhTWYRlJw9lVNPaQxkYnUnLYeWIdG3sAxWdZsepGhm2y+dgTQAAAA
+hDQS1FQ0RTQQEC
+-----END OPENSSH PRIVATE KEY-----
diff --git a/test/Renci.SshNet.IntegrationTests/server/ca/host_ca_ed25519_key b/test/Renci.SshNet.IntegrationTests/server/ca/host_ca_ed25519_key
new file mode 100644
index 000000000..2f54646e9
--- /dev/null
+++ b/test/Renci.SshNet.IntegrationTests/server/ca/host_ca_ed25519_key
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACCgTBKYawpTnGkumT6IXiHGdiqk0C+4rPVB1Vs7uVFuxAAAAJCdPWEKnT1h
+CgAAAAtzc2gtZWQyNTUxOQAAACCgTBKYawpTnGkumT6IXiHGdiqk0C+4rPVB1Vs7uVFuxA
+AAAECxa3d+jGIfVQT5AF0Ssb1pPxUJVH88BPbKy5LxwMGbgaBMEphrClOcaS6ZPoheIcZ2
+KqTQL7is9UHVWzu5UW7EAAAACkNBLUVEMjU1MTkBAgM=
+-----END OPENSSH PRIVATE KEY-----
diff --git a/test/Renci.SshNet.IntegrationTests/server/ca/host_ca_rsa_key b/test/Renci.SshNet.IntegrationTests/server/ca/host_ca_rsa_key
new file mode 100644
index 000000000..ec0f892c0
--- /dev/null
+++ b/test/Renci.SshNet.IntegrationTests/server/ca/host_ca_rsa_key
@@ -0,0 +1,49 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+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
+bZjU9P/dMt0AAAAGQ0EtUlNBAQIDBAU=
+-----END OPENSSH PRIVATE KEY-----
diff --git a/test/Renci.SshNet.IntegrationTests/server/ca/user_ca_ecdsa_key b/test/Renci.SshNet.IntegrationTests/server/ca/user_ca_ecdsa_key
new file mode 100644
index 000000000..da06bb528
--- /dev/null
+++ b/test/Renci.SshNet.IntegrationTests/server/ca/user_ca_ecdsa_key
@@ -0,0 +1,9 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
+1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQS78VMiSkfSccbrbtrDBKGdTLCMVHTD
+amHKZm1F/GLuJHRsflq+JLTl7/ArHqF0pGNltv5YfIP66BOjeB+nEFpPAAAAqCFPmN8hT5
+jfAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLvxUyJKR9Jxxutu
+2sMEoZ1MsIxUdMNqYcpmbUX8Yu4kdGx+Wr4ktOXv8CseoXSkY2W2/lh8g/roE6N4H6cQWk
+8AAAAgaEEu4ZRGDRhhSTG9pU2pACyIY4M2mVSIM5/g2RAA9z0AAAAOcm9iZXJ0QFJIRTE0
+RzMBAg==
+-----END OPENSSH PRIVATE KEY-----
diff --git a/test/Renci.SshNet.IntegrationTests/server/ca/user_ca_rsa_key b/test/Renci.SshNet.IntegrationTests/server/ca/user_ca_rsa_key
new file mode 100644
index 000000000..876f5835e
--- /dev/null
+++ b/test/Renci.SshNet.IntegrationTests/server/ca/user_ca_rsa_key
@@ -0,0 +1,49 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+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
+-----END OPENSSH PRIVATE KEY-----
diff --git a/test/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_ecdsa256_key-cert_rsa.pub b/test/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_ecdsa256_key-cert_rsa.pub
new file mode 100644
index 000000000..b6db899e5
--- /dev/null
+++ b/test/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_ecdsa256_key-cert_rsa.pub
@@ -0,0 +1 @@
+ecdsa-sha2-nistp256-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgR6jvisYMr7lS9maXh96zzfyYb2rdf9mEmq1RyRN/ZjUAAAAIbmlzdHAyNTYAAABBBO0OhyLQJoSDJH6n56uyJiW0pRKpnH8x99amcQ98aCcUjzWG5DDQP179KoAFt4e2vNxJayRJlzR/aOMXHKpWSWIAAAAAAAAAAAAAAAIAAAAPZWNkc2EyNTZjZXJ0UlNBAAAAAAAAAAAAAAAA//////////8AAAAAAAAAAAAAAAAAAAIXAAAAB3NzaC1yc2EAAAADAQABAAACAQCrcoVYAVaFda7O86SEab33pyP6hS1p7skrD/qRQ0sZEOATdSUvCD0y45zl3jwGcBPOX7mdxeFaFBAO/QRnE++4FkIJmpXRHh/Zy+D+CoOPWudFck8b8Zsx36wfA8DsVuqZdAVRrik5xgPy2yP/g6h27sGFIL0Ced3A9GuUAje22O8IAcg09TqMJzsyaWORo2ez+kXIiJkxeAgiyOHS4kxZG/hAr20xfsHx3pG7UlX+EYuqL7/v2uT3A4ceFAi+7pob/06SxozOm3B1Bl3xGLw0M5o1eQBja4pBVJ8ccUM/BII+vlEgLoMMPPgOkYxMAnAateGqD5HoRWYTkYbTHwMtqkNyMx9Bo6OnIrFQ3jYRwFFyu8B2vm+Tod15YVWWtB4FXQ5FHW5f6v87TT9fB4HE3lhSSKT76WMxrLxUS1I1etNFuCb5Fwfe6JCfqAjy10Q0Ad60M2bgPK69j4+xtEicXZ6iZIO+AqYUSzWF91JUWKC5DjGMqUQxckJe4X37kZb1O1BIgD9e3tivg/mnYY4kDxhtE8CUgPPnBBlCFf+f2JSsALQ7PcNI33BfpXWRpQ7dYrr8lL65YUdrZ/V7WZlinmA2RlZichRnm0aAsgzYIALxLkQDGEIkuf0Ql4rmLxoftmbzb7RWGX56ilsxpie7YrK3xxnHeZAiBKxPL5dmawAAAhQAAAAMcnNhLXNoYTItNTEyAAACADGhUG+gliMrVfKD+WfYkmhL15rmzGJIPdqDwezEqSOcGl3y7QiT3DGIBSSWGvolYF3f2wf7+6COrnOkONIxvpPpH0OSECmDRW5tbcaijyJEEBituqNfTDTJNTDKyYW2jPxWbJCeyn3ESaKJ5q8EhO2I25w63mYxRdMaZUk1WABi2vqo5hPycbGEvwfV30F4+QUpw2RFcUVmbg8gC90OOIjGwonBxG0VPadlMj+ms7YEGm7GiZEdBksrhHdobO7b9T6y3Je9UBkxJMEAIdYSLKkzu4TheGrzIk6/k5Lh8Qq1oVFbe0VpQAE4i6Go39C2gm7noJAO6rPtFMjH789av4sFPssMydwrYaUpfLoDJ8Px/hbD/M61gZSYnccAbadV6fekJH57/MeJFHtaCVMP93i4KbCo/UwQc4lJy3fw2UQOLk/XwuPmkaL0Kb4EI2zjSQEf6AvwuC84D7ObGKcufO6o7BCsUIuQFr2ps0Lscs/jHItXNoMpd/dgBj6RoMNXJujxCkA6bBH33pFGDfyxqO6l37gXCDI7muLSH8oWq9lf+jKHoo9kz46KkbcIG4NAmQHc8PCTh0WqvJA4CQ52GfdtWgqqW27VoYDT5NcU/C28rDGZ8y6uZBA7bFpL+bWkIoqhBOlCwtTVT4NSuLuCWEd7sRGpgshJ+VZ9I3ZkeUjl (null)
diff --git a/test/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_ecdsa384_key-cert_ecdsa.pub b/test/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_ecdsa384_key-cert_ecdsa.pub
new file mode 100644
index 000000000..4a11a1d43
--- /dev/null
+++ b/test/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_ecdsa384_key-cert_ecdsa.pub
@@ -0,0 +1 @@
+ecdsa-sha2-nistp384-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHAzODQtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgphXMu5lsJKjbFrUqc4ksYylRjgBP3CjCTohjqO8G03EAAAAIbmlzdHAzODQAAABhBOAOQhPx63nmC8GRm1qPNjA5UH/gt36h3eR7PL3D8V0ZBKT+NhT1fB0eArVzdl32NTyBMRaTviL7uFWGCPn+kBq4e89QExo/BttXaBR3FSVTPfT384rTCccOg8ZC19BpQQAAAAAAAAAAAAAAAgAAABFlY2RzYTM4NGNlcnRFY2RzYQAAAAAAAAAAAAAAAP//////////AAAAAAAAAAAAAAAAAAAArAAAABNlY2RzYS1zaGEyLW5pc3RwNTIxAAAACG5pc3RwNTIxAAAAhQQBOm4mqGkmRjg+N/BstCoV+3OYDp5Qq/s3Lp/CLCNjAHsyEKQJ7U0gHTybdXskhPZwPEQpTZRyXmNyyZUxvRGu6jIB9njA8bq6m8tOrbJz3SY26gcJPtVQgPzz7RkGHLJIvopkzAdKYLtf/vYpiCAP8SU0aZLL8j/WaQQRoZ36QJr2Rp8AAACmAAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAACLAAAAQVOOpC8htogU5LX6pIMaNHZ+6+Q3oRQme1kVhjILPDgMyv5kOzlz1EcI9qDwWjtmoewxYaIVvRHoTCf4WW3QjJ7HAAAAQgG6iDMHm/8oJ55mxWtorVxf/mBqUZDfH2VCx1T34Sw+6GW7Z59hWp6DOanlNDuJEm2eT8sDqF6hHxaYVw4exDYPvg== (null)
diff --git a/test/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_ecdsa521_key-cert_ed25519.pub b/test/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_ecdsa521_key-cert_ed25519.pub
new file mode 100644
index 000000000..84913acf6
--- /dev/null
+++ b/test/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_ecdsa521_key-cert_ed25519.pub
@@ -0,0 +1 @@
+ecdsa-sha2-nistp521-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHA1MjEtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgc5mujscEbh/GGGq3kjeEbZEXT0Rt8waJ9hE2Ckz3tfEAAAAIbmlzdHA1MjEAAACFBAErBBojetXoSOJoH71QvOyAUyaEN9kQKUzouR0V2yCkZpBdS+TYLcVduDkXU0dvrj6nqbS32IrG6x3PDk/B4kduJAF0q80dgoCHPWE5t3bwJvGAZh0ABEEETeV/qFtTIZ/HDINcXFfhH6zWVLCw5g+i9YOqx24xwZbQRzVsbuwERCe+7QAAAAAAAAAAAAAAAgAAABNlY2RzYTUyMWNlcnRFZDI1NTE5AAAAAAAAAAAAAAAA//////////8AAAAAAAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIKBMEphrClOcaS6ZPoheIcZ2KqTQL7is9UHVWzu5UW7EAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEBgDUlR8fk92XP05DVIy3O1UuJ8ArHamImOS6Vd0ZLsJiFZL+s9VpR7BTgqxXhTS5tgAI2B9a/+gc1BLl8UdbIF (null)
diff --git a/test/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_ed25519_key-cert_ecdsa.pub b/test/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_ed25519_key-cert_ecdsa.pub
new file mode 100644
index 000000000..763009b4a
--- /dev/null
+++ b/test/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_ed25519_key-cert_ecdsa.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIL4D+xZeR0A4m3jiDUGNP42yfOXevsOVY2Tcr1az8su1AAAAIIIxbdWe4p+PoDEFDuAnioB8SDD9gmX2rcBfJ6ULD453AAAAAAAAAAAAAAACAAAADGhvc3RfZWQyNTUxOQAAAAAAAAAAAAAAAP//////////AAAAAAAAAAAAAAAAAAAArAAAABNlY2RzYS1zaGEyLW5pc3RwNTIxAAAACG5pc3RwNTIxAAAAhQQBOm4mqGkmRjg+N/BstCoV+3OYDp5Qq/s3Lp/CLCNjAHsyEKQJ7U0gHTybdXskhPZwPEQpTZRyXmNyyZUxvRGu6jIB9njA8bq6m8tOrbJz3SY26gcJPtVQgPzz7RkGHLJIvopkzAdKYLtf/vYpiCAP8SU0aZLL8j/WaQQRoZ36QJr2Rp8AAACmAAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAACLAAAAQgFBd6Z6VCjfr/SXZ23IjhEVWM2oHJviwBtrTTB39rr6xONV0GzbyHz9w10B5usM1nS6DsYKF6OgXeOLGNXxxPbxnwAAAEFmMbiXJbQf5D/1WRpwwn3inqoAfP0jx/Wzd/VbpoTHYJYVoti/NXi8f/KNlXWTKJzuM2PFjciFdpkkjlU8oWTNIw== root@Ubuntu1910Desktop
diff --git a/test/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_rsa_key-cert_rsa.pub b/test/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_rsa_key-cert_rsa.pub
new file mode 100644
index 000000000..37d4424da
--- /dev/null
+++ b/test/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_rsa_key-cert_rsa.pub
@@ -0,0 +1 @@
+ssh-rsa-cert-v01@openssh.com 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 root@Ubuntu1910Desktop
diff --git a/test/Renci.SshNet.IntegrationTests/server/ssh/user-ca.pub b/test/Renci.SshNet.IntegrationTests/server/ssh/user-ca.pub
new file mode 100644
index 000000000..3bffdb7cd
--- /dev/null
+++ b/test/Renci.SshNet.IntegrationTests/server/ssh/user-ca.pub
@@ -0,0 +1,2 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCvWBFUpRdXqyRAHLIqG2oFFaqXWZ1YORiIx0l7fiO1yG0Bn41SaQaWnzBrCWnPGJkWDnCb9d3b0o81i6ggibwTYPHF67eiJyyaNCEnYMJOcvovfmEB8q/A0ZfN+oqSOXBUoUZhvqdOG4FFzbI/faTVsiM8HH8piZH1eHcRK8i2UsKzNAykKzYxmdYIFrHP6McgRrta0TY6s8XWWNqwojrH8UPnUSxOYCX89eDs3wPKzyrysN+N/proLIqG0xPLjylKTOzc1Mg2cy84P8fLNiliZL1rWG6OU6PyPGJedwf+9rFjAi7ZORnAEjJ5zAn7K51sefo9wX12/ddIRGin02/ySrvc28+AqBHA2gd3usvsNvlQdLPVG4t36+KqttFi6naFd4M4v+oY2lwow+kvcWzxPsZJAUfADGhrPnIScr49m9E04dprb2NDCw50MRTEFvYE71L0jOvRB/6s2ZBOXA5PiexxEB5RokRiHroOmwowhdwY7JQxqRKl+DblMDc4ENUZQ0l3P5ijXWZGIzp8LbUK2N1nDc8uqQAaM1kSUTkUEZI5aUTcqdaOl5/14XHkWskBp09zJNsI6VoV17C1E/SdwznLbCDPf8WgJtYNKh5q3OR5ZFBiBsR2PYKZg9sk/wrrlnu8VfgVYQ5o4HzgZg8vYx79acfj9JosLnVGcyA5Ew==
+ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLvxUyJKR9Jxxutu2sMEoZ1MsIxUdMNqYcpmbUX8Yu4kdGx+Wr4ktOXv8CseoXSkY2W2/lh8g/roE6N4H6cQWk8=
diff --git a/test/Renci.SshNet.TestTools.OpenSSH/SshdConfig.cs b/test/Renci.SshNet.TestTools.OpenSSH/SshdConfig.cs
index e19197a02..17f88dabd 100644
--- a/test/Renci.SshNet.TestTools.OpenSSH/SshdConfig.cs
+++ b/test/Renci.SshNet.TestTools.OpenSSH/SshdConfig.cs
@@ -53,6 +53,8 @@ private SshdConfig()
///
public List HostKeyFiles { get; }
+ public string? HostCertificate { get; set; }
+
///
/// Gets or sets a value specifying whether challenge-response authentication is allowed.
///
@@ -118,6 +120,11 @@ private SshdConfig()
///
public List MessageAuthenticationCodeAlgorithms { get; private set; }
+ ///
+ /// Gets the filepaths of the trusted user CA (certificate authority) keys.
+ ///
+ public string? TrustedUserCAKeys { get; private set; }
+
///
/// Gets a value indicating whether sshd should print /etc/motd when a user logs in interactively.
///
@@ -291,6 +298,11 @@ public void SaveTo(TextWriter writer)
writer.WriteLine("HostKeyAlgorithms " + string.Join(",", HostKeyAlgorithms.Select(c => c.Name).ToArray()));
}
+ if (HostCertificate is not null)
+ {
+ writer.WriteLine("HostCertificate " + HostCertificate);
+ }
+
if (KeyExchangeAlgorithms.Count > 0)
{
writer.WriteLine("KexAlgorithms " + string.Join(",", KeyExchangeAlgorithms.Select(c => c.Name).ToArray()));
@@ -306,6 +318,11 @@ public void SaveTo(TextWriter writer)
writer.WriteLine("PubkeyAcceptedAlgorithms " + string.Join(",", PublicKeyAcceptedAlgorithms.Select(c => c.Name).ToArray()));
}
+ if (TrustedUserCAKeys is not null)
+ {
+ writer.WriteLine("TrustedUserCAKeys " + TrustedUserCAKeys);
+ }
+
foreach (var match in Matches)
{
_matchFormatter.Format(match, writer);
@@ -384,6 +401,12 @@ private static void ProcessGlobalOption(SshdConfig sshdConfig, string line)
case "AllowTcpForwarding":
sshdConfig.AllowTcpForwarding = ToBool(value);
break;
+ case "TrustedUserCAKeys":
+ sshdConfig.TrustedUserCAKeys = value;
+ break;
+ case "HostCertificate":
+ sshdConfig.HostCertificate = value;
+ break;
case "KeyRegenerationInterval":
case "HostbasedAuthentication":
case "ServerKeyBits":
diff --git a/test/Renci.SshNet.Tests/Classes/PrivateKeyFileTest.cs b/test/Renci.SshNet.Tests/Classes/PrivateKeyFileTest.cs
index 261455332..32dba2cb0 100644
--- a/test/Renci.SshNet.Tests/Classes/PrivateKeyFileTest.cs
+++ b/test/Renci.SshNet.Tests/Classes/PrivateKeyFileTest.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -16,6 +17,12 @@ namespace Renci.SshNet.Tests.Classes
[TestClass]
public class PrivateKeyFileTest : TestBase
{
+#if NETFRAMEWORK
+ private static readonly DateTimeOffset UnixEpoch = new(1970, 01, 01, 00, 00, 00, TimeSpan.Zero);
+#else
+ private static readonly DateTimeOffset UnixEpoch = DateTimeOffset.UnixEpoch;
+#endif
+
private string _temporaryFile;
[TestInitialize]
@@ -364,6 +371,111 @@ public void Test_PrivateKey(string name, string passPhrase, Type expectedKeyType
}
}
+ [TestMethod]
+ public void Test_Certificate_OPENSSH_RSA()
+ {
+ PrivateKeyFile pkFile;
+
+ using (var privateKeyStream = GetData("Key.OPENSSH.RSA.txt"))
+ using (var certificateStream = GetData("Key.OPENSSH.RSA-cert.pub"))
+ {
+ pkFile = new PrivateKeyFile(privateKeyStream, passPhrase: null, certificateStream);
+ }
+
+ Certificate cert = pkFile.Certificate;
+
+ // ssh-keygen -L -f Key.OPENSSH.RSA-cert.pub
+
+ Assert.AreEqual("ssh-rsa-cert-v01@openssh.com", cert.Name);
+
+ Assert.IsInstanceOfType(cert.Key);
+ CollectionAssert.AreEqual(((RsaKey)pkFile.Key).Public, ((RsaKey)cert.Key).Public);
+ Assert.AreEqual(0UL, cert.Serial);
+ Assert.AreEqual(Certificate.CertificateType.User, cert.Type);
+ Assert.AreEqual("rsa-cert-rsa", cert.KeyId);
+ CollectionAssert.AreEqual(new string[] { "sshnet" }, cert.ValidPrincipals.ToList());
+ Assert.AreEqual(0, cert.CriticalOptions.Count);
+ Assert.IsTrue(cert.ValidAfter.EqualsExact(new DateTimeOffset(2024, 07, 17, 20, 50, 34, TimeSpan.Zero)));
+ Assert.AreEqual(ulong.MaxValue, cert.ValidBeforeUnixSeconds);
+ Assert.AreEqual(DateTimeOffset.MaxValue, cert.ValidBefore);
+ CollectionAssert.AreEqual(new Dictionary
+ {
+ ["permit-X11-forwarding"] = "",
+ ["permit-agent-forwarding"] = "",
+ ["permit-port-forwarding"] = "",
+ ["permit-pty"] = "",
+ ["permit-user-rc"] = "",
+ }, new Dictionary(cert.Extensions));
+ Assert.AreEqual("NqLEgdYti0XjUkYjGyQv2Ddy1O5v2NZDZFRtlfESLIA", cert.CertificateAuthorityKeyFingerPrint);
+
+ Assert.AreEqual(6, pkFile.HostKeyAlgorithms.Count);
+
+ var algorithms = pkFile.HostKeyAlgorithms.ToList();
+
+ Assert.AreEqual("rsa-sha2-512-cert-v01@openssh.com", algorithms[0].Name);
+ Assert.AreEqual("rsa-sha2-256-cert-v01@openssh.com", algorithms[1].Name);
+ Assert.AreEqual("ssh-rsa-cert-v01@openssh.com", algorithms[2].Name);
+ Assert.AreEqual("ssh-rsa", algorithms[3].Name);
+ Assert.AreEqual("rsa-sha2-512", algorithms[4].Name);
+ Assert.AreEqual("rsa-sha2-256", algorithms[5].Name);
+ }
+
+ [TestMethod]
+ public void Test_CertificateKeyMismatch()
+ {
+ using (var privateKey = GetData("Key.OPENSSH.RSA.txt"))
+ using (var certificate = GetData("Key.OPENSSH.ECDSA521-cert.pub"))
+ {
+ Assert.ThrowsException(() => new PrivateKeyFile(privateKey, passPhrase: null, certificate));
+ }
+ }
+
+ [TestMethod]
+ public void Test_Certificate_OPENSSH_ECDSA()
+ {
+ PrivateKeyFile pkFile;
+
+ using (var privateKeyStream = GetData("Key.OPENSSH.ECDSA521.txt"))
+ using (var certificateStream = GetData("Key.OPENSSH.ECDSA521-cert.pub"))
+ {
+ pkFile = new PrivateKeyFile(privateKeyStream, passPhrase: null, certificateStream);
+ }
+
+ Certificate cert = pkFile.Certificate;
+
+ // ssh-keygen -L -f Key.OPENSSH.ECDSA521-cert.pub
+
+ Assert.AreEqual("ecdsa-sha2-nistp521-cert-v01@openssh.com", cert.Name);
+
+ Assert.IsInstanceOfType(cert.Key);
+ CollectionAssert.AreEqual(((EcdsaKey)pkFile.Key).Public, ((EcdsaKey)cert.Key).Public);
+ Assert.AreEqual(0UL, cert.Serial);
+ Assert.AreEqual(Certificate.CertificateType.User, cert.Type);
+ Assert.AreEqual("ecdsa521certEcdsa", cert.KeyId);
+ CollectionAssert.AreEqual(new string[] { "sshnet" }, cert.ValidPrincipals.ToList());
+ Assert.AreEqual(0, cert.CriticalOptions.Count);
+ Assert.AreEqual(0UL, cert.ValidAfterUnixSeconds);
+ Assert.IsTrue(cert.ValidAfter.EqualsExact(UnixEpoch));
+ Assert.AreEqual(ulong.MaxValue, cert.ValidBeforeUnixSeconds);
+ Assert.AreEqual(DateTimeOffset.MaxValue, cert.ValidBefore);
+ CollectionAssert.AreEqual(new Dictionary
+ {
+ ["permit-X11-forwarding"] = "",
+ ["permit-agent-forwarding"] = "",
+ ["permit-port-forwarding"] = "",
+ ["permit-pty"] = "",
+ ["permit-user-rc"] = "",
+ }, new Dictionary(cert.Extensions));
+ Assert.AreEqual("r/t6I+bZQzN5BhSuntFSHDHlrnNHVM2lAo6gbvynG/4", cert.CertificateAuthorityKeyFingerPrint);
+
+ Assert.AreEqual(2, pkFile.HostKeyAlgorithms.Count);
+
+ var algorithms = pkFile.HostKeyAlgorithms.ToList();
+
+ Assert.AreEqual("ecdsa-sha2-nistp521-cert-v01@openssh.com", algorithms[0].Name);
+ Assert.AreEqual("ecdsa-sha2-nistp521", algorithms[1].Name);
+ }
+
private void SaveStreamToFile(Stream stream, string fileName)
{
var buffer = new byte[4000];
diff --git a/test/Renci.SshNet.Tests/Classes/Security/CertificateHostAlgorithmTest.cs b/test/Renci.SshNet.Tests/Classes/Security/CertificateHostAlgorithmTest.cs
new file mode 100644
index 000000000..55adae4e4
--- /dev/null
+++ b/test/Renci.SshNet.Tests/Classes/Security/CertificateHostAlgorithmTest.cs
@@ -0,0 +1,389 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+using Renci.SshNet.Security;
+using Renci.SshNet.Security.Cryptography;
+using Renci.SshNet.Tests.Common;
+
+namespace Renci.SshNet.Tests.Classes.Security
+{
+ [TestClass]
+ public class CertificateHostAlgorithmTest : TestBase
+ {
+ [TestMethod]
+ public void NoSuppliedDigitalSignature_PropertyIsKeyDigitalSignature()
+ {
+ (RsaKey key, Certificate certificate) = GetRsaKey();
+
+ CertificateHostAlgorithm algorithm = new("ssh-rsa-cert-v01@openssh.com", key, certificate);
+
+ Assert.AreEqual("ssh-rsa-cert-v01@openssh.com", algorithm.Name);
+ Assert.AreSame(key, algorithm.Key);
+ Assert.AreSame(certificate, algorithm.Certificate);
+ Assert.AreSame(key.DigitalSignature, algorithm.DigitalSignature);
+ }
+
+ [TestMethod]
+ public void SuppliedDigitalSignature_PropertyIsSuppliedDigitalSignature()
+ {
+ (RsaKey key, Certificate certificate) = GetRsaKey();
+
+ RsaDigitalSignature rsaDigitalSignature = new(key, HashAlgorithmName.SHA256);
+
+ CertificateHostAlgorithm algorithm = new(
+ "rsa-sha2-256-cert-v01@openssh.com",
+ key,
+ certificate,
+ rsaDigitalSignature);
+
+ Assert.AreEqual("rsa-sha2-256-cert-v01@openssh.com", algorithm.Name);
+ Assert.AreSame(key, algorithm.Key);
+ Assert.AreSame(certificate, algorithm.Certificate);
+ Assert.AreSame(rsaDigitalSignature, algorithm.DigitalSignature);
+ }
+
+ [TestMethod]
+ public void HostAlgorithmData_IsRawCertificateBytes()
+ {
+ PrivateKeyFile pkFile;
+ byte[] certificateData;
+
+ using (Stream keyStream = GetData("Key.OPENSSH.RSA.txt"))
+ using (Stream certStream = GetData("Key.OPENSSH.RSA-cert.pub"))
+ {
+ using MemoryStream ms = new();
+ certStream.CopyTo(ms);
+
+ certificateData = Convert.FromBase64String(Encoding.UTF8.GetString(ms.ToArray()).Split(' ')[1]);
+
+ ms.Position = 0;
+
+ pkFile = new PrivateKeyFile(keyStream, null, ms);
+ }
+
+ List certAlgs = pkFile.HostKeyAlgorithms.OfType().ToList();
+
+ Assert.AreEqual(3, certAlgs.Count);
+
+ for (int i = 0; i < 3; i++)
+ {
+ Assert.IsNotNull(certAlgs[i].Certificate);
+ Assert.AreSame(pkFile.Certificate, certAlgs[i].Certificate);
+ CollectionAssert.AreEqual(certificateData, certAlgs[i].Data);
+ }
+ }
+
+ [TestMethod]
+ public void SshRsa_SignAndVerify()
+ {
+ byte[] data = Encoding.UTF8.GetBytes("hello world");
+
+ (RsaKey key, Certificate certificate) = GetRsaKey();
+
+ CertificateHostAlgorithm algorithm = new("ssh-rsa-cert-v01@openssh.com", key, certificate);
+
+ byte[] expectedEncodedSignatureBytes = new byte[]
+ {
+ 0, 0, 0, 7, // byte count of "ssh-rsa"
+ (byte)'s', (byte)'s', (byte)'h', (byte)'-', (byte)'r', (byte)'s', (byte)'a', // ssh-rsa
+ 0, 0, 1, 0, // byte count of signature (=256)
+
+ // ssh-keygen -e -f Key.OPENSSH.RSA.txt -m PEM -p
+ // echo -n 'hello world' | openssl dgst -sha1 -sign Key.OPENSSH.RSA.txt -out test.signed
+ 0x2d, 0x54, 0x2e, 0x6a, 0x5f, 0x7c, 0x29, 0x7d, 0x2d, 0x81, 0xf6, 0x34, 0x45, 0x7a, 0x3f, 0xd0,
+ 0xa5, 0x06, 0x55, 0x9c, 0xab, 0x8c, 0x28, 0x76, 0x27, 0xc0, 0x8a, 0x32, 0x23, 0xa4, 0x62, 0xd1,
+ 0x8c, 0x72, 0x05, 0x52, 0x47, 0x4d, 0xd0, 0xde, 0x86, 0xdd, 0xfc, 0x38, 0x54, 0x47, 0x4e, 0x17,
+ 0xef, 0x6b, 0x9a, 0x2e, 0x4d, 0x55, 0xf3, 0x2a, 0x11, 0xa7, 0x3a, 0x8b, 0x37, 0xbb, 0x61, 0x2d,
+ 0xb8, 0x4c, 0x1f, 0xa1, 0x0f, 0xb4, 0xbe, 0x06, 0xea, 0xc1, 0x4e, 0x17, 0x3c, 0x53, 0x01, 0x1b,
+ 0x41, 0x3b, 0x3c, 0x86, 0xb7, 0x55, 0x4d, 0xe6, 0xcb, 0x9d, 0x0e, 0x6f, 0x18, 0x10, 0x63, 0x3c,
+ 0xcd, 0x02, 0x32, 0x9f, 0xbe, 0x58, 0x22, 0xa1, 0x24, 0x61, 0xf3, 0x1e, 0xa8, 0xbd, 0xf7, 0x0e,
+ 0x9a, 0xeb, 0x42, 0x5c, 0xf5, 0xdb, 0x3b, 0x65, 0x22, 0xb1, 0x54, 0x7f, 0xe0, 0x62, 0xae, 0xb3,
+ 0xab, 0x7b, 0xfe, 0x4b, 0x80, 0x7a, 0xd1, 0x5e, 0xd2, 0x0a, 0xa3, 0x4d, 0x1a, 0xf5, 0xa8, 0xbf,
+ 0x87, 0xfc, 0x91, 0x57, 0xf1, 0xc2, 0x58, 0xea, 0x7a, 0xbc, 0xdf, 0x86, 0xb4, 0x24, 0x32, 0x10,
+ 0x72, 0x2e, 0x91, 0x15, 0xa7, 0x39, 0xb5, 0x22, 0x7a, 0xe1, 0x88, 0xbd, 0x23, 0xa6, 0x05, 0xe2,
+ 0x20, 0x22, 0x46, 0x68, 0x56, 0x34, 0x2e, 0x08, 0x35, 0xa7, 0x4b, 0x4f, 0x54, 0xcb, 0xf9, 0x53,
+ 0xd1, 0x41, 0xf6, 0xac, 0x23, 0xf8, 0x0e, 0x90, 0x1e, 0xea, 0x4c, 0xdb, 0xa3, 0xb6, 0xdb, 0x5f,
+ 0xf9, 0xc4, 0xf3, 0x08, 0x12, 0x32, 0xa8, 0xa2, 0xa1, 0x8c, 0x1d, 0x5f, 0xf7, 0x18, 0x79, 0x4c,
+ 0xd4, 0x28, 0xc6, 0xe9, 0x55, 0xbc, 0x80, 0xc2, 0x08, 0x1f, 0x8f, 0x8d, 0x35, 0x0b, 0xa9, 0x49,
+ 0x80, 0xba, 0x32, 0xba, 0xe0, 0xf6, 0x2f, 0x7f, 0xf2, 0xb7, 0xaf, 0xfa, 0xfd, 0xc8, 0x7a, 0x66,
+ };
+
+ CollectionAssert.AreEqual(expectedEncodedSignatureBytes, algorithm.Sign(data));
+
+ algorithm = new CertificateHostAlgorithm(
+ "ssh-rsa-cert-v01@openssh.com",
+ certificate,
+ DefaultKeyAlgs);
+
+ Assert.IsTrue(algorithm.VerifySignature(data, expectedEncodedSignatureBytes));
+ }
+
+ [TestMethod]
+ public void RsaSha256_SignAndVerify()
+ {
+ byte[] data = Encoding.UTF8.GetBytes("hello world");
+
+ (RsaKey key, Certificate certificate) = GetRsaKey();
+
+ CertificateHostAlgorithm algorithm = new(
+ "rsa-sha2-256-cert-v01@openssh.com",
+ key,
+ certificate,
+ new RsaDigitalSignature(key, HashAlgorithmName.SHA256));
+
+ byte[] expectedEncodedSignatureBytes = new byte[]
+ {
+ 0, 0, 0, 12, // byte count of "rsa-sha2-256"
+ (byte)'r', (byte)'s', (byte)'a', (byte)'-', (byte)'s', (byte)'h', (byte)'a', (byte)'2',
+ (byte)'-', (byte)'2', (byte)'5', (byte)'6',
+ 0, 0, 1, 0, // byte count of signature (=256)
+
+ // ssh-keygen -e -f Key.OPENSSH.RSA.txt -m PEM -p
+ // echo -n 'hello world' | openssl dgst -sha256 -sign Key.OPENSSH.RSA.txt -out test.signed
+ 0x18, 0xf4, 0x3e, 0xa9, 0xdf, 0x89, 0x92, 0x6b, 0xc1, 0x6a, 0x35, 0x72, 0x42, 0x56, 0xf7, 0x50,
+ 0x32, 0x33, 0xff, 0xc4, 0x91, 0x3d, 0x49, 0x12, 0x37, 0x52, 0x98, 0x37, 0xb8, 0xeb, 0xeb, 0xaa,
+ 0xe5, 0x4e, 0xd4, 0x99, 0x74, 0xfd, 0xea, 0xd6, 0x8f, 0x34, 0xa0, 0x3a, 0x0e, 0xfd, 0xcb, 0xae,
+ 0x04, 0x20, 0x01, 0x1c, 0x67, 0x98, 0x94, 0x6c, 0xdb, 0x26, 0x9a, 0x0c, 0x5b, 0xcf, 0x9a, 0x06,
+ 0xa5, 0x90, 0xfb, 0x62, 0xe8, 0x56, 0x91, 0xdf, 0x63, 0x1f, 0xc3, 0xb1, 0xd3, 0x4f, 0x18, 0x2b,
+ 0x2e, 0xfa, 0xb4, 0x61, 0x1d, 0x54, 0xdd, 0x63, 0x14, 0x17, 0x31, 0x8e, 0x86, 0xe3, 0xc2, 0xb1,
+ 0x30, 0x42, 0x1e, 0x5a, 0x43, 0x87, 0x54, 0x64, 0xd5, 0xbb, 0xcb, 0x37, 0x7b, 0xa6, 0x97, 0x75,
+ 0xca, 0x3b, 0x0d, 0xb2, 0x24, 0x34, 0x0b, 0xfc, 0xde, 0x67, 0xbf, 0xdf, 0x2a, 0x8b, 0xc6, 0xac,
+ 0x51, 0x0d, 0x98, 0x54, 0xed, 0x57, 0x5e, 0xa9, 0xbe, 0x0f, 0x0c, 0x0f, 0x30, 0x23, 0x96, 0x83,
+ 0x65, 0x74, 0x87, 0x91, 0x99, 0x21, 0x88, 0x80, 0x6d, 0xe4, 0xec, 0xcb, 0x51, 0xe5, 0xe5, 0x3a,
+ 0x2b, 0x34, 0x9b, 0x10, 0x70, 0xef, 0x57, 0x40, 0x59, 0x45, 0x94, 0x58, 0xd0, 0x65, 0x84, 0x23,
+ 0x5e, 0xcd, 0x49, 0xea, 0x18, 0x51, 0x29, 0xdd, 0x84, 0x05, 0x24, 0xe4, 0x65, 0x0c, 0x38, 0x8e,
+ 0x42, 0x33, 0xdf, 0xcb, 0x3c, 0xa0, 0x0d, 0xe2, 0x2d, 0x13, 0xbd, 0xea, 0x51, 0x06, 0xdd, 0x61,
+ 0x87, 0x05, 0xbe, 0xef, 0xaa, 0x77, 0xe4, 0xef, 0x25, 0x6b, 0xbf, 0x24, 0xd7, 0xe4, 0xba, 0x25,
+ 0x28, 0x49, 0x26, 0xc2, 0x31, 0xca, 0xbb, 0x1a, 0x2c, 0x19, 0xa3, 0x7b, 0x62, 0x12, 0x59, 0x75,
+ 0x12, 0x03, 0x38, 0xc9, 0x69, 0x93, 0xe6, 0xec, 0xc8, 0x13, 0x25, 0x48, 0xd2, 0x6c, 0x67, 0x10,
+ };
+
+ CollectionAssert.AreEqual(expectedEncodedSignatureBytes, algorithm.Sign(data));
+
+ algorithm = new CertificateHostAlgorithm(
+ "rsa-sha2-256-cert-v01@openssh.com",
+ certificate,
+ new RsaDigitalSignature((RsaKey)certificate.Key, HashAlgorithmName.SHA256),
+ DefaultKeyAlgs);
+
+ Assert.IsTrue(algorithm.VerifySignature(data, expectedEncodedSignatureBytes));
+ }
+
+ [TestMethod]
+ public void RsaSha512_SignAndVerify()
+ {
+ byte[] data = Encoding.UTF8.GetBytes("hello world");
+
+ (RsaKey key, Certificate certificate) = GetRsaKey();
+
+ CertificateHostAlgorithm algorithm = new(
+ "rsa-sha2-512-cert-v01@openssh.com",
+ key,
+ certificate,
+ new RsaDigitalSignature(key, HashAlgorithmName.SHA512));
+
+ byte[] expectedEncodedSignatureBytes = new byte[]
+ {
+ 0, 0, 0, 12, // byte count of "rsa-sha2-512"
+ (byte)'r', (byte)'s', (byte)'a', (byte)'-', (byte)'s', (byte)'h', (byte)'a', (byte)'2',
+ (byte)'-', (byte)'5', (byte)'1', (byte)'2',
+ 0, 0, 1, 0, // byte count of signature (=256)
+
+ // ssh-keygen -e -f Key.OPENSSH.RSA.txt -m PEM -p
+ // echo -n 'hello world' | openssl dgst -sha512 -sign Key.OPENSSH.RSA.txt -out test.signed
+ 0x1d, 0x64, 0xc6, 0x82, 0xb0, 0xc4, 0x2b, 0xe1, 0x71, 0x13, 0x1f, 0x62, 0xab, 0x8f, 0xf8, 0x72,
+ 0x43, 0xe8, 0x95, 0x4c, 0x8d, 0xa6, 0xf7, 0xcd, 0x62, 0xc9, 0x6f, 0xe5, 0xbf, 0x23, 0x1b, 0xc7,
+ 0xa0, 0x93, 0xc6, 0xc0, 0xa2, 0x06, 0x2d, 0x07, 0x16, 0x59, 0xbc, 0x0d, 0xe5, 0x00, 0x39, 0x56,
+ 0xa7, 0xde, 0x4b, 0x17, 0xf4, 0x02, 0xf6, 0x5d, 0x8f, 0xc5, 0x76, 0xe2, 0xb7, 0xae, 0xe5, 0xa2,
+ 0x7f, 0xd8, 0x34, 0x04, 0x2c, 0xbc, 0xdf, 0x84, 0x51, 0x69, 0x83, 0xda, 0x7a, 0x74, 0x19, 0xe9,
+ 0x6e, 0x02, 0xf8, 0x51, 0x20, 0xa2, 0x67, 0x43, 0xbb, 0xde, 0x7a, 0xa7, 0x12, 0xe7, 0x89, 0x7c,
+ 0x50, 0xf3, 0xd5, 0x07, 0xc9, 0x70, 0x22, 0xed, 0x2e, 0x45, 0x1e, 0x49, 0x23, 0x94, 0x69, 0xae,
+ 0x8f, 0x5d, 0x3b, 0x34, 0xdb, 0xc8, 0x49, 0x26, 0x09, 0x81, 0x7d, 0xad, 0x77, 0xb5, 0x6d, 0xad,
+ 0x0c, 0x9f, 0x66, 0x29, 0x56, 0xff, 0xea, 0xa7, 0x6f, 0x7f, 0xcd, 0xc0, 0x15, 0x05, 0xdc, 0xee,
+ 0xfb, 0xac, 0xfd, 0x59, 0x19, 0x30, 0x32, 0x6e, 0x16, 0xe0, 0x4e, 0x74, 0x6a, 0x13, 0xa7, 0x9f,
+ 0x5b, 0x71, 0x75, 0x13, 0xcf, 0xa5, 0xf3, 0x07, 0x8f, 0xfb, 0xa2, 0xa2, 0x92, 0xc2, 0x41, 0xc4,
+ 0xbc, 0x14, 0x75, 0x22, 0xe3, 0x4b, 0xb7, 0xc0, 0x54, 0xc3, 0x25, 0x87, 0xbb, 0x52, 0xde, 0x70,
+ 0x69, 0xc6, 0x68, 0x66, 0x3a, 0x88, 0xf6, 0x3b, 0x8e, 0x44, 0x00, 0x25, 0x17, 0xc9, 0x44, 0x7c,
+ 0xcc, 0x0c, 0x63, 0xab, 0xa3, 0x2c, 0xaa, 0x4c, 0x34, 0xda, 0xe0, 0x96, 0x71, 0x83, 0xe5, 0x7a,
+ 0xec, 0x56, 0xbe, 0x85, 0x27, 0x7c, 0xe7, 0x79, 0xfd, 0xb8, 0x77, 0x41, 0x05, 0x25, 0x30, 0x57,
+ 0x24, 0x45, 0xa9, 0x12, 0x9e, 0xdc, 0x9e, 0x23, 0x43, 0x13, 0x67, 0x38, 0x59, 0xae, 0x4b, 0x76,
+ };
+
+ CollectionAssert.AreEqual(expectedEncodedSignatureBytes, algorithm.Sign(data));
+
+ algorithm = new CertificateHostAlgorithm(
+ "rsa-sha2-512-cert-v01@openssh.com",
+ certificate,
+ new RsaDigitalSignature((RsaKey)certificate.Key, HashAlgorithmName.SHA512),
+ DefaultKeyAlgs);
+
+ Assert.IsTrue(algorithm.VerifySignature(data, expectedEncodedSignatureBytes));
+ }
+
+ [TestMethod]
+ public void VerifySignature_NoCorrespondingAlgorithm_ReturnsFalse()
+ {
+ byte[] data = Encoding.UTF8.GetBytes("hello world");
+
+ (RsaKey key, Certificate certificate) = GetRsaKey();
+
+ CertificateHostAlgorithm algorithm = new(
+ "rsa-sha2-512-cert-v01@openssh.com",
+ key,
+ certificate,
+ new RsaDigitalSignature(key, HashAlgorithmName.SHA512));
+
+ byte[] signature = algorithm.Sign(data);
+
+ algorithm = new CertificateHostAlgorithm(
+ "rsa-sha2-512-cert-v01@openssh.com",
+ certificate,
+ new RsaDigitalSignature((RsaKey)certificate.Key, HashAlgorithmName.SHA512),
+ new Dictionary>());
+
+ Assert.IsFalse(algorithm.VerifySignature(data, signature));
+ }
+
+ [TestMethod]
+ public void VerifySignature_NoSuppliedAlgorithms_Throws()
+ {
+ byte[] data = Encoding.UTF8.GetBytes("hello world");
+
+ (RsaKey key, Certificate certificate) = GetRsaKey();
+
+ CertificateHostAlgorithm algorithm = new(
+ "rsa-sha2-512-cert-v01@openssh.com",
+ key,
+ certificate,
+ new RsaDigitalSignature(key, HashAlgorithmName.SHA512));
+
+ byte[] signature = algorithm.Sign(data);
+
+ var ex = Assert.ThrowsException(() => algorithm.VerifySignature(data, signature));
+ Assert.IsTrue(ex.Message.StartsWith("Invalid usage", StringComparison.Ordinal));
+ }
+
+ [TestMethod]
+ public void CertificateBadCASignature_VerifySignatureReturnsFalse()
+ {
+ // ssh-keygen -s Key.OPENSSH.ED25519.txt -I test Key.OPENSSH.ECDSA.txt
+ string goodCertString = "ecdsa-sha2-nistp256-cert-v01@openssh.com " +
+ "AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb20AA" +
+ "AAg1vQFCYTYufJiCBFJBWc63sOGwnJ3BHQn4ig499dtB0AAAAAIbmlzdHAyNT" +
+ "YAAABBBI/dlNvfssW9KYrB67TcDmz9zBzDf7eMvUupAroP3b3FjUnYnpL3Utc" +
+ "4GkF/PiX7w2DuxaG70/+EX/CYHZBHKCsAAAAAAAAAAAAAAAEAAAAEdGVzdAAA" +
+ "AAAAAAAAAAAAAP//////////AAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3Y" +
+ "XJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcG" +
+ "VybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAA" +
+ "OcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAg" +
+ "DQlmcNCvFBlw0At9lgbss8BbUxgQa9VbmeN7s6UwYyIAAABTAAAAC3NzaC1lZ" +
+ "DI1NTE5AAAAQA8+LXQ++nb1/gNEtURKt5Yo/geUc/+3+Bv3EPGno5JhxvekjJ" +
+ "PD7/nXcyxnY3zALlPQTxb19EVx5lz58BS96gg=";
+
+ char[] chars = goodCertString.ToCharArray();
+ chars[^10] = 'a';
+ string badCertString = new string(chars);
+
+ Assert.IsTrue(VerifySignature(goodCertString));
+ Assert.IsFalse(VerifySignature(badCertString));
+
+ static bool VerifySignature(string certString)
+ {
+ PrivateKeyFile pk;
+
+ using (Stream keyStream = GetData("Key.OPENSSH.ECDSA.txt"))
+ using (MemoryStream certStream = new MemoryStream(Encoding.UTF8.GetBytes(certString)))
+ {
+ pk = new PrivateKeyFile(keyStream, null, certStream);
+ }
+
+ Assert.IsNotNull(pk.Certificate);
+
+ byte[] data = Encoding.UTF8.GetBytes("hello world");
+
+ CertificateHostAlgorithm certificateAlgorithm = new(
+ "ecdsa-sha2-nistp256-cert-v01@openssh.com",
+ pk.Certificate,
+ DefaultKeyAlgs);
+
+ KeyHostAlgorithm keyHostAlgorithm = new KeyHostAlgorithm("ecdsa-sha2-nistp256", pk.Key);
+
+ byte[] signature = keyHostAlgorithm.Sign(data);
+
+ Assert.IsTrue(keyHostAlgorithm.VerifySignature(data, signature));
+
+ return certificateAlgorithm.VerifySignature(data, signature);
+ }
+ }
+
+ [TestMethod]
+ public void CertificateValidityPeriodExpired_VerifySignatureReturnsFalse()
+ {
+ // ssh-keygen -s Key.OPENSSH.ED25519.txt -I nolongervalid -V always:20240101 Key.OPENSSH.ECDSA.txt
+ string certString = "ecdsa-sha2-nistp256-cert-v01@openssh.com " +
+ "AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb" +
+ "20AAAAg5BUo6CqGzTDc0UgNcLUqna2bH3C69NZCzd9CrQ8apQUAAAAIbm" +
+ "lzdHAyNTYAAABBBI/dlNvfssW9KYrB67TcDmz9zBzDf7eMvUupAroP3b3" +
+ "FjUnYnpL3Utc4GkF/PiX7w2DuxaG70/+EX/CYHZBHKCsAAAAAAAAAAAAA" +
+ "AAEAAAANbm9sb25nZXJ2YWxpZAAAAAAAAAAAAAAAAAAAAABlkgCAAAAAA" +
+ "AAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaX" +
+ "QtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2F" +
+ "yZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXIt" +
+ "cmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAgDQlmcNCvFBlw0" +
+ "At9lgbss8BbUxgQa9VbmeN7s6UwYyIAAABTAAAAC3NzaC1lZDI1NTE5AA" +
+ "AAQMonLi0J282GmuMVyHGKS/PRoLpdj5GgmR0wrIkExRRCzKZaycLfPDL" +
+ "+CGMa2jsH2QhFhTCG5AtKWVQbkqdHVAY= (null)";
+
+ PrivateKeyFile pk;
+
+ using (Stream keyStream = GetData("Key.OPENSSH.ECDSA.txt"))
+ using (MemoryStream certStream = new MemoryStream(Encoding.UTF8.GetBytes(certString)))
+ {
+ pk = new PrivateKeyFile(keyStream, null, certStream);
+ }
+
+ Assert.IsNotNull(pk.Certificate);
+ Assert.AreEqual(0uL, pk.Certificate.ValidAfterUnixSeconds);
+ Assert.AreEqual(new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero), pk.Certificate.ValidBefore);
+
+ byte[] data = Encoding.UTF8.GetBytes("hello world");
+
+ CertificateHostAlgorithm certificateAlgorithm = new(
+ "ecdsa-sha2-nistp256-cert-v01@openssh.com",
+ pk.Certificate,
+ DefaultKeyAlgs);
+
+ KeyHostAlgorithm keyHostAlgorithm = new KeyHostAlgorithm("ecdsa-sha2-nistp256", pk.Key);
+
+ byte[] signature = keyHostAlgorithm.Sign(data);
+
+ Assert.IsTrue(keyHostAlgorithm.VerifySignature(data, signature));
+ Assert.IsFalse(certificateAlgorithm.VerifySignature(data, signature));
+ }
+
+ private static (RsaKey Key, Certificate Certificate) GetRsaKey()
+ {
+ using (Stream keyStream = GetData("Key.OPENSSH.RSA.txt"))
+ using (Stream certStream = GetData("Key.OPENSSH.RSA-cert.pub"))
+ {
+ var pkFile = new PrivateKeyFile(keyStream, null, certStream);
+ return (Key: (RsaKey)pkFile.Key, pkFile.Certificate);
+ }
+ }
+
+ private static IReadOnlyDictionary> DefaultKeyAlgs
+ {
+ get
+ {
+ return new Dictionary>(
+ new PasswordConnectionInfo("x", "y", "z").HostKeyAlgorithms);
+ }
+ }
+ }
+}
diff --git a/test/Renci.SshNet.Tests/Classes/Security/KeyAlgorithmTest.cs b/test/Renci.SshNet.Tests/Classes/Security/KeyHostAlgorithmTest.cs
similarity index 100%
rename from test/Renci.SshNet.Tests/Classes/Security/KeyAlgorithmTest.cs
rename to test/Renci.SshNet.Tests/Classes/Security/KeyHostAlgorithmTest.cs
diff --git a/version.json b/version.json
index f933e7fd9..1f394f172 100644
--- a/version.json
+++ b/version.json
@@ -1,6 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
- "version": "2024.1.1-prerelease.{height}",
+ "version": "2024.2.0-prerelease.{height}",
"publicReleaseRefSpec": [
"^refs/heads/develop$",
"^refs/tags/\\d{4}\\.\\d+\\.\\d+"