Skip to content
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 OpenSSL PKCS#8 private key format #1496

Merged
merged 9 commits into from
Sep 21, 2024
Merged
29 changes: 23 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,19 +96,36 @@ The main types provided by this library are:
## Public Key Authentication

**SSH.NET** supports the following private key formats:
* RSA in OpenSSL PEM ("BEGIN RSA PRIVATE KEY") and ssh.com ("BEGIN SSH2 ENCRYPTED PRIVATE KEY") format
* DSA in OpenSSL PEM ("BEGIN DSA PRIVATE KEY") and ssh.com ("BEGIN SSH2 ENCRYPTED PRIVATE KEY") format
* ECDSA 256/384/521 in OpenSSL PEM format ("BEGIN EC PRIVATE KEY")
* ECDSA 256/384/521, ED25519 and RSA in OpenSSH key format ("BEGIN OPENSSH PRIVATE KEY")

Private keys in OpenSSL PEM and ssh.com format can be encrypted using one of the following cipher methods:
* RSA in
* OpenSSL traditional PEM format ("BEGIN RSA PRIVATE KEY")
* OpenSSL PKCS#8 PEM format ("BEGIN PRIVATE KEY", "BEGIN ENCRYPTED PRIVATE KEY")
* ssh.com format ("BEGIN SSH2 ENCRYPTED PRIVATE KEY")
* OpenSSH key format ("BEGIN OPENSSH PRIVATE KEY")
* DSA in
* OpenSSL traditional PEM format ("BEGIN DSA PRIVATE KEY")
* OpenSSL PKCS#8 PEM format ("BEGIN PRIVATE KEY", "BEGIN ENCRYPTED PRIVATE KEY")
* ssh.com format ("BEGIN SSH2 ENCRYPTED PRIVATE KEY")
* ECDSA 256/384/521 in
* OpenSSL traditional PEM format ("BEGIN EC PRIVATE KEY")
* OpenSSL PKCS#8 PEM format ("BEGIN PRIVATE KEY", "BEGIN ENCRYPTED PRIVATE KEY")
* OpenSSH key format ("BEGIN OPENSSH PRIVATE KEY")
* ED25519 in
* OpenSSL PKCS#8 PEM format ("BEGIN PRIVATE KEY", "BEGIN ENCRYPTED PRIVATE KEY")
* OpenSSH key format ("BEGIN OPENSSH PRIVATE KEY")

Private keys in OpenSSL traditional PEM format can be encrypted using one of the following cipher methods:
* DES-EDE3-CBC
* DES-EDE3-CFB
* DES-CBC
* AES-128-CBC
* AES-192-CBC
* AES-256-CBC

Private keys in OpenSSL PKCS#8 PEM format can be encrypted using any cipher method BouncyCastle supports.

Private keys in ssh.com format can be encrypted using one of the following cipher methods:
* 3des-cbc

Private keys in OpenSSH key format can be encrypted using one of the following cipher methods:
* 3des-cbc
* aes128-cbc
Expand Down
2 changes: 1 addition & 1 deletion src/Renci.SshNet/PrivateKeyConnectionInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public class PrivateKeyConnectionInfo : ConnectionInfo, IDisposable
/// <param name="host">Connection host.</param>
/// <param name="username">Connection username.</param>
/// <param name="keyFiles">Connection key files.</param>
public PrivateKeyConnectionInfo(string host, string username, params PrivateKeyFile[] keyFiles)
public PrivateKeyConnectionInfo(string host, string username, params IPrivateKeySource[] keyFiles)
: this(host, DefaultPort, username, ProxyTypes.None, string.Empty, 0, string.Empty, string.Empty, keyFiles)
{
}
Expand Down
166 changes: 154 additions & 12 deletions src/Renci.SshNet/PrivateKeyFile.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
#nullable enable
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Formats.Asn1;
using System.Globalization;
using System.IO;
using System.Numerics;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;

using Org.BouncyCastle.Asn1.EdEC;
using Org.BouncyCastle.Asn1.Pkcs;
using Org.BouncyCastle.Asn1.X9;
using Org.BouncyCastle.Pkcs;

