Add support for OpenSSH certificates (#1498)

Co-authored-by: cedricMicrovision <cedric.legoff@microvision.fr>
This commit is contained in:
Rob Hague
2024-10-19 14:36:41 +02:00
committed by GitHub
parent fbedaabb9c
commit 3e12c96317
41 changed files with 1634 additions and 72 deletions
+28 -1
View File
@@ -59,12 +59,39 @@ using (var client = new SshClient("sftp.foo.com", "guest", "pwd"))
{
client.HostKeyReceived += (sender, e) =>
{
e.CanTrust = expectedFingerPrint.Equals(e.FingerPrintSHA256);
e.CanTrust = e.FingerPrintSHA256 == expectedFingerPrint;
};
client.Connect();
}
```
When expecting the server to present a certificate signed by a trusted certificate authority:
```cs
string expectedCAFingerPrint = "tF3DRTUXtYFZ5Yz0SBOrEbixHaCifHmNVK6FtptXZVM";
using (var client = new SshClient("sftp.foo.com", "guest", "pwd"))
{
client.HostKeyReceived += (sender, e) =>
{
e.CanTrust = e.Certificate?.CertificateAuthorityKeyFingerPrint == expectedCAFingerPrint;
};
client.Connect();
}
```
### Authenticating with a user certificate
When you have a certificate for your key which is signed by a certificate authority that the server trusts:
```cs
using (var privateKeyFile = new PrivateKeyFile("path/to/my/key", passPhrase: null, "path/to/my/certificate.pub"))
using (var client = new SshClient("sftp.foo.com", "guest", privateKeyFile))
{
client.Connect();
}
```
### Open a Shell
```cs
+14 -2
View File
@@ -1,4 +1,5 @@
using System;
#nullable enable
using System;
using Renci.SshNet.Abstractions;
using Renci.SshNet.Security;
@@ -83,6 +84,12 @@ namespace Renci.SshNet.Common
/// </value>
public int KeyLength { get; private set; }
/// <summary>
/// Gets the certificate presented by the host, or <see langword="null"/> if the host
/// did not present a certificate.
/// </summary>
public Certificate? Certificate { get; }
/// <summary>
/// Initializes a new instance of the <see cref="HostKeyEventArgs"/> class.
/// </summary>
@@ -93,7 +100,7 @@ namespace Renci.SshNet.Common
ThrowHelper.ThrowIfNull(host);
CanTrust = true;
HostKey = host.Data;
HostKey = host.KeyData.GetBytes();
HostKeyName = host.Name;
KeyLength = host.Key.KeyLength;
@@ -107,6 +114,11 @@ namespace Renci.SshNet.Common
return BitConverter.ToString(FingerPrint).Replace('-', ':').ToLowerInvariant();
#pragma warning restore CA1308 // Normalize strings to uppercase
});
if (host is CertificateHostAlgorithm certificateAlg)
{
Certificate = certificateAlg.Certificate;
}
}
}
}
+18 -11
View File
@@ -387,19 +387,26 @@ namespace Renci.SshNet
{ "hmac-sha1-etm@openssh.com", new HashInfo(20*8, key => new HMACSHA1(key), isEncryptThenMAC: true) },
};
HostKeyAlgorithms = new Dictionary<string, Func<byte[], KeyHostAlgorithm>>
{
{ "ssh-ed25519", data => new KeyHostAlgorithm("ssh-ed25519", new ED25519Key(new SshKeyData(data))) },
{ "ecdsa-sha2-nistp256", data => new KeyHostAlgorithm("ecdsa-sha2-nistp256", new EcdsaKey(new SshKeyData(data))) },
{ "ecdsa-sha2-nistp384", data => new KeyHostAlgorithm("ecdsa-sha2-nistp384", new EcdsaKey(new SshKeyData(data))) },
{ "ecdsa-sha2-nistp521", data => new KeyHostAlgorithm("ecdsa-sha2-nistp521", new EcdsaKey(new SshKeyData(data))) },
#pragma warning disable SA1107 // Code should not contain multiple statements on one line
{ "rsa-sha2-512", data => { var key = new RsaKey(new SshKeyData(data)); return new KeyHostAlgorithm("rsa-sha2-512", key, new RsaDigitalSignature(key, HashAlgorithmName.SHA512)); } },
{ "rsa-sha2-256", data => { var key = new RsaKey(new SshKeyData(data)); return new KeyHostAlgorithm("rsa-sha2-256", key, new RsaDigitalSignature(key, HashAlgorithmName.SHA256)); } },
var hostAlgs = new Dictionary<string, Func<byte[], KeyHostAlgorithm>>();
hostAlgs.Add("ssh-ed25519-cert-v01@openssh.com", data => { var cert = new Certificate(data); return new CertificateHostAlgorithm("ssh-ed25519-cert-v01@openssh.com", cert, hostAlgs); });
hostAlgs.Add("ecdsa-sha2-nistp256-cert-v01@openssh.com", data => { var cert = new Certificate(data); return new CertificateHostAlgorithm("ecdsa-sha2-nistp256-cert-v01@openssh.com", cert, hostAlgs); });
hostAlgs.Add("ecdsa-sha2-nistp384-cert-v01@openssh.com", data => { var cert = new Certificate(data); return new CertificateHostAlgorithm("ecdsa-sha2-nistp384-cert-v01@openssh.com", cert, hostAlgs); });
hostAlgs.Add("ecdsa-sha2-nistp521-cert-v01@openssh.com", data => { var cert = new Certificate(data); return new CertificateHostAlgorithm("ecdsa-sha2-nistp521-cert-v01@openssh.com", cert, hostAlgs); });
hostAlgs.Add("rsa-sha2-512-cert-v01@openssh.com", data => { var cert = new Certificate(data); return new CertificateHostAlgorithm("rsa-sha2-512-cert-v01@openssh.com", cert, new RsaDigitalSignature((RsaKey)cert.Key, HashAlgorithmName.SHA512), hostAlgs); });
hostAlgs.Add("rsa-sha2-256-cert-v01@openssh.com", data => { var cert = new Certificate(data); return new CertificateHostAlgorithm("rsa-sha2-256-cert-v01@openssh.com", cert, new RsaDigitalSignature((RsaKey)cert.Key, HashAlgorithmName.SHA256), hostAlgs); });
hostAlgs.Add("ssh-rsa-cert-v01@openssh.com", data => { var cert = new Certificate(data); return new CertificateHostAlgorithm("ssh-rsa-cert-v01@openssh.com", cert, hostAlgs); });
hostAlgs.Add("ssh-dss-cert-v01@openssh.com", data => { var cert = new Certificate(data); return new CertificateHostAlgorithm("ssh-dss-cert-v01@openssh.com", cert, hostAlgs); });
hostAlgs.Add("ssh-ed25519", data => new KeyHostAlgorithm("ssh-ed25519", new ED25519Key(new SshKeyData(data))));
hostAlgs.Add("ecdsa-sha2-nistp256", data => new KeyHostAlgorithm("ecdsa-sha2-nistp256", new EcdsaKey(new SshKeyData(data))));
hostAlgs.Add("ecdsa-sha2-nistp384", data => new KeyHostAlgorithm("ecdsa-sha2-nistp384", new EcdsaKey(new SshKeyData(data))));
hostAlgs.Add("ecdsa-sha2-nistp521", data => new KeyHostAlgorithm("ecdsa-sha2-nistp521", new EcdsaKey(new SshKeyData(data))));
hostAlgs.Add("rsa-sha2-512", data => { var key = new RsaKey(new SshKeyData(data)); return new KeyHostAlgorithm("rsa-sha2-512", key, new RsaDigitalSignature(key, HashAlgorithmName.SHA512)); });
hostAlgs.Add("rsa-sha2-256", data => { var key = new RsaKey(new SshKeyData(data)); return new KeyHostAlgorithm("rsa-sha2-256", key, new RsaDigitalSignature(key, HashAlgorithmName.SHA256)); });
hostAlgs.Add("ssh-rsa", data => new KeyHostAlgorithm("ssh-rsa", new RsaKey(new SshKeyData(data))));
hostAlgs.Add("ssh-dss", data => new KeyHostAlgorithm("ssh-dss", new DsaKey(new SshKeyData(data))));
#pragma warning restore SA1107 // Code should not contain multiple statements on one line
{ "ssh-rsa", data => new KeyHostAlgorithm("ssh-rsa", new RsaKey(new SshKeyData(data))) },
{ "ssh-dss", data => new KeyHostAlgorithm("ssh-dss", new DsaKey(new SshKeyData(data))) },
};
HostKeyAlgorithms = hostAlgs;
CompressionAlgorithms = new Dictionary<string, Func<Compressor>>
{
+116 -4
View File
@@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Formats.Asn1;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Security.Cryptography;
using System.Text;
@@ -119,15 +120,20 @@ namespace Renci.SshNet
public partial class PrivateKeyFile : IPrivateKeySource, IDisposable
{
private const string PrivateKeyPattern = @"^-+ *BEGIN (?<keyName>\w+( \w+)*) *-+\r?\n((Proc-Type: 4,ENCRYPTED\r?\nDEK-Info: (?<cipherName>[A-Z0-9-]+),(?<salt>[A-F0-9]+)\r?\n\r?\n)|(Comment: ""?[^\r\n]*""?\r?\n))?(?<data>([a-zA-Z0-9/+=]{1,80}\r?\n)+)(\r?\n)?-+ *END \k<keyName> *-+";
private const string CertificatePattern = @"(?<type>[-\w]+@openssh\.com)\s(?<data>[a-zA-Z0-9\/+=]*)(\s+(?<comment>.*))?";
#if NET7_0_OR_GREATER
private static readonly Regex PrivateKeyRegex = GetPrivateKeyRegex();
private static readonly Regex CertificateRegex = GetCertificateRegex();
[GeneratedRegex(PrivateKeyPattern, RegexOptions.Multiline | RegexOptions.ExplicitCapture)]
private static partial Regex GetPrivateKeyRegex();
[GeneratedRegex(CertificatePattern, RegexOptions.ExplicitCapture)]
private static partial Regex GetCertificateRegex();
#else
private static readonly Regex PrivateKeyRegex = new Regex(PrivateKeyPattern,
RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.ExplicitCapture);
private static readonly Regex PrivateKeyRegex = new Regex(PrivateKeyPattern, RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.ExplicitCapture);
private static readonly Regex CertificateRegex = new Regex(CertificatePattern, RegexOptions.Compiled | RegexOptions.ExplicitCapture);
#endif
private readonly List<HostAlgorithm> _hostAlgorithms = new List<HostAlgorithm>();
@@ -156,6 +162,13 @@ namespace Renci.SshNet
}
}
/// <summary>
/// Gets the public key certificate associated with this key,
/// or <see langword="null"/> if no certificate data
/// has been passed to the constructor.
/// </summary>
public Certificate? Certificate { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="PrivateKeyFile"/> class.
/// </summary>
@@ -173,7 +186,7 @@ namespace Renci.SshNet
/// </summary>
/// <param name="privateKey">The private key.</param>
public PrivateKeyFile(Stream privateKey)
: this(privateKey, passPhrase: null)
: this(privateKey, passPhrase: null, certificate: null)
{
}
@@ -186,7 +199,7 @@ namespace Renci.SshNet
/// This method calls <see cref="File.Open(string, FileMode)"/> internally, this method does not catch exceptions from <see cref="File.Open(string, FileMode)"/>.
/// </remarks>
public PrivateKeyFile(string fileName)
: this(fileName, passPhrase: null)
: this(fileName, passPhrase: null, certificateFileName: null)
{
}
@@ -200,6 +213,18 @@ namespace Renci.SshNet
/// This method calls <see cref="File.Open(string, FileMode)"/> internally, this method does not catch exceptions from <see cref="File.Open(string, FileMode)"/>.
/// </remarks>
public PrivateKeyFile(string fileName, string? passPhrase)
: this(fileName, passPhrase, certificateFileName: null)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="PrivateKeyFile"/> class.
/// </summary>
/// <param name="fileName">The path of the private key file.</param>
/// <param name="passPhrase">The pass phrase for the private key.</param>
/// <param name="certificateFileName">The path of a certificate file which certifies the private key.</param>
/// <exception cref="ArgumentNullException"><paramref name="fileName"/> is <see langword="null"/>.</exception>
public PrivateKeyFile(string fileName, string? passPhrase, string? certificateFileName)
{
ThrowHelper.ThrowIfNull(fileName);
@@ -208,6 +233,16 @@ namespace Renci.SshNet
Open(keyFile, passPhrase);
}
if (certificateFileName is not null)
{
using (var certificateFile = File.OpenRead(certificateFileName))
{
OpenCertificate(certificateFile);
}
Debug.Assert(Certificate is not null, $"{nameof(Certificate)} is null.");
}
Debug.Assert(Key is not null, $"{nameof(Key)} is null.");
Debug.Assert(HostKeyAlgorithms.Count > 0, $"{nameof(HostKeyAlgorithms)} is not set.");
}
@@ -219,11 +254,29 @@ namespace Renci.SshNet
/// <param name="passPhrase">The pass phrase.</param>
/// <exception cref="ArgumentNullException"><paramref name="privateKey"/> is <see langword="null"/>.</exception>
public PrivateKeyFile(Stream privateKey, string? passPhrase)
: this(privateKey, passPhrase, certificate: null)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="PrivateKeyFile"/> class.
/// </summary>
/// <param name="privateKey">The private key.</param>
/// <param name="passPhrase">The pass phrase for the private key.</param>
/// <param name="certificate">A certificate which certifies the private key.</param>
public PrivateKeyFile(Stream privateKey, string? passPhrase, Stream? certificate)
{
ThrowHelper.ThrowIfNull(privateKey);
Open(privateKey, passPhrase);
if (certificate is not null)
{
OpenCertificate(certificate);
Debug.Assert(Certificate is not null, $"{nameof(Certificate)} is null.");
}
Debug.Assert(Key is not null, $"{nameof(Key)} is null.");
Debug.Assert(HostKeyAlgorithms.Count > 0, $"{nameof(HostKeyAlgorithms)} is not set.");
}
@@ -854,6 +907,65 @@ namespace Renci.SshNet
throw new SshException(string.Format(CultureInfo.InvariantCulture, "Private key algorithm \"{0}\" is not supported.", algorithmOid));
}
/// <summary>
/// Opens the specified certificate.
/// </summary>
/// <param name="certificate">The certificate.</param>
private void OpenCertificate(Stream certificate)
{
Debug.Assert(certificate is not null, "Should have validated not-null in the constructor.");
Match certificateMatch;
using (var sr = new StreamReader(certificate))
{
var text = sr.ReadToEnd();
certificateMatch = CertificateRegex.Match(text);
}
if (!certificateMatch.Success)
{
throw new SshException("Invalid certificate file.");
}
var data = certificateMatch.Result("${data}");
Certificate = new Certificate(Convert.FromBase64String(data));
Debug.Assert(Key is not null, $"{nameof(Key)} should have been initialised already.");
if (!Certificate.Key.Public.SequenceEqual(Key.Public))
{
throw new ArgumentException("The supplied certificate does not certify the supplied key.");
}
if (Key is RsaKey rsaKey)
{
Debug.Assert(Certificate.Key is RsaKey,
$"Expected {nameof(Certificate)}.{nameof(Certificate.Key)} to be {nameof(RsaKey)} but was {Certificate.Key?.GetType()}");
_hostAlgorithms.Insert(0, new CertificateHostAlgorithm("ssh-rsa-cert-v01@openssh.com", Key, Certificate));
#pragma warning disable CA2000 // Dispose objects before losing scope
_hostAlgorithms.Insert(0, new CertificateHostAlgorithm(
"rsa-sha2-256-cert-v01@openssh.com",
Key,
Certificate,
new RsaDigitalSignature(rsaKey, HashAlgorithmName.SHA256)));
_hostAlgorithms.Insert(0, new CertificateHostAlgorithm(
"rsa-sha2-512-cert-v01@openssh.com",
Key,
Certificate,
new RsaDigitalSignature(rsaKey, HashAlgorithmName.SHA512)));
#pragma warning restore CA2000 // Dispose objects before losing scope
}
else
{
_hostAlgorithms.Insert(0, new CertificateHostAlgorithm(Certificate.Name, Key, Certificate));
}
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
+432
View File
@@ -0,0 +1,432 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using Renci.SshNet.Abstractions;
using Renci.SshNet.Common;
namespace Renci.SshNet.Security
{
/// <summary>
/// Represents an OpenSSH certificate as described in
/// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.certkeys.
/// </summary>
// The xmldoc comments in the class are mostly lifted from the linked document.
#pragma warning disable SA1623 // Property summary documentation should match accessors; for the above reason
public class Certificate
{
/// <summary>
/// The type identifier of the certificate.
/// </summary>
/// <remarks>
/// The value is one of the following:
/// <list type="bullet">
/// <item>ssh-rsa-cert-v01@openssh.com</item>
/// <item>ssh-dss-cert-v01@openssh.com</item>
/// <item>ecdsa-sha2-nistp256-cert-v01@openssh.com</item>
/// <item>ecdsa-sha2-nistp384-cert-v01@openssh.com</item>
/// <item>ecdsa-sha2-nistp521-cert-v01@openssh.com</item>
/// <item>ssh-ed25519-cert-v01@openssh.com</item>
/// </list>
/// </remarks>
public string Name
{
get
{
return _data.Name;
}
}
/// <summary>
/// A CA-provided random bitstring of arbitrary length
/// (but typically 16 or 32 bytes) included to make attacks that depend on
/// inducing collisions in the signature hash infeasible.
/// </summary>
public byte[] Nonce
{
get
{
return _data.Nonce;
}
}
/// <summary>
/// The public key that has been certified by the certificate authority.
/// </summary>
public Key Key
{
get
{
return _data.Key;
}
}
internal SshKeyData KeyData
{
get
{
return _data.KeyData;
}
}
/// <summary>
/// An optional certificate serial number set by the CA to
/// provide an abbreviated way to refer to certificates from that CA.
/// If a CA does not wish to number its certificates, it must set this
/// field to zero.
/// </summary>
public ulong Serial
{
get
{
return _data.Serial;
}
}
/// <summary>
/// Specifies whether this certificate is for identification of a user
/// or a host.
/// </summary>
public CertificateType Type
{
get
{
return (CertificateType)_data.Type;
}
}
/// <summary>
/// A free-form text field that is filled in by the CA at the time
/// of signing; the intention is that the contents of this field are used to
/// identify the identity principal in log messages.
/// </summary>
public string KeyId
{
get
{
return _data.KeyId;
}
}
/// <summary>
/// The names for which this certificate is valid;
/// hostnames for SSH_CERT_TYPE_HOST certificates and
/// usernames for SSH_CERT_TYPE_USER certificates. As a special case, a
/// zero-length "valid principals" field means the certificate is valid for
/// any principal of the specified type.
/// </summary>
public IList<string> ValidPrincipals
{
get
{
return _data.ValidPrincipals;
}
}
/// <summary>
/// The beginning of the validity period of the certificate, as the number
/// of seconds elapsed since 1970-01-01T00:00:00Z.
/// </summary>
/// <seealso cref="ValidAfter"/>
public ulong ValidAfterUnixSeconds
{
get
{
return _data.ValidAfter;
}
}
/// <summary>
/// The beginning of the validity period of the certificate.
/// </summary>
public DateTimeOffset ValidAfter
{
get
{
return DateTimeOffset.FromUnixTimeSeconds((long)_data.ValidAfter);
}
}
/// <summary>
/// The end of the validity period of the certificate, as the number
/// of seconds elapsed since 1970-01-01T00:00:00Z.
/// </summary>
public ulong ValidBeforeUnixSeconds
{
get
{
return _data.ValidBefore;
}
}
/// <summary>
/// The end of the validity period of the certificate.
/// </summary>
public DateTimeOffset ValidBefore
{
get
{
return _data.ValidBefore == ulong.MaxValue
? DateTimeOffset.MaxValue
: DateTimeOffset.FromUnixTimeSeconds((long)_data.ValidBefore);
}
}
/// <summary>
/// A set of zero or more options on the certificate's validity.
/// The key identifies the option and the value encodes
/// option-specific information.
/// All such options are "critical" in the sense that an implementation
/// must refuse to authorise a key that has an unrecognised option.
/// </summary>
public IDictionary<string, string> CriticalOptions
{
get
{
return _data.CriticalOptions;
}
}
/// <summary>
/// A set of zero or more optional extensions. These extensions
/// are not critical, and an implementation that encounters one that it does
/// not recognise may safely ignore it.
/// </summary>
public IDictionary<string, string> Extensions
{
get
{
return _data.Extensions;
}
}
/// <summary>
/// The CA key used to sign the certificate.
/// The valid key types for CA keys are ssh-rsa,
/// ssh-dss, ssh-ed25519 and the ECDSA types ecdsa-sha2-nistp256,
/// ecdsa-sha2-nistp384, ecdsa-sha2-nistp521. "Chained" certificates, where
/// the signature key type is a certificate type itself are NOT supported.
/// Note that it is possible for a RSA certificate key to be signed by a
/// Ed25519 or ECDSA CA key and vice-versa.
/// </summary>
public byte[] CertificateAuthorityKey
{
get
{
return _data.SignatureKey;
}
}
/// <summary>
/// Gets the SHA256 fingerprint of the certificate authority key in the same format
/// as the ssh command, i.e. non-padded base64, but without the <c>SHA256:</c> prefix.
/// </summary>
/// <example><c>ohD8VZEXGWo6Ez8GSEJQ9WpafgLFsOfLOtGGQCQo6Og</c>.</example>
/// <value>
/// Base64 encoded SHA256 fingerprint with padding (equals sign) removed.
/// </value>
public string CertificateAuthorityKeyFingerPrint
{
get
{
return Convert.ToBase64String(CryptoAbstraction.HashSHA256(CertificateAuthorityKey)).TrimEnd('=');
}
}
/// <summary>
/// The signature computed over all preceding fields from the initial string
/// up to, and including the signature key. Signatures are computed and
/// encoded according to the rules defined for the CA's public key algorithm
/// (RFC4253 section 6.6 for ssh-rsa and ssh-dss, RFC5656 for the ECDSA
/// types, and RFC8032 for Ed25519).
/// </summary>
public byte[] Signature
{
get
{
return _data.Signature;
}
}
/// <summary>
/// The encoded certificate bytes.
/// </summary>
internal byte[] Bytes { get; }
/// <summary>
/// The encoded bytes of the certificate which are used
/// to calculate <see cref="Signature"/>.
/// This consists of all of the fields before (i.e. except from)
/// <see cref="Signature"/>.
/// </summary>
internal byte[] BytesForSignature
{
get
{
return Bytes.Take((int)_data.ByteCountBeforeSignature);
}
}
private readonly CertificateData _data;
/// <summary>
/// Initializes a new instance of the <see cref="Certificate"/>
/// class based on the data encoded in <paramref name="data"/>.
/// </summary>
/// <param name="data">The encoded public-key certificate data.</param>
public Certificate(byte[] data)
{
Bytes = data;
_data = new CertificateData();
_data.Load(Bytes);
}
private sealed class CertificateData : SshData
{
public string Name { get; private set; }
public byte[] Nonce { get; private set; }
public Key Key { get; private set; }
public SshKeyData KeyData { get; private set; }
public ulong Serial { get; private set; }
public uint Type { get; private set; }
public string KeyId { get; private set; }
public List<string> ValidPrincipals { get; private set; }
public ulong ValidAfter { get; private set; }
public ulong ValidBefore { get; private set; }
public Dictionary<string, string> CriticalOptions { get; private set; }
public Dictionary<string, string> Extensions { get; private set; }
public byte[] SignatureKey { get; private set; }
/// <summary>
/// Returns the number of bytes in the encoded certificate data
/// up to and including <see cref="SignatureKey"/>.
/// Used for verifying <see cref="Signature"/> which is calculated
/// from those bytes.
/// </summary>
public long ByteCountBeforeSignature { get; private set; }
public byte[] Signature { get; private set; }
protected override void LoadData()
{
Name = ReadString();
Nonce = ReadBinary();
Key = ReadPublicKey(out var keyData);
KeyData = keyData;
Serial = ReadUInt64();
Type = ReadUInt32();
KeyId = ReadString();
ValidPrincipals = ReadValidPrincipals(ReadBinary());
ValidAfter = ReadUInt64();
ValidBefore = ReadUInt64();
CriticalOptions = ReadExtensionPair(ReadBinary());
Extensions = ReadExtensionPair(ReadBinary());
_ = ReadBinary(); // Unused reserved field
SignatureKey = ReadBinary();
ByteCountBeforeSignature = DataStream.Position;
Signature = ReadBinary();
}
private Key ReadPublicKey(out SshKeyData keyData)
{
switch (Name)
{
case "ssh-rsa-cert-v01@openssh.com":
keyData = new SshKeyData("ssh-rsa", LoadPublicKeys(2));
return new RsaKey(keyData);
case "ssh-dss-cert-v01@openssh.com":
keyData = new SshKeyData("ssh-dss", LoadPublicKeys(4));
return new DsaKey(keyData);
case "ecdsa-sha2-nistp256-cert-v01@openssh.com":
case "ecdsa-sha2-nistp384-cert-v01@openssh.com":
case "ecdsa-sha2-nistp521-cert-v01@openssh.com":
keyData = new SshKeyData(Name.Substring(0, 19), LoadPublicKeys(2));
return new EcdsaKey(keyData);
case "ssh-ed25519-cert-v01@openssh.com":
keyData = new SshKeyData("ssh-ed25519", LoadPublicKeys(1));
return new ED25519Key(keyData);
default:
throw new NotSupportedException($"Certificate type '{Name}'.");
}
BigInteger[] LoadPublicKeys(int numPublicKeyFields)
{
var keys = new BigInteger[numPublicKeyFields];
for (var i = 0; i < numPublicKeyFields; i++)
{
keys[i] = ReadBinary().ToBigInteger();
}
return keys;
}
}
private static Dictionary<string, string> ReadExtensionPair(byte[] data)
{
var result = new Dictionary<string, string>();
using var reader = new SshDataStream(data);
while (!reader.IsEndOfData)
{
var extensionName = reader.ReadString();
var extensionData = reader.ReadString();
result.Add(extensionName, extensionData);
}
return result;
}
private static List<string> ReadValidPrincipals(byte[] data)
{
var result = new List<string>();
using var reader = new SshDataStream(data);
while (!reader.IsEndOfData)
{
result.Add(reader.ReadString());
}
return result;
}
protected override void SaveData()
{
throw new NotImplementedException();
}
}
/// <summary>
/// Used to specify whether a certificate is for identification of a user
/// or a host.
/// </summary>
#pragma warning disable CA1028 // Enum Storage should be Int32; match the type specified in PROTOCOL.certkeys
public enum CertificateType : uint
#pragma warning restore CA1028 // Enum Storage should be Int32
{
/// <summary>
/// The certificate is for identification of a user (SSH_CERT_TYPE_USER).
/// </summary>
User = 1,
/// <summary>
/// The certificate is for identification of a host (SSH_CERT_TYPE_HOST).
/// </summary>
Host = 2
}
}
}
@@ -1,50 +1,152 @@
using System;
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Renci.SshNet.Security.Cryptography;
namespace Renci.SshNet.Security
{
/// <summary>
/// Implements certificate support for host algorithm.
/// </summary>
public class CertificateHostAlgorithm : HostAlgorithm
public class CertificateHostAlgorithm : KeyHostAlgorithm
{
/// <summary>
/// Gets the host key data.
/// The <see cref="KeyHostAlgorithm"/> factories which may be used in order to verify
/// the signature within the certificate.
/// </summary>
private readonly IReadOnlyDictionary<string, Func<byte[], KeyHostAlgorithm>>? _keyAlgorithms;
/// <summary>
/// Gets certificate used in this host key algorithm.
/// </summary>
public Certificate Certificate { get; }
/// <inheritdoc/>
internal override SshKeyData KeyData
{
get
{
return Certificate.KeyData;
}
}
/// <summary>
/// Gets the encoded bytes of the certificate.
/// </summary>
public override byte[] Data
{
get { throw new NotImplementedException(); }
get { return Certificate.Bytes; }
}
/// <summary>
/// Initializes a new instance of the <see cref="CertificateHostAlgorithm"/> class.
/// </summary>
/// <param name="name">The host key name.</param>
public CertificateHostAlgorithm(string name)
: base(name)
/// <param name="name">The algorithm identifier.</param>
/// <param name="privateKey">The private key used for this host algorithm.</param>
/// <param name="certificate">The certificate which certifies <paramref name="privateKey"/>.</param>
public CertificateHostAlgorithm(string name, Key privateKey, Certificate certificate)
: base(name, privateKey)
{
Certificate = certificate;
}
/// <summary>
/// Signs the specified data.
/// Initializes a new instance of the <see cref="CertificateHostAlgorithm"/> class.
/// </summary>
/// <param name="data">The data.</param>
/// <returns>Signed data.</returns>
/// <exception cref="NotImplementedException">Always.</exception>
/// <param name="name">The algorithm identifier.</param>
/// <param name="privateKey">The private key used for this host algorithm.</param>
/// <param name="certificate">The certificate which certifies <paramref name="privateKey"/>.</param>
/// <param name="digitalSignature"><inheritdoc cref="KeyHostAlgorithm.DigitalSignature" path="/summary"/></param>
public CertificateHostAlgorithm(string name, Key privateKey, Certificate certificate, DigitalSignature digitalSignature)
: base(name, privateKey, digitalSignature)
{
Certificate = certificate;
}
/// <summary>
/// Initializes a new instance of the <see cref="CertificateHostAlgorithm"/> class.
/// </summary>
/// <param name="name">The algorithm identifier.</param>
/// <param name="certificate">The certificate.</param>
/// <param name="keyAlgorithms"><inheritdoc cref="_keyAlgorithms" path="/summary"/></param>
public CertificateHostAlgorithm(string name, Certificate certificate, IReadOnlyDictionary<string, Func<byte[], KeyHostAlgorithm>> keyAlgorithms)
: base(name, certificate.Key)
{
Certificate = certificate;
_keyAlgorithms = keyAlgorithms;
}
/// <summary>
/// Initializes a new instance of the <see cref="CertificateHostAlgorithm"/> class.
/// </summary>
/// <param name="name">The algorithm identifier.</param>
/// <param name="certificate">The certificate.</param>
/// <param name="digitalSignature"><inheritdoc cref="KeyHostAlgorithm.DigitalSignature" path="/summary"/></param>
/// <param name="keyAlgorithms"><inheritdoc cref="_keyAlgorithms" path="/summary"/></param>
public CertificateHostAlgorithm(string name, Certificate certificate, DigitalSignature digitalSignature, IReadOnlyDictionary<string, Func<byte[], KeyHostAlgorithm>> keyAlgorithms)
: base(name, certificate.Key, digitalSignature)
{
Certificate = certificate;
_keyAlgorithms = keyAlgorithms;
}
/// <inheritdoc/>
public override byte[] Sign(byte[] data)
{
throw new NotImplementedException();
Debug.Assert("-cert-v01@openssh.com".Length == 21);
var signatureFormatIdentifier = Name.EndsWith("-cert-v01@openssh.com", StringComparison.Ordinal)
? Name.Substring(0, Name.Length - 21)
: Name;
return new SignatureKeyData(signatureFormatIdentifier, DigitalSignature.Sign(data)).GetBytes();
}
/// <summary>
/// Verifies the signature.
/// </summary>
/// <param name="data">The data.</param>
/// <param name="signature">The signature.</param>
/// <returns><see langword="true"/> if signature was successfully verified; otherwise <see langword="false"/>.</returns>
/// <exception cref="NotImplementedException">Always.</exception>
public override bool VerifySignature(byte[] data, byte[] signature)
/// <param name="data">The data to verify the signature against.</param>
/// <param name="signatureBlob">The signature blob in format specific encoding.</param>
/// <returns>
/// <see langword="true"/> if <paramref name="signatureBlob"/> is the result of signing
/// <paramref name="data"/> with the corresponding private key to <see cref="Certificate"/>,
/// and <see cref="Certificate"/> is valid with respect to its validity period and to its
/// signature therein as signed by the certificate authority.
/// </returns>
internal override bool VerifySignatureBlob(byte[] data, byte[] signatureBlob)
{
throw new NotImplementedException();
// Validate the session signature against the public key as normal.
if (!base.VerifySignatureBlob(data, signatureBlob))
{
return false;
}
// Validate the validity period of the certificate.
var unixNow = (ulong)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
if (unixNow < Certificate.ValidAfterUnixSeconds || unixNow > Certificate.ValidBeforeUnixSeconds)
{
return false;
}
// Validate the certificate (i.e. the signature contained within) against
// the CA public key (also contained in the certificate).
var certSignatureData = new SignatureKeyData();
certSignatureData.Load(Certificate.Signature);
if (_keyAlgorithms is null)
{
throw new InvalidOperationException($"Invalid usage of {nameof(CertificateHostAlgorithm)}.{nameof(VerifySignature)}. " +
$"Use a constructor which passes key algorithms.");
}
return _keyAlgorithms.TryGetValue(certSignatureData.AlgorithmName, out var keyAlgFactory) &&
keyAlgFactory(Certificate.CertificateAuthorityKey).VerifySignatureBlob(Certificate.BytesForSignature, certSignatureData.Signature);
}
}
}
+28 -10
View File
@@ -368,22 +368,40 @@ namespace Renci.SshNet.Security
{
var exchangeHash = CalculateHash();
// We need to inspect both the key and signature format identifers to find the correct
// HostAlgorithm instance. Example cases:
// Key identifier Signature identifier | Algorithm name
// ssh-rsa ssh-rsa | ssh-rsa
// ssh-rsa rsa-sha2-256 | rsa-sha2-256
// ssh-rsa-cert-v01@openssh.com ssh-rsa | ssh-rsa-cert-v01@openssh.com
// ssh-rsa-cert-v01@openssh.com rsa-sha2-256 | rsa-sha2-256-cert-v01@openssh.com
var signatureData = new KeyHostAlgorithm.SignatureKeyData();
signatureData.Load(encodedSignature);
var keyAlgorithm = Session.ConnectionInfo.HostKeyAlgorithms[signatureData.AlgorithmName](encodedKey);
Session.ConnectionInfo.CurrentHostKeyAlgorithm = signatureData.AlgorithmName;
if (CanTrustHostKey(keyAlgorithm))
string keyName;
using (var keyReader = new SshDataStream(encodedKey))
{
// 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);
keyName = keyReader.ReadString();
}
return false;
string algorithmName;
if (signatureData.AlgorithmName.StartsWith("rsa-sha2", StringComparison.Ordinal))
{
algorithmName = keyName.Replace("ssh-rsa", signatureData.AlgorithmName);
}
else
{
algorithmName = keyName;
}
var keyAlgorithm = Session.ConnectionInfo.HostKeyAlgorithms[algorithmName](encodedKey);
Session.ConnectionInfo.CurrentHostKeyAlgorithm = algorithmName;
return keyAlgorithm.VerifySignatureBlob(exchangeHash, signatureData.Signature) && CanTrustHostKey(keyAlgorithm);
}
/// <summary>
+35 -7
View File
@@ -20,6 +20,18 @@ namespace Renci.SshNet.Security
/// </summary>
public DigitalSignature DigitalSignature { get; private set; }
/// <summary>
/// Gets the encoded public key data.
/// </summary>
internal virtual SshKeyData KeyData
{
get
{
var keyFormatIdentifier = Key is RsaKey ? "ssh-rsa" : Name;
return new SshKeyData(keyFormatIdentifier, Key.Public);
}
}
/// <summary>
/// Gets the encoded public key data.
/// </summary>
@@ -30,8 +42,7 @@ namespace Renci.SshNet.Security
{
get
{
var keyFormatIdentifier = Key is RsaKey ? "ssh-rsa" : Name;
return new SshKeyData(keyFormatIdentifier, Key.Public).GetBytes();
return KeyData.GetBytes();
}
}
@@ -77,20 +88,37 @@ namespace Renci.SshNet.Security
}
/// <summary>
/// Verifies the signature.
/// Verifies the encoded signature.
/// </summary>
/// <param name="data">The data to verify the signature against.</param>
/// <param name="signature">The encoded signature data.</param>
/// <param name="signature">
/// The encoded signature data, as the signature format identifier followed by the signature blob.
/// </param>
/// <returns>
/// <see langword="true"/> if <paramref name="signature"/> is the result of signing <paramref name="data"/>
/// with the corresponding private key to <see cref="Key"/>.
/// <see langword="true"/> if <paramref name="signature"/> is the result of signing and encoding
/// <paramref name="data"/> with the corresponding private key to <see cref="Key"/>.
/// </returns>
/// <remarks>See <see href="https://datatracker.ietf.org/doc/html/rfc4253#section-6.6"/>.</remarks>
public override bool VerifySignature(byte[] data, byte[] signature)
{
var signatureData = new SignatureKeyData();
signatureData.Load(signature);
return DigitalSignature.Verify(data, signatureData.Signature);
return VerifySignatureBlob(data, signatureData.Signature);
}
/// <summary>
/// Verifies the signature.
/// </summary>
/// <param name="data">The data to verify the signature against.</param>
/// <param name="signatureBlob">The signature blob in format specific encoding.</param>
/// <returns>
/// <see langword="true"/> if <paramref name="signatureBlob"/> is the result of signing <paramref name="data"/>
/// with the corresponding private key to <see cref="Key"/>.
/// </returns>
internal virtual bool VerifySignatureBlob(byte[] data, byte[] signatureBlob)
{
return DigitalSignature.Verify(data, signatureBlob);
}
internal sealed class SignatureKeyData : SshData
+1
View File
@@ -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)
@@ -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
+1
View File
@@ -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)
@@ -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
+1
View File
@@ -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)
@@ -0,0 +1 @@
ecdsa-sha2-nistp521-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHA1MjEtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgvnIWATz6TOlL4gCr4t6yk2PIxN2eHym+YSDgJs0wMukAAAAIbmlzdHA1MjEAAACFBAGdkuygP8JJOCTbo3G6pxSBXIODWBNkYaidj4Z47o+r4OiAiAcEgpZSsQn1qhXMOtzeqBMvRe7IphOWFXBae2viVABR5JNwUB08HtxRG6zgr6jv5vrDqggHGNAcShOcluRrWu87nIQgIw2N5unSX6HhdOQ3VYRTUI3kAAfx/8WI6GSfvQAAAAAAAAAAAAAAAQAAABFlY2RzYTUyMUNlcnRFY2RzYQAAAAoAAAAGc3NobmV0AAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAABoAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLvxUyJKR9Jxxutu2sMEoZ1MsIxUdMNqYcpmbUX8Yu4kdGx+Wr4ktOXv8CseoXSkY2W2/lh8g/roE6N4H6cQWk8AAABjAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAABIAAAAIFnvTwo0GBTn91FYop1TCjS1KSNH2Y9OrKAQJwA8eJI4AAAAIAZ1vRormvCd1lOlBx52MmWZOlitfR6DN/tUe5cyXTMF SSH.NET
+1
View File
@@ -0,0 +1 @@
ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIDZqX9y0awmstGNnKnsJk+rljDcgUKP+lPg7L3DX1zm3AAAAIA0JZnDQrxQZcNALfZYG7LPAW1MYEGvVW5nje7OlMGMiAAAAAAAAAAAAAAABAAAAEmVkMjU1MTktY2VydC1lY2RzYQAAAAoAAAAGc3NobmV0AAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAABoAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLvxUyJKR9Jxxutu2sMEoZ1MsIxUdMNqYcpmbUX8Yu4kdGx+Wr4ktOXv8CseoXSkY2W2/lh8g/roE6N4H6cQWk8AAABkAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAABJAAAAIQD0JteEHgKpsM+xWqLsSl5CXj8pC8VlSTBKaIHDbQ0mhwAAACB+RIVgmktyCbm5/D1geigyzLnB2pHPrPIlNtTo+0vtLQ== Key.OPENSSH.ED25519
@@ -0,0 +1 @@
ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIO17UOT7XYGpB0kekNKXWNafEvDaZzmYz40s5X0h7mT1AAAAIBquS79wnun0ksZv6JJMgHzoZhzqH6Lkft+1sHTHFOjYAAAAAAAAAAAAAAABAAAAEGVkMjU1MTlDZXJ0RWNkc2EAAAAKAAAABnNzaG5ldAAAAAAAAAAA//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAaAAAABNlY2RzYS1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQS78VMiSkfSccbrbtrDBKGdTLCMVHTDamHKZm1F/GLuJHRsflq+JLTl7/ArHqF0pGNltv5YfIP66BOjeB+nEFpPAAAAYwAAABNlY2RzYS1zaGEyLW5pc3RwMjU2AAAASAAAACBeyEF9YGumQTDU0WIMG44g7p5xmtDQ9iwCgSazPRQ6UQAAACBbuMtgLhGK1uaqNYTrVpFvQr2kKU2jB4F8S/1Oopx5/Q== (null)
+1
View File
@@ -0,0 +1 @@
ssh-rsa-cert-v01@openssh.com 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 Key.OPENSSH.RSA
@@ -0,0 +1 @@
ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgZNxnyRWolCEcnQsDVptdToRYyis0Cb8bl6loA+vFRGsAAAADAQABAAABgQC2ZDTbfK8NhmdhDTXIP9lfwjTJQoIIrpWIBFyBi5Lca+sZx9qaxB+6vJZvXXE3SEqAspfqbqXxKXEhybT/yOj3n05W9/lp3XSw7YMNaV7Od5fj9n3PQRo9bjVlqcT2vKUfqpsk4d8c3+WH8e5W4ZYdv50gtj5AIk7cDMOZovAmXifS3O3JTJ7oFqhXMXwjbtf12Fsnx4MMfJrMlqpuuB49M+8zXIqSFGHx1T/uNuA/8YVZd51qiLciFDVUHefV/BoOp2caWx5ILLYcsnKIYyJLNVW+BxD9s1YrjVrHbxcv+xtydCv7PDFEuK1Kj4XsYE6TYrdta0QWsajTK+CK62+DTs/pjj7aK8/vZdFKHEcOQw0EQOgIwBjrF9DqKgBB8xkkd9//Ankyt9Kg2YdX2oKkoaVxsz9VJFAKpOiDmASAQ5d5b9P/7KoOaIxdyyslOij7tO5DsZbZHoqtqxvb7Dy+l9GEkblOWbx8/6TBsVmCbQypt6N7wobzSgNIKpHupoMAAAAAAAAAAAAAAAEAAAANcnNhQ2VydFJzYTUxMgAAAAoAAAAGc3NobmV0AAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAIXAAAAB3NzaC1yc2EAAAADAQABAAACAQCvWBFUpRdXqyRAHLIqG2oFFaqXWZ1YORiIx0l7fiO1yG0Bn41SaQaWnzBrCWnPGJkWDnCb9d3b0o81i6ggibwTYPHF67eiJyyaNCEnYMJOcvovfmEB8q/A0ZfN+oqSOXBUoUZhvqdOG4FFzbI/faTVsiM8HH8piZH1eHcRK8i2UsKzNAykKzYxmdYIFrHP6McgRrta0TY6s8XWWNqwojrH8UPnUSxOYCX89eDs3wPKzyrysN+N/proLIqG0xPLjylKTOzc1Mg2cy84P8fLNiliZL1rWG6OU6PyPGJedwf+9rFjAi7ZORnAEjJ5zAn7K51sefo9wX12/ddIRGin02/ySrvc28+AqBHA2gd3usvsNvlQdLPVG4t36+KqttFi6naFd4M4v+oY2lwow+kvcWzxPsZJAUfADGhrPnIScr49m9E04dprb2NDCw50MRTEFvYE71L0jOvRB/6s2ZBOXA5PiexxEB5RokRiHroOmwowhdwY7JQxqRKl+DblMDc4ENUZQ0l3P5ijXWZGIzp8LbUK2N1nDc8uqQAaM1kSUTkUEZI5aUTcqdaOl5/14XHkWskBp09zJNsI6VoV17C1E/SdwznLbCDPf8WgJtYNKh5q3OR5ZFBiBsR2PYKZg9sk/wrrlnu8VfgVYQ5o4HzgZg8vYx79acfj9JosLnVGcyA5EwAAAhQAAAAMcnNhLXNoYTItNTEyAAACACnh1fWBZMNWJBoL+8scPHh7xmm5U9pBeQM/Vu7Yyw7Aub7ldYQIl5IpP9hmECjE4L593EME4t3+KUE206SPKnz/uoN7Vg/pac8IeqdTopL3CjrYk1DxoNIVN1OXza8aKIAzvF7BBmaLCVEnTZARVFSe0lX3SGfOmwpzDh2b8P69TUjbWhiwweajm2vKv25bGkDlnkhYzQgue+pwU2gN5NXr9t+cKt1X9bmbliu5ovUpnDpTYncixZ6rcT0y4gaRIH/u8mKcgYfCy95KHoCkMFFZEX6IdeqEuSMLJKhAB5aw81RnQdTZSdToGW97dpdQvnUplFQbOGiLyX9fPg9nft7GjflSS6wNfKWCesw62Uc6EHXWi5fLnc6ZxClMq7+v0UEgKjpc0EFyfJ0i06C8IsED/QNLVbWLV3PTVDMEdA/eH0qwgK8VHQPAPe6gQJyi18EjpmfrQBZGGU/S4OsdKtkM4iQHuV+iTLbegzJf4Kp0GyyfuoOgvPHd5pvEF0qIvHgOdHKmcEPBVXW1x0D19RJ7DUrG8nnTBVVudIoCHj83W97pxRh3MfnKugZSFxFnywzdTt9nhLQ1Om4d0R91bwJG5jaInooH9Y4PncPup2q4kNjSmchbJUIZsbD7/uSK1XRF5N1jdooN34E6UgZ1V3ZwHLFvtwiis+Go9HAcaX6N SSH.NET
@@ -15,6 +15,7 @@ namespace Renci.SshNet.IntegrationTests.Common
.WithLogLevel(LogLevel.Debug3)
.ClearHostKeyFiles()
.AddHostKeyFile(HostKeyFile.Rsa.FilePath)
.WithHostKeyCertificate(null)
.ClearSubsystems()
.AddSubsystem(new Subsystem("sftp", "/usr/lib/ssh/sftp-server"))
.ClearCiphers()
@@ -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 && \
@@ -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; }
}
}
@@ -1,5 +1,6 @@
using Renci.SshNet.Common;
using Renci.SshNet.IntegrationTests.Common;
using Renci.SshNet.Security;
using Renci.SshNet.TestTools.OpenSSH;
namespace Renci.SshNet.IntegrationTests
@@ -71,12 +72,49 @@ namespace Renci.SshNet.IntegrationTests
DoTest(HostKeyAlgorithm.EcdsaSha2Nistp521, HostKeyFile.Ecdsa521);
}
private void DoTest(HostKeyAlgorithm hostKeyAlgorithm, HostKeyFile hostKeyFile)
[TestMethod]
public void SshRsaCertificate()
{
DoTest(HostKeyAlgorithm.SshRsaCertV01OpenSSH, HostCertificateFile.RsaCertRsa);
}
[TestMethod]
public void SshRsaSha256Certificate()
{
DoTest(HostKeyAlgorithm.RsaSha2256CertV01OpenSSH, HostCertificateFile.RsaCertRsa);
}
[TestMethod]
public void Ecdsa256Certificate()
{
DoTest(HostKeyAlgorithm.EcdsaSha2Nistp256CertV01OpenSSH, HostCertificateFile.Ecdsa256CertRsa);
}
[TestMethod]
public void Ecdsa384Certificate()
{
DoTest(HostKeyAlgorithm.EcdsaSha2Nistp384CertV01OpenSSH, HostCertificateFile.Ecdsa384CertEcdsa);
}
[TestMethod]
public void Ecdsa521Certificate()
{
DoTest(HostKeyAlgorithm.EcdsaSha2Nistp521CertV01OpenSSH, HostCertificateFile.Ecdsa521CertEd25519);
}
[TestMethod]
public void Ed25519Certificate()
{
DoTest(HostKeyAlgorithm.SshEd25519CertV01OpenSSH, HostCertificateFile.Ed25519CertEcdsa);
}
private void DoTest(HostKeyAlgorithm hostKeyAlgorithm, HostKeyFile hostKeyFile, HostCertificateFile hostCertificateFile = null)
{
_remoteSshdConfig.ClearHostKeyAlgorithms()
.AddHostKeyAlgorithm(hostKeyAlgorithm)
.ClearHostKeyFiles()
.AddHostKeyFile(hostKeyFile.FilePath)
.WithHostKeyCertificate(hostCertificateFile?.FilePath)
.Update()
.Restart();
@@ -93,6 +131,22 @@ namespace Renci.SshNet.IntegrationTests
Assert.AreEqual(hostKeyAlgorithm.Name, hostKeyEventsArgs.HostKeyName);
Assert.AreEqual(hostKeyFile.KeyLength, hostKeyEventsArgs.KeyLength);
CollectionAssert.AreEqual(hostKeyFile.FingerPrint, hostKeyEventsArgs.FingerPrint);
if (hostCertificateFile is not null)
{
Assert.IsNotNull(hostKeyEventsArgs.Certificate);
Assert.AreEqual(Certificate.CertificateType.Host, hostKeyEventsArgs.Certificate.Type);
Assert.AreEqual(hostCertificateFile.CAFingerPrint, hostKeyEventsArgs.Certificate.CertificateAuthorityKeyFingerPrint);
}
else
{
Assert.IsNull(hostKeyEventsArgs.Certificate);
}
}
private void DoTest(HostKeyAlgorithm hostKeyAlgorithm, HostCertificateFile hostCertificateFile)
{
DoTest(hostKeyAlgorithm, hostCertificateFile.HostKeyFile, hostCertificateFile);
}
}
}
@@ -70,14 +70,74 @@ namespace Renci.SshNet.IntegrationTests
DoTest(PublicKeyAlgorithm.SshEd25519, "Data.Key.OPENSSH.ED25519.Encrypted.txt", "12345");
}
private void DoTest(PublicKeyAlgorithm publicKeyAlgorithm, string keyResource, string passPhrase = null)
// The private keys used for the certificate tests below should stay out of authorized_keys for a proper test.
[TestMethod]
public void SshRsaCertificate()
{
// ssh-keygen -L -f Key.OPENSSH.RSA.Encrypted.Aes.192.CTR-cert.pub
// Type: ssh-rsa-cert-v01@openssh.com user certificate
// Public key: RSA-CERT SHA256:MMIzDVhQHqU9SAZ8p3x2wo6JpXixCWO/7qf6h0l8DJA
// Signing CA: RSA SHA256:NqLEgdYti0XjUkYjGyQv2Ddy1O5v2NZDZFRtlfESLIA (using rsa-sha2-512)
// And we will authenticate (sign) with ssh-rsa (SHA-1)
DoTest(PublicKeyAlgorithm.SshRsaCertV01OpenSSH, "Data.Key.OPENSSH.RSA.Encrypted.Aes.192.CTR.txt", "12345", "Data.Key.OPENSSH.RSA.Encrypted.Aes.192.CTR-cert.pub");
}
[TestMethod]
public void SshRsaSha256Certificate()
{
// As above, but we will authenticate (sign) with rsa-sha2-256
DoTest(PublicKeyAlgorithm.SshRsaCertV01OpenSSH, "Data.Key.OPENSSH.RSA.Encrypted.Aes.192.CTR.txt", "12345", "Data.Key.OPENSSH.RSA.Encrypted.Aes.192.CTR-cert.pub");
}
[TestMethod]
public void Ecdsa256Certificate()
{
// ssh-keygen -L -f Key.OPENSSH.ECDSA.Encrypted.Aes.128.CTR-cert.pub
// Type: ecdsa-sha2-nistp256-cert-v01@openssh.com user certificate
// Public key: ECDSA-CERT SHA256:ufAaMwjTmKrjvt4CQiLPal1/HrmB2D7oL+H2lh/Om8c
// Signing CA: RSA SHA256:NqLEgdYti0XjUkYjGyQv2Ddy1O5v2NZDZFRtlfESLIA (using rsa-sha2-512)
DoTest(PublicKeyAlgorithm.EcdsaSha2Nistp256CertV01OpenSSH, "Data.Key.OPENSSH.ECDSA.Encrypted.Aes.128.CTR.txt", "12345", "Data.Key.OPENSSH.ECDSA.Encrypted.Aes.128.CTR-cert.pub");
}
[TestMethod]
public void Ecdsa384Certificate()
{
// ssh-keygen -L -f Key.OPENSSH.ECDSA384.Encrypted.Aes.256.GCM-cert.pub
// Type: ecdsa-sha2-nistp384-cert-v01@openssh.com user certificate
// Public key: ECDSA-CERT SHA256:wy4X47uddqD8nggcsGHG7Rcs0qcnh4r6NrdBGdh/8us
// Signing CA: RSA SHA256:NqLEgdYti0XjUkYjGyQv2Ddy1O5v2NZDZFRtlfESLIA (using rsa-sha2-256)
DoTest(PublicKeyAlgorithm.EcdsaSha2Nistp384CertV01OpenSSH, "Data.Key.OPENSSH.ECDSA384.Encrypted.Aes.256.GCM.txt", "12345", "Data.Key.OPENSSH.ECDSA384.Encrypted.Aes.256.GCM-cert.pub");
}
[TestMethod]
public void Ecdsa521Certificate()
{
// ssh-keygen -L -f Key.OPENSSH.ECDSA521.Encrypted.Aes.192.CBC-cert.pub
// Type: ecdsa-sha2-nistp521-cert-v01@openssh.com user certificate
// Public key: ECDSA-CERT SHA256:U3wBX0sSPYxso31gi1QPz7O+1eMOTb0LoOSOjWRwyYE
// Signing CA: ECDSA SHA256:r/t6I+bZQzN5BhSuntFSHDHlrnNHVM2lAo6gbvynG/4 (using ecdsa-sha2-nistp256)
DoTest(PublicKeyAlgorithm.EcdsaSha2Nistp521CertV01OpenSSH, "Data.Key.OPENSSH.ECDSA521.Encrypted.Aes.192.CBC.txt", "12345", "Data.Key.OPENSSH.ECDSA521.Encrypted.Aes.192.CBC-cert.pub");
}
[TestMethod]
public void Ed25519Certificate()
{
// ssh-keygen -L -f Key.OPENSSH.ED25519.Encrypted.ChaCha20.Poly1305-cert.pub
// Type: ssh-ed25519-cert-v01@openssh.com user certificate
// Public key: ED25519-CERT SHA256:gwO3eBcuPqChqg9B/kHsQo1/bYTAjaEZCanA7hqSuEg
// Signing CA: ECDSA SHA256:r/t6I+bZQzN5BhSuntFSHDHlrnNHVM2lAo6gbvynG/4 (using ecdsa-sha2-nistp256)
DoTest(PublicKeyAlgorithm.SshEd25519CertV01OpenSSH, "Data.Key.OPENSSH.ED25519.Encrypted.ChaCha20.Poly1305.txt", "12345", "Data.Key.OPENSSH.ED25519.Encrypted.ChaCha20.Poly1305-cert.pub");
}
private void DoTest(PublicKeyAlgorithm publicKeyAlgorithm, string keyResource, string passPhrase = null, string certificateResource = null)
{
_remoteSshdConfig.ClearPublicKeyAcceptedAlgorithms()
.AddPublicKeyAcceptedAlgorithm(publicKeyAlgorithm)
.Update()
.Restart();
var connectionInfo = _connectionInfoFactory.Create(CreatePrivateKeyAuthenticationMethod(keyResource, passPhrase));
var connectionInfo = _connectionInfoFactory.Create(CreatePrivateKeyAuthenticationMethod(keyResource, passPhrase, certificateResource));
using (var client = new SshClient(connectionInfo))
{
@@ -85,12 +145,26 @@ namespace Renci.SshNet.IntegrationTests
}
}
private static PrivateKeyAuthenticationMethod CreatePrivateKeyAuthenticationMethod(string keyResource, string passPhrase)
private static PrivateKeyAuthenticationMethod CreatePrivateKeyAuthenticationMethod(string keyResource, string passPhrase, string certificateResource)
{
using (var stream = GetData(keyResource))
PrivateKeyFile privateKey;
using (var keyStream = GetData(keyResource))
{
return new PrivateKeyAuthenticationMethod(Users.Regular.UserName, new PrivateKeyFile(stream, passPhrase));
if (certificateResource is not null)
{
using (var certificateStream = GetData(certificateResource))
{
privateKey = new PrivateKeyFile(keyStream, passPhrase, certificateStream);
}
}
else
{
privateKey = new PrivateKeyFile(keyStream, passPhrase);
}
}
return new PrivateKeyAuthenticationMethod(Users.Regular.UserName, privateKey);
}
}
}
@@ -205,6 +205,12 @@ namespace Renci.SshNet.IntegrationTests
return this;
}
public RemoteSshdConfig WithHostKeyCertificate(string hostKeyCertificate)
{
_config.HostCertificate = hostKeyCertificate;
return this;
}
public RemoteSshd Update()
{
using (var client = new ScpClient(_connectionInfoFactory.Create()))
@@ -2,10 +2,6 @@
using DotNet.Testcontainers.Containers;
using DotNet.Testcontainers.Images;
#if !NET && !NETSTANDARD2_1_OR_GREATER
using Renci.SshNet.Abstractions;
#endif
namespace Renci.SshNet.IntegrationTests.TestsFixtures
{
public sealed class InfrastructureFixture : IDisposable
@@ -36,10 +32,6 @@ namespace Renci.SshNet.IntegrationTests.TestsFixtures
public SshUser User = new SshUser("sshnet", "ssh4ever");
// To get the sshd logs (also uncomment WithOutputConsumer below)
private readonly Stream _fsOut = Stream.Null; // File.Create("fsout.txt");
private readonly Stream _fsErr = Stream.Null; // File.Create("fserr.txt");
public async Task InitializeAsync()
{
_sshServerImage = new ImageFromDockerfileBuilder()
@@ -55,7 +47,6 @@ namespace Renci.SshNet.IntegrationTests.TestsFixtures
.WithHostname("renci-ssh-tests-server")
.WithImage(_sshServerImage)
.WithPortBinding(22, true)
//.WithOutputConsumer(Consume.RedirectStdoutAndStderrToStream(_fsOut, _fsErr))
.Build();
await _sshServer.StartAsync();
@@ -76,6 +67,15 @@ namespace Renci.SshNet.IntegrationTests.TestsFixtures
{
if (_sshServer != null)
{
//try
//{
// File.WriteAllBytes(@"C:\tmp\auth.log", await _sshServer.ReadFileAsync("/var/log/auth.log"));
//}
//catch (Exception ex)
//{
// Console.Error.WriteLine(ex.ToString());
//}
await _sshServer.DisposeAsync();
}
@@ -83,9 +83,6 @@ namespace Renci.SshNet.IntegrationTests.TestsFixtures
{
await _sshServerImage.DisposeAsync();
}
await _fsOut.DisposeAsync();
await _fsErr.DisposeAsync();
}
public void Dispose()
@@ -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-----
@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACCgTBKYawpTnGkumT6IXiHGdiqk0C+4rPVB1Vs7uVFuxAAAAJCdPWEKnT1h
CgAAAAtzc2gtZWQyNTUxOQAAACCgTBKYawpTnGkumT6IXiHGdiqk0C+4rPVB1Vs7uVFuxA
AAAECxa3d+jGIfVQT5AF0Ssb1pPxUJVH88BPbKy5LxwMGbgaBMEphrClOcaS6ZPoheIcZ2
KqTQL7is9UHVWzu5UW7EAAAACkNBLUVEMjU1MTkBAgM=
-----END OPENSSH PRIVATE KEY-----
@@ -0,0 +1,49 @@
-----BEGIN OPENSSH PRIVATE KEY-----
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
bZjU9P/dMt0AAAAGQ0EtUlNBAQIDBAU=
-----END OPENSSH PRIVATE 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-----
@@ -0,0 +1,49 @@
-----BEGIN OPENSSH PRIVATE KEY-----
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
-----END OPENSSH PRIVATE KEY-----
@@ -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)
@@ -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)
@@ -0,0 +1 @@
ecdsa-sha2-nistp521-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHA1MjEtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgc5mujscEbh/GGGq3kjeEbZEXT0Rt8waJ9hE2Ckz3tfEAAAAIbmlzdHA1MjEAAACFBAErBBojetXoSOJoH71QvOyAUyaEN9kQKUzouR0V2yCkZpBdS+TYLcVduDkXU0dvrj6nqbS32IrG6x3PDk/B4kduJAF0q80dgoCHPWE5t3bwJvGAZh0ABEEETeV/qFtTIZ/HDINcXFfhH6zWVLCw5g+i9YOqx24xwZbQRzVsbuwERCe+7QAAAAAAAAAAAAAAAgAAABNlY2RzYTUyMWNlcnRFZDI1NTE5AAAAAAAAAAAAAAAA//////////8AAAAAAAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIKBMEphrClOcaS6ZPoheIcZ2KqTQL7is9UHVWzu5UW7EAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEBgDUlR8fk92XP05DVIy3O1UuJ8ArHamImOS6Vd0ZLsJiFZL+s9VpR7BTgqxXhTS5tgAI2B9a/+gc1BLl8UdbIF (null)
@@ -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
@@ -0,0 +1 @@
ssh-rsa-cert-v01@openssh.com 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 root@Ubuntu1910Desktop
@@ -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=
@@ -53,6 +53,8 @@ namespace Renci.SshNet.TestTools.OpenSSH
/// </value>
public List<string> HostKeyFiles { get; }
public string? HostCertificate { get; set; }
/// <summary>
/// Gets or sets a value specifying whether challenge-response authentication is allowed.
/// </summary>
@@ -118,6 +120,11 @@ namespace Renci.SshNet.TestTools.OpenSSH
/// </summary>
public List<MessageAuthenticationCodeAlgorithm> MessageAuthenticationCodeAlgorithms { get; private set; }
/// <summary>
/// Gets the filepaths of the trusted user CA (certificate authority) keys.
/// </summary>
public string? TrustedUserCAKeys { get; private set; }
/// <summary>
/// Gets a value indicating whether <c>sshd</c> should print <c>/etc/motd</c> when a user logs in interactively.
/// </summary>
@@ -291,6 +298,11 @@ namespace Renci.SshNet.TestTools.OpenSSH
writer.WriteLine("HostKeyAlgorithms " + string.Join(",", HostKeyAlgorithms.Select(c => c.Name).ToArray()));
}
if (HostCertificate is not null)
{
writer.WriteLine("HostCertificate " + HostCertificate);
}
if (KeyExchangeAlgorithms.Count > 0)
{
writer.WriteLine("KexAlgorithms " + string.Join(",", KeyExchangeAlgorithms.Select(c => c.Name).ToArray()));
@@ -306,6 +318,11 @@ namespace Renci.SshNet.TestTools.OpenSSH
writer.WriteLine("PubkeyAcceptedAlgorithms " + string.Join(",", PublicKeyAcceptedAlgorithms.Select(c => c.Name).ToArray()));
}
if (TrustedUserCAKeys is not null)
{
writer.WriteLine("TrustedUserCAKeys " + TrustedUserCAKeys);
}
foreach (var match in Matches)
{
_matchFormatter.Format(match, writer);
@@ -384,6 +401,12 @@ namespace Renci.SshNet.TestTools.OpenSSH
case "AllowTcpForwarding":
sshdConfig.AllowTcpForwarding = ToBool(value);
break;
case "TrustedUserCAKeys":
sshdConfig.TrustedUserCAKeys = value;
break;
case "HostCertificate":
sshdConfig.HostCertificate = value;
break;
case "KeyRegenerationInterval":
case "HostbasedAuthentication":
case "ServerKeyBits":
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -16,6 +17,12 @@ namespace Renci.SshNet.Tests.Classes
[TestClass]
public class PrivateKeyFileTest : TestBase
{
#if NETFRAMEWORK
private static readonly DateTimeOffset UnixEpoch = new(1970, 01, 01, 00, 00, 00, TimeSpan.Zero);
#else
private static readonly DateTimeOffset UnixEpoch = DateTimeOffset.UnixEpoch;
#endif
private string _temporaryFile;
[TestInitialize]
@@ -364,6 +371,111 @@ namespace Renci.SshNet.Tests.Classes
}
}
[TestMethod]
public void Test_Certificate_OPENSSH_RSA()
{
PrivateKeyFile pkFile;
using (var privateKeyStream = GetData("Key.OPENSSH.RSA.txt"))
using (var certificateStream = GetData("Key.OPENSSH.RSA-cert.pub"))
{
pkFile = new PrivateKeyFile(privateKeyStream, passPhrase: null, certificateStream);
}
Certificate cert = pkFile.Certificate;
// ssh-keygen -L -f Key.OPENSSH.RSA-cert.pub
Assert.AreEqual("ssh-rsa-cert-v01@openssh.com", cert.Name);
Assert.IsInstanceOfType<RsaKey>(cert.Key);
CollectionAssert.AreEqual(((RsaKey)pkFile.Key).Public, ((RsaKey)cert.Key).Public);
Assert.AreEqual(0UL, cert.Serial);
Assert.AreEqual(Certificate.CertificateType.User, cert.Type);
Assert.AreEqual("rsa-cert-rsa", cert.KeyId);
CollectionAssert.AreEqual(new string[] { "sshnet" }, cert.ValidPrincipals.ToList());
Assert.AreEqual(0, cert.CriticalOptions.Count);
Assert.IsTrue(cert.ValidAfter.EqualsExact(new DateTimeOffset(2024, 07, 17, 20, 50, 34, TimeSpan.Zero)));
Assert.AreEqual(ulong.MaxValue, cert.ValidBeforeUnixSeconds);
Assert.AreEqual(DateTimeOffset.MaxValue, cert.ValidBefore);
CollectionAssert.AreEqual(new Dictionary<string, string>
{
["permit-X11-forwarding"] = "",
["permit-agent-forwarding"] = "",
["permit-port-forwarding"] = "",
["permit-pty"] = "",
["permit-user-rc"] = "",
}, new Dictionary<string, string>(cert.Extensions));
Assert.AreEqual("NqLEgdYti0XjUkYjGyQv2Ddy1O5v2NZDZFRtlfESLIA", cert.CertificateAuthorityKeyFingerPrint);
Assert.AreEqual(6, pkFile.HostKeyAlgorithms.Count);
var algorithms = pkFile.HostKeyAlgorithms.ToList();
Assert.AreEqual("rsa-sha2-512-cert-v01@openssh.com", algorithms[0].Name);
Assert.AreEqual("rsa-sha2-256-cert-v01@openssh.com", algorithms[1].Name);
Assert.AreEqual("ssh-rsa-cert-v01@openssh.com", algorithms[2].Name);
Assert.AreEqual("ssh-rsa", algorithms[3].Name);
Assert.AreEqual("rsa-sha2-512", algorithms[4].Name);
Assert.AreEqual("rsa-sha2-256", algorithms[5].Name);
}
[TestMethod]
public void Test_CertificateKeyMismatch()
{
using (var privateKey = GetData("Key.OPENSSH.RSA.txt"))
using (var certificate = GetData("Key.OPENSSH.ECDSA521-cert.pub"))
{
Assert.ThrowsException<ArgumentException>(() => new PrivateKeyFile(privateKey, passPhrase: null, certificate));
}
}
[TestMethod]
public void Test_Certificate_OPENSSH_ECDSA()
{
PrivateKeyFile pkFile;
using (var privateKeyStream = GetData("Key.OPENSSH.ECDSA521.txt"))
using (var certificateStream = GetData("Key.OPENSSH.ECDSA521-cert.pub"))
{
pkFile = new PrivateKeyFile(privateKeyStream, passPhrase: null, certificateStream);
}
Certificate cert = pkFile.Certificate;
// ssh-keygen -L -f Key.OPENSSH.ECDSA521-cert.pub
Assert.AreEqual("ecdsa-sha2-nistp521-cert-v01@openssh.com", cert.Name);
Assert.IsInstanceOfType<EcdsaKey>(cert.Key);
CollectionAssert.AreEqual(((EcdsaKey)pkFile.Key).Public, ((EcdsaKey)cert.Key).Public);
Assert.AreEqual(0UL, cert.Serial);
Assert.AreEqual(Certificate.CertificateType.User, cert.Type);
Assert.AreEqual("ecdsa521certEcdsa", cert.KeyId);
CollectionAssert.AreEqual(new string[] { "sshnet" }, cert.ValidPrincipals.ToList());
Assert.AreEqual(0, cert.CriticalOptions.Count);
Assert.AreEqual(0UL, cert.ValidAfterUnixSeconds);
Assert.IsTrue(cert.ValidAfter.EqualsExact(UnixEpoch));
Assert.AreEqual(ulong.MaxValue, cert.ValidBeforeUnixSeconds);
Assert.AreEqual(DateTimeOffset.MaxValue, cert.ValidBefore);
CollectionAssert.AreEqual(new Dictionary<string, string>
{
["permit-X11-forwarding"] = "",
["permit-agent-forwarding"] = "",
["permit-port-forwarding"] = "",
["permit-pty"] = "",
["permit-user-rc"] = "",
}, new Dictionary<string, string>(cert.Extensions));
Assert.AreEqual("r/t6I+bZQzN5BhSuntFSHDHlrnNHVM2lAo6gbvynG/4", cert.CertificateAuthorityKeyFingerPrint);
Assert.AreEqual(2, pkFile.HostKeyAlgorithms.Count);
var algorithms = pkFile.HostKeyAlgorithms.ToList();
Assert.AreEqual("ecdsa-sha2-nistp521-cert-v01@openssh.com", algorithms[0].Name);
Assert.AreEqual("ecdsa-sha2-nistp521", algorithms[1].Name);
}
private void SaveStreamToFile(Stream stream, string fileName)
{
var buffer = new byte[4000];
@@ -0,0 +1,389 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Renci.SshNet.Security;
using Renci.SshNet.Security.Cryptography;
using Renci.SshNet.Tests.Common;
namespace Renci.SshNet.Tests.Classes.Security
{
[TestClass]
public class CertificateHostAlgorithmTest : TestBase
{
[TestMethod]
public void NoSuppliedDigitalSignature_PropertyIsKeyDigitalSignature()
{
(RsaKey key, Certificate certificate) = GetRsaKey();
CertificateHostAlgorithm algorithm = new("ssh-rsa-cert-v01@openssh.com", key, certificate);
Assert.AreEqual("ssh-rsa-cert-v01@openssh.com", algorithm.Name);
Assert.AreSame(key, algorithm.Key);
Assert.AreSame(certificate, algorithm.Certificate);
Assert.AreSame(key.DigitalSignature, algorithm.DigitalSignature);
}
[TestMethod]
public void SuppliedDigitalSignature_PropertyIsSuppliedDigitalSignature()
{
(RsaKey key, Certificate certificate) = GetRsaKey();
RsaDigitalSignature rsaDigitalSignature = new(key, HashAlgorithmName.SHA256);
CertificateHostAlgorithm algorithm = new(
"rsa-sha2-256-cert-v01@openssh.com",
key,
certificate,
rsaDigitalSignature);
Assert.AreEqual("rsa-sha2-256-cert-v01@openssh.com", algorithm.Name);
Assert.AreSame(key, algorithm.Key);
Assert.AreSame(certificate, algorithm.Certificate);
Assert.AreSame(rsaDigitalSignature, algorithm.DigitalSignature);
}
[TestMethod]
public void HostAlgorithmData_IsRawCertificateBytes()
{
PrivateKeyFile pkFile;
byte[] certificateData;
using (Stream keyStream = GetData("Key.OPENSSH.RSA.txt"))
using (Stream certStream = GetData("Key.OPENSSH.RSA-cert.pub"))
{
using MemoryStream ms = new();
certStream.CopyTo(ms);
certificateData = Convert.FromBase64String(Encoding.UTF8.GetString(ms.ToArray()).Split(' ')[1]);
ms.Position = 0;
pkFile = new PrivateKeyFile(keyStream, null, ms);
}
List<CertificateHostAlgorithm> certAlgs = pkFile.HostKeyAlgorithms.OfType<CertificateHostAlgorithm>().ToList();
Assert.AreEqual(3, certAlgs.Count);
for (int i = 0; i < 3; i++)
{
Assert.IsNotNull(certAlgs[i].Certificate);
Assert.AreSame(pkFile.Certificate, certAlgs[i].Certificate);
CollectionAssert.AreEqual(certificateData, certAlgs[i].Data);
}
}
[TestMethod]
public void SshRsa_SignAndVerify()
{
byte[] data = Encoding.UTF8.GetBytes("hello world");
(RsaKey key, Certificate certificate) = GetRsaKey();
CertificateHostAlgorithm algorithm = new("ssh-rsa-cert-v01@openssh.com", key, certificate);
byte[] expectedEncodedSignatureBytes = new byte[]
{
0, 0, 0, 7, // byte count of "ssh-rsa"
(byte)'s', (byte)'s', (byte)'h', (byte)'-', (byte)'r', (byte)'s', (byte)'a', // ssh-rsa
0, 0, 1, 0, // byte count of signature (=256)
// ssh-keygen -e -f Key.OPENSSH.RSA.txt -m PEM -p
// echo -n 'hello world' | openssl dgst -sha1 -sign Key.OPENSSH.RSA.txt -out test.signed
0x2d, 0x54, 0x2e, 0x6a, 0x5f, 0x7c, 0x29, 0x7d, 0x2d, 0x81, 0xf6, 0x34, 0x45, 0x7a, 0x3f, 0xd0,
0xa5, 0x06, 0x55, 0x9c, 0xab, 0x8c, 0x28, 0x76, 0x27, 0xc0, 0x8a, 0x32, 0x23, 0xa4, 0x62, 0xd1,
0x8c, 0x72, 0x05, 0x52, 0x47, 0x4d, 0xd0, 0xde, 0x86, 0xdd, 0xfc, 0x38, 0x54, 0x47, 0x4e, 0x17,
0xef, 0x6b, 0x9a, 0x2e, 0x4d, 0x55, 0xf3, 0x2a, 0x11, 0xa7, 0x3a, 0x8b, 0x37, 0xbb, 0x61, 0x2d,
0xb8, 0x4c, 0x1f, 0xa1, 0x0f, 0xb4, 0xbe, 0x06, 0xea, 0xc1, 0x4e, 0x17, 0x3c, 0x53, 0x01, 0x1b,
0x41, 0x3b, 0x3c, 0x86, 0xb7, 0x55, 0x4d, 0xe6, 0xcb, 0x9d, 0x0e, 0x6f, 0x18, 0x10, 0x63, 0x3c,
0xcd, 0x02, 0x32, 0x9f, 0xbe, 0x58, 0x22, 0xa1, 0x24, 0x61, 0xf3, 0x1e, 0xa8, 0xbd, 0xf7, 0x0e,
0x9a, 0xeb, 0x42, 0x5c, 0xf5, 0xdb, 0x3b, 0x65, 0x22, 0xb1, 0x54, 0x7f, 0xe0, 0x62, 0xae, 0xb3,
0xab, 0x7b, 0xfe, 0x4b, 0x80, 0x7a, 0xd1, 0x5e, 0xd2, 0x0a, 0xa3, 0x4d, 0x1a, 0xf5, 0xa8, 0xbf,
0x87, 0xfc, 0x91, 0x57, 0xf1, 0xc2, 0x58, 0xea, 0x7a, 0xbc, 0xdf, 0x86, 0xb4, 0x24, 0x32, 0x10,
0x72, 0x2e, 0x91, 0x15, 0xa7, 0x39, 0xb5, 0x22, 0x7a, 0xe1, 0x88, 0xbd, 0x23, 0xa6, 0x05, 0xe2,
0x20, 0x22, 0x46, 0x68, 0x56, 0x34, 0x2e, 0x08, 0x35, 0xa7, 0x4b, 0x4f, 0x54, 0xcb, 0xf9, 0x53,
0xd1, 0x41, 0xf6, 0xac, 0x23, 0xf8, 0x0e, 0x90, 0x1e, 0xea, 0x4c, 0xdb, 0xa3, 0xb6, 0xdb, 0x5f,
0xf9, 0xc4, 0xf3, 0x08, 0x12, 0x32, 0xa8, 0xa2, 0xa1, 0x8c, 0x1d, 0x5f, 0xf7, 0x18, 0x79, 0x4c,
0xd4, 0x28, 0xc6, 0xe9, 0x55, 0xbc, 0x80, 0xc2, 0x08, 0x1f, 0x8f, 0x8d, 0x35, 0x0b, 0xa9, 0x49,
0x80, 0xba, 0x32, 0xba, 0xe0, 0xf6, 0x2f, 0x7f, 0xf2, 0xb7, 0xaf, 0xfa, 0xfd, 0xc8, 0x7a, 0x66,
};
CollectionAssert.AreEqual(expectedEncodedSignatureBytes, algorithm.Sign(data));
algorithm = new CertificateHostAlgorithm(
"ssh-rsa-cert-v01@openssh.com",
certificate,
DefaultKeyAlgs);
Assert.IsTrue(algorithm.VerifySignature(data, expectedEncodedSignatureBytes));
}
[TestMethod]
public void RsaSha256_SignAndVerify()
{
byte[] data = Encoding.UTF8.GetBytes("hello world");
(RsaKey key, Certificate certificate) = GetRsaKey();
CertificateHostAlgorithm algorithm = new(
"rsa-sha2-256-cert-v01@openssh.com",
key,
certificate,
new RsaDigitalSignature(key, HashAlgorithmName.SHA256));
byte[] expectedEncodedSignatureBytes = new byte[]
{
0, 0, 0, 12, // byte count of "rsa-sha2-256"
(byte)'r', (byte)'s', (byte)'a', (byte)'-', (byte)'s', (byte)'h', (byte)'a', (byte)'2',
(byte)'-', (byte)'2', (byte)'5', (byte)'6',
0, 0, 1, 0, // byte count of signature (=256)
// ssh-keygen -e -f Key.OPENSSH.RSA.txt -m PEM -p
// echo -n 'hello world' | openssl dgst -sha256 -sign Key.OPENSSH.RSA.txt -out test.signed
0x18, 0xf4, 0x3e, 0xa9, 0xdf, 0x89, 0x92, 0x6b, 0xc1, 0x6a, 0x35, 0x72, 0x42, 0x56, 0xf7, 0x50,
0x32, 0x33, 0xff, 0xc4, 0x91, 0x3d, 0x49, 0x12, 0x37, 0x52, 0x98, 0x37, 0xb8, 0xeb, 0xeb, 0xaa,
0xe5, 0x4e, 0xd4, 0x99, 0x74, 0xfd, 0xea, 0xd6, 0x8f, 0x34, 0xa0, 0x3a, 0x0e, 0xfd, 0xcb, 0xae,
0x04, 0x20, 0x01, 0x1c, 0x67, 0x98, 0x94, 0x6c, 0xdb, 0x26, 0x9a, 0x0c, 0x5b, 0xcf, 0x9a, 0x06,
0xa5, 0x90, 0xfb, 0x62, 0xe8, 0x56, 0x91, 0xdf, 0x63, 0x1f, 0xc3, 0xb1, 0xd3, 0x4f, 0x18, 0x2b,
0x2e, 0xfa, 0xb4, 0x61, 0x1d, 0x54, 0xdd, 0x63, 0x14, 0x17, 0x31, 0x8e, 0x86, 0xe3, 0xc2, 0xb1,
0x30, 0x42, 0x1e, 0x5a, 0x43, 0x87, 0x54, 0x64, 0xd5, 0xbb, 0xcb, 0x37, 0x7b, 0xa6, 0x97, 0x75,
0xca, 0x3b, 0x0d, 0xb2, 0x24, 0x34, 0x0b, 0xfc, 0xde, 0x67, 0xbf, 0xdf, 0x2a, 0x8b, 0xc6, 0xac,
0x51, 0x0d, 0x98, 0x54, 0xed, 0x57, 0x5e, 0xa9, 0xbe, 0x0f, 0x0c, 0x0f, 0x30, 0x23, 0x96, 0x83,
0x65, 0x74, 0x87, 0x91, 0x99, 0x21, 0x88, 0x80, 0x6d, 0xe4, 0xec, 0xcb, 0x51, 0xe5, 0xe5, 0x3a,
0x2b, 0x34, 0x9b, 0x10, 0x70, 0xef, 0x57, 0x40, 0x59, 0x45, 0x94, 0x58, 0xd0, 0x65, 0x84, 0x23,
0x5e, 0xcd, 0x49, 0xea, 0x18, 0x51, 0x29, 0xdd, 0x84, 0x05, 0x24, 0xe4, 0x65, 0x0c, 0x38, 0x8e,
0x42, 0x33, 0xdf, 0xcb, 0x3c, 0xa0, 0x0d, 0xe2, 0x2d, 0x13, 0xbd, 0xea, 0x51, 0x06, 0xdd, 0x61,
0x87, 0x05, 0xbe, 0xef, 0xaa, 0x77, 0xe4, 0xef, 0x25, 0x6b, 0xbf, 0x24, 0xd7, 0xe4, 0xba, 0x25,
0x28, 0x49, 0x26, 0xc2, 0x31, 0xca, 0xbb, 0x1a, 0x2c, 0x19, 0xa3, 0x7b, 0x62, 0x12, 0x59, 0x75,
0x12, 0x03, 0x38, 0xc9, 0x69, 0x93, 0xe6, 0xec, 0xc8, 0x13, 0x25, 0x48, 0xd2, 0x6c, 0x67, 0x10,
};
CollectionAssert.AreEqual(expectedEncodedSignatureBytes, algorithm.Sign(data));
algorithm = new CertificateHostAlgorithm(
"rsa-sha2-256-cert-v01@openssh.com",
certificate,
new RsaDigitalSignature((RsaKey)certificate.Key, HashAlgorithmName.SHA256),
DefaultKeyAlgs);
Assert.IsTrue(algorithm.VerifySignature(data, expectedEncodedSignatureBytes));
}
[TestMethod]
public void RsaSha512_SignAndVerify()
{
byte[] data = Encoding.UTF8.GetBytes("hello world");
(RsaKey key, Certificate certificate) = GetRsaKey();
CertificateHostAlgorithm algorithm = new(
"rsa-sha2-512-cert-v01@openssh.com",
key,
certificate,
new RsaDigitalSignature(key, HashAlgorithmName.SHA512));
byte[] expectedEncodedSignatureBytes = new byte[]
{
0, 0, 0, 12, // byte count of "rsa-sha2-512"
(byte)'r', (byte)'s', (byte)'a', (byte)'-', (byte)'s', (byte)'h', (byte)'a', (byte)'2',
(byte)'-', (byte)'5', (byte)'1', (byte)'2',
0, 0, 1, 0, // byte count of signature (=256)
// ssh-keygen -e -f Key.OPENSSH.RSA.txt -m PEM -p
// echo -n 'hello world' | openssl dgst -sha512 -sign Key.OPENSSH.RSA.txt -out test.signed
0x1d, 0x64, 0xc6, 0x82, 0xb0, 0xc4, 0x2b, 0xe1, 0x71, 0x13, 0x1f, 0x62, 0xab, 0x8f, 0xf8, 0x72,
0x43, 0xe8, 0x95, 0x4c, 0x8d, 0xa6, 0xf7, 0xcd, 0x62, 0xc9, 0x6f, 0xe5, 0xbf, 0x23, 0x1b, 0xc7,
0xa0, 0x93, 0xc6, 0xc0, 0xa2, 0x06, 0x2d, 0x07, 0x16, 0x59, 0xbc, 0x0d, 0xe5, 0x00, 0x39, 0x56,
0xa7, 0xde, 0x4b, 0x17, 0xf4, 0x02, 0xf6, 0x5d, 0x8f, 0xc5, 0x76, 0xe2, 0xb7, 0xae, 0xe5, 0xa2,
0x7f, 0xd8, 0x34, 0x04, 0x2c, 0xbc, 0xdf, 0x84, 0x51, 0x69, 0x83, 0xda, 0x7a, 0x74, 0x19, 0xe9,
0x6e, 0x02, 0xf8, 0x51, 0x20, 0xa2, 0x67, 0x43, 0xbb, 0xde, 0x7a, 0xa7, 0x12, 0xe7, 0x89, 0x7c,
0x50, 0xf3, 0xd5, 0x07, 0xc9, 0x70, 0x22, 0xed, 0x2e, 0x45, 0x1e, 0x49, 0x23, 0x94, 0x69, 0xae,
0x8f, 0x5d, 0x3b, 0x34, 0xdb, 0xc8, 0x49, 0x26, 0x09, 0x81, 0x7d, 0xad, 0x77, 0xb5, 0x6d, 0xad,
0x0c, 0x9f, 0x66, 0x29, 0x56, 0xff, 0xea, 0xa7, 0x6f, 0x7f, 0xcd, 0xc0, 0x15, 0x05, 0xdc, 0xee,
0xfb, 0xac, 0xfd, 0x59, 0x19, 0x30, 0x32, 0x6e, 0x16, 0xe0, 0x4e, 0x74, 0x6a, 0x13, 0xa7, 0x9f,
0x5b, 0x71, 0x75, 0x13, 0xcf, 0xa5, 0xf3, 0x07, 0x8f, 0xfb, 0xa2, 0xa2, 0x92, 0xc2, 0x41, 0xc4,
0xbc, 0x14, 0x75, 0x22, 0xe3, 0x4b, 0xb7, 0xc0, 0x54, 0xc3, 0x25, 0x87, 0xbb, 0x52, 0xde, 0x70,
0x69, 0xc6, 0x68, 0x66, 0x3a, 0x88, 0xf6, 0x3b, 0x8e, 0x44, 0x00, 0x25, 0x17, 0xc9, 0x44, 0x7c,
0xcc, 0x0c, 0x63, 0xab, 0xa3, 0x2c, 0xaa, 0x4c, 0x34, 0xda, 0xe0, 0x96, 0x71, 0x83, 0xe5, 0x7a,
0xec, 0x56, 0xbe, 0x85, 0x27, 0x7c, 0xe7, 0x79, 0xfd, 0xb8, 0x77, 0x41, 0x05, 0x25, 0x30, 0x57,
0x24, 0x45, 0xa9, 0x12, 0x9e, 0xdc, 0x9e, 0x23, 0x43, 0x13, 0x67, 0x38, 0x59, 0xae, 0x4b, 0x76,
};
CollectionAssert.AreEqual(expectedEncodedSignatureBytes, algorithm.Sign(data));
algorithm = new CertificateHostAlgorithm(
"rsa-sha2-512-cert-v01@openssh.com",
certificate,
new RsaDigitalSignature((RsaKey)certificate.Key, HashAlgorithmName.SHA512),
DefaultKeyAlgs);
Assert.IsTrue(algorithm.VerifySignature(data, expectedEncodedSignatureBytes));
}
[TestMethod]
public void VerifySignature_NoCorrespondingAlgorithm_ReturnsFalse()
{
byte[] data = Encoding.UTF8.GetBytes("hello world");
(RsaKey key, Certificate certificate) = GetRsaKey();
CertificateHostAlgorithm algorithm = new(
"rsa-sha2-512-cert-v01@openssh.com",
key,
certificate,
new RsaDigitalSignature(key, HashAlgorithmName.SHA512));
byte[] signature = algorithm.Sign(data);
algorithm = new CertificateHostAlgorithm(
"rsa-sha2-512-cert-v01@openssh.com",
certificate,
new RsaDigitalSignature((RsaKey)certificate.Key, HashAlgorithmName.SHA512),
new Dictionary<string, Func<byte[], KeyHostAlgorithm>>());
Assert.IsFalse(algorithm.VerifySignature(data, signature));
}
[TestMethod]
public void VerifySignature_NoSuppliedAlgorithms_Throws()
{
byte[] data = Encoding.UTF8.GetBytes("hello world");
(RsaKey key, Certificate certificate) = GetRsaKey();
CertificateHostAlgorithm algorithm = new(
"rsa-sha2-512-cert-v01@openssh.com",
key,
certificate,
new RsaDigitalSignature(key, HashAlgorithmName.SHA512));
byte[] signature = algorithm.Sign(data);
var ex = Assert.ThrowsException<InvalidOperationException>(() => algorithm.VerifySignature(data, signature));
Assert.IsTrue(ex.Message.StartsWith("Invalid usage", StringComparison.Ordinal));
}
[TestMethod]
public void CertificateBadCASignature_VerifySignatureReturnsFalse()
{
// ssh-keygen -s Key.OPENSSH.ED25519.txt -I test Key.OPENSSH.ECDSA.txt
string goodCertString = "ecdsa-sha2-nistp256-cert-v01@openssh.com " +
"AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb20AA" +
"AAg1vQFCYTYufJiCBFJBWc63sOGwnJ3BHQn4ig499dtB0AAAAAIbmlzdHAyNT" +
"YAAABBBI/dlNvfssW9KYrB67TcDmz9zBzDf7eMvUupAroP3b3FjUnYnpL3Utc" +
"4GkF/PiX7w2DuxaG70/+EX/CYHZBHKCsAAAAAAAAAAAAAAAEAAAAEdGVzdAAA" +
"AAAAAAAAAAAAAP//////////AAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3Y" +
"XJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcG" +
"VybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAA" +
"OcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAg" +
"DQlmcNCvFBlw0At9lgbss8BbUxgQa9VbmeN7s6UwYyIAAABTAAAAC3NzaC1lZ" +
"DI1NTE5AAAAQA8+LXQ++nb1/gNEtURKt5Yo/geUc/+3+Bv3EPGno5JhxvekjJ" +
"PD7/nXcyxnY3zALlPQTxb19EVx5lz58BS96gg=";
char[] chars = goodCertString.ToCharArray();
chars[^10] = 'a';
string badCertString = new string(chars);
Assert.IsTrue(VerifySignature(goodCertString));
Assert.IsFalse(VerifySignature(badCertString));
static bool VerifySignature(string certString)
{
PrivateKeyFile pk;
using (Stream keyStream = GetData("Key.OPENSSH.ECDSA.txt"))
using (MemoryStream certStream = new MemoryStream(Encoding.UTF8.GetBytes(certString)))
{
pk = new PrivateKeyFile(keyStream, null, certStream);
}
Assert.IsNotNull(pk.Certificate);
byte[] data = Encoding.UTF8.GetBytes("hello world");
CertificateHostAlgorithm certificateAlgorithm = new(
"ecdsa-sha2-nistp256-cert-v01@openssh.com",
pk.Certificate,
DefaultKeyAlgs);
KeyHostAlgorithm keyHostAlgorithm = new KeyHostAlgorithm("ecdsa-sha2-nistp256", pk.Key);
byte[] signature = keyHostAlgorithm.Sign(data);
Assert.IsTrue(keyHostAlgorithm.VerifySignature(data, signature));
return certificateAlgorithm.VerifySignature(data, signature);
}
}
[TestMethod]
public void CertificateValidityPeriodExpired_VerifySignatureReturnsFalse()
{
// ssh-keygen -s Key.OPENSSH.ED25519.txt -I nolongervalid -V always:20240101 Key.OPENSSH.ECDSA.txt
string certString = "ecdsa-sha2-nistp256-cert-v01@openssh.com " +
"AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb" +
"20AAAAg5BUo6CqGzTDc0UgNcLUqna2bH3C69NZCzd9CrQ8apQUAAAAIbm" +
"lzdHAyNTYAAABBBI/dlNvfssW9KYrB67TcDmz9zBzDf7eMvUupAroP3b3" +
"FjUnYnpL3Utc4GkF/PiX7w2DuxaG70/+EX/CYHZBHKCsAAAAAAAAAAAAA" +
"AAEAAAANbm9sb25nZXJ2YWxpZAAAAAAAAAAAAAAAAAAAAABlkgCAAAAAA" +
"AAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaX" +
"QtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2F" +
"yZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXIt" +
"cmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAgDQlmcNCvFBlw0" +
"At9lgbss8BbUxgQa9VbmeN7s6UwYyIAAABTAAAAC3NzaC1lZDI1NTE5AA" +
"AAQMonLi0J282GmuMVyHGKS/PRoLpdj5GgmR0wrIkExRRCzKZaycLfPDL" +
"+CGMa2jsH2QhFhTCG5AtKWVQbkqdHVAY= (null)";
PrivateKeyFile pk;
using (Stream keyStream = GetData("Key.OPENSSH.ECDSA.txt"))
using (MemoryStream certStream = new MemoryStream(Encoding.UTF8.GetBytes(certString)))
{
pk = new PrivateKeyFile(keyStream, null, certStream);
}
Assert.IsNotNull(pk.Certificate);
Assert.AreEqual(0uL, pk.Certificate.ValidAfterUnixSeconds);
Assert.AreEqual(new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero), pk.Certificate.ValidBefore);
byte[] data = Encoding.UTF8.GetBytes("hello world");
CertificateHostAlgorithm certificateAlgorithm = new(
"ecdsa-sha2-nistp256-cert-v01@openssh.com",
pk.Certificate,
DefaultKeyAlgs);
KeyHostAlgorithm keyHostAlgorithm = new KeyHostAlgorithm("ecdsa-sha2-nistp256", pk.Key);
byte[] signature = keyHostAlgorithm.Sign(data);
Assert.IsTrue(keyHostAlgorithm.VerifySignature(data, signature));
Assert.IsFalse(certificateAlgorithm.VerifySignature(data, signature));
}
private static (RsaKey Key, Certificate Certificate) GetRsaKey()
{
using (Stream keyStream = GetData("Key.OPENSSH.RSA.txt"))
using (Stream certStream = GetData("Key.OPENSSH.RSA-cert.pub"))
{
var pkFile = new PrivateKeyFile(keyStream, null, certStream);
return (Key: (RsaKey)pkFile.Key, pkFile.Certificate);
}
}
private static IReadOnlyDictionary<string, Func<byte[], KeyHostAlgorithm>> DefaultKeyAlgs
{
get
{
return new Dictionary<string, Func<byte[], KeyHostAlgorithm>>(
new PasswordConnectionInfo("x", "y", "z").HostKeyAlgorithms);
}
}
}
}
+1 -1
View File
@@ -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+"