1- #nullable enable
1+ #nullable enable
22using System ;
33using System . Collections . Generic ;
44using System . Diagnostics ;
55using System . Diagnostics . CodeAnalysis ;
6+ using System . Formats . Asn1 ;
67using System . Globalization ;
78using System . IO ;
89using System . Numerics ;
910using System . Security . Cryptography ;
1011using System . Text ;
1112using System . Text . RegularExpressions ;
1213
14+ using Org . BouncyCastle . Asn1 . EdEC ;
15+ using Org . BouncyCastle . Asn1 . Pkcs ;
16+ using Org . BouncyCastle . Asn1 . X9 ;
17+ using Org . BouncyCastle . Pkcs ;
18+
1319using Renci . SshNet . Common ;
1420using Renci . SshNet . Security ;
1521using Renci . SshNet . Security . Cryptography ;
@@ -36,12 +42,12 @@ namespace Renci.SshNet
3642 /// <description>ECDSA 256/384/521 in OpenSSL PEM and OpenSSH key format</description>
3743 /// </item>
3844 /// <item>
39- /// <description>ED25519 in OpenSSH key format</description>
45+ /// <description>ED25519 in OpenSSL PEM and OpenSSH key format</description>
4046 /// </item>
4147 /// </list>
4248 /// </para>
4349 /// <para>
44- /// The following encryption algorithms are supported for OpenSSL PEM and ssh.com format :
50+ /// The following encryption algorithms are supported for OpenSSL traditional PEM :
4551 /// <list type="bullet">
4652 /// <item>
4753 /// <description>DES-EDE3-CBC</description>
@@ -62,6 +68,19 @@ namespace Renci.SshNet
6268 /// <description>AES-256-CBC</description>
6369 /// </item>
6470 /// </list>
71+ /// </para>
72+ /// <para>
73+ /// Private keys in OpenSSL PKCS#8 PEM format can be encrypted using any cipher method BouncyCastle supports.
74+ /// </para>
75+ /// <para>
76+ /// The following encryption algorithms are supported for ssh.com format:
77+ /// <list type="bullet">
78+ /// <item>
79+ /// <description>3des-cbc</description>
80+ /// </item>
81+ /// </list>
82+ /// </para>
83+ /// <para>
6584 /// The following encryption algorithms are supported for OpenSSH format:
6685 /// <list type="bullet">
6786 /// <item>
@@ -99,7 +118,7 @@ namespace Renci.SshNet
99118 /// </remarks>
100119 public partial class PrivateKeyFile : IPrivateKeySource , IDisposable
101120 {
102- 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 *-+" ;
121+ 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> *-+" ;
103122
104123#if NET7_0_OR_GREATER
105124 private static readonly Regex PrivateKeyRegex = GetPrivateKeyRegex ( ) ;
@@ -233,6 +252,11 @@ private void Open(Stream privateKey, string? passPhrase)
233252 }
234253
235254 var keyName = privateKeyMatch . Result ( "${keyName}" ) ;
255+ if ( ! keyName . EndsWith ( "PRIVATE KEY" , StringComparison . Ordinal ) )
256+ {
257+ throw new SshException ( "Invalid private key file." ) ;
258+ }
259+
236260 var cipherName = privateKeyMatch . Result ( "${cipherName}" ) ;
237261 var salt = privateKeyMatch . Result ( "${salt}" ) ;
238262 var data = privateKeyMatch . Result ( "${data}" ) ;
@@ -288,7 +312,7 @@ private void Open(Stream privateKey, string? passPhrase)
288312
289313 switch ( keyName )
290314 {
291- case "RSA" :
315+ case "RSA PRIVATE KEY " :
292316 var rsaKey = new RsaKey ( decryptedData ) ;
293317 _key = rsaKey ;
294318 _hostAlgorithms . Add ( new KeyHostAlgorithm ( "ssh-rsa" , _key ) ) ;
@@ -297,16 +321,17 @@ private void Open(Stream privateKey, string? passPhrase)
297321 _hostAlgorithms . Add ( new KeyHostAlgorithm ( "rsa-sha2-256" , _key , new RsaDigitalSignature ( rsaKey , HashAlgorithmName . SHA256 ) ) ) ;
298322#pragma warning restore CA2000 // Dispose objects before losing scope
299323 break ;
300- case "DSA" :
324+ case "DSA PRIVATE KEY " :
301325 _key = new DsaKey ( decryptedData ) ;
302326 _hostAlgorithms . Add ( new KeyHostAlgorithm ( "ssh-dss" , _key ) ) ;
303327 break ;
304- case "EC" :
328+ case "EC PRIVATE KEY " :
305329 _key = new EcdsaKey ( decryptedData ) ;
306330 _hostAlgorithms . Add ( new KeyHostAlgorithm ( _key . ToString ( ) , _key ) ) ;
307331 break ;
308- case "OPENSSH" :
309- _key = ParseOpenSshV1Key ( decryptedData , passPhrase ) ;
332+ case "PRIVATE KEY" :
333+ var privateKeyInfo = PrivateKeyInfo . GetInstance ( binaryData ) ;
334+ _key = ParseOpenSslPkcs8PrivateKey ( privateKeyInfo ) ;
310335 if ( _key is RsaKey parsedRsaKey )
311336 {
312337 _hostAlgorithms . Add ( new KeyHostAlgorithm ( "ssh-rsa" , _key ) ) ;
@@ -315,13 +340,55 @@ private void Open(Stream privateKey, string? passPhrase)
315340 _hostAlgorithms . Add ( new KeyHostAlgorithm ( "rsa-sha2-256" , _key , new RsaDigitalSignature ( parsedRsaKey , HashAlgorithmName . SHA256 ) ) ) ;
316341#pragma warning restore CA2000 // Dispose objects before losing scope
317342 }
343+ else if ( _key is DsaKey parsedDsaKey )
344+ {
345+ _hostAlgorithms . Add ( new KeyHostAlgorithm ( "ssh-dss" , _key ) ) ;
346+ }
318347 else
319348 {
320349 _hostAlgorithms . Add ( new KeyHostAlgorithm ( _key . ToString ( ) , _key ) ) ;
321350 }
322351
323352 break ;
324- case "SSH2 ENCRYPTED" :
353+ case "ENCRYPTED PRIVATE KEY" :
354+ var encryptedPrivateKeyInfo = EncryptedPrivateKeyInfo . GetInstance ( binaryData ) ;
355+ privateKeyInfo = PrivateKeyInfoFactory . CreatePrivateKeyInfo ( passPhrase ? . ToCharArray ( ) , encryptedPrivateKeyInfo ) ;
356+ _key = ParseOpenSslPkcs8PrivateKey ( privateKeyInfo ) ;
357+ if ( _key is RsaKey parsedRsaKey2 )
358+ {
359+ _hostAlgorithms . Add ( new KeyHostAlgorithm ( "ssh-rsa" , _key ) ) ;
360+ #pragma warning disable CA2000 // Dispose objects before losing scope
361+ _hostAlgorithms . Add ( new KeyHostAlgorithm ( "rsa-sha2-512" , _key , new RsaDigitalSignature ( parsedRsaKey2 , HashAlgorithmName . SHA512 ) ) ) ;
362+ _hostAlgorithms . Add ( new KeyHostAlgorithm ( "rsa-sha2-256" , _key , new RsaDigitalSignature ( parsedRsaKey2 , HashAlgorithmName . SHA256 ) ) ) ;
363+ #pragma warning restore CA2000 // Dispose objects before losing scope
364+ }
365+ else if ( _key is DsaKey parsedDsaKey )
366+ {
367+ _hostAlgorithms . Add ( new KeyHostAlgorithm ( "ssh-dss" , _key ) ) ;
368+ }
369+ else
370+ {
371+ _hostAlgorithms . Add ( new KeyHostAlgorithm ( _key . ToString ( ) , _key ) ) ;
372+ }
373+
374+ break ;
375+ case "OPENSSH PRIVATE KEY" :
376+ _key = ParseOpenSshV1Key ( decryptedData , passPhrase ) ;
377+ if ( _key is RsaKey parsedRsaKey3 )
378+ {
379+ _hostAlgorithms . Add ( new KeyHostAlgorithm ( "ssh-rsa" , _key ) ) ;
380+ #pragma warning disable CA2000 // Dispose objects before losing scope
381+ _hostAlgorithms . Add ( new KeyHostAlgorithm ( "rsa-sha2-512" , _key , new RsaDigitalSignature ( parsedRsaKey3 , HashAlgorithmName . SHA512 ) ) ) ;
382+ _hostAlgorithms . Add ( new KeyHostAlgorithm ( "rsa-sha2-256" , _key , new RsaDigitalSignature ( parsedRsaKey3 , HashAlgorithmName . SHA256 ) ) ) ;
383+ #pragma warning restore CA2000 // Dispose objects before losing scope
384+ }
385+ else
386+ {
387+ _hostAlgorithms . Add ( new KeyHostAlgorithm ( _key . ToString ( ) , _key ) ) ;
388+ }
389+
390+ break ;
391+ case "SSH2 ENCRYPTED PRIVATE KEY" :
325392 var reader = new SshDataReader ( decryptedData ) ;
326393 var magicNumber = reader . ReadUInt32 ( ) ;
327394 if ( magicNumber != 0x3f6ff9eb )
@@ -488,8 +555,8 @@ private static byte[] DecryptKey(CipherInfo cipherInfo, byte[] cipherData, strin
488555 }
489556
490557 /// <summary>
491- /// Parses an OpenSSH V1 key file (i.e. ED25519 key) according to the the key spec:
492- /// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key.
558+ /// Parses an OpenSSH V1 key file according to the key spec:
559+ /// <see href=" https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key"/> .
493560 /// </summary>
494561 /// <param name="keyFileData">The key file data (i.e. base64 encoded data between the header/footer).</param>
495562 /// <param name="passPhrase">Passphrase or <see langword="null"/> if there isn't one.</param>
@@ -712,6 +779,81 @@ private static Key ParseOpenSshV1Key(byte[] keyFileData, string? passPhrase)
712779 return parsedKey ;
713780 }
714781
782+ /// <summary>
783+ /// Parses an OpenSSL PKCS#8 key file according to RFC5208:
784+ /// <see href="https://www.rfc-editor.org/rfc/rfc5208#section-5"/>.
785+ /// </summary>
786+ /// <param name="privateKeyInfo">The <see cref="PrivateKeyInfo"/>.</param>
787+ /// <returns>
788+ /// The <see cref="Key"/>.
789+ /// </returns>
790+ /// <exception cref="SshException">Algorithm not supported.</exception>
791+ private static Key ParseOpenSslPkcs8PrivateKey ( PrivateKeyInfo privateKeyInfo )
792+ {
793+ var algorithmOid = privateKeyInfo . PrivateKeyAlgorithm . Algorithm ;
794+ var key = privateKeyInfo . PrivateKey . GetOctets ( ) ;
795+ if ( algorithmOid . Equals ( PkcsObjectIdentifiers . RsaEncryption ) )
796+ {
797+ return new RsaKey ( key ) ;
798+ }
799+
800+ if ( algorithmOid . Equals ( X9ObjectIdentifiers . IdDsa ) )
801+ {
802+ var parameters = privateKeyInfo . PrivateKeyAlgorithm . Parameters . GetDerEncoded ( ) ;
803+ var parametersReader = new AsnReader ( parameters , AsnEncodingRules . BER ) ;
804+ var sequenceReader = parametersReader . ReadSequence ( ) ;
805+ parametersReader . ThrowIfNotEmpty ( ) ;
806+
807+ var p = sequenceReader . ReadInteger ( ) ;
808+ var q = sequenceReader . ReadInteger ( ) ;
809+ var g = sequenceReader . ReadInteger ( ) ;
810+ sequenceReader . ThrowIfNotEmpty ( ) ;
811+
812+ var keyReader = new AsnReader ( key , AsnEncodingRules . BER ) ;
813+ var x = keyReader . ReadInteger ( ) ;
814+ keyReader . ThrowIfNotEmpty ( ) ;
815+
816+ var y = BigInteger . ModPow ( g , x , p ) ;
817+
818+ return new DsaKey ( p , q , g , y , x ) ;
819+ }
820+
821+ if ( algorithmOid . Equals ( X9ObjectIdentifiers . IdECPublicKey ) )
822+ {
823+ var parameters = privateKeyInfo . PrivateKeyAlgorithm . Parameters . GetDerEncoded ( ) ;
824+ var parametersReader = new AsnReader ( parameters , AsnEncodingRules . DER ) ;
825+ var curve = parametersReader . ReadObjectIdentifier ( ) ;
826+ parametersReader . ThrowIfNotEmpty ( ) ;
827+
828+ var privateKeyReader = new AsnReader ( key , AsnEncodingRules . DER ) ;
829+ var sequenceReader = privateKeyReader . ReadSequence ( ) ;
830+ privateKeyReader . ThrowIfNotEmpty ( ) ;
831+
832+ var version = sequenceReader . ReadInteger ( ) ;
833+ if ( version != BigInteger . One )
834+ {
835+ throw new NotSupportedException ( string . Format ( CultureInfo . CurrentCulture , "EC version '{0}' is not supported." , version ) ) ;
836+ }
837+
838+ var privatekey = sequenceReader . ReadOctetString ( ) ;
839+
840+ var publicKeyReader = sequenceReader . ReadSequence ( new Asn1Tag ( TagClass . ContextSpecific , 1 , isConstructed : true ) ) ;
841+ var publickey = publicKeyReader . ReadBitString ( out _ ) ;
842+ publicKeyReader . ThrowIfNotEmpty ( ) ;
843+
844+ sequenceReader . ThrowIfNotEmpty ( ) ;
845+
846+ return new EcdsaKey ( curve , publickey , privatekey . TrimLeadingZeros ( ) ) ;
847+ }
848+
849+ if ( algorithmOid . Equals ( EdECObjectIdentifiers . id_Ed25519 ) )
850+ {
851+ return new ED25519Key ( key ) ;
852+ }
853+
854+ throw new SshException ( string . Format ( CultureInfo . InvariantCulture , "Private key algorithm \" {0}\" is not supported." , algorithmOid ) ) ;
855+ }
856+
715857 /// <summary>
716858 /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
717859 /// </summary>
0 commit comments