using Renci.SshNet.Common;
using Renci.SshNet.Security;
using Renci.SshNet.Security.Cryptography;
Expand All @@ -36,12 +42,12 @@ namespace Renci.SshNet
/// <description>ECDSA 256/384/521 in OpenSSL PEM and OpenSSH key format</description>
/// </item>
/// <item>
/// <description>ED25519 in OpenSSH key format</description>
/// <description>ED25519 in OpenSSL PEM and OpenSSH key format</description>
/// </item>
/// </list>
/// </para>
/// <para>
/// The following encryption algorithms are supported for OpenSSL PEM and ssh.com format:
/// The following encryption algorithms are supported for OpenSSL traditional PEM:
/// <list type="bullet">
/// <item>
/// <description>DES-EDE3-CBC</description>
Expand All @@ -62,6 +68,19 @@ namespace Renci.SshNet
/// <description>AES-256-CBC</description>
/// </item>
/// </list>
/// </para>
/// <para>
/// Private keys in OpenSSL PKCS#8 PEM format can be encrypted using any cipher method BouncyCastle supports.
/// </para>
/// <para>
/// The following encryption algorithms are supported for ssh.com format:
/// <list type="bullet">
/// <item>
/// <description>3des-cbc</description>
/// </item>
/// </list>
/// </para>
/// <para>
/// The following encryption algorithms are supported for OpenSSH format:
/// <list type="bullet">
/// <item>
Expand Down Expand Up @@ -99,7 +118,7 @@ namespace Renci.SshNet
/// </remarks>
public partial class PrivateKeyFile : IPrivateKeySource, IDisposable
{
private const string PrivateKeyPattern = @"^-+ *BEGIN (?<keyName>\w+( \w+)*) PRIVATE KEY *-+\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 KEY *-+";
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> *-+";

#if NET7_0_OR_GREATER
private static readonly Regex PrivateKeyRegex = GetPrivateKeyRegex();
Expand Down Expand Up @@ -233,6 +252,11 @@ private void Open(Stream privateKey, string? passPhrase)
}

var keyName = privateKeyMatch.Result("${keyName}");
if (!keyName.EndsWith("PRIVATE KEY", StringComparison.Ordinal))
{
throw new SshException("Invalid private key file.");
}

var cipherName = privateKeyMatch.Result("${cipherName}");
var salt = privateKeyMatch.Result("${salt}");
var data = privateKeyMatch.Result("${data}");
Expand Down Expand Up @@ -288,7 +312,7 @@ private void Open(Stream privateKey, string? passPhrase)

