-
-
Notifications
You must be signed in to change notification settings - Fork 940
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for OpenSSH certificates #1498
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -387,19 +387,26 @@ public ConnectionInfo(string host, int port, string username, ProxyTypes proxyTy | |
{ "[email protected]", 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("[email protected]", data => { var cert = new Certificate(data); return new CertificateHostAlgorithm("[email protected]", cert, hostAlgs); }); | ||
hostAlgs.Add("[email protected]", data => { var cert = new Certificate(data); return new CertificateHostAlgorithm("[email protected]", cert, hostAlgs); }); | ||
hostAlgs.Add("[email protected]", data => { var cert = new Certificate(data); return new CertificateHostAlgorithm("[email protected]", cert, hostAlgs); }); | ||
hostAlgs.Add("[email protected]", data => { var cert = new Certificate(data); return new CertificateHostAlgorithm("[email protected]", cert, hostAlgs); }); | ||
hostAlgs.Add("[email protected]", data => { var cert = new Certificate(data); return new CertificateHostAlgorithm("[email protected]", cert, new RsaDigitalSignature((RsaKey)cert.Key, HashAlgorithmName.SHA512), hostAlgs); }); | ||
hostAlgs.Add("[email protected]", data => { var cert = new Certificate(data); return new CertificateHostAlgorithm("[email protected]", cert, new RsaDigitalSignature((RsaKey)cert.Key, HashAlgorithmName.SHA256), hostAlgs); }); | ||
hostAlgs.Add("[email protected]", data => { var cert = new Certificate(data); return new CertificateHostAlgorithm("[email protected]", cert, hostAlgs); }); | ||
hostAlgs.Add("[email protected]", data => { var cert = new Certificate(data); return new CertificateHostAlgorithm("[email protected]", 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>> | ||
{ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 (?<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 @@ public Key Key | |
} | ||
} | ||
|
||
/// <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 @@ public PrivateKeyFile(Key key) | |
/// </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 @@ public PrivateKeyFile(Stream privateKey) | |
/// 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 @@ public PrivateKeyFile(string fileName) | |
/// 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 @@ 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) | |
/// <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 @@ private static Key ParseOpenSslPkcs8PrivateKey(PrivateKeyInfo privateKeyInfo) | |
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("[email protected]", Key, Certificate)); | ||
|
||
#pragma warning disable CA2000 // Dispose objects before losing scope | ||
_hostAlgorithms.Insert(0, new CertificateHostAlgorithm( | ||
"[email protected]", | ||
Key, | ||
Certificate, | ||
new RsaDigitalSignature(rsaKey, HashAlgorithmName.SHA256))); | ||
|
||
_hostAlgorithms.Insert(0, new CertificateHostAlgorithm( | ||
"[email protected]", | ||
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> | ||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In order to make the host validation example work, the certificate algorithms should be listed first (so that the server knows to send its certificate). Doing this comes with a risk in case there is an unknown bug in the new code. I think the test coverage is good enough, but we could list the new algorithms last to avoid this risk.