Prechádzať zdrojové kódy

Add support for OpenSSH certificates (#1498)

Co-authored-by: cedricMicrovision <cedric.legoff@microvision.fr>
Rob Hague 1 rok pred
rodič
commit
3e12c96317
41 zmenil súbory, kde vykonal 1627 pridanie a 70 odobranie
  1. 28 1
      docfx/examples.md
  2. 14 2
      src/Renci.SshNet/Common/HostKeyEventArgs.cs
  3. 18 11
      src/Renci.SshNet/ConnectionInfo.cs
  4. 116 4
      src/Renci.SshNet/PrivateKeyFile.cs
  5. 432 0
      src/Renci.SshNet/Security/Certificate.cs
  6. 120 18
      src/Renci.SshNet/Security/CertificateHostAlgorithm.cs
  7. 26 8
      src/Renci.SshNet/Security/KeyExchange.cs
  8. 35 7
      src/Renci.SshNet/Security/KeyHostAlgorithm.cs
  9. 1 0
      test/Data/Key.OPENSSH.ECDSA-cert.pub
  10. 1 0
      test/Data/Key.OPENSSH.ECDSA.Encrypted.Aes.128.CTR-cert.pub
  11. 0 0
      test/Data/Key.OPENSSH.ECDSA384-cert.pub
  12. 0 0
      test/Data/Key.OPENSSH.ECDSA384.Encrypted.Aes.256.GCM-cert.pub
  13. 1 0
      test/Data/Key.OPENSSH.ECDSA521-cert.pub
  14. 1 0
      test/Data/Key.OPENSSH.ECDSA521.Encrypted.Aes.192.CBC-cert.pub
  15. 1 0
      test/Data/Key.OPENSSH.ED25519-cert.pub
  16. 1 0
      test/Data/Key.OPENSSH.ED25519.Encrypted.ChaCha20.Poly1305-cert.pub
  17. 0 0
      test/Data/Key.OPENSSH.RSA-cert.pub
  18. 0 0
      test/Data/Key.OPENSSH.RSA.Encrypted.Aes.192.CTR-cert.pub
  19. 1 0
      test/Renci.SshNet.IntegrationTests/Common/RemoteSshdConfigExtensions.cs
  20. 1 0
      test/Renci.SshNet.IntegrationTests/Dockerfile.TestServer
  21. 24 0
      test/Renci.SshNet.IntegrationTests/HostCertificateFile.cs
  22. 55 1
      test/Renci.SshNet.IntegrationTests/HostKeyAlgorithmTests.cs
  23. 79 5
      test/Renci.SshNet.IntegrationTests/PrivateKeyAuthenticationTests.cs
  24. 6 0
      test/Renci.SshNet.IntegrationTests/RemoteSshdConfig.cs
  25. 9 12
      test/Renci.SshNet.IntegrationTests/TestsFixtures/InfrastructureFixture.cs
  26. 12 0
      test/Renci.SshNet.IntegrationTests/server/ca/host_ca_ecdsa_key
  27. 7 0
      test/Renci.SshNet.IntegrationTests/server/ca/host_ca_ed25519_key
  28. 49 0
      test/Renci.SshNet.IntegrationTests/server/ca/host_ca_rsa_key
  29. 9 0
      test/Renci.SshNet.IntegrationTests/server/ca/user_ca_ecdsa_key
  30. 49 0
      test/Renci.SshNet.IntegrationTests/server/ca/user_ca_rsa_key
  31. 1 0
      test/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_ecdsa256_key-cert_rsa.pub
  32. 1 0
      test/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_ecdsa384_key-cert_ecdsa.pub
  33. 1 0
      test/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_ecdsa521_key-cert_ed25519.pub
  34. 1 0
      test/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_ed25519_key-cert_ecdsa.pub
  35. 0 0
      test/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_rsa_key-cert_rsa.pub
  36. 2 0
      test/Renci.SshNet.IntegrationTests/server/ssh/user-ca.pub
  37. 23 0
      test/Renci.SshNet.TestTools.OpenSSH/SshdConfig.cs
  38. 112 0
      test/Renci.SshNet.Tests/Classes/PrivateKeyFileTest.cs
  39. 389 0
      test/Renci.SshNet.Tests/Classes/Security/CertificateHostAlgorithmTest.cs
  40. 0 0
      test/Renci.SshNet.Tests/Classes/Security/KeyHostAlgorithmTest.cs
  41. 1 1
      version.json

+ 28 - 1
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

+ 14 - 2
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 @@ namespace Renci.SshNet.Common
         /// </value>
         public int KeyLength { get; private set; }
 
+        /// <summary>
+        /// Gets the certificate presented by the host, or <see langword="null"/> if the host
+        /// did not present a certificate.
+        /// </summary>
+        public Certificate? Certificate { get; }
+
         /// <summary>
         /// Initializes a new instance of the <see cref="HostKeyEventArgs"/> class.
         /// </summary>
@@ -93,7 +100,7 @@ namespace Renci.SshNet.Common
             ThrowHelper.ThrowIfNull(host);
 
             CanTrust = true;
-            HostKey = host.Data;
+            HostKey = host.KeyData.GetBytes();
             HostKeyName = host.Name;
             KeyLength = host.Key.KeyLength;
 
@@ -107,6 +114,11 @@ namespace Renci.SshNet.Common
                     return BitConverter.ToString(FingerPrint).Replace('-', ':').ToLowerInvariant();
 #pragma warning restore CA1308 // Normalize strings to uppercase
                 });
