1
- #nullable enable
1
+ #nullable enable
2
2
using System ;
3
3
using System . Collections . Generic ;
4
4
using System . Diagnostics ;
5
5
using System . Diagnostics . CodeAnalysis ;
6
+ using System . Formats . Asn1 ;
6
7
using System . Globalization ;
7
8
using System . IO ;
8
9
using System . Numerics ;
9
10
using System . Security . Cryptography ;
10
11
using System . Text ;
11
12
using System . Text . RegularExpressions ;
12
13
14
+ using Org . BouncyCastle . Asn1 . EdEC ;
15
+ using Org . BouncyCastle . Asn1 . Pkcs ;
16
+ using Org . BouncyCastle . Asn1 . X9 ;
17
+ using Org . BouncyCastle . Pkcs ;
18
+
13
19
using Renci . SshNet . Common ;
14
20
using Renci . SshNet . Security ;
15
21
using Renci . SshNet . Security . Cryptography ;
@@ -36,12 +42,12 @@ namespace Renci.SshNet
36
42
/// <description>ECDSA 256/384/521 in OpenSSL PEM and OpenSSH key format</description>
37
43
/// </item>
38
44
/// <item>
39
- /// <description>ED25519 in OpenSSH key format</description>
45
+ /// <description>ED25519 in OpenSSL PEM and OpenSSH key format</description>
40
46
/// </item>
41
47
/// </list>
42
48
/// </para>
43
49
/// <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 :
45
51
/// <list type="bullet">
46
52
/// <item>
47
53
/// <description>DES-EDE3-CBC</description>
@@ -62,6 +68,19 @@ namespace Renci.SshNet
62
68
/// <description>AES-256-CBC</description>
63
69
/// </item>
64
70
/// </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>
65
84
/// The following encryption algorithms are supported for OpenSSH format:
66
85
/// <list type="bullet">
67
86
/// <item>
@@ -99,7 +118,7 @@ namespace Renci.SshNet
99
118
/// </remarks>
100
119
public partial class PrivateKeyFile : IPrivateKeySource , IDisposable
101
120
{
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> *-+" ;
103
122
104
123
#if NET7_0_OR_GREATER
105
124
private static readonly Regex PrivateKeyRegex = GetPrivateKeyRegex ( ) ;
@@ -233,6 +252,11 @@ private void Open(Stream privateKey, string? passPhrase)
233
252
}
234
253
235
254
var keyName = privateKeyMatch . Result ( "${keyName}" ) ;
255
+ if ( ! keyName . EndsWith ( "PRIVATE KEY" , StringComparison . Ordinal ) )
256
+ {
257
+ throw new SshException ( "Invalid private key file." ) ;
258
+ }
259
+
236
260
var cipherName = privateKeyMatch . Result ( "${cipherName}" ) ;
237
261
var salt = privateKeyMatch . Result ( "${salt}" ) ;
238
262
var data = privateKeyMatch . Result ( "${data}" ) ;
@@ -288,7 +312,7 @@ private void Open(Stream privateKey, string? passPhrase)
288
312
289
313
switch ( keyName )
290
314
{
291
- case "RSA" :
315
+ case "RSA PRIVATE KEY " :
292
316
var rsaKey = new RsaKey ( decryptedData ) ;
293
317
_key = rsaKey ;
294
318
_hostAlgorithms . Add ( new KeyHostAlgorithm ( "ssh-rsa" , _key ) ) ;
@@ -297,16 +321,17 @@ private void Open(Stream privateKey, string? passPhrase)
297
321
_hostAlgorithms . Add ( new KeyHostAlgorithm ( "rsa-sha2-256" , _key , new RsaDigitalSignature ( rsaKey , HashAlgorithmName . SHA256 ) ) ) ;
298
322
#pragma warning restore CA2000 // Dispose objects before losing scope
299
323
break ;
300
- case "DSA" :
324
+ case "DSA PRIVATE KEY " :
301
325
_key = new DsaKey ( decryptedData ) ;
302
326
_hostAlgorithms . Add ( new KeyHostAlgorithm ( "ssh-dss" , _key ) ) ;
303
327
break ;
304
- case "EC" :
328
+ case "EC PRIVATE KEY " :
305
329
_key = new EcdsaKey ( decryptedData ) ;
306
330
_hostAlgorithms . Add ( new KeyHostAlgorithm ( _key . ToString ( ) , _key ) ) ;
307
331
break ;
308
- case "OPENSSH" :
309
- _key = ParseOpenSshV1Key ( decryptedData , passPhrase ) ;
332
+ case "PRIVATE KEY" :
333
+ var privateKeyInfo = PrivateKeyInfo . GetInstance ( binaryData ) ;
334
+ _key = ParseOpenSslPkcs8PrivateKey ( privateKeyInfo ) ;
310
335
if ( _key is RsaKey parsedRsaKey )
311
336
{
312
337
_hostAlgorithms . Add ( new KeyHostAlgorithm ( "ssh-rsa" , _key ) ) ;
@@ -315,13 +340,55 @@ private void Open(Stream privateKey, string? passPhrase)
315
340
_hostAlgorithms . Add ( new KeyHostAlgorithm ( "rsa-sha2-256" , _key , new RsaDigitalSignature ( parsedRsaKey , HashAlgorithmName . SHA256 ) ) ) ;
316
341
#pragma warning restore CA2000 // Dispose objects before losing scope
317
342
}
343
+ else if ( _key is DsaKey parsedDsaKey )
344
+ {
345
+ _hostAlgorithms . Add ( new KeyHostAlgorithm ( "ssh-dss" , _key ) ) ;
346
+ }
318
347
else
319
348
{
320
349
_hostAlgorithms . Add ( new KeyHostAlgorithm ( _key . ToString ( ) , _key ) ) ;
321
350
}
322
351
323
352
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" :
325
392
var reader = new SshDataReader ( decryptedData ) ;
326
393
var magicNumber = reader . ReadUInt32 ( ) ;
327
394
if ( magicNumber != 0x3f6ff9eb )
@@ -488,8 +555,8 @@ private static byte[] DecryptKey(CipherInfo cipherInfo, byte[] cipherData, strin
488
555
}
489
556
490
557
/// <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"/> .
493
560
/// </summary>
494
561
/// <param name="keyFileData">The key file data (i.e. base64 encoded data between the header/footer).</param>
495
562
/// <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)
712
779
return parsedKey ;
713
780
}
714
781
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
+
715
857
/// <summary>
716
858
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
717
859
/// </summary>
0 commit comments