dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.66k stars 4.58k forks source link

Incorrect X509Certificate being generated on ubuntu:20.04 with mono #103102

Open nmalowmicrosoft opened 1 month ago

nmalowmicrosoft commented 1 month ago

Description

The constructor "new X509Certificate2(privateKeyBytes, password)" is generating a different and incorrect certificate when run on ubuntu:20.04 with mono. It is working fine on Windows 11 with dotnet. I have verified the privateKeyBytes is the same on both systems and the password is the same.

Reproduction Steps

Run the line "new X509Certificate2(privateKeyBytes, password)" from a kubernetes pod with ubuntu 20.04 using mono.

Expected behavior

Generates a certificate that is the same as in Windows

Actual behavior

It generates a different invalid certificate than the one from Windows

Regression?

No response

Known Workarounds

No response

Configuration

--Framework:.NETFramework,Version=v4.8.1 --Platform:x64 ubuntu:20.04

The ubuntu:20.04 runs the tests with mono MyTest.exe. The windows successfully runs the tests with dotnet test MyTest.dll and vstest.console.exe

Other information

No response

bartonjs commented 1 month ago

Does your problem go away if you use your PFX with .NET 6 or .NET 8, as opposed to mono?

If it's really mono-only, then I think the issue belongs in a different repository. I know at one point Mono was trying to unify onto our libraries, but I don't know if that finished or if X509Certificate made the cut.

nmalowmicrosoft commented 1 month ago

@bartonjs I just tried using .NET 8. The issue is still there

vcsjones commented 1 month ago

It generates a different invalid certificate than the one from Windows

Can you be more specific what "generates an invalid certificate" means here? Using the X509Certificate constructor does not generate a certificate, it just loads whatever is in the PFX.

What about it is invalid? Do you get an error? Do you have an example that reproduces the problem?

nmalowmicrosoft commented 1 month ago

There is no error thrown from the constructor. I got an error that the certificate with thumbprint XXXXX did not exist when trying to use the X509Certificate2 object. The first thing I noticed was the Thumbprint of the X509Certificate2 was different from the one I got when I ran it through Windows. Then when I looked at the entire X509Certificate2 object, I noticed there was no private key, the subject and issuer were different from Windows, the public key was different (basically the entire X509Certificate2 was different when I printed it out with certificate.ToString(true)) I didn't see anything in common between the X509Certificate2 object generated on Windows and the X509Certificate2 generated on Ubuntu:20.04.

Here is some example code to what I am doing. I printed out the s.Value and confirmed it was the same/correct between both systems. I then printed the hash of the privateKeyBytes and got the same value on both systems, so I know the error must be with the X509Certificate2 constructor.

public async Task GetCertificateAsync(string certificateName, string certificateVersion = null, CancellationToken cancellationToken = default(CancellationToken)) { KeyVaultSecret s = await GetSecretAsync(certificateName, certificateVersion, cancellationToken); var privateKeyBytes = Convert.FromBase64String(s.Value) SecureString password = new SecureString(); var cert = new X509Certificate2(privateKeyBytes, password) return cert; }

vcsjones commented 1 month ago

The first thing I noticed was the Thumbprint of the X509Certificate2 was different from the one I got when I ran it through Windows

The only way the thumbprint can be different is if they are two different certificates. The thumbprint is just a hash over the certificate. It is not parsed or otherwise "read" from the certificate.

the subject and issuer were different from Windows

There are basically two possibilities here.

  1. There is a bug in your application, deployment, or configuration that is resulting in the certificate / PFX you are think you are using not being what is actually being used.
  2. Your PFX contains multiple certificates, and there is a loading difference between Windows and Linux on which one gets loaded. Generally this should not happen as we have some good test coverage on how certificates are read from the PFX. It's possible though.

I printed out the s.Value and confirmed it was the same/correct between both systems

For the first case, there is not actionable for us. For the second case, that would require some way to reproduce this.

What we would need is

string input = "";
byte[] inputBytes = Convert.FromBase64String(input);
var cert = new X509Certificate2(inputBytes, "<the password>");
Console.WriteLine(cert.Thumbprint);

If you can provide sample data where you can put something in input and it result in the thumbprint being different between Windows and Linux, then we will have an actionable bug to fix. Until then we can't really speculate what is happening.

nmalowmicrosoft commented 1 month ago

I am not sure how to safely provide the example input as it is generated in an Azure Keyvault. Do you have a sample input I can try from my end, maybe from one of your test cases? Or could you generate a certificate on your end https://learn.microsoft.com/en-us/azure/key-vault/certificates/certificate-scenarios

nmalowmicrosoft commented 1 month ago

I generated a dummy cert and the thumbprints are the same in this example. However, the linux one is missing its Private Key field and the Windows one has a Private Key field code:

    // Create a self-signed certificate
    var ecdsa = ECDsa.Create(); // generate asymmetric key pair
    var req = new CertificateRequest("cn=dummy", ecdsa, HashAlgorithmName.SHA256);
    var cert = req.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(5));

    // Export the certificate as a PFX file
    var pfxBytes = cert.Export(X509ContentType.Pfx);

    // Convert the PFX file to a Base64 string
    var input = Convert.ToBase64String(pfxBytes);

    // Print the Base64 string
    Console.WriteLine(input);
vcsjones commented 1 month ago

linux one is missing its Private Key field and the Windows one has a Private Key field

Can you describe how you are making this observation? When I use the that code on Linux (Ubuntu 24.04)

using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

// Create a self-signed certificate
var ecdsa = ECDsa.Create(); // generate asymmetric key pair
var req = new CertificateRequest("cn=dummy", ecdsa, HashAlgorithmName.SHA256);
var cert = req.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(5));

// Export the certificate as a PFX file
var pfxBytes = cert.Export(X509ContentType.Pfx);

// Convert the PFX file to a Base64 string
var input = Convert.ToBase64String(pfxBytes);

// Print the Base64 string
Console.WriteLine(input);

var loaded = new X509Certificate2(pfxBytes);
Console.WriteLine(loaded.HasPrivateKey);

This prints "true" for me on Linux.

nmalowmicrosoft commented 1 month ago

I printed the output of loaded.ToString(true) and I get [Private Key]

.

vcsjones commented 1 month ago

Thanks! The presence of [Private Key] is the indicator there is a private key. The text under the section just differ between Windows and Linux.

On Windows, it's going to look something like this:

[Private Key]

  Key Store: User
  Provider Name: Microsoft Software Key Storage Provider
  Provider type: 0
  Key Spec: 0
  Key Container Name: {00000000-0000-0000-0000-000000000000}

Those values, Key Store, Provider Name, Provider type... etc. they are all Windows only concepts. There is no such thing as a Key Container Name on Linux. .NET does this on Windows because that is what .NET Framework did many many years ago.

So your certificate does have a private key. It just doesn't show you that bit of Windows information about the key because you aren't on Windows.

The better way to determine if a certificate has a private key or not is to use the HasPrivateKey property.

nmalowmicrosoft commented 1 month ago

I found the issue for my specific certificate. Linux is using the root certificate instead of the leaf certificate. Windows is getting the leaf cert. My PFX file contains a root cert, intermediary cert, and leaf cert

vcsjones commented 1 month ago

I found the issue for my specific certificate. Linux is using the root certificate instead of the leaf certificate. Windows is getting the leaf cert. My PFX file contains a root cert, intermediary cert, and leaf cert

That is some good info to go on. Let me see if I can reproduce it. Was the PFX generated by AKV or something else?

nmalowmicrosoft commented 1 month ago

AKV