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

Fold inbox crypto into one assembly #64307

Merged
merged 16 commits into from
Jan 30, 2022
Merged

Conversation

bartonjs
Copy link
Member

@bartonjs bartonjs commented Jan 26, 2022

This change does not move any of the tests associated with the code moves.

For System.Security.Cryptography.Csp.Tests.dll,
System.Security.Cryptography.Cng.Tests.dll, and
System.Security.Cryptography.OpenSsl.Tests.dll, the crypto test infrastructure needs
to be updated to allow for multiple algorithm providers to be tested at once.
Currently, those test libraries have static provider/factory types that say that (e.g.)
the RSA to test is RSA.Create() / RSACryptoServiceProvider / RSACng / RSAOpenSsl.

For System.Security.Cryptography.X509Certificates.Tests.dll, the tests could be moved,
but require a different level of thinking for path and namespace unification than was
required for previous phases. So, for consistency within this PR, the X509 tests also
aren't moving yet.

This unification revealed several places were our OS support attributes weren't quite right,
due to implementations being automatically generated as PNSE.

  • Constructors of CNG types all gained [SupportedOS("windows")]
  • Constructors of OpenSsl types all gained [UnsupportedOS("android")] / browser / ios / tvos / windows (works on macOS and linux)
  • Constructors and static methods on X509Certificate(2) and the X509Chain.Build method gained [UnsupportedOS("browser")]
  • The CertificateRequest class gained [UnsupportedOS("browser")]. It's unlikely to be used as an exchange type, so the type got painted instead of constructors.

@dotnet-issue-labeler
Copy link

Note regarding the new-api-needs-documentation label:

This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, to please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change.

@ghost
Copy link

ghost commented Jan 26, 2022

Tagging subscribers to this area: @dotnet/area-system-security, @vcsjones
See info in area-owners.md if you want to be subscribed.

Issue Details

This change does not move any of the tests associated with the code moves.

For System.Security.Cryptography.Csp.Tests.dll,
System.Security.Cryptography.Cng.Tests.dll, and
System.Security.Cryptography.OpenSsl.Tests.dll, the crypto test infrastructure needs
to be updated to allow for multiple algorithm providers to be tested at once.
Currently, those test libraries have static provider/factory types that say that (e.g.)
the RSA to test is RSA.Create() / RSACryptoServiceProvider / RSACng / RSAOpenSsl.

For System.Security.Cryptography.X509Certificates.Tests.dll, the tests could be moved,
but require a different level of thinking for path and namespace unification than was
required for previous phases. So, for consistency within this PR, the X509 tests also
aren't moving yet.

This unification revealed several places were our OS support attributes weren't quite right,
due to implementations being automatically generated as PNSE.

  • Constructors of CNG types all gained [SupportedOS("windows")]
  • Constructors of OpenSsl types all gained [UnsupportedOS("browser")] / ios / tvos / windows (works on macOS and linux)
  • Constructors and static methods on X509Certificate(2) and the X509Chain.Build method gained [UnsupportedOS("browser")]
  • The CertificateRequest class gained [UnsupportedOS("browser")]. It's unlikely to be used as an exchange type, so the type got painted instead of constructors.
Author: bartonjs
Assignees: bartonjs
Labels:

area-System.Security, new-api-needs-documentation

Milestone: -

@@ -11,432 +11,425 @@

namespace System.Security.Cryptography
{
#if INTERNAL_ASYMMETRIC_IMPLEMENTATIONS
internal static partial class DSAImplementation
public sealed partial class DSACng : DSA
Copy link
Member Author

Choose a reason for hiding this comment

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

All of the implementation files that are in libraries/Common/src/... could be moved to under System.Security.Cryptography at this point; but I chose not to move them at this time to keep the PR a bit more... sane.

@@ -115,14 +112,17 @@ internal static CngAlgorithm EcdhCurveNameToAlgorithm(string name)
{
case "nistP256":
case "ECDH_P256":
case "ECDSA_P256":
Copy link
Member Author

Choose a reason for hiding this comment

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

There were two copies of these, one had this line, the other didn't. Apparently one got hit by ECDiffieHellman.Create(), the other git hit by ECDiffieHellmanCng.

@bartonjs
Copy link
Member Author

@vcsjones / @filipnavara The failing tests all seem to look like

using (ECDsa key = ECDsa.Create(...))
{
    Make/get a cert from somewhere
    cert.CopyWithPrivateKey(key);  // <-- fails
}

This is because previously the as check in CopyWithPrivateKey failed for ECDsa.Create() (it was an ECSecurityTransforms, but from a different assembly, so the as said no/null).

Presumably the failure is that either
a) we never tested cert.CopyWithPrivateKey using an ECC key from a keychain (aka we never tested the as being true)
b) ECDsa.Create() and cert.GetECDsaPrivateKey() are doing something different (some other ephemeral key vs keychain keys?)