+
+            if (host is CertificateHostAlgorithm certificateAlg)
+            {
+                Certificate = certificateAlg.Certificate;
+            }
         }
     }
 }

+ 18 - 11
src/Renci.SshNet/ConnectionInfo.cs

@@ -387,19 +387,26 @@ namespace Renci.SshNet
                     { "hmac-sha1-etm@openssh.com", new HashInfo(20*8, key => new HMACSHA1(key), isEncryptThenMAC: true) },
                 };
 
-            HostKeyAlgorithms = new Dictionary<string, Func<byte[], KeyHostAlgorithm>>
-                {
-                    { "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<string, Func<byte[], KeyHostAlgorithm>>();
+            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<string, Func<Compressor>>
                 {

+ 116 - 4
src/Renci.SshNet/PrivateKeyFile.cs

@@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis;
 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 (?<keyName>\w+( \w+)*) *-+\r?\n((Proc-Type: 4,ENCRYPTED\r?\nDEK-Info: (?<cipherName>[A-Z0-9-]+),(?<salt>[A-F0-9]+)\r?\n\r?\n)|(Comment: ""?[^\r\n]*""?\r?\n))?(?<data>([a-zA-Z0-9/+=]{1,80}\r?\n)+)(\r?\n)?-+ *END \k<keyName> *-+";
+        private const string CertificatePattern = @"(?<type>[-\w]+@openssh\.com)\s(?<data>[a-zA-Z0-9\/+=]*)(\s+(?<comment>.*))?";
 
 #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<HostAlgorithm> _hostAlgorithms = new List<HostAlgorithm>();
@@ -156,6 +162,13 @@ namespace Renci.SshNet
             }
         }
 
+        /// <summary>
+        /// Gets the public key certificate associated with this key,
+        /// or <see langword="null"/> if no certificate data
+        /// has been passed to the constructor.
+        /// </summary>
+        public Certificate? Certificate { get; private set; }
+
         /// <summary>
         /// Initializes a new instance of the <see cref="PrivateKeyFile"/> class.
         /// </summary>
@@ -173,7 +186,7 @@ namespace Renci.SshNet
         /// </summary>
         /// <param name="privateKey">The private key.</param>
         public PrivateKeyFile(Stream privateKey)
-            : this(privateKey, passPhrase: null)
+            : this(privateKey, passPhrase: null, certificate: null)
         {
         }
 
@@ -186,7 +199,7 @@ namespace Renci.SshNet
         /// This method calls <see cref="File.Open(string, FileMode)"/> internally, this method does not catch exceptions from <see cref="File.Open(string, FileMode)"/>.
         /// </remarks>
         public PrivateKeyFile(string fileName)
-            : this(fileName, passPhrase: null)
+            : this(fileName, passPhrase: null, certificateFileName: null)
         {
         }
 
@@ -200,6 +213,18 @@ namespace Renci.SshNet
         /// This method calls <see cref="File.Open(string, FileMode)"/> internally, this method does not catch exceptions from <see cref="File.Open(string, FileMode)"/>.
         /// </remarks>
         public PrivateKeyFile(string fileName, string? passPhrase)
+            : this(fileName, passPhrase, certificateFileName: null)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PrivateKeyFile"/> class.
+        /// </summary>
+        /// <param name="fileName">The path of the private key file.</param>
+        /// <param name="passPhrase">The pass phrase for the private key.</param>
+        /// <param name="certificateFileName">The path of a certificate file which certifies the private key.</param>
+        /// <exception cref="ArgumentNullException"><paramref name="fileName"/> is <see langword="null"/>.</exception>
+        public PrivateKeyFile(string fileName, string? passPhrase, string? certificateFileName)
         {
             ThrowHelper.ThrowIfNull(fileName);
 
@@ -208,6 +233,16 @@ namespace Renci.SshNet
                 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 @@ namespace Renci.SshNet
         /// <param name="passPhrase">The pass phrase.</param>
         /// <exception cref="ArgumentNullException"><paramref name="privateKey"/> is <see langword="null"/>.</exception>
         public PrivateKeyFile(Stream privateKey, string? passPhrase)
+            : this(privateKey, passPhrase, certificate: null)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PrivateKeyFile"/> class.
+        /// </summary>
+        /// <param name="privateKey">The private key.</param>
+        /// <param name="passPhrase">The pass phrase for the private key.</param>
+        /// <param name="certificate">A certificate which certifies the private key.</param>
+        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 @@ namespace Renci.SshNet
             throw new SshException(string.Format(CultureInfo.InvariantCulture, "Private key algorithm \"{0}\" is not supported.", algorithmOid));
         }
 
+        /// <summary>
+        /// Opens the specified certificate.
+        /// </summary>
+        /// <param name="certificate">The certificate.</param>
+        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));
+            }
+        }
+
         /// <summary>
         /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
         /// </summary>

