Sfoglia il codice sorgente

Add support for OpenSSL PKCS#8 private key format (#1496)

* 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
Scott Xu 1 anno fa
parent
commit
548ef23e12

+ 23 - 6
README.md

@@ -96,12 +96,24 @@ 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
@@ -109,6 +121,11 @@ Private keys in OpenSSL PEM and ssh.com format can be encrypted using one of the
 * 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

+ 1 - 1
src/Renci.SshNet/PrivateKeyConnectionInfo.cs

@@ -22,7 +22,7 @@ namespace Renci.SshNet
         /// <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)
         {
         }

+ 154 - 12
src/Renci.SshNet/PrivateKeyFile.cs

@@ -1,8 +1,9 @@
-#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;
@@ -10,6 +11,11 @@ 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;
@@ -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>
@@ -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>
@@ -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();
@@ -233,6 +252,11 @@ namespace Renci.SshNet
             }
 
             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}");
@@ -288,7 +312,7 @@ namespace Renci.SshNet
 
             switch (keyName)
             {
-                case "RSA":
+                case "RSA PRIVATE KEY":
                     var rsaKey = new RsaKey(decryptedData);
                     _key = rsaKey;
                     _hostAlgorithms.Add(new KeyHostAlgorithm("ssh-rsa", _key));
@@ -297,16 +321,17 @@ namespace Renci.SshNet
                     _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));
@@ -315,13 +340,55 @@ namespace Renci.SshNet
                         _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)
@@ -488,8 +555,8 @@ namespace Renci.SshNet
         }
 
         /// <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>
@@ -712,6 +779,81 @@ namespace Renci.SshNet
             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>

+ 9 - 6
src/Renci.SshNet/Security/Cryptography/EcdsaKey.cs

@@ -204,7 +204,7 @@ namespace Renci.SshNet.Security
         /// <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)
@@ -266,24 +266,27 @@ namespace Renci.SshNet.Security
 #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>

+ 12 - 0
test/Data/Key.DSA.PKCS8.Encrypted.Aes.256.CBC.12345.txt

@@ -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 - 0
test/Data/Key.DSA.PKCS8.txt

@@ -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 - 0
test/Data/Key.ECDSA.PKCS8.Encrypted.Aes.256.CBC.12345.txt

@@ -0,0 +1,7 @@
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIHsMFcGCSqGSIb3DQEFDTBKMCkGCSqGSIb3DQEFDDAcBAithDR1n5wCYQICCAAw
+DAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEMSzAQ3FSZ5hQHbmb7K9aB0EgZCS
+RVfVmMp1SBllrUMvdMEz5Zwvthaa1/M3Mc6MEVEzgROEXY3X+ywECU9q18aIOct+
+m5bmFcRcwoxo/hj6fsnmeH567KRfnN4Al219azq5ccwTr68y8tasYsZHOFCkn3ve
+Hkzu0+gylHZGWqo5TWif9O9DrII/KszsoX86jJxhORwqCnxMmKQQ/gGvexpAYJA=
+-----END ENCRYPTED PRIVATE KEY-----

+ 5 - 0
test/Data/Key.ECDSA.PKCS8.txt

@@ -0,0 +1,5 @@
+-----BEGIN PRIVATE KEY-----
+MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgR2poUqAkEiJtWPJS
+HW/tjfkvAhAmuhx1NpgUvCXuIHShRANCAARAPkw7+f3KpINOzPDNWkCkvHlJAV5w
+Tll8OSDGpV0dB5ybUEA+jNnh4oY2EqfvaFPv2YuWn0ddf6g0Ry5VPzcf
+-----END PRIVATE KEY-----

+ 6 - 0
test/Data/Key.OPENSSH.ED25519.PKCS8.Encrypted.Aes.256.CBC.12345.txt

@@ -0,0 +1,6 @@
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIGbMFcGCSqGSIb3DQEFDTBKMCkGCSqGSIb3DQEFDDAcBAhiAnoQd2VMZwICCAAw
+DAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEENed/l3AFuDblxDnswMZXDcEQIpo
+fCVdEbDerN0Rrh9i+Ymu+qpEqGlc6jycwR3rPtyL9jy0k5kauBxRn3Z5uCSlGzJL
+JXxlMR+DWG6QDJdxrHI=
+-----END ENCRYPTED PRIVATE KEY-----

