Skip to content

Commit

Permalink
Add support for OpenSSL PKCS#8 private key format (#1496)
Browse files Browse the repository at this point in the history
* Add support for OpenSSL PKCS#8 private key format

* Update comments

* Convert public key to ssh format

* Convert existing keys instead of generate new keys; Use DataRow for testing

* Minimize the change

* Minimize the change

* Fix build
  • Loading branch information
scott-xu authored Sep 21, 2024
1 parent 3b55ba3 commit 548ef23
Show file tree
Hide file tree
Showing 13 changed files with 295 additions and 25 deletions.
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

0 comments on commit 548ef23

Please sign in to comment.