switch (keyName)
{
case "RSA":
case "RSA PRIVATE KEY":
var rsaKey = new RsaKey(decryptedData);
_key = rsaKey;
_hostAlgorithms.Add(new KeyHostAlgorithm("ssh-rsa", _key));
Expand All @@ -297,16 +321,17 @@ private void Open(Stream privateKey, string? passPhrase)
_hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-256", _key, new RsaDigitalSignature(rsaKey, HashAlgorithmName.SHA256)));
#pragma warning restore CA2000 // Dispose objects before losing scope
break;
case "DSA":
case "DSA PRIVATE KEY":
_key = new DsaKey(decryptedData);
_hostAlgorithms.Add(new KeyHostAlgorithm("ssh-dss", _key));
break;
case "EC":
case "EC PRIVATE KEY":
_key = new EcdsaKey(decryptedData);
_hostAlgorithms.Add(new KeyHostAlgorithm(_key.ToString(), _key));
break;
case "OPENSSH":
_key = ParseOpenSshV1Key(decryptedData, passPhrase);
case "PRIVATE KEY":
var privateKeyInfo = PrivateKeyInfo.GetInstance(binaryData);
_key = ParseOpenSslPkcs8PrivateKey(privateKeyInfo);
if (_key is RsaKey parsedRsaKey)
{
_hostAlgorithms.Add(new KeyHostAlgorithm("ssh-rsa", _key));
Expand All @@ -315,13 +340,55 @@ private void Open(Stream privateKey, string? passPhrase)
_hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-256", _key, new RsaDigitalSignature(parsedRsaKey, HashAlgorithmName.SHA256)));
#pragma warning restore CA2000 // Dispose objects before losing scope
}
else if (_key is DsaKey parsedDsaKey)
{
_hostAlgorithms.Add(new KeyHostAlgorithm("ssh-dss", _key));
}
else
{
_hostAlgorithms.Add(new KeyHostAlgorithm(_key.ToString(), _key));
}

break;
case "SSH2 ENCRYPTED":
case "ENCRYPTED PRIVATE KEY":
var encryptedPrivateKeyInfo = EncryptedPrivateKeyInfo.GetInstance(binaryData);
privateKeyInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(passPhrase?.ToCharArray(), encryptedPrivateKeyInfo);
_key = ParseOpenSslPkcs8PrivateKey(privateKeyInfo);
if (_key is RsaKey parsedRsaKey2)
{
_hostAlgorithms.Add(new KeyHostAlgorithm("ssh-rsa", _key));
#pragma warning disable CA2000 // Dispose objects before losing scope
_hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-512", _key, new RsaDigitalSignature(parsedRsaKey2, HashAlgorithmName.SHA512)));
_hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-256", _key, new RsaDigitalSignature(parsedRsaKey2, HashAlgorithmName.SHA256)));
#pragma warning restore CA2000 // Dispose objects before losing scope
}
else if (_key is DsaKey parsedDsaKey)
{
_hostAlgorithms.Add(new KeyHostAlgorithm("ssh-dss", _key));
}
else
{
_hostAlgorithms.Add(new KeyHostAlgorithm(_key.ToString(), _key));
}

break;
case "OPENSSH PRIVATE KEY":
_key = ParseOpenSshV1Key(decryptedData, passPhrase);
if (_key is RsaKey parsedRsaKey3)
{
_hostAlgorithms.Add(new KeyHostAlgorithm("ssh-rsa", _key));
#pragma warning disable CA2000 // Dispose objects before losing scope
_hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-512", _key, new RsaDigitalSignature(parsedRsaKey3, HashAlgorithmName.SHA512)));
_hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-256", _key, new RsaDigitalSignature(parsedRsaKey3, HashAlgorithmName.SHA256)));
#pragma warning restore CA2000 // Dispose objects before losing scope
}
else
{
_hostAlgorithms.Add(new KeyHostAlgorithm(_key.ToString(), _key));
}

break;
case "SSH2 ENCRYPTED PRIVATE KEY":
var reader = new SshDataReader(decryptedData);
var magicNumber = reader.ReadUInt32();
if (magicNumber != 0x3f6ff9eb)
Expand Down Expand Up @@ -488,8 +555,8 @@ private static byte[] DecryptKey(CipherInfo cipherInfo, byte[] cipherData, strin
}

/// <summary>
/// Parses an OpenSSH V1 key file (i.e. ED25519 key) according to the the key spec:
/// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key.
/// Parses an OpenSSH V1 key file according to the key spec:
/// <see href="https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key"/>.
/// </summary>
/// <param name="keyFileData">The key file data (i.e. base64 encoded data between the header/footer).</param>
/// <param name="passPhrase">Passphrase or <see langword="null"/> if there isn't one.</param>
Expand Down Expand Up @@ -712,6 +779,81 @@ private static Key ParseOpenSshV1Key(byte[] keyFileData, string? passPhrase)
return parsedKey;
}

