dotnet / runtime

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

X509Certificate2/OpenSSL: Subject includes incorrectly-encoded UID attribute #18256

Open qmfrederik opened 8 years ago

qmfrederik commented 8 years ago

If the Subject of a certificate includes a UID (OID 0.9.2342.19200300.100.1.1) attribute, this attribute is encoded incorrectly in the string representation of the Subject DN as userId instead of UID.

dotnet run
Project certificates (.NETCoreApp,Version=v1.0) was previously compiled. Skipping compilation.
C=US, O=Quamotion sprl, OU=PWWS5TKNBM, CN=iPhone Distribution: Quamotion sprl (PWWS5TKNBM), userId=PWWS5TKNBM

Valid representations are those returned by X509Certificate2.Subject on full .NET:

dotnet run
Project certificates (.NETCoreApp,Version=v1.0) was previously compiled. Skipping compilation.
C=US, O=Quamotion sprl, OU=PWWS5TKNBM, CN=iPhone Distribution: Quamotion sprl (PWWS5TKNBM), OID.0.9.2342.19200300.100.1.1=PWWS5TKNBM

and by openssl.exe:

openssl x509 -inform der -in rawdata.bin -subject
subject= /UID=PWWS5TKNBM/CN=iPhone Distribution: Quamotion sprl (PWWS5TKNBM)/OU=PWWS5TKNBM/O=Quamotion sprl/C=US

I believe that RFC4514 String Representation of Distinguished Names describes how the subject of a certificate should be represented, and it states the following:

2.3. Converting AttributeTypeAndValue If the AttributeType is defined to have a short name (descriptor) [RFC4512] and that short name is known to be registered [REGISTRY] [RFC4520] as identifying the AttributeType, that short name, a

, is used. Otherwise the AttributeType is encoded as the dotted-decimal encoding, a , of its OBJECT IDENTIFIER. The and are defined in [RFC4512].

and section 3 contains the following:

Implementations MUST recognize AttributeType name strings (descriptors) listed in the following table, but MAY recognize other name strings.

String X.500 AttributeType
UID userId (0.9.2342.19200300.100.1.1)

and RFC4519 finally defines the UID attribute:

0.9.2342.19200300.100.1.1 NAME 'uid' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )

(userId was used in RFC 1274 which RFC 4519 supersedes).

This program can be used to reproduce the behavior:

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

namespace ConsoleApplication
{
    public class Program
    {
        public static void Main(string[] args)
        {
            byte[] rawData = Convert.FromBase64String("MIIFnzCCBIegAwIBAgIIdo+s3oP49T8wDQYJKoZIhvcNAQEFBQAwgZYxCzAJBgNVBAYTAlVTMRMwEQYDVQQKDApBcHBsZSBJbmMuMSwwKgYDVQQLDCNBcHBsZSBXb3JsZHdpZGUgRGV2ZWxvcGVyIFJlbGF0aW9uczFEMEIGA1UEAww7QXBwbGUgV29ybGR3aWRlIERldmVsb3BlciBSZWxhdGlvbnMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTYwMjA4MTU0MzQwWhcNMTcwMjA3MTU0MzQwWjCBkjEaMBgGCgmSJomT8ixk" + 
                "AQEMClBXV1M1VEtOQk0xOTA3BgNVBAMMMGlQaG9uZSBEaXN0cmlidXRpb246IFF1YW1vdGlvbiBzcHJsIChQV1dTNVRLTkJNKTETMBEGA1UECwwKUFdXUzVUS05CTTEXMBUGA1UECgwOUXVhbW90aW9uIHNwcmwxCzAJBgNVBAYTAlVTMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsZRJE85+iTE/tODPi98Xn82C/oCtB97dTHToWoaQRA4kNCNmreedVnTFLGHa5JnKIBtMao+0qPkWxn/o76PuzlRa0r1oXcwwkgAYUE6S8Okzd9PmOGeXhunpuwPNRmyBJ0+oeo0UTg" +  
                "UY97YUvypvrg6z8ZJpcXGICP6bCaHm8XjPaNPJRH8HCL56HPULtq+9Vt+xMCcCgt4joRFtvzEA8m3vItenUQvVrHI6kyjYcfQh/ANvSXWZyAkDQ1T1AqrljZ1SK/BTH4uIENWu32GglLFJj3UVed3FWO2uy6GYjarhcRkwmYbztJcMc5vLHyBesPPDqpWPmTjk5CvY8qYxTwIDAQABo4IB8TCCAe0wHQYDVR0OBBYEFMXm/a/Rf5kzjcap9LxSO7aYNzVdMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUiCcXCam2GGCL7Ou69kdZxVJUo7cwggEPBgNVHSAEggEGMIIBAjCB/wYJKoZI" + 
                "hvdjZAUBMIHxMIHDBggrBgEFBQcCAjCBtgyBs1JlbGlhbmNlIG9uIHRoaXMgY2VydGlmaWNhdGUgYnkgYW55IHBhcnR5IGFzc3VtZXMgYWNjZXB0YW5jZSBvZiB0aGUgdGhlbiBhcHBsaWNhYmxlIHN0YW5kYXJkIHRlcm1zIGFuZCBjb25kaXRpb25zIG9mIHVzZSwgY2VydGlmaWNhdGUgcG9saWN5IGFuZCBjZXJ0aWZpY2F0aW9uIHByYWN0aWNlIHN0YXRlbWVudHMuMCkGCCsGAQUFBwIBFh1odHRwOi8vd3d3LmFwcGxlLmNvbS9hcHBsZWNhLzBNBgNVHR8ERjBEMEKgQKA+hj" + 
                "xodHRwOi8vZGV2ZWxvcGVyLmFwcGxlLmNvbS9jZXJ0aWZpY2F0aW9uYXV0aG9yaXR5L3d3ZHJjYS5jcmwwDgYDVR0PAQH/BAQDAgeAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMDMBMGCiqGSIb3Y2QGAQQBAf8EAgUAMA0GCSqGSIb3DQEBBQUAA4IBAQCPqzl6lemCu/TUdag7qnCP81c++y6E29gFy+dWJ1sYx/heytn5TX+Byd3OdEz2/DoSkhbNswLwuZ1SLfjJs1L/1FC5y8D9W5oaMTnbToqIcYY6jLLIkV3ALvBf08ReKdh+875yvE9vsPC0Uzfljh/nrZ/1104POJiTYLHrbIAa" + 
                "5sEOxrn/lGhH6hGPkmMKHRqMh7v++Z12tat63Ul0mZNCqrShBQi+mH7jV7nik/gZVZW+knxtfcGuDsSZZHLRSGpa6JUe7qCJQ8Y8r5QZqXJGdue11VFGq7/H+L/bQ4X0QxYq9IweD12DoTGWu/UXaL62w868wec808VNOKynDFBS");

            File.WriteAllBytes("rawdata.bin", rawData);

            var certificate = new X509Certificate2(
                rawData: rawData,
                password: null,
                keyStorageFlags: X509KeyStorageFlags.Exportable);

            Console.WriteLine(certificate.Subject);
        }
    }
}
bartonjs commented 8 years ago

We just use OpenSSL to translate OIDs to names. Perhaps we're reading the wrong description (they have a couple of different string name options), since I see that they have both UID and userId mapped in the input file for that OID.

qmfrederik commented 8 years ago

@bartonjs Would you accept a PR for this? If you can point me to the code where you translate the OIDs to names, I'd be happy to take a look.

bartonjs commented 8 years ago

@qmfrederik Sure.

https://github.com/dotnet/corefx/blob/master/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/X500NameEncoder.cs#L109-L112 gets an OID pointer and passes it to GetOidValue at https://github.com/dotnet/corefx/blob/d0dc5fc099946adc1035b34a8b1f6042eddb0c75/src/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.ASN1.cs#L78 which calls OpenSSL's OBJ_obj2txt function via https://github.com/dotnet/corefx/blob/7e2bd07936179c192e682d979b2938b4a7e32030/src/Native/Unix/System.Security.Cryptography.Native/pal_asn1.cpp#L20-L23 to turn it into the dotted decimal string.

That it sends through new Oid() to get the friendly name, which ultimately means CryptoNative_LookupFriendlyNameByOid (https://github.com/dotnet/corefx/blob/6a10e2774369f65bcf627a1ae04cfc83f2ad25e4/src/Native/Unix/System.Security.Cryptography.Native/openssl.c#L1196-L1244).

Presumably this means that "UID" is the short name (sn) and "userId" is the long name (ln). What we don't have written down (at least not right there) is why I picked ln over sn; but I have to presume that it was ln matched Windows more often than sn did for normal Oid lookup. But that maybe for X500Name the right answer is always sn so it needs to get there via a different route.

karelz commented 7 years ago

We need to get it under debugger and find out if it is OpenSSL bug, or .NET Core bug.

nikitozz commented 7 years ago

As @bartonjs mentioned, the issue is CryptoNative_LookupFriendlyNameByOid uses OBJ_nid2ln to get a friendly which causes a long name to be returned.

On the other hand, most of the commonly used OIDs are hard-coded/cached in the following dictionary s_friendlyNameToOid: https://github.com/dotnet/corefx/blob/bffef76f6af208e2042a2f27bc081ee908bb390b/src/System.Security.Cryptography.Encoding/src/Internal/Cryptography/OidLookup.cs#L110 The dictionary, in contrast, uses short names.

There are a couple of possible solutions for the issue:

  1. Start using OBJ_nid2sn in CryptoNative_LookupFriendlyNameByOid to make the implementation compatible with the dictionary. Ideally, we have to make the implementation compatible with Windows as well. But I don't have access to the Windows CryptFindOIDInfo method source code, so I'm not sure if Windows Crypto API prefers short or long names.
  2. Add UID to the dictionary (this will change Windows behavior as well since, currently, Windows Crypto API doesn't seem to have a friendly name for 0.9.2342.19200300.100.1.1).

@karelz, @bartonjs Please, let me know if you have any preferences on the proposed solutions, and I can take this ticket.

Thank you.

filipnavara commented 6 years ago

The OpenSSL-based X500 name parsing was dropped entirely in favor of the managed version. Maybe this issue can be closed now?

amin1best commented 2 years ago

Hi, Will this problem be solved soon, at least for windows? @bartonjs

bartonjs commented 2 years ago

Will this problem be solved soon, at least for windows?

On Windows, the conversion of an X500DistinguishedName to text is done by the OS, and we don't currently plan to change that.

You can register the OID text with the OS via the CryptRegisterOIDInfo function.

amin1best commented 2 years ago

@bartonjs, Thanks for CryptRegisterOIDInfo I wrote the following code: But to run, it requires Administrator access. Is there a solution that does not require Administrator access?

CRYPT_OID_INFO oidInfo = new CRYPT_OID_INFO();
oidInfo.cbSize = Marshal.SizeOf(typeof(CRYPT_OID_INFO));
oidInfo.pszOID = "0.9.2342.19200300.100.1.1";
oidInfo.pwszName = "UID";
oidInfo.dwGroupId = 5;
bool result = CryptFindOIDInfo(1, "0.9.2342.19200300.100.1.1", 0);
if (!result)
    CryptRegisterOIDInfo(oidInfo, 0);

[DllImport("Crypt32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern Boolean CryptRegisterOIDInfo(CRYPT_OID_INFO pInfo, int dwFlags);
[DllImport("Crypt32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern Boolean CryptFindOIDInfo(int dwKeyType, [MarshalAs(UnmanagedType.LPStr)] string szOID, int dwGroupId);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
struct CRYPT_OID_INFO
{
    public int cbSize;
    [MarshalAs(UnmanagedType.LPStr)]
    public string pszOID;
    [MarshalAs(UnmanagedType.LPWStr)]
    public string pwszName;
    public int dwGroupId;
    public int dwValue;
    public CRYPTOAPI_BLOB ExtraInfo;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct CRYPTOAPI_BLOB
{
    public UInt32 cbData;
    public IntPtr pbData;
}