+ 432 - 0
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
+{
+    /// <summary>
+    /// Represents an OpenSSH certificate as described in
+    /// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.certkeys.
+    /// </summary>
+    // 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
+    {
+        /// <summary>
+        /// The type identifier of the certificate.
+        /// </summary>
+        /// <remarks>
+        /// The value is one of the following:
+        /// <list type="bullet">
+        ///     <item>ssh-rsa-cert-v01@openssh.com</item>
+        ///     <item>ssh-dss-cert-v01@openssh.com</item>
+        ///     <item>ecdsa-sha2-nistp256-cert-v01@openssh.com</item>
+        ///     <item>ecdsa-sha2-nistp384-cert-v01@openssh.com</item>
+        ///     <item>ecdsa-sha2-nistp521-cert-v01@openssh.com</item>
+        ///     <item>ssh-ed25519-cert-v01@openssh.com</item>
+        /// </list>
+        /// </remarks>
+        public string Name
+        {
+            get
+            {
+                return _data.Name;
+            }
+        }
+
+        /// <summary>
+        /// 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.
+        /// </summary>
+        public byte[] Nonce
+        {
+            get
+            {
+                return _data.Nonce;
+            }
+        }
+
+        /// <summary>
+        /// The public key that has been certified by the certificate authority.
+        /// </summary>
+        public Key Key
+        {
+            get
+            {
+                return _data.Key;
+            }
+        }
+
+        internal SshKeyData KeyData
+        {
+            get
+            {
+                return _data.KeyData;
+            }
+        }
+
+        /// <summary>
+        /// 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.
+        /// </summary>
+        public ulong Serial
+        {
+            get
+            {
+                return _data.Serial;
+            }
+        }
+
+        /// <summary>
+        /// Specifies whether this certificate is for identification of a user
+        /// or a host.
+        /// </summary>
+        public CertificateType Type
+        {
+            get
+            {
+                return (CertificateType)_data.Type;
+            }
+        }
+
+        /// <summary>
+        /// 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.
+        /// </summary>
+        public string KeyId
+        {
+            get
+            {
+                return _data.KeyId;
+            }
+        }
+
+        /// <summary>
+        /// 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.
+        /// </summary>
+        public IList<string> ValidPrincipals
+        {
+            get
+            {
+                return _data.ValidPrincipals;
+            }
+        }
+
+        /// <summary>
+        /// The beginning of the validity period of the certificate, as the number
+        /// of seconds elapsed since 1970-01-01T00:00:00Z.
+        /// </summary>
+        /// <seealso cref="ValidAfter"/>
+        public ulong ValidAfterUnixSeconds
+        {
+            get
+            {
+                return _data.ValidAfter;
+            }
+        }
+
+        /// <summary>
+        /// The beginning of the validity period of the certificate.
+        /// </summary>
+        public DateTimeOffset ValidAfter
+        {
+            get
+            {
+                return DateTimeOffset.FromUnixTimeSeconds((long)_data.ValidAfter);
+            }
+        }
+
+        /// <summary>
+        /// The end of the validity period of the certificate, as the number
+        /// of seconds elapsed since 1970-01-01T00:00:00Z.
+        /// </summary>
+        public ulong ValidBeforeUnixSeconds
+        {
+            get
+            {
+                return _data.ValidBefore;
+            }
+        }
+
+        /// <summary>
+        /// The end of the validity period of the certificate.
+        /// </summary>
+        public DateTimeOffset ValidBefore
+        {
+            get
+            {
+                return _data.ValidBefore == ulong.MaxValue
+                    ? DateTimeOffset.MaxValue
+                    : DateTimeOffset.FromUnixTimeSeconds((long)_data.ValidBefore);
+            }
+        }
+
+        /// <summary>
+        /// 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.
+        /// </summary>
+        public IDictionary<string, string> CriticalOptions
+        {
+            get
+            {
+                return _data.CriticalOptions;
+            }
+        }
+
+        /// <summary>
+        /// 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.
+        /// </summary>
+        public IDictionary<string, string> Extensions
+        {
+            get
+            {
+                return _data.Extensions;
+            }
+        }
+
+        /// <summary>
+        /// 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.
+        /// </summary>
+        public byte[] CertificateAuthorityKey
+        {
+            get
+            {
+                return _data.SignatureKey;
+            }
+        }
+
+        /// <summary>
+        /// 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 <c>SHA256:</c> prefix.
+        /// </summary>
+        /// <example><c>ohD8VZEXGWo6Ez8GSEJQ9WpafgLFsOfLOtGGQCQo6Og</c>.</example>
+        /// <value>
+        /// Base64 encoded SHA256 fingerprint with padding (equals sign) removed.
+        /// </value>
+        public string CertificateAuthorityKeyFingerPrint
+        {
+            get
+            {
+                return Convert.ToBase64String(CryptoAbstraction.HashSHA256(CertificateAuthorityKey)).TrimEnd('=');
+            }
+        }
+
+        /// <summary>
+        /// 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).
+        /// </summary>
+        public byte[] Signature
+        {
+            get
+            {
+                return _data.Signature;
+            }
+        }
+
+        /// <summary>
+        /// The encoded certificate bytes.
+        /// </summary>
+        internal byte[] Bytes { get; }
+
+        /// <summary>
+        /// The encoded bytes of the certificate which are used
+        /// to calculate <see cref="Signature"/>.
+        /// This consists of all of the fields before (i.e. except from)
+        /// <see cref="Signature"/>.
+        /// </summary>
+        internal byte[] BytesForSignature
+        {
+            get
+            {
+                return Bytes.Take((int)_data.ByteCountBeforeSignature);
+            }
+        }
+
+        private readonly CertificateData _data;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="Certificate"/>
+        /// class based on the data encoded in <paramref name="data"/>.
+        /// </summary>
+        /// <param name="data">The encoded public-key certificate data.</param>
+        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<string> ValidPrincipals { get; private set; }
+
+            public ulong ValidAfter { get; private set; }
+
+            public ulong ValidBefore { get; private set; }
+
+            public Dictionary<string, string> CriticalOptions { get; private set; }
+
+            public Dictionary<string, string> Extensions { get; private set; }
+
+            public byte[] SignatureKey { get; private set; }
+
+            /// <summary>
+            /// Returns the number of bytes in the encoded certificate data
+            /// up to and including <see cref="SignatureKey"/>.
+            /// Used for verifying <see cref="Signature"/> which is calculated
+            /// from those bytes.
+            /// </summary>
+            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<string, string> ReadExtensionPair(byte[] data)
+            {
+                var result = new Dictionary<string, string>();
+                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<string> ReadValidPrincipals(byte[] data)
+            {
+                var result = new List<string>();
+                using var reader = new SshDataStream(data);
+
+                while (!reader.IsEndOfData)
+                {
+                    result.Add(reader.ReadString());
+                }
+
+                return result;
+            }
+
+            protected override void SaveData()
+            {
+                throw new NotImplementedException();
+            }
+        }
+
+        /// <summary>
+        /// Used to specify whether a certificate is for identification of a user
+        /// or a host.
+        /// </summary>
+#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
+        {
+            /// <summary>
+            /// The certificate is for identification of a user (SSH_CERT_TYPE_USER).
+            /// </summary>
+            User = 1,
+
+            /// <summary>
+            /// The certificate is for identification of a host (SSH_CERT_TYPE_HOST).
+            /// </summary>
+            Host = 2
+        }
+    }
+}