Having gotten that far in the investigation my brain is sort of fried, so looking for easy pointers on how to update the check (

public ICertificatePal CopyWithPrivateKey(ECDsa privateKey)
{
var typedKey = privateKey as ECDsaImplementation.ECDsaSecurityTransforms;
if (typedKey != null)
{
return CopyWithPrivateKey(typedKey.GetKeys());
) easily. I mean... I could easily make ECDsa.Create() and ECDiffieHellman.Create() both use the wrapper type that CNG/OpenSsl use... that'd probably hide the problem in the short term... but if there's an easier check, I'd love to know what it is.

Interestingly, RSA doesn't have that problem, and either DSA doesn't or we don't test DSA.

@vcsjones
Copy link
Member

@bartonjs I can take a look Friday / Saturday. This rings a bell when I spiked out the key services changes.

@vcsjones
Copy link
Member

Sounds like we're running in to the situation I ran in to in the 3rd paragraph of my spike: #38101

@filipnavara
Copy link
Member

filipnavara commented Jan 28, 2022

It definitely rings a bell. I think I intentionally ignored the scenario because it couldn't happen when the implementations were in different assemblies. Mixing the two types of keys (keychain/CSSM vs. the new iOS-style one) just doesn't work in many cases so recreating the ephemeral keypair by export+import is probably the best that can be done. It should also result in the fast native implementation being used. The CopyWithPrivateKey(SecKeyPair) code path would work only if both keys come from keychain.

Essentially it boils down to couple of options:

  • Removing the CopyWithPrivateKey(SecKeyPair) code path altogether for ECC. Presumably it doesn't make much sense to use two implementations from keychain here. Even if you would then the export+import code path would result in the faster ECC implementation being used.
  • Tracking in ECDsaSecurityTransforms which type of key it uses and keeping the CopyWithPrivateKey(SecKeyPair) code path only when both keys originated from the keychain.
  • Detecting the failure in CopyWithPrivateKey(SecKeyPair) and doing a fallback to export+import.
  • Updating CopyWithPrivateKey(SecKeyPair) to do the export+import fallback in the native code.

Interestingly, RSA doesn't have that problem...

The Apple's code is much more forgiving when mixing the legacy CSSM keys and the iOS-style keys with RSA. However, it could be a hidden performance trap. If the CopyWithPrivateKey(SecKeyPair) code path succeeds now it would likely return an implementation backed by the slow CSSM keys. It's likely not going to be a huge issue but definitely something to be aware of. Nevermind, the performance trap is non-existant since it ends up in ImportEphemeralKey anyway and thus always creates CSSM keys.

...and either DSA doesn't or we don't test DSA.

DSA has only single legacy implementation (CSSM) in the Apple's native code. There's no DSA in iOS and also no iOS-style DSA keys on macOS. Thus SecKeyRef always uses the same backing implementation and doesn't run into problems.

@filipnavara
Copy link
Member

Here's a proof of concept fix: ed16db6

@bartonjs
Copy link
Member Author

Removing the CopyWithPrivateKey(SecKeyPair) code path altogether for ECC. Presumably it doesn't make much sense to use two implementations from keychain here.

Well... that path would be the only one that works if you have a non-exportable key stored in a keychain. Since we don't expose the Apple interop types as public API the only way to get one of those is from GetECDsaPrivateKey(). (Which, basically limits the scenario to "I want to renew/regen a cert with the same key", I suppose)

Thanks for the workaround code. I don't like duplicating the logic of the "internal representation" of Apple EC keys, but I think I see a path forward from that.

@filipnavara
Copy link
Member

Well... that path would be the only one that works if you have a non-exportable key stored in a keychain. Since we don't expose the Apple interop types as public API the only way to get one of those is from GetECDsaPrivateKey(). (Which, basically limits the scenario to "I want to renew/regen a cert with the same key", I suppose)

I had the same line of thinking and went back and forth with striking the option from the list :-) Eventually I left it there to paint the full picture so someone without sleep deprivation can re-read it...

Thanks for the workaround code. I don't like duplicating the logic of the "internal representation" of Apple EC keys, but I think I see a path forward from that.

Not a big fan of it myself but I wanted to unblock the PR.

I've tried to implement other options from the list but they were quite a big mess. The iOS-style keys only support X.963 format export, the legacy SecItemExport API doesn't work for them at all. That basically ruled out handling the situation in native code because it would get too complex. So next best thing is handling it in the managed part of the code and taking different path for iOS-style keys and the CSSM ones. The key type can be inferred by calling SecKeyCopyExternalRepresentation, fails for CSSM keys and succeeds for iOS-style ones. It's a relatively cheap call that just copies an array but it felt wrong to just throw away the array.

@bartonjs
Copy link
Member Author

Instead of duplicating the data format handler, I factored it out in 1243105. More total lines, but gives me a bit of a warm fuzzy feeling.

@filipnavara
Copy link
Member

Tests look good :shipit:

@bartonjs
Copy link
Member Author

Rebased for merge conflicts... again... hopefully for the last time.

@@ -94,7 +91,7 @@ public static HashAlgorithmName NameOrOidToHashAlgorithmName(string nameOrOid)
return null;
}

public static HashAlgorithmName? OidToHashAlgorithmName(string oid)
internal static HashAlgorithmName? OidToHashAlgorithmName(string oid)
Copy link
Member

@vcsjones vcsjones Jan 29, 2022

Choose a reason for hiding this comment

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

I think this can be replaced entirely by HashAlgorithmName.TryFromOid now. Don't have to do it here, just leaving a note as something one of us can do afterward.

// Do not wrap ExportPkcs8PrivateKey, let it fall back to reconstructing it from parameters
// so that the ECDiffieHellman.Create()-returned object uses the same set of attributes on all platforms.
// (CNG adds the key usage attribute to distinguish ECDSA from ECDH)
//public override byte[] ExportPkcs8PrivateKey() => _wrapped.ExportPkcs8PrivateKey();
Copy link
Member

Choose a reason for hiding this comment

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

Interesting. It this just so we get bit-for-bit (or thereabouts) the same exports on platforms? Seems reasonable, but is there now a discrepancy between this attribute's presence between encrypted and non-encrypted PKCS8 exports?

Copy link
Member Author

Choose a reason for hiding this comment

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

Hm, you're right, I might need to block the encrypted ones as well.

We had a test for the unencrypted ones. Encrypted export from CNG is special, since they have complicated export permissions... but that shouldn't apply for ECDsa.Create() keys.

public static partial ECDsa Create(ECParameters parameters)
{
ECDsa ecdsa = Create();
ecdsa.ImportParameters(parameters);
Copy link
Member

Choose a reason for hiding this comment

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

Do we want to catch, dispose, re-throw here?

public static partial ECDsa Create(ECParameters parameters)
{
ECDsa ec = new ECDsaCng();
ec.ImportParameters(parameters);
Copy link
Member

Choose a reason for hiding this comment

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

Same catch, dispose, re-throw question.

ECDsa ec = new ECDsaImplementation.ECDsaCng();
ec.ImportParameters(parameters);
return ec;
_core.Dispose();
Copy link
Member

Choose a reason for hiding this comment

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

Should this be inside if (disposing)? Is finalizer somewhere going to mean _core.Dispose() going to get disposed twice?

Copy link
Member Author

Choose a reason for hiding this comment

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

For sanity/consistency, probably. But since the Cng classes are sealed, there's not a sneaky place a finalizer can come in. While it looks like I massively rewrote this file, it's really "move the one from the other project over top this", where the Dispose already looked like that.


protected override void Dispose(bool disposing)
{
_core.Dispose();
Copy link
Member

Choose a reason for hiding this comment

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

Another if (disposing) question.

@vcsjones
Copy link
Member

Aside from the ref SHA512CryptoServiceProvider, everything else is just some questions or nits.

Copy link
Member

@vcsjones vcsjones left a comment

Choose a reason for hiding this comment

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

I wouldn't consider any of my remaining feedback blocking. It can be done in follow up PRs, if chosen to be done at all.


protected abstract bool ReleaseNativeHandle();
}
public sealed partial class SafeNCryptKeyHandle : SafeNCryptHandle
Copy link
Member

Choose a reason for hiding this comment

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

nit missing newline before type

Copy link
Member

Choose a reason for hiding this comment

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

and below..

{
return new ECDsaImplementation.ECDsaCng();
get
Copy link
Member

Choose a reason for hiding this comment

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

someone can change these to expression bodied form someday.

Copy link
Member

@danmoseley danmoseley left a comment

Choose a reason for hiding this comment

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

Is there a note somewhere explaining the resaoning behind these moves, as I'm out of the loop?

@danmoseley danmoseley merged commit c12bea8 into dotnet:main Jan 30, 2022
@vcsjones
Copy link
Member

@danmoseley the issue for this is at #55690.

@ericstj
Copy link
Member

ericstj commented Jan 31, 2022

@ViktorHofer -- seems like this should reduce the number of P2Ps we require in the repo if I'm not mistaken. Could be interesting for the build perf work. @bartonjs -- did you consider removing P2Ps to assemblies which are now full facades from other projects in the repo?

@bartonjs
Copy link
Member Author

@ericstj I feel like I tried it in the earlier phases and almost everything had to be put back (might be observation bias error), so I didn't look into it for this phase. But now that it's complete I can believe there's probably some reduction to be had.

@EgorBo
Copy link
Member

EgorBo commented Feb 7, 2022

@bartonjs I'm seeing noticeable improvements for "publish size", this PR is the only suspect 👍
image

https://aka.ms/aspnet/benchmarks
10th page,

@ghost ghost locked as resolved and limited conversation to collaborators Mar 10, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants