SSH.NET icon indicating copy to clipboard operation
SSH.NET copied to clipboard

Failure parsing private key with PEM format generated by OpenSSH 9.9p1 (macOS 15.4)

Open emaf opened this issue 9 months ago • 3 comments

The OpenSSH version that macOS 15.4 ships (9.9p1) generates PEM private keys including some extra metadata that makes SSH.NET fail to parse it.

Steps to reproduce it:

  1. Generate a private key on macOS 15.4 using the following command:
ssh-keygen -t ecdsa -b 521 -m PEM -f ./pkey.pem -q
  1. Add the following code to a .NET app:
var pkey = new PrivateKeyFile("<path to pkey.pem>");
  1. Run the app and you will get the following exception:
Exception has occurred: CLR/System.Formats.Asn1.AsnContentException
An unhandled exception of type 'System.Formats.Asn1.AsnContentException' occurred in System.Formats.Asn1.dll: 'The provided data is tagged with 'Universal' class value '16', but it should have been 'Universal' class value '6'.'
   at System.Formats.Asn1.AsnDecoder.CheckExpectedTag(Asn1Tag tag, Asn1Tag expectedTag, UniversalTagNumber tagNumber)
   at System.Formats.Asn1.AsnDecoder.GetPrimitiveContentSpan(ReadOnlySpan`1 source, AsnEncodingRules ruleSet, Asn1Tag expectedTag, UniversalTagNumber tagNumber, Int32& bytesConsumed)
   at System.Formats.Asn1.AsnDecoder.ReadObjectIdentifier(ReadOnlySpan`1 source, AsnEncodingRules ruleSet, Int32& bytesConsumed, Nullable`1 expectedTag)
   at System.Formats.Asn1.AsnReader.ReadObjectIdentifier(Nullable`1 expectedTag)
   at Renci.SshNet.Security.EcdsaKey..ctor(Byte[] data)
   at Renci.SshNet.PrivateKeyFile.Open(Stream privateKey, String passPhrase)
   at Renci.SshNet.PrivateKeyFile..ctor(String fileName, String passPhrase, String certificateFileName)
   at Renci.SshNet.PrivateKeyFile..ctor(String fileName)
   at Program.<Main>$(String[] args) in 

If you compare the result of running the same ssh-keygen command in macOS 15.4 (OpenSSH 9.9p1) and macOS 14.6.1 (OpenSHH 9.7p1), you'll see the newer version generates a lot of extra content that seems to be the culprit of the parsing content exception.

emaf avatar Apr 09 '25 22:04 emaf

I'm not able to reproduce it with 9.9p2 in WSL. I get a key like:

-----BEGIN EC PRIVATE KEY-----
MIHcAgEBBEIAPrBErAKobrDch+bduyeZ+bQfOjhKNPuwUsZlLCg1wpRsbDXSIS0x
QQe4wldV7bAOTh7GqAPgN9W5SswNmRK86J+gBwYFK4EEACOhgYkDgYYABADJL9qw
3O5ASozmvJwTG1yL2t/9U9na52ssbZvNirmCxbsZhAQ/C8XuzWWOveBvFtQbXtJ7
2fXWn0FiCB1hwmj8awGZLQJKvImjOr64c/Pn9jS3H/lQJW3iewUgqeVIziOMC3G1
y5tS8eZ9JsWDJs5DO8eO/Zkrqznj8wxPFQ2MVy+bGA==
-----END EC PRIVATE KEY-----

$ openssl asn1parse -in pkey.pem -i
    0:d=0  hl=3 l= 220 cons: SEQUENCE
    3:d=1  hl=2 l=   1 prim:  INTEGER           :01
    6:d=1  hl=2 l=  66 prim:  OCTET STRING      [HEX DUMP]:003EB044AC02A86EB0DC87E6DDBB2799F9B41F3A384A34FBB052C6652C2835C2946C6C35D2212D314107B8C25755EDB00E4E1EC6A803E037D5B94ACC0D9912BCE89F
   74:d=1  hl=2 l=   7 cons:  cont [ 0 ]
   76:d=2  hl=2 l=   5 prim:   OBJECT            :secp521r1
   83:d=1  hl=3 l= 137 cons:  cont [ 1 ]
   86:d=2  hl=3 l= 134 prim:   BIT STRING

Can you paste a key here which doesn't work?

Rob-Hague avatar Apr 16 '25 16:04 Rob-Hague

@Rob-Hague this is what I get with 9.9p1 in macOS 15.4.1

-----BEGIN EC PRIVATE KEY-----
MIICnQIBAQRCAVNb14beg8V2WP3f0ojF7b4wQ8o/6zoWe48NOExO6P0OhXk5VzTw
XoWRXgcFr0T3LfWelaXcKOJ6CGhdrMB2gKJ3oIIBxjCCAcICAQEwTQYHKoZIzj0B
AQJCAf//////////////////////////////////////////////////////////
////////////////////////////MIGeBEIB////////////////////////////
//////////////////////////////////////////////////////////wEQVGV
PrlhjhyaH5KaIaC2hUDuotpyW5mzFfO4tImRjvEJ4VYZOVHsfpN7FlLAvTuxvwc1
c9+IPSw08e9FH9RrUD8AAxUA0J6IACkcuFOWzGcXOTKEqqDaZLoEgYUEAMaFjga3
BATpzZ4+y2YjlbRCnGSBOQU/tSH4KK9ga009uqFLXnfv51ko/h3BJ6L/qN4zSLPB
hWpCm/l+fjHC5b1mARg5KWp4mjvABFyKX7QsfRvZmPVESVebRGgXr70XJz5mLJfu
cple9CZAxVC5AT+tB2E1PHCGonLCQIi+lHaf0WZQAkIB////////////////////
///////////////////////6UYaHg78vlmt/zAFI9wml0Du1ybiJnEeuu2+3HpE4
ZAkCAQGhgYkDgYYABADdJGVfsxtBKv4PHOQkfbKTZ0AFiWVeuUUJekfq9vgDPxAb
T2W7/z9INXUt0pgEqyAGabX5NLuTZ7Nyzx3SRTxbGAGQ+z6ErrcN0azmOvuGfObl
y9QbFh1LDYmZegwyANj/ciupbd4pdUPR1CAAOJyaBsTDU+3mjpR40lG6ktKOitQ8
Ig==
-----END EC PRIVATE KEY-----

openssl asn1parse -in pkey2.pem -i                 
    0:d=0  hl=4 l= 669 cons: SEQUENCE          
    4:d=1  hl=2 l=   1 prim:  INTEGER           :01
    7:d=1  hl=2 l=  66 prim:  OCTET STRING      [HEX DUMP]:01535BD786DE83C57658FDDFD288C5EDBE3043CA3FEB3A167B8F0D384C4EE8FD0E8579395734F05E85915E0705AF44F72DF59E95A5DC28E27A08685DACC07680A277
   75:d=1  hl=4 l= 454 cons:  cont [ 0 ]        
   79:d=2  hl=4 l= 450 cons:   SEQUENCE          
   83:d=3  hl=2 l=   1 prim:    INTEGER           :01
   86:d=3  hl=2 l=  77 cons:    SEQUENCE          
   88:d=4  hl=2 l=   7 prim:     OBJECT            :prime-field
   97:d=4  hl=2 l=  66 prim:     INTEGER           :01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
  165:d=3  hl=3 l= 158 cons:    SEQUENCE          
  168:d=4  hl=2 l=  66 prim:     OCTET STRING      [HEX DUMP]:01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC
  236:d=4  hl=2 l=  65 prim:     OCTET STRING      [HEX DUMP]:51953EB9618E1C9A1F929A21A0B68540EEA2DA725B99B315F3B8B489918EF109E156193951EC7E937B1652C0BD3BB1BF073573DF883D2C34F1EF451FD46B503F00
  303:d=4  hl=2 l=  21 prim:     BIT STRING        
  326:d=3  hl=3 l= 133 prim:    OCTET STRING      [HEX DUMP]:0400C6858E06B70404E9CD9E3ECB662395B4429C648139053FB521F828AF606B4D3DBAA14B5E77EFE75928FE1DC127A2FFA8DE3348B3C1856A429BF97E7E31C2E5BD66011839296A789A3BC0045C8A5FB42C7D1BD998F54449579B446817AFBD17273E662C97EE72995EF42640C550B9013FAD0761353C7086A272C24088BE94769FD16650
  462:d=3  hl=2 l=  66 prim:    INTEGER           :01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA51868783BF2F966B7FCC0148F709A5D03BB5C9B8899C47AEBB6FB71E91386409
  530:d=3  hl=2 l=   1 prim:    INTEGER           :01
  533:d=1  hl=3 l= 137 cons:  cont [ 1 ]        
  536:d=2  hl=3 l= 134 prim:   BIT STRING     

emaf avatar Apr 21 '25 17:04 emaf

The key is using explicit parameters rather than a named curve, despite actually appearing to be secp521r1 by comparison of the parameters. The trouble is, SSH uses named algorithms e.g. ecdsa-sha2-nistp521 which would necessitate inspecting the parameters to figure out if it is a known curve. It would not be that hard since we only support nistp256, nistp384, nistp521 atm (per https://datatracker.ietf.org/doc/html/rfc5656#section-10.1).

At a glance, https://github.com/openssh/openssh-portable/commit/7bdfc20516e288b58c8c847958059c7b141eeff9 appears to be related. It adds some logic to do that mapping to named curves.

The parameter structure is defined on pages 102 & 103 of https://www.secg.org/sec1-v2.pdf. The .NET implementation is at

https://github.com/dotnet/runtime/blob/1a7343c82afe5d8f527d51b7946900640b7d6b03/src/libraries/Common/src/System/Security/Cryptography/Asn1/ECPrivateKey.xml.cs#L87 https://github.com/dotnet/runtime/blob/1a7343c82afe5d8f527d51b7946900640b7d6b03/src/libraries/Common/src/System/Security/Cryptography/Asn1/ECDomainParameters.xml.cs#L99 https://github.com/dotnet/runtime/blob/1a7343c82afe5d8f527d51b7946900640b7d6b03/src/libraries/Common/src/System/Security/Cryptography/Asn1/SpecifiedECDomain.xml.cs#L96

(we could use ECDsa.ImportECPrivateKey but it is not available on net462 and netstandard2.0. maybe BouncyCastle can help - but this still does not solve the mapping to named curves)

Rob-Hague avatar Apr 26 '25 19:04 Rob-Hague