0

Consider this test:

  1. I open a cmd prompt.
  2. dotnet new console --name Keys
  3. Replace the contents of Program.cs with the following code snippet:
using System.Security.Cryptography;

const string originalKey = "MIICXQIBAAKBgQC9/9FGsQFqJim5XaNp12yHjySy3RO9q0ZWPl97bmep4XR/Tx3bDfTPzIcp/NWrRQP0XSbSwBTFq2ypYWXLtWg5CCMtrwzkK0iSy5KrKA1XwsatAW/AfmCtbyUK0BUfs/D54vXdB0WS48TK+Ab/sIcvupag4O4e9NFB+y/dlr4MVwIDAQABAoGASsSm2EjDo8AM31NIAViy7s2XxYNWR2dlMH8vF+WkiaedLpQ1zYQ6eKOl9RH4C4QHQFx/8KOCCR+ijS003+stbcZtPrigAFTLU3bmbBbSfkQjvgMCgMiiqIITlnFFPWI5OqBn5PcKqun6EJvvUxdrAxXQEuXFH9j34O6t0wVJ+CECQQDjAPy1sCAyDkSYRtcolAhbE8TcUGk8atchILdyE/sc0JbssdcZDfvMqdRC155W8V3iPgm3iVZs2WDGDum24mCJAkEA1kTIeJIt9X+1MZcKPi7ArZSCyiwbsDo874mDEIrk3l8h43rcyVq61qdlRGcdyLMT2NSzxVsebsIUez2kiD0N3wJAUalRP6sUae1oD7+sNxTJzLnX38mtkeZ9bZVvaMJ3W25OXOe9EW5OXtnZWhJnC6/YrkLTDAuD47Rvc9B5kyjswQJBALWHUpwrpDpANt9LikcCTwUANApach7MSEHcK6kBM0NeL5TMy27fqjkfWsEn52jYprDmC2PhfZfyX23F3LX7m9sCQQC1uu65Dwl/UopM34Km7NRm+N1TC26UiaWxcYXgYafE22Dy2XGhUMpolIAMoz9wkw2HW4QihtZ6Jwq6VXbOdQu4";

// Import the key
using var rsa = RSA.Create();
rsa.ImportRSAPrivateKey(Convert.FromBase64String(originalKey), out _);

// Export the key
var exported = Convert.ToBase64String(rsa.ExportRSAPrivateKey());

if (exported == originalKey)
    Console.WriteLine("Pass");
else
    Console.WriteLine($"Failed: exported key is \"{exported}\".");
  1. dotnet run

I have run this test on three different machines, which I'll call Alice, Bob, and Carrie.

Alice has dotnet --version 6.0.400, Windows 10, and Visual Studio 2022. On Alice, this test prints "Pass".

Bob has dotnet --version 6.0.400, Windows 10, and Visual Studio 2019. On Bob, it prints "Pass".

Carrie has dotnet --version 6.0.400, Windows Server 2016 (Version 1607), and Visual Studio 2019. On Carrie, it prints "Failed" with this exported key:

MIICXQIBAAKBgQC9/9FGsQFqJim5XaNp12yHjySy3RO9q0ZWPl97bmep4XR/Tx3bDfTPzIcp/NWrRQP0XSbSwBTFq2ypYWXLtWg5CCMtrwzkK0iSy5KrKA1XwsatAW/AfmCtbyUK0BUfs/D54vXdB0WS48TK+Ab/sIcvupag4O4e9NFB+y/dlr4MVwIDAQABAoGAC29hFg3DKwipoYlm3hDkFvM2NI76XYOjE7+57sDXUQchBCSBLyo+M1945xMGJ8JbRD1y/7jQceZ+VLdoRq61W1bOG+MHI6jidcuqKNZkTrDERuSxbO1kIA0/+6zIfXn4z5ok10AWYX8o4CEB5zx0w8CkHG8XPHs7R1tiDegVGNECQQDjAPy1sCAyDkSYRtcolAhbE8TcUGk8atchILdyE/sc0JbssdcZDfvMqdRC155W8V3iPgm3iVZs2WDGDum24mCJAkEA1kTIeJIt9X+1MZcKPi7ArZSCyiwbsDo874mDEIrk3l8h43rcyVq61qdlRGcdyLMT2NSzxVsebsIUez2kiD0N3wJAUalRP6sUae1oD7+sNxTJzLnX38mtkeZ9bZVvaMJ3W25OXOe9EW5OXtnZWhJnC6/YrkLTDAuD47Rvc9B5kyjswQJBALWHUpwrpDpANt9LikcCTwUANApach7MSEHcK6kBM0NeL5TMy27fqjkfWsEn52jYprDmC2PhfZfyX23F3LX7m9sCQQC1uu65Dwl/UopM34Km7NRm+N1TC26UiaWxcYXgYafE22Dy2XGhUMpolIAMoz9wkw2HW4QihtZ6Jwq6VXbOdQu4

If I reimport this into an RSA key, all the individual RSAParameters fields are the same on both machines except the D parameter, which is completely different.

On all three machines, RSA.Create().GetType().Assembly.Location is the same, and the assembly at the given location is the same (or has the same checksum at least).

If I do this with a new randomly generated key, it usually passes on all three machines. There must be something uncooperative about this particular key. But I would have expected an RSA key to be a task that is independent of the machine doing the task, particularly in a high-level runtime like dotnet 6.

Yes, I could just replace the key to a new one in order to make it pass, but I'm more concerned with finding out: What could be going on here to cause this to fail on one machine but pass on others? (particularly given that the .net sdk is the same version on both)