+ 120 - 18
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
 {
     /// <summary>
     /// Implements certificate support for host algorithm.
     /// </summary>
-    public class CertificateHostAlgorithm : HostAlgorithm
+    public class CertificateHostAlgorithm : KeyHostAlgorithm
     {
         /// <summary>
-        /// Gets the host key data.
+        /// The <see cref="KeyHostAlgorithm"/> factories which may be used in order to verify
+        /// the signature within the certificate.
+        /// </summary>
+        private readonly IReadOnlyDictionary<string, Func<byte[], KeyHostAlgorithm>>? _keyAlgorithms;
+
+        /// <summary>
+        /// Gets certificate used in this host key algorithm.
+        /// </summary>
+        public Certificate Certificate { get; }
+
+        /// <inheritdoc/>
+        internal override SshKeyData KeyData
+        {
+            get
+            {
+                return Certificate.KeyData;
+            }
+        }
+
+        /// <summary>
+        /// Gets the encoded bytes of the certificate.
         /// </summary>
         public override byte[] Data
         {
-            get { throw new NotImplementedException(); }
+            get { return Certificate.Bytes; }
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="CertificateHostAlgorithm"/> class.
+        /// </summary>
+        /// <param name="name">The algorithm identifier.</param>
+        /// <param name="privateKey">The private key used for this host algorithm.</param>
+        /// <param name="certificate">The certificate which certifies <paramref name="privateKey"/>.</param>
+        public CertificateHostAlgorithm(string name, Key privateKey, Certificate certificate)
+            : base(name, privateKey)
+        {
+            Certificate = certificate;
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="CertificateHostAlgorithm"/> class.
+        /// </summary>
+        /// <param name="name">The algorithm identifier.</param>
+        /// <param name="privateKey">The private key used for this host algorithm.</param>
+        /// <param name="certificate">The certificate which certifies <paramref name="privateKey"/>.</param>
+        /// <param name="digitalSignature"><inheritdoc cref="KeyHostAlgorithm.DigitalSignature" path="/summary"/></param>
+        public CertificateHostAlgorithm(string name, Key privateKey, Certificate certificate, DigitalSignature digitalSignature)
+            : base(name, privateKey, digitalSignature)
+        {
+            Certificate = certificate;
         }
 
         /// <summary>
         /// Initializes a new instance of the <see cref="CertificateHostAlgorithm"/> class.
         /// </summary>
-        /// <param name="name">The host key name.</param>
-        public CertificateHostAlgorithm(string name)
-            : base(name)
+        /// <param name="name">The algorithm identifier.</param>
+        /// <param name="certificate">The certificate.</param>
+        /// <param name="keyAlgorithms"><inheritdoc cref="_keyAlgorithms" path="/summary"/></param>
+        public CertificateHostAlgorithm(string name, Certificate certificate, IReadOnlyDictionary<string, Func<byte[], KeyHostAlgorithm>> keyAlgorithms)
+            : base(name, certificate.Key)
         {
+            Certificate = certificate;
+            _keyAlgorithms = keyAlgorithms;
         }
 
         /// <summary>
-        /// Signs the specified data.
+        /// Initializes a new instance of the <see cref="CertificateHostAlgorithm"/> class.
         /// </summary>
-        /// <param name="data">The data.</param>
-        /// <returns>Signed data.</returns>
-        /// <exception cref="NotImplementedException">Always.</exception>
+        /// <param name="name">The algorithm identifier.</param>
+        /// <param name="certificate">The certificate.</param>
+        /// <param name="digitalSignature"><inheritdoc cref="KeyHostAlgorithm.DigitalSignature" path="/summary"/></param>
+        /// <param name="keyAlgorithms"><inheritdoc cref="_keyAlgorithms" path="/summary"/></param>
+        public CertificateHostAlgorithm(string name, Certificate certificate, DigitalSignature digitalSignature, IReadOnlyDictionary<string, Func<byte[], KeyHostAlgorithm>> keyAlgorithms)
+            : base(name, certificate.Key, digitalSignature)
+        {
+            Certificate = certificate;
+            _keyAlgorithms = keyAlgorithms;
+        }
+
+        /// <inheritdoc/>
         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();
         }
 
         /// <summary>
         /// Verifies the signature.
         /// </summary>
-        /// <param name="data">The data.</param>
-        /// <param name="signature">The signature.</param>
-        /// <returns><see langword="true"/> if signature was successfully verified; otherwise <see langword="false"/>.</returns>
-        /// <exception cref="NotImplementedException">Always.</exception>
-        public override bool VerifySignature(byte[] data, byte[] signature)
+        /// <param name="data">The data to verify the signature against.</param>
+        /// <param name="signatureBlob">The signature blob in format specific encoding.</param>
+        /// <returns>
+        /// <see langword="true"/> if <paramref name="signatureBlob"/> is the result of signing
+        /// <paramref name="data"/> with the corresponding private key to <see cref="Certificate"/>,
+        /// and <see cref="Certificate"/> is valid with respect to its validity period and to its
+        /// signature therein as signed by the certificate authority.
+        /// </returns>
+        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);
         }
     }
 }