+ 3 - 0
test/Data/Key.OPENSSH.ED25519.PKCS8.txt

@@ -0,0 +1,3 @@
+-----BEGIN PRIVATE KEY-----
+MC4CAQAwBQYDK2VwBCIEIADMEXUw9TGuz7JykmHbzPOj8XebpZwo76iuxJtHkvAp
+-----END PRIVATE KEY-----

+ 30 - 0
test/Data/Key.RSA.PKCS8.Encrypted.Aes.256.CBC.12345.txt

@@ -0,0 +1,30 @@
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIFHTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIr0PK2BHcNKsCAggA
+MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBCClZC7H7nFtlsAd+cURqvqBIIE
+wOYCGtgoABpWHCk5chj4EF3t2T+Eu753vz5FudgbFkkoMFVmxKLGB0BYVyxMIws/
+GLj6WYq8o51E867hr3f3XOc5nIJEffGSxF/fhYIMGuE9teOWYVTAc/vGlFzEWska
+cXGgffBT+DqA4uaLEgcuYHd+hjhlNhKmNIFDsIP40sbHMP31NhuQyd1a63sTUJro
+lcLEsESC1H4jru36RrrlRUrTjdgwPrraeu/GUw3Dq7Op5wTfLTsv06Ci3OAKWkGs
+SMog36KgvU834YC1btAHL/T/R7lE2QFp8bHPSHA0iigsDiO6NoGcn5Ktvj0yxYzm
+8T+uiKs0aEzHWRX+08J8iBIhbSuNm6z/J+CILT3ce5/1vE9Kc2Ml4iVAvwvIYvKB
+TO1ZPe4/8gou/pMAHZ39/4euywZpYn9dMSPrsbLt0Wmqg+vVg9mVPD57eZiBgj0j
+RqgOlrhymaIA/tDYQTt+DJOKVtGNMbPrXqVpkpaAbXVa7uXYq/euTi2OgzMOSIZj
+JFLCWyZo/kpHa6/scIiRjFEDRLqIuhgKca5c4HQ23tRrtWvI+kMd5FK6IAAUplOy
+rP1EOztLrwk7NmSpL/zbOJ585AgiGPkq8AAxWfEpMt5GTvcomMjQpv1bs82jLPSG
+dC3N+7AOgMQQBDamwfallDgDpvlS7+pgmQJVsv9oT+XoHIXrkD7q7junA8T8Zd9a
+LsBgFxMVid2X1PWRkC8+5M4zb7HZdqIyK/vI1kPVWvdUYTvRMHgXxa5wBW/kB3q9
+CQYGfOvg/Gsk7h/WqzlApqvcNZ8OJsxkhlElr2KkH5R9W3snAGedm/fHd1XWpkkL
+tY7qHb9z4WW2QOZWBkFY1mfUTU0wrYexAsgLhVniHTy71zj3LLVmezPxDCIvYslX
+hSY9/cofCsTx4W5toh/jh1NMsgQgaN2dGgCmHL+PwQTN3bL13KtKQUogNmHxKBMc
+QZepJqoiiOtjT6LjBkQI9PY+Yzky4zToo/3Gh5i7ohYAABvT9MXtK4hbQP26JB0T
+GL8pVLJSUCBQFj+87sJVILW+n1ak8OoEZQLNgs/V6lbtSc+0TW6t1ixZs7Xxh6KJ
+1zLDu0IwebONt8R8KVLUPeV0aFAjr5R9KGNvaL+F0EexG+fNSe1vJ+iBf3fhQLb1
+EDI20FGYBlMfSwtNw0OhKZ5cG1lSx3BUtW1/nl3Y0+sYhRRtP628JjZ2YYifJZdc
++iNGSC2m4r6li9spPvQUyJLHb2vMIwHEqbAi+OE4fNCBVgm0vNPdhEZHAwa8HvRt
+iQd2vsEE1tKSfj8z0XQl0T3u4WDVB0DvQqsSjVIfDL+vxlCZ540l26KWd1rJJOWi
+fWRF4+MV07GQgoE8aL8bbLUNBJq5OJPTHSgUs7ke7D0sHas1Z2ue/LccZeMbi39U
+kTqAyB0ag2/aaqL3/sUo1mi2HZBtqjg4P2gI6ymeVHeSQu8Hx62mw58C8WmoRk12
+3YfiHXZI6LHBt+CK+nIXvsf7ZXeHHVnvRJEJaeX+jrCyCKyqCz8r47ZUUb2sgqFf
+8hTpheQKvCbYM+XtkkkuiudVevY67xrHpjAjDIU/4BXxC5KvlBQ8i8u21qtSq70P
+fFQUbG2XVVscckIFQa8DuDc=
+-----END ENCRYPTED PRIVATE KEY-----

+ 28 - 0
test/Data/Key.RSA.PKCS8.txt

@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQC5O1ef4Fq1fWgm
+6+Gp8lnDmNz+lwjElQ+a6gUIff5td8oEn/3iLE0RPNkFqzK9P+jNugBsIbepwk5j
+F/YER5MAhd7WMsChN3UYoLAy9k7KOew833n+UKHB92cFszOllhMZ+hTKVeZ7+bOO
+Mu78nSpeBHmXKT0cVP7HlgS1GXxVIeIOQspNnft3CGyqByz4+R+9gxQr4Lx6+d8T
+S2BaApmTQRq2XzuctbJVcHgvOFIO0YosI8A6Ctft9h+mUPAnZYrU3qcbQWfFbUeE
+N5Irt7ZNsBra9lCC8Vcxac7g781kqngI6k5F7KWJaF20oCOv/5wPjIN8+OGOMo5h
+/Fu91EbhAgEjAoIBACpWtPFX2jgcqheGX3dNVliX9/+tfljRnSq5JbjMV2l6d1GD
+p76rCk0VOOtaVL2K82I5JKsAZH6S0BamZB48PtuMUDD1qF9dIhRB/GNrf7kxz5ji
+n4qWFlg4jJOWrLgiTYJHyjzgbzJHtAM+14oyjVdReuC4+AZ509XZJaW8rrRfIBeq
+x7AOGLtJ4iZmiYrNuHfdJeNXFqTMd9rwERR4gh5H2gWVAy5h2jgaseEU7ZP6D+Cr
+D+RV3XOR4TC3Tp9lUMStXCn63B7DHG/CuTgIpQLNq9+EN+Lmwnlh3OEMFGCTIGAk
+j5/Xl0rKqo6IClqtX367OsPq8i42eVWipACK6AsCgYEA8GWLLfZ7zeeyEGScGfkc
+4ras7drXMfG9iv83MhNe11k6Nq03J9e2TCIuX96Xm1mEm8p72ci1Ij/jDua+W9dl
+9byHcxHSpvxq3iNZrzJraHAR6upomieqONv54GGDbuW1khBGTwG/oCldMBaRz50U
+aEQjKyiQNBftXvYF2uLO6nUCgYEAxUEscrTkgVvzHt1qeUCYNfpuoI5sh3g9x7Y5
+440UiPWMijC8JdoTS33NThflJ03m6Oq7gOpeDNR014pwnFakU7vgwNHFPcJszPfp
++KQme/FaX/6rQdqVi4JjyCbXhVhxOFQECujdz3jUg257JsY0sgD2NztEyewW3JVO
+81ilpT0CgYAUmv1NFSCN/eqw8q5LXn7RmqEbs6wLmGC0JIES67esDvZcdT897esN
+1wtKC8PaHZ2m9Bk+jYveXT9ZDHa3ajvwfd+5Z+18BwHYhq/qcgk01mfvkG9dq6Dg
+TV6Pk1Rol1i0v5D/dS2upHWzqinBVptZZO0SU+8aaHNuih3CTfR6fwKBgDK4/M0J
+8Z2bTSUxnwk8fulPBoEOrjF2sMz0WAdQKdoTQWVc/S5OBPYnqdJAqKO08jvklp1+
+GC8vUT69MuZfbBWIFTjle9yuVn3ZWWvScEvBuCdQHWi0jNq7IPj0C4iwV6DFJZxn
+xAImYogyWi6KvRfUXJHcCl/O/pB+Kj6TI0e/AoGBAK3DGiOTNRJ9XfMO3pthIsHO
+BeCj9mv2VL6BKCA/vYPMg2ZbaBsDR+QzZm4mW1x9qQq/iBVf/XOijP7u37huPp2q
+35uXuFjPRWdNINka6jksoOJ//iBIat1uULZAtO5fYeNYJl2y57uZHAvtxdHjOUig
+T335HKF3G9znAdJ9iv8k
+-----END PRIVATE KEY-----

+ 8 - 0
test/Renci.SshNet.Tests/Classes/PrivateKeyFileTest.cs

@@ -303,8 +303,12 @@ namespace Renci.SshNet.Tests.Classes
         }
 
         [TestMethod]
+        [DataRow("Key.DSA.PKCS8.Encrypted.Aes.256.CBC.12345.txt", "12345", typeof(DsaKey))]
+        [DataRow("Key.DSA.PKCS8.txt", null, typeof(DsaKey))]
         [DataRow("Key.DSA.txt", null, typeof(DsaKey))]
         [DataRow("Key.ECDSA.Encrypted.txt", "12345", typeof(EcdsaKey))]
+        [DataRow("Key.ECDSA.PKCS8.Encrypted.Aes.256.CBC.12345.txt", "12345", typeof(EcdsaKey))]
+        [DataRow("Key.ECDSA.PKCS8.txt", null, typeof(EcdsaKey))]
         [DataRow("Key.ECDSA.txt", null, typeof(EcdsaKey))]
         [DataRow("Key.ECDSA384.Encrypted.txt", "12345", typeof(EcdsaKey))]
         [DataRow("Key.ECDSA384.txt", null, typeof(EcdsaKey))]
@@ -326,6 +330,8 @@ namespace Renci.SshNet.Tests.Classes
         [DataRow("Key.OPENSSH.ED25519.Encrypted.Aes.256.CTR.txt", "12345", typeof(ED25519Key))]
         [DataRow("Key.OPENSSH.ED25519.Encrypted.ChaCha20.Poly1305.txt", "12345", typeof(ED25519Key))]
         [DataRow("Key.OPENSSH.ED25519.Encrypted.txt", "12345", typeof(ED25519Key))]
+        [DataRow("Key.OPENSSH.ED25519.PKCS8.Encrypted.Aes.256.CBC.12345.txt", "12345", typeof(ED25519Key))]
+        [DataRow("Key.OPENSSH.ED25519.PKCS8.txt", null, typeof(ED25519Key))]
         [DataRow("Key.OPENSSH.ED25519.txt", null, typeof(ED25519Key))]
         [DataRow("Key.OPENSSH.RSA.Encrypted.Aes.192.CTR.txt", "12345", typeof(RsaKey))]
         [DataRow("Key.OPENSSH.RSA.Encrypted.txt", "12345", typeof(RsaKey))]
@@ -336,6 +342,8 @@ namespace Renci.SshNet.Tests.Classes
         [DataRow("Key.RSA.Encrypted.Des.CBC.12345.txt", "12345", typeof(RsaKey))]
         [DataRow("Key.RSA.Encrypted.Des.Ede3.CBC.12345.txt", "12345", typeof(RsaKey))]
         [DataRow("Key.RSA.Encrypted.Des.Ede3.CFB.1234567890.txt", "1234567890", typeof(RsaKey))]
+        [DataRow("Key.RSA.PKCS8.Encrypted.Aes.256.CBC.12345.txt", "12345", typeof(RsaKey))]
+        [DataRow("Key.RSA.PKCS8.txt", null, typeof(RsaKey))]
         [DataRow("Key.RSA.txt", null, typeof(RsaKey))]
         [DataRow("Key.SSH2.DSA.Encrypted.Des.CBC.12345.txt", "12345", typeof(DsaKey))]
         [DataRow("Key.SSH2.DSA.txt", null, typeof(DsaKey))]