kanders84152
  • 1,251
  • 10
  • 17

1 Answers1

1

The original key used Euler's totient function when calculating the private exponent (d = 0x4AC4A6...), while the modified key used Carmichael's totient function when calculating the private exponent (d = 0x0B6F61...).
Nowadays Carmichael's totient function is mostly used. Apparently, the first two machines do not change the key, while the last machine somehow differs and forces a conversion that conforms to the algorithm that uses the Carmichael's totient function.

Both private keys have the same public key and are equivalent in that either private key can be used to decrypt a message encrypted with their (identical) public key.
This follows from the fact that any d satisfying d⋅e ≡ 1 (mod φ(n)) also satisfies d⋅e ≡ 1 (mod λ(n)), where φ(n) is Euler's totient function and λ(n) is Carmichael's totient function. I.e. the private exponents of the original and modified key satisfy d⋅e ≡ 1 (mod λ(n)). For details s. here.

The d determined with Euler's totient function can be larger than λ(n). This is e.g. the case for the original key. Since some standards/FIPS require d < λ(n), this can lead to problems. A too large d can be reduced modulo λ(n) to d < λ(n) (giving the d determined with the Carmichael's totient function).

Here the underlying values:

n = 0xbdffd146b1016a2629b95da369d76c878f24b2dd13bdab46563e5f7b6e67a9e1747f4f1ddb0df4cfcc8729fcd5ab4503f45d26d2c014c5ab6ca96165cbb5683908232daf0ce42b4892cb92ab280d57c2c6ad016fc07e60ad6f250ad0151fb3f0f9e2f5dd074592e3c4caf806ffb0872fba96a0e0ee1ef4d141fb2fdd96be0c57
e = 0x010001
p = 0xe300fcb5b020320e449846d72894085b13c4dc50693c6ad72120b77213fb1cd096ecb1d7190dfbcca9d442d79e56f15de23e09b789566cd960c60ee9b6e26089
q = 0xd644c878922df57fb531970a3e2ec0ad9482ca2c1bb03a3cef8983108ae4de5f21e37adcc95abad6a76544671dc8b313d8d4b3c55b1e6ec2147b3da4883d0ddf

φ(n) = (p − 1)(q − 1) = 0xbdffd146b1016a2629b95da369d76c878f24b2dd13bdab46563e5f7b6e67a9e1747f4f1ddb0df4cfcc8729fcd5ab4503f45d26d2c014c5ab6ca96165cbb568374edd6880ca9603ba9901b4c9c14a8eba1e655af33b91bb995e7ad04d763fb8c14112c92924dcdc40739170c84390e2bdff83e36409aa1935ccb9e34f579e9df0
d_eul = e^−1 (mod φ(n)) = 0x4ac4a6d848c3a3c00cdf53480158b2eecd97c58356476765307f2f17e5a489a79d2e9435cd843a78a3a5f511f80b8407405c7ff0a382091fa28d2d34dfeb2d6dc66d3eb8a00054cb5376e66c16d27e4423be030280c8a2a882139671453d62393aa067e4f70aaae9fa109bef53176b0315d012e5c51fd8f7e0eeadd30549f821

λ(n) = lcm(p − 1, q − 1) = 0x1faaa2e11d803c5bb19ee4f091a3e76bed30c87a2df4f1e10e5fba9492669c503e1537da4f2cfe22a21686ff78f1e0d5fe0f86787558cb9c921c3ae64c9e3c0937cf916acc6e55f46ed59e21a03717c9afbb8f2889ed9f443a69cd623e5ff42035832186db7a24b568983d76b5ed7b1faa95fb3b56f1aede4cc9a5e28e9a6fa8
d_car = e^−1 (mod λ(n)) = 0x0b6f61160dc32b08a9a18966de10e416f336348efa5d83a313bfb9eec0d75107210424812f2a3e335f78e7130627c25b443d72ffb8d071e67e54b76846aeb55b56ce1be30723a8e275cbaa28d6644eb0c446e4b16ced64200d3ffbacc87d79f8cf9a24d74016617f28e02101e73c74c3c0a41c6f173c7b3b475b620de81518d1

d_eul⋅e = 1 (mod λ(n))
d_eul > λ(n): d_eul (mod λ(n)) = d_car
φ(n) = 6⋅λ(n)


Edit:
In this post found by the OP it is additionally explained how the combination of provider, .NET version and OS version can have different effects regarding the generation as well as import/export of d.
Depending on the combination, d is retained or recalculated for export, and Euler's totient function or Carmichael's totient function is used for the calculation.

Topaco
  • 40,594
  • 4
  • 35
  • 62
  • Very useful and informative analysis! Have an upvote and my thanks. I'm still somewhat baffled as to what's causing the two machines to behave differently, but this is very useful with respect to understanding the consequences of the issue. – kanders84152 Sep 14 '22 at 11:21
  • Ok, apparently .net 6 uses Windows CNG which discards D on import and re-computes it for export, so it is a difference in the OS that's causing this. Ref: https://stackoverflow.com/a/67590449/1992137 But this is the correct answer, so I'll click the checkmark. Thanks again. – kanders84152 Sep 14 '22 at 12:22
  • @kanders84152 - Yes, provider, .NET version and/or OS version as underlying cause of the different behavior makes sense. However, none of the combinations described in the post seem to explain how a key determined with Euler's totient function could be converted on export/import to a key determined with Carmichael's totient function. But possibly there are more combinations than listed. I have added a link to this post in my answer. – Topaco Sep 14 '22 at 15:47