+ 26 - 8
src/Renci.SshNet/Security/KeyExchange.cs

@@ -368,22 +368,40 @@ namespace Renci.SshNet.Security
         {
             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);
         }
 
         /// <summary>

+ 35 - 7
src/Renci.SshNet/Security/KeyHostAlgorithm.cs

@@ -20,6 +20,18 @@ namespace Renci.SshNet.Security
         /// </summary>
         public DigitalSignature DigitalSignature { get; private set; }
 
+        /// <summary>
+        /// Gets the encoded public key data.
+        /// </summary>
+        internal virtual SshKeyData KeyData
+        {
+            get
+            {
+                var keyFormatIdentifier = Key is RsaKey ? "ssh-rsa" : Name;
+                return new SshKeyData(keyFormatIdentifier, Key.Public);
+            }
+        }
+
         /// <summary>
         /// Gets the encoded public key data.
         /// </summary>
@@ -30,8 +42,7 @@ namespace Renci.SshNet.Security
         {
             get
             {
-                var keyFormatIdentifier = Key is RsaKey ? "ssh-rsa" : Name;
-                return new SshKeyData(keyFormatIdentifier, Key.Public).GetBytes();
+                return KeyData.GetBytes();
             }
         }
 
@@ -77,20 +88,37 @@ namespace Renci.SshNet.Security
         }
 
         /// <summary>
-        /// Verifies the signature.
+        /// Verifies the encoded signature.
         /// </summary>
         /// <param name="data">The data to verify the signature against.</param>
-        /// <param name="signature">The encoded signature data.</param>
+        /// <param name="signature">
+        /// The encoded signature data, as the signature format identifier followed by the signature blob.
+        /// </param>
         /// <returns>
-        /// <see langword="true"/> if <paramref name="signature"/> is the result of signing <paramref name="data"/>
-        /// with the corresponding private key to <see cref="Key"/>.
+        /// <see langword="true"/> if <paramref name="signature"/> is the result of signing and encoding
+        /// <paramref name="data"/> with the corresponding private key to <see cref="Key"/>.
         /// </returns>
+        /// <remarks>See <see href="https://datatracker.ietf.org/doc/html/rfc4253#section-6.6"/>.</remarks>
         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);
+        }
+
+        /// <summary>
+        /// Verifies the signature.
+        /// </summary>
+        /// <param name="data">The data to verify the signature against.</param>
+        /// <param name="signatureBlob">The signature blob in format specific encoding.</param>
+        /// <returns>
+        /// <see langword="true"/> if <paramref name="signatureBlob"/> is the result of signing <paramref name="data"/>
+        /// with the corresponding private key to <see cref="Key"/>.
+        /// </returns>
+        internal virtual bool VerifySignatureBlob(byte[] data, byte[] signatureBlob)
+        {
+            return DigitalSignature.Verify(data, signatureBlob);
         }
 
         internal sealed class SignatureKeyData : SshData

