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