/// <summary>
/// Parses an OpenSSL PKCS#8 key file according to RFC5208:
/// <see href="https://www.rfc-editor.org/rfc/rfc5208#section-5"/>.
/// </summary>
/// <param name="privateKeyInfo">The <see cref="PrivateKeyInfo"/>.</param>
/// <returns>
/// The <see cref="Key"/>.
/// </returns>
/// <exception cref="SshException">Algorithm not supported.</exception>
private static Key ParseOpenSslPkcs8PrivateKey(PrivateKeyInfo privateKeyInfo)
{
var algorithmOid = privateKeyInfo.PrivateKeyAlgorithm.Algorithm;
var key = privateKeyInfo.PrivateKey.GetOctets();
if (algorithmOid.Equals(PkcsObjectIdentifiers.RsaEncryption))
{
return new RsaKey(key);
}

if (algorithmOid.Equals(X9ObjectIdentifiers.IdDsa))
{
var parameters = privateKeyInfo.PrivateKeyAlgorithm.Parameters.GetDerEncoded();
var parametersReader = new AsnReader(parameters, AsnEncodingRules.BER);
var sequenceReader = parametersReader.ReadSequence();
parametersReader.ThrowIfNotEmpty();

var p = sequenceReader.ReadInteger();
var q = sequenceReader.ReadInteger();
var g = sequenceReader.ReadInteger();
sequenceReader.ThrowIfNotEmpty();

var keyReader = new AsnReader(key, AsnEncodingRules.BER);
var x = keyReader.ReadInteger();
keyReader.ThrowIfNotEmpty();

var y = BigInteger.ModPow(g, x, p);

return new DsaKey(p, q, g, y, x);
}

if (algorithmOid.Equals(X9ObjectIdentifiers.IdECPublicKey))
{
var parameters = privateKeyInfo.PrivateKeyAlgorithm.Parameters.GetDerEncoded();
var parametersReader = new AsnReader(parameters, AsnEncodingRules.DER);
var curve = parametersReader.ReadObjectIdentifier();
parametersReader.ThrowIfNotEmpty();

var privateKeyReader = new AsnReader(key, AsnEncodingRules.DER);
var sequenceReader = privateKeyReader.ReadSequence();
privateKeyReader.ThrowIfNotEmpty();

var version = sequenceReader.ReadInteger();
if (version != BigInteger.One)
{
throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "EC version '{0}' is not supported.", version));
}

var privatekey = sequenceReader.ReadOctetString();

var publicKeyReader = sequenceReader.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, 1, isConstructed: true));
var publickey = publicKeyReader.ReadBitString(out _);
publicKeyReader.ThrowIfNotEmpty();

sequenceReader.ThrowIfNotEmpty();

return new EcdsaKey(curve, publickey, privatekey.TrimLeadingZeros());
}

if (algorithmOid.Equals(EdECObjectIdentifiers.id_Ed25519))
{
return new ED25519Key(key);
}

throw new SshException(string.Format(CultureInfo.InvariantCulture, "Private key algorithm \"{0}\" is not supported.", algorithmOid));
}

/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
Expand Down
15 changes: 9 additions & 6 deletions src/Renci.SshNet/Security/Cryptography/EcdsaKey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ public EcdsaKey(SshKeyData publicKeyData)
/// <summary>
/// Initializes a new instance of the <see cref="EcdsaKey"/> class.
/// </summary>
/// <param name="curve">The curve name.</param>
/// <param name="curve">The curve name or oid.</param>
/// <param name="publickey">Value of publickey.</param>
/// <param name="privatekey">Value of privatekey.</param>
public EcdsaKey(string curve, byte[] publickey, byte[] privatekey)
Expand Down Expand Up @@ -266,24 +266,27 @@ private static Impl Import(string curve_oid, byte[] publickey, byte[]? privateke
#endif
}

private static string GetCurveOid(string curve_s)
private static string GetCurveOid(string curve)
{
if (string.Equals(curve_s, "nistp256", StringComparison.OrdinalIgnoreCase))
if (string.Equals(curve, "nistp256", StringComparison.OrdinalIgnoreCase) ||
string.Equals(curve, ECDSA_P256_OID_VALUE))
{
return ECDSA_P256_OID_VALUE;
}

if (string.Equals(curve_s, "nistp384", StringComparison.OrdinalIgnoreCase))
if (string.Equals(curve, "nistp384", StringComparison.OrdinalIgnoreCase) ||
string.Equals(curve, ECDSA_P384_OID_VALUE))
{
return ECDSA_P384_OID_VALUE;
}

if (string.Equals(curve_s, "nistp521", StringComparison.OrdinalIgnoreCase))
if (string.Equals(curve, "nistp521", StringComparison.OrdinalIgnoreCase) ||
string.Equals(curve, ECDSA_P521_OID_VALUE))
{
return ECDSA_P521_OID_VALUE;
}

throw new SshException("Unexpected Curve Name: " + curve_s);
throw new SshException("Unexpected Curve: " + curve);
}