+ 1 - 0
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)

+ 1 - 0
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

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
test/Data/Key.OPENSSH.ECDSA384-cert.pub


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
test/Data/Key.OPENSSH.ECDSA384.Encrypted.Aes.256.GCM-cert.pub


+ 1 - 0
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)

+ 1 - 0
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

+ 1 - 0
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

+ 1 - 0
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)

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
test/Data/Key.OPENSSH.RSA-cert.pub


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
test/Data/Key.OPENSSH.RSA.Encrypted.Aes.192.CTR-cert.pub


+ 1 - 0
test/Renci.SshNet.IntegrationTests/Common/RemoteSshdConfigExtensions.cs

@@ -15,6 +15,7 @@ namespace Renci.SshNet.IntegrationTests.Common
                             .WithLogLevel(LogLevel.Debug3)
                             .ClearHostKeyFiles()
                             .AddHostKeyFile(HostKeyFile.Rsa.FilePath)
+                            .WithHostKeyCertificate(null)
                             .ClearSubsystems()
                             .AddSubsystem(new Subsystem("sftp", "/usr/lib/ssh/sftp-server"))
                             .ClearCiphers()

+ 1 - 0
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 && \

+ 24 - 0
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; }
+    }
+}

+ 55 - 1
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 @@ namespace Renci.SshNet.IntegrationTests
             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 @@ namespace Renci.SshNet.IntegrationTests
             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);
         }
     }
 }

+ 79 - 5
test/Renci.SshNet.IntegrationTests/PrivateKeyAuthenticationTests.cs

@@ -70,14 +70,74 @@ namespace Renci.SshNet.IntegrationTests
             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 @@ namespace Renci.SshNet.IntegrationTests
             }
         }
 
-        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);
         }
     }
 }

+ 6 - 0
test/Renci.SshNet.IntegrationTests/RemoteSshdConfig.cs

@@ -205,6 +205,12 @@ namespace Renci.SshNet.IntegrationTests
             return this;
         }
 
+        public RemoteSshdConfig WithHostKeyCertificate(string hostKeyCertificate)
+        {
+            _config.HostCertificate = hostKeyCertificate;
+            return this;
+        }
+
         public RemoteSshd Update()
         {
             using (var client = new ScpClient(_connectionInfoFactory.Create()))

+ 9 - 12
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 @@ namespace Renci.SshNet.IntegrationTests.TestsFixtures
 
         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 @@ namespace Renci.SshNet.IntegrationTests.TestsFixtures
                 .WithHostname("renci-ssh-tests-server")
                 .WithImage(_sshServerImage)
                 .WithPortBinding(22, true)
-                //.WithOutputConsumer(Consume.RedirectStdoutAndStderrToStream(_fsOut, _fsErr))
                 .Build();
 
             await _sshServer.StartAsync();
@@ -76,6 +67,15 @@ namespace Renci.SshNet.IntegrationTests.TestsFixtures
         {
             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 @@ namespace Renci.SshNet.IntegrationTests.TestsFixtures
             {
                 await _sshServerImage.DisposeAsync();
             }
-
-            await _fsOut.DisposeAsync();
-            await _fsErr.DisposeAsync();
         }
 
         public void Dispose()

+ 12 - 0
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-----

+ 7 - 0
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-----

+ 49 - 0
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-----

+ 9 - 0
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-----

+ 49 - 0
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-----

+ 1 - 0
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)

+ 1 - 0
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)

+ 1 - 0
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)

+ 1 - 0
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

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
test/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_rsa_key-cert_rsa.pub


+ 2 - 0
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=

+ 23 - 0
test/Renci.SshNet.TestTools.OpenSSH/SshdConfig.cs

@@ -53,6 +53,8 @@ namespace Renci.SshNet.TestTools.OpenSSH
         /// </value>
         public List<string> HostKeyFiles { get; }
 
+        public string? HostCertificate { get; set; }
+
         /// <summary>
         /// Gets or sets a value specifying whether challenge-response authentication is allowed.
         /// </summary>
@@ -118,6 +120,11 @@ namespace Renci.SshNet.TestTools.OpenSSH
         /// </summary>
         public List<MessageAuthenticationCodeAlgorithm> MessageAuthenticationCodeAlgorithms { get; private set; }
 
+        /// <summary>
+        /// Gets the filepaths of the trusted user CA (certificate authority) keys.
+        /// </summary>
+        public string? TrustedUserCAKeys { get; private set; }
+
         /// <summary>
         /// Gets a value indicating whether <c>sshd</c> should print <c>/etc/motd</c> when a user logs in interactively.
         /// </summary>
@@ -291,6 +298,11 @@ namespace Renci.SshNet.TestTools.OpenSSH
                 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 @@ namespace Renci.SshNet.TestTools.OpenSSH
                 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 @@ namespace Renci.SshNet.TestTools.OpenSSH
                 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":

+ 112 - 0
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 @@ namespace Renci.SshNet.Tests.Classes
             }
         }
 
+        [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<RsaKey>(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<string, string>
+            {
+                ["permit-X11-forwarding"] = "",
+                ["permit-agent-forwarding"] = "",
+                ["permit-port-forwarding"] = "",
+                ["permit-pty"] = "",
+                ["permit-user-rc"] = "",
+            }, new Dictionary<string, string>(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<ArgumentException>(() => 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<EcdsaKey>(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<string, string>
+            {
+                ["permit-X11-forwarding"] = "",
+                ["permit-agent-forwarding"] = "",
+                ["permit-port-forwarding"] = "",
+                ["permit-pty"] = "",
+                ["permit-user-rc"] = "",
+            }, new Dictionary<string, string>(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];

+ 389 - 0
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<CertificateHostAlgorithm> certAlgs = pkFile.HostKeyAlgorithms.OfType<CertificateHostAlgorithm>().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<string, Func<byte[], KeyHostAlgorithm>>());
+
+            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<InvalidOperationException>(() => 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<string, Func<byte[], KeyHostAlgorithm>> DefaultKeyAlgs
+        {
+            get
+            {
+                return new Dictionary<string, Func<byte[], KeyHostAlgorithm>>(
+                    new PasswordConnectionInfo("x", "y", "z").HostKeyAlgorithms);
+            }
+        }
+    }
+}

+ 0 - 0
test/Renci.SshNet.Tests/Classes/Security/KeyAlgorithmTest.cs → test/Renci.SshNet.Tests/Classes/Security/KeyHostAlgorithmTest.cs


+ 1 - 1
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+"

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov