Failure parsing private key with PEM format generated by OpenSSH 9.9p1 (macOS 15.4)
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:
- Generate a private key on macOS 15.4 using the following command:
ssh-keygen -t ecdsa -b 521 -m PEM -f ./pkey.pem -q
- Add the following code to a .NET app:
var pkey = new PrivateKeyFile("<path to pkey.pem>");
- 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.
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 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
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)