/// <summary>
Expand Down
12 changes: 12 additions & 0 deletions test/Data/Key.DSA.PKCS8.Encrypted.Aes.256.CBC.12345.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIBrTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIjn9BgD9X0loCAggA
MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBB3Zthr23nQDulzKryFEUTFBIIB
UDW8/IR0K5DRScH4Cl7HOoK20aR+TmUOGczE027RL++iosgk5rYUpIKn0pxIKM0U
StFGTqLz3G+bEh/Bm2Vt03Qv0Q2QZoX2e1Vktt32X2cLBNzGWfEpLuCD4vG8QDRW
uGkE1NHxJKQTJWQt/gwGituyhMThGoE3ZcuqeLmRlhUSgRccO6WJ2HkNOW7TM5RB
QbeBXmYB1H5S3FjpRAvd2p9dEzDsyquQaltFM4kekIxGjwiw5WSd+KsCGXFLa2Y2
OXvcjRIIqGBJr+xvEVA86TNTfad+sKGqGUFszRmnGXA+VxEZju2OCpVhxTLEMX4Q
2vYz9i8jE78tpx7C6PTKoJe5FTdlTatvWvYD5cvcbazPUjuZbraI9ha4XvNtERGC
J0voz/7yeuNkW1ofxTUOu+snGhySC4AXkC44eZG4wUPfuQAswP8dFiQi2BthgVyP
kA==
-----END ENCRYPTED PRIVATE KEY-----
9 changes: 9 additions & 0 deletions test/Data/Key.DSA.PKCS8.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-----BEGIN PRIVATE KEY-----
MIIBSgIBADCCASsGByqGSM44BAEwggEeAoGBALVl3fae2O4qwsAK95SUShX0KMUN
P+yl/uT3lGH9T/ZptnHSlrTxnTWXCl0g91KEeCaEnDDhLxm4aCv1Ag4B/yvcM4u3
4qkmaNLy2LiAxiqdobZcNG61Pqwqd5IDkp38LBsn8tmb12xu9NalpUfOiSEB1cyC
r4zFZMrm0wtdyJQVAhUArvojZKn/2DgGI2Kx0ghxZlgHxGECgYAOVJ434UAR3Hn6
lA5nWNfFOuUVH3W7nJaP0FQJiIPx7GUbdxO9qtDNTbWkWL3c9qx5+B7Ole4xM7cv
yXPrNQUYDHCFlS+Ue2x3IeJrkdfZkH9ePP25y5A0J4/c+8XXvQaj4zA5nfw13oy5
Ptyd7d3Kq5tEDM8KiVdIhwkXjUA3PQQWAhQYRjs5PgIpnqG/euBPPh7EDZcnXg==
-----END PRIVATE KEY-----
7 changes: 7 additions & 0 deletions test/Data/Key.ECDSA.PKCS8.Encrypted.Aes.256.CBC.12345.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIHsMFcGCSqGSIb3DQEFDTBKMCkGCSqGSIb3DQEFDDAcBAithDR1n5wCYQICCAAw
DAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEMSzAQ3FSZ5hQHbmb7K9aB0EgZCS
RVfVmMp1SBllrUMvdMEz5Zwvthaa1/M3Mc6MEVEzgROEXY3X+ywECU9q18aIOct+
m5bmFcRcwoxo/hj6fsnmeH567KRfnN4Al219azq5ccwTr68y8tasYsZHOFCkn3ve
Hkzu0+gylHZGWqo5TWif9O9DrII/KszsoX86jJxhORwqCnxMmKQQ/gGvexpAYJA=
-----END ENCRYPTED PRIVATE KEY-----
5 changes: 5 additions & 0 deletions test/Data/Key.ECDSA.PKCS8.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgR2poUqAkEiJtWPJS
HW/tjfkvAhAmuhx1NpgUvCXuIHShRANCAARAPkw7+f3KpINOzPDNWkCkvHlJAV5w
Tll8OSDGpV0dB5ybUEA+jNnh4oY2EqfvaFPv2YuWn0ddf6g0Ry5VPzcf
-----END PRIVATE KEY-----
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIGbMFcGCSqGSIb3DQEFDTBKMCkGCSqGSIb3DQEFDDAcBAhiAnoQd2VMZwICCAAw
DAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEENed/l3AFuDblxDnswMZXDcEQIpo
fCVdEbDerN0Rrh9i+Ymu+qpEqGlc6jycwR3rPtyL9jy0k5kauBxRn3Z5uCSlGzJL
JXxlMR+DWG6QDJdxrHI=
-----END ENCRYPTED PRIVATE KEY-----
Loading