Skip to content
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

Use BCL ECDiffieHellman for KeyExchange instead of BouncyCastle (.NET 8.0 onward only) #1371

Merged
merged 33 commits into from
Jul 31, 2024

Conversation

scott-xu
Copy link
Collaborator

@scott-xu scott-xu commented Apr 6, 2024

This PR leverages BCL's ECDiffieHellman for key exchange instead of using BouncyCastle.
Cryptographic operations in BCL are done by operating system (OS) libraries.

Advantages:

It inherits the advantages described in https://learn.microsoft.com/en-us/dotnet/standard/security/cross-platform-cryptography

  • .NET apps benefit from OS reliability. Keeping cryptography libraries safe from vulnerabilities is a high priority for OS vendors. To do that, they provide updates that system administrators should be applying.
  • .NET apps have access to FIPS-validated algorithms if the OS libraries are FIPS-validated.

Notes:

  1. It is only for .NET 8.0+ because method ECDiffieHellman.DeriveRawSecretAgreement(ECDiffieHellmanPublicKey) is avaliable only at .NET 8.0+. See [API Proposal]: Add ECDiffieHellman.DeriveSecretAgreement method dotnet/runtime#71613.
  2. It will throw PlatformNotSupportedException if the OS is Windows and below Windows 10. See this line of code. For this case, we use BouncyCastle as fallback. Based on below lifecycle table, I think it should be fine to throw PlatformNotSupportedException in Windows 8.1 and Windows Server 2012 R2 rather than use BouncyCastle as fallback. Then we can remove the BouncyCastle dependency when target .NET 8.0 onward.

@scott-xu scott-xu marked this pull request as ready for review April 6, 2024 13:41
@scott-xu scott-xu requested a review from Rob-Hague April 6, 2024 13:41
@scott-xu
Copy link
Collaborator Author

scott-xu commented Apr 6, 2024

Windows 8.1 and Windows Server 2012 R2 does not support ECDiffieHellman.DeriveRawSecretAgreement(ECDiffieHellmanPublicKey).
Here's their lifecyle for reference.

Windows 8.1 Lifecycle

Support Dates

Listing Start Date Mainstream End Date Extended End Date
Windows 8.1 Nov 13, 2013 Jan 9, 2018 Jan 10, 2023

Windows 8.1 EOL announcement: https://learn.microsoft.com/en-us/lifecycle/announcements/windows-8-1-end-support-january-2023

Windows Server 2012 R2 Lifecycle

Support Dates

Listing Start Date Mainstream End Date Extended End Date
Windows Server 2012 R2 Nov 25, 2013 Oct 9, 2018 Oct 10, 2023

Releases

Version Start Date End Date
Extended Security Update Year 3 Oct 15, 2025 Oct 13, 2026
Extended Security Update Year 2 Oct 9, 2024 Oct 14, 2025
Extended Security Update Year 1 Oct 11, 2023 Oct 8, 2024
Original Release Nov 25, 2013 Oct 10, 2023

Windows Server 2012 R2 EOL announcement: https://learn.microsoft.com/en-us/lifecycle/announcements/windows-server-2012-r2-end-of-support

Here's the link for Extended Security Updates Overview

@scott-xu scott-xu self-assigned this Apr 6, 2024
@Rob-Hague
Copy link
Collaborator

Thanks. It's a nice idea but I'm not sure it's worth the additional complexity at this time - with respect to the OS support and that we still have the old code which is no longer exercised in CI. Of course we could add a target for net6.0 on the integration tests but my gut feeling is that this change would be better left for the future. I could be convinced otherwise.

@scott-xu
Copy link
Collaborator Author

scott-xu commented Apr 7, 2024

Thanks, I updated the description with Advantages to elaborate on the change. I also updated note2.

@scott-xu
Copy link
Collaborator Author

scott-xu commented Apr 9, 2024

I think we don't need to worry about OS issue too much. Using an EOL Windows with the latest .NET 8.0+ seems bizarre to me. If they really want, they can use .NET Framework or .NET 6.0. What do you think? @Rob-Hague

@mus65
Copy link
Contributor

mus65 commented Apr 16, 2024

Using an EOL Windows with the latest .NET 8.0+ seems bizarre to me.

I don't think it's that bizarre, I have to deal with this exact situation at work myself. Our application is already on .NET 8 but there are quite a few customers who self-host and are still running Win2012.

But I think dropping support for this would be fine as long as this is mentioned in the release notes. This would actually give me a good reason to drop support for Win2012 in our application. :)

@Rob-Hague
Copy link
Collaborator

I think we can take this, if:

  1. We guard it in the same way the BCL does, with a fallback to BouncyCastle, i.e.:
#if NET8_0_OR_GREATER
if (!OperatingSystem.IsWindows() || OperatingSystem.IsWindowsVersionAtLeast(10))
{
    // BCL stuff
    // wrap this in try { } catch (PlatformNotSupportedException) just to be safe?

    return ...
}
#endif

// BouncyCastle fallback
  1. It provides a worthwhile performance improvement over the BouncyCastle path

We don't want to break anyone for no reason, and we will inevitably depend on BouncyCastle, so the only tangible benefit here might be performance

@scott-xu
Copy link
Collaborator Author

scott-xu commented Jun 13, 2024

Done except

// wrap this in try { } catch (PlatformNotSupportedException) just to be safe?

What do you mean by safe?

@Rob-Hague
Copy link
Collaborator

I was thinking, since support does not seem to be universal across platforms, that maybe we wrap the BCL code in try-catch so that it still falls back to BouncyCastle if it's not supported. But I think it's OK, only Windows seems like a problem case from what I can tell: dotnet/runtime@be823a1

