-
-
Notifications
You must be signed in to change notification settings - Fork 939
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for OpenSSL PKCS#8 private key format #1496
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The chosen approach seems reasonable. I don't think see why any of DsaKey, EcdsaKey, RsaKey types need to change (in particular, breaking changes)
/// Initializes a new instance of the <see cref="DsaKey"/> class. | ||
/// </summary> | ||
/// <param name="privateKeyData">DER encoded private key data.</param> | ||
public DsaKey(byte[] privateKeyData) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it necessary to remove this?
Same applies to the other key constructors
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I remove it because there could be other DER data structures. The current Key constructors with byte array parameter assume the data is DER encoded OpenSSL traditional format which is not always true. It could be PKCS#8 format as well.
In practice, most package consumers are not necessarily call Key constructors directly. It might not bring huge impacts.
Anyway, to gracefully deprecate the constructors with byte array parameter, we can apply Obsolete
attribute to the constructors and remove them in the future release. What do you think? @Rob-Hague
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am sorry, I do not think there is a reason to break the existing contract.
I agree there is probably not many people using these constructors, and if we were starting from scratch we might do it differently. And although we make breaking changes practically every release, I think we can justify each of them. I do not believe this one can be justified.
As library developers, when we make a breaking change we are telling the user: "we expect you to bear the pain for our work". As a user, I can sometimes accept that when the user experience is improved and when it is easy to react to the change. Otherwise I find it annoying and frankly inconsiderate from the developers.
Whether we make this change or not, we experience no pain, so we should not cause any pain to users. We can instead improve things here by clarifying in the documentation that this constructor expects an OpenSSL format.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry I didn't explain well.
The current Key constructors with byte array parameter assume the data is DER encoded OpenSSL traditional PKCS#1 format.
OpenSSL use PKCS#8 by default. It only uses PKCS#1 when option "-traditional" is specified.
The newer "genpkey" command even doesn't show "-traditional" option in the guide.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand the situation, I just don't think it is worth deleting the constructor or changing its behaviour, unless you can somehow detect which format it is using while reading. If not, then the constructor should stay the same
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After some more thinking, I'm leaning towards to just delete those constructors.
We removed old target frameworks. People who are using old frameworks should target to new frameworks. See #1109
We removed legacy algorithms. People who are using legacy algorithms should use new algorithms. See #1442
This change only affects people who are directly calling the constructor with PKCS#1 data. They should use PKCS#8 data instead.
Note: People who are using PrivateKeyFile
are not affected. I believe most people are using PrivateKeyFile
instead of each Key
class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This improvement does not require changing public API. It brings us zero benefits, and brings detriment to users
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If everyone agrees, then I can add them back. Would you mind sharing your thoughts here? @drieseng @WojciechNagorski @mus65
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree removing it is too much. But imho it should at least be obsoleted since it is ambigious and confusing (.NET 9 recently made a similiar API obsolete). But then there should be an alternative public API people can move to (I assume there is? I haven't checked the whole PR).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree removing it is too much. But imho it should at least be obsoleted since it is ambigious and confusing (.NET 9 recently made a similiar API obsolete). But then there should be an alternative public API people can move to (I assume there is? I haven't checked the whole PR).
Thanks for sharing the info. I know I'm not alone.
BTW, Microsoft calls their new design "one method, one purpose" which is exactly what I want to apply for method static string GetCurveOid(string curve_s)
.
@@ -207,34 +206,12 @@ public EcdsaKey(SshKeyData publicKeyData) | |||
/// <summary> | |||
/// Initializes a new instance of the <see cref="EcdsaKey"/> class. | |||
/// </summary> | |||
/// <param name="curve">The curve name.</param> | |||
/// <param name="curve">The curve oid.</param> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a nasty break. Let's try to avoid it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By reading the DER data, we get the curve oid string directly. It looks strange if we convert curve oid string to curve name in PrivateKeyFile and then convert name back to oid string in Ecdsakey.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it's strange and unfortunate, but it's the publicly exposed contract, so my other comment applies here too. If you like, you can change it to accept either the name or oid
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to either document the breaking change in the release notes, or keep the current behavior but increase the code complexity (accept either name or oid). 😢
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be the latter. Does not seem that complex to me
diff --git a/src/Renci.SshNet/Security/Cryptography/EcdsaKey.cs b/src/Renci.SshNet/Security/Cryptography/EcdsaKey.cs
index 70e2e1a9..6f275b20 100644
--- a/src/Renci.SshNet/Security/Cryptography/EcdsaKey.cs
+++ b/src/Renci.SshNet/Security/Cryptography/EcdsaKey.cs
@@ -271,17 +271,20 @@ private static Impl Import(string curve_oid, byte[] publickey, byte[]? privateke
private static string GetCurveOid(string curve_s)
{
- if (string.Equals(curve_s, "nistp256", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(curve_s, "nistp256", StringComparison.OrdinalIgnoreCase)
+ || curve_s == ECDSA_P256_OID_VALUE)
{
return ECDSA_P256_OID_VALUE;
}
- if (string.Equals(curve_s, "nistp384", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(curve_s, "nistp384", StringComparison.OrdinalIgnoreCase)
+ || curve_s == ECDSA_P384_OID_VALUE)
{
return ECDSA_P384_OID_VALUE;
}
- if (string.Equals(curve_s, "nistp521", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(curve_s, "nistp521", StringComparison.OrdinalIgnoreCase)
+ || curve_s == ECDSA_P521_OID_VALUE)
{
return ECDSA_P521_OID_VALUE;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure from coding perspective, it is not complex. It is just the method signature definition. Now the first parameter could be either name or oid. Would it break single responsibility principle?
src/Renci.SshNet/PrivateKeyFile.cs
Outdated
/// <returns>The <see cref="EcdsaKey" />.</returns> | ||
private static EcdsaKey ParseECPrivateKey_SEC1(byte[] keyData) | ||
{ | ||
var keyReader = new AsnReader(keyData, AsnEncodingRules.DER); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on the comment above, should this be
var keyReader = new AsnReader(keyData, AsnEncodingRules.DER); | |
var keyReader = new AsnReader(keyData, AsnEncodingRules.BER); |
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DERs are subset of BERs. I think DERs are enough here since the data is generated using DERs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am reading this part of the comment: "receivers SHOULD be prepared to handle Basic Encoding Rules (BER) and DER"
In my understanding, that means using AsnEncodingRules.BER to match the comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually I'm confused about the doc. Do you know why it says "BER and DER".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess they are trying to be explicit but they do not make it any clearer by doing so
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please restore the public constructors
Sorry about the conflicts
src/Renci.SshNet/PrivateKeyFile.cs
Outdated
/// <returns>The <see cref="EcdsaKey" />.</returns> | ||
private static EcdsaKey ParseECPrivateKey_SEC1(byte[] keyData) | ||
{ | ||
var keyReader = new AsnReader(keyData, AsnEncodingRules.DER); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am reading this part of the comment: "receivers SHOULD be prepared to handle Basic Encoding Rules (BER) and DER"
In my understanding, that means using AsnEncodingRules.BER to match the comment
@@ -207,34 +206,12 @@ public EcdsaKey(SshKeyData publicKeyData) | |||
/// <summary> | |||
/// Initializes a new instance of the <see cref="EcdsaKey"/> class. | |||
/// </summary> | |||
/// <param name="curve">The curve name.</param> | |||
/// <param name="curve">The curve oid.</param> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be the latter. Does not seem that complex to me
diff --git a/src/Renci.SshNet/Security/Cryptography/EcdsaKey.cs b/src/Renci.SshNet/Security/Cryptography/EcdsaKey.cs
index 70e2e1a9..6f275b20 100644
--- a/src/Renci.SshNet/Security/Cryptography/EcdsaKey.cs
+++ b/src/Renci.SshNet/Security/Cryptography/EcdsaKey.cs
@@ -271,17 +271,20 @@ private static Impl Import(string curve_oid, byte[] publickey, byte[]? privateke
private static string GetCurveOid(string curve_s)
{
- if (string.Equals(curve_s, "nistp256", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(curve_s, "nistp256", StringComparison.OrdinalIgnoreCase)
+ || curve_s == ECDSA_P256_OID_VALUE)
{
return ECDSA_P256_OID_VALUE;
}
- if (string.Equals(curve_s, "nistp384", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(curve_s, "nistp384", StringComparison.OrdinalIgnoreCase)
+ || curve_s == ECDSA_P384_OID_VALUE)
{
return ECDSA_P384_OID_VALUE;
}
- if (string.Equals(curve_s, "nistp521", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(curve_s, "nistp521", StringComparison.OrdinalIgnoreCase)
+ || curve_s == ECDSA_P521_OID_VALUE)
{
return ECDSA_P521_OID_VALUE;
}
/// Initializes a new instance of the <see cref="DsaKey"/> class. | ||
/// </summary> | ||
/// <param name="privateKeyData">DER encoded private key data.</param> | ||
public DsaKey(byte[] privateKeyData) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This improvement does not require changing public API. It brings us zero benefits, and brings detriment to users
dfee3ef
to
0c3bcae
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good, thanks for the changes
TBH, I don't like my changes. Life is short. I may leave for a while. |
Understood @scott-xu. I know it has not always been smooth sailing, but you make valuable contributions nonetheless. Thanks |
This PR adds support for PKCS#8 private key format (in practice, OpenSSL is used to generate the keys)
There are several options to add support for PKCS#8 private key format:
ImportPkcs8PrivateKey
orImportFromEncryptedPem
from relevant concrete class. Both methods applied to netstandard2.1+ and Core 3.0+. However, I couldn't find a way to detect the key type before call the methods, for example a static method to load PKCS#8 private key and returns BCL'sAsymmetricAlgorithm
.AsymmetricKeyParameter
. Or use BouncyCastle's underlying PrivateKeyFactory toCreateKey
/DecryptKey
which also returns BouncyCastle'sAsymmetricKeyParameter
. Then we need to bridge BouncyCastle'sAsymmetricKeyParameter
with SSH.NET'sKey
. This option may add some complexity to map types and unnecessary memory allocations.AsnReader
and finally construct the SSH.NET relevant concreteKey
class.With this option, there's no extra NuGet dependency and performance is balanced.
I also considered using
Impl
pattern forPrivateKeyFile
class, but haven't had time to try. We can continue evolving this PR or create a new PR if needs be.We can also consider dropping support for OpenSSL traditional private key format and drop ssh.com private key format as they are old and weak.
Fix #1090