@@ -55,7 +55,7 @@ public override void Start(Session session, KeyExchangeInitMessage message, bool
return;
}
#endif
var curveParameter = SecNamedCurves.GetByName(CurveName);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I can tell, going through SshNamedCurves causes a bunch of extra lookup and static initialization compared to SecNamedCurves. Is there any reason to use it?

It's a shame we can't just directly refer to the static BC object when we know the curve we want.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess SshNamedCurves is designed for libraries like SSH.NET to consume?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe, but I don't see why we would. If I run this program in a loop:

public static void Main()
{
    var s = Stopwatch.StartNew();

    //if (!SecNamedCurves.GetByName("secp256r1").G.IsInfinity)
    if (!SshNamedCurves.GetByName("nistp256").G.IsInfinity)
    {
        Console.WriteLine(s.ElapsedTicks);
    }
}
dotnet build -c Release; do { dotnet run -c Release --no-build } while ($true)

The result is about 28.3±0.7ms vs 29.2±0.8ms. Both extremely high (also includes first JIT of relevant BouncyCastle methods), but 1ms is 1ms

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind trying SecNamedCurves.GetByOid(SecObjectIdentifiers.SecP256r1)?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't try it but I'm pretty sure it would be the same as SecNamedCurves.GetByName. The problem with SshNamedCurves is the amount of static constructors that it forces to run

@Rob-Hague
Copy link
Collaborator

  1. It provides a worthwhile performance improvement over the BouncyCastle path

Any idea here?

@scott-xu
Copy link
Collaborator Author

  1. It provides a worthwhile performance improvement over the BouncyCastle path

Any idea here?

Actually I'm more leaning towards to the policy "we don't implement cryptographic algorithms, but instead defer to an OS implementation" rather than performance improvement.

@scott-xu scott-xu marked this pull request as draft July 29, 2024 06:27
@scott-xu scott-xu marked this pull request as ready for review July 29, 2024 07:32
@scott-xu
Copy link
Collaborator Author

Now the code structure is very similar with AesCipher's implementation. Ready to review and merge @Rob-Hague @WojciechNagorski

Copy link
Collaborator

@Rob-Hague Rob-Hague left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks nice thanks

Comment on lines +48 to +54
else
{
_impl = new BouncyCastleImpl(CurveParameter);
}
#else
_impl = new BouncyCastleImpl(CurveParameter);
#endif
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was going to suggest this but I imagine our style settings might have something to say about it.

Suggested change
else
{
_impl = new BouncyCastleImpl(CurveParameter);
}
#else
_impl = new BouncyCastleImpl(CurveParameter);
#endif
else
#endif
{
_impl = new BouncyCastleImpl(CurveParameter);
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Rob-Hague Rob-Hague merged commit 58c85ee into sshnet:develop Jul 31, 2024
1 check passed
@scott-xu scott-xu deleted the ecdh-bcl branch August 5, 2024 15:15
scott-xu added a commit to scott-xu/SSH.NET that referenced this pull request Aug 9, 2024
Rob-Hague pushed a commit that referenced this pull request Aug 19, 2024
…#1450)

* [AesGcmCipher] Use BouncyCastle as a fallback if BCL does not support.

* Switch back to collection initializer

* Remove conditional compilation

* Throw SshConnectionException with Reason MacError when authentication tag mismatch

* Separate BCL and BouncyCastle implementation

* Update AesGcmCipher.BclImpl.cs

* Naming enhancement

* Remove empty line

* Disable S1199. See #1371 (comment)

* Set InnerException when MAC error. Remove Message check.

* Store KeyParameter as private field

* Use GcmCipher.ProcessAadBytes to avoid the copy of associated data

* Move nonce to constructor to avoid creating AeadParameters each packet

* Use const int for tag size

---------

Co-authored-by: Wojciech Nagórski <wojtpl2@gmail.com>
sendevman pushed a commit to sendevman/SSH.NET that referenced this pull request Sep 1, 2024
… (#1450)

* [AesGcmCipher] Use BouncyCastle as a fallback if BCL does not support.

* Switch back to collection initializer

* Remove conditional compilation

* Throw SshConnectionException with Reason MacError when authentication tag mismatch

* Separate BCL and BouncyCastle implementation

* Update AesGcmCipher.BclImpl.cs

* Naming enhancement

* Remove empty line

* Disable S1199. See sshnet/SSH.NET#1371 (comment)

* Set InnerException when MAC error. Remove Message check.

* Store KeyParameter as private field

* Use GcmCipher.ProcessAadBytes to avoid the copy of associated data

* Move nonce to constructor to avoid creating AeadParameters each packet

* Use const int for tag size

---------

Co-authored-by: Wojciech Nagórski <wojtpl2@gmail.com>
@mattzink
Copy link

So this won't remove the BouncyCastle reference for .NET 8+? That's a bummer for us since we are forbidden to ship BouncyCastle library, as it's been a historically high source of security vulnerabilities.

@Rob-Hague
Copy link
Collaborator

Correct 😕 the library in previous versions was already using an internal point-in-time copy of BouncyCastle for ECDH, plus an internal point-in-time copy of Chaos.NaCl for ed25519. Now the supported BouncyCastle library is used for both.

We decided the benefits of directly referencing the library (not having unmaintained copies of cryptography implementations) outweighed the drawbacks (size on disk, potential false positive vulnerabilities from unused parts of the library)

Unfortunately, even .NET 9 does not provide the APIs we would need to remove third-party dependencies

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants