Repository navigation
Make it safer and easier to build an X500DistinguishedName #44738
Description
What / Why
The CertificateRequest functionality takes an X500DistingishedName type as input for the subject of the certificate request in many of its constructor arguments. However, (correctly) and safely building one of these is a bit difficult at the moment, and requires either input sanitation, escaping, or a combination of both. Consider code that tries to do something like this:
string userEmailInput = "kevin@example.com, DC=contoso";
X500DistinguishedName dn = new X500DistinguishedName($"E = {userEmailInput}");
CertificateRequest req = new CertificateRequest(dn, ECDsa.Create(), HashAlgorithmName.SHA512);A program that does no or little email validation input would end up allowing someone to inject a component in to the distinguished name.
Ideally, something like SubjectAlternativeNameBuilder would exist that makes it easier and safer to encode an X500DistinguishedName.
Proposal
Update from before:
- The Add* methods no longer take an encoding option, they will just do the optimal thing for their data type.
- Instead of using Asn1Tag or UniversalTagNumber from System.Formats.Asn1 add a new nested enum for the power-user Add(OID, value, encoding) method.
- This is because there were complexities in an inbox-only contract referencing a semi-OOB
namespace System.Security.Cryptography.X509Certificates
{
public sealed partial class X500DistinguishedNameBuilder
{
public void AddEmailAddress(string emailAddress); // IA5String
public void AddDomainComponent(string domainComponent); // IA5String, needs to use IdnMapping.
public void AddLocalityName(string localityName); // UnboundDirectoryString (encode as UTF8)
public void AddCommonName(string commonName); // UnboundDirectoryString (encode as UTF8)
public void AddCountryOrRegion(string twoLetterCode); // Always encoded as PrintableString
public void AddOrganizationName(string organizationName); // UnboundDirectoryString (encode as UTF8)
public void AddOrganizationalUnitName(string organizationalUnitName); // UnboundDirectoryString (encode as UTF8)
public void AddStateOrProvinceName(string stateOrProvinceName); // UnboundDirectoryString (encode as UTF8)
public void Add(string oidValue, string value, X500DistinguishedNameBuilder.EncodingType stringEncodingType = default);
public void Add(Oid oid, string value, X500DistinguishedNameBuilder.EncodingType stringEncodingType = default);
public void AddEncoded(Oid oid, byte[] encodedValue);
public void AddEncoded(Oid oid, ReadOnlySpan<byte> encodedValue);
public void AddEncoded(string oidValue, byte[] encodedValue);
public void AddEncoded(string oidValue, ReadOnlySpan<byte> encodedValue);
public X500DistinguishedName Build();
public enum EncodingType
{
Default = 0, // Will be treated like UTF8String but is a distinct value of "0" so that default(EncodingType) has a sane value
UTF8String = 12,
PrintableString = 19,
TeletexString = 20,
BMPString = 30,
}
}
}Previous proposal:
namespace System.Security.Cryptography.X509Certificates
{
public sealed class X500DistinguishedNameBuilder
{
public void AddEmailAddress(ReadOnlySpan<char> emailAddress, Asn1Tag? stringEncodingType = null);
public void AddDomainComponent(ReadOnlySpan<char> domainComponent, Asn1Tag? stringEncodingType = null);
public void AddLocalityName(ReadOnlySpan<char> localityName, Asn1Tag? stringEncodingType = null);
public void AddCommonName(ReadOnlySpan<char> commonName, Asn1Tag? stringEncodingType = null);
public void AddCountryOrRegion(ReadOnlySpan<char> twoLetterCode, Asn1Tag? stringEncodingType = null);
public void AddOrganizationName(ReadOnlySpan<char> organizationName, Asn1Tag? stringEncodingType = null);
public void AddOrganizationalUnitName(ReadOnlySpan<char> organizationalUnitName, Asn1Tag? stringEncodingType = null);
public void AddStateOrProvinceName(ReadOnlySpan<char> stateOrProvinceName, Asn1Tag? stringEncodingType = null);
public void Add(Oid oid, ReadOnlySpan<char> value, Asn1Tag? stringEncodingType = null);
public void Add(ReadOnlySpan<char> oidValue, ReadOnlySpan<char> value, Asn1Tag? stringEncodingType = null);
public void AddEncoded(Oid oid, ReadOnlySpan<byte> encodedValue);
public void AddEncoded(ReadOnlySpan<char> oidValue, ReadOnlySpan<byte> encodedValue);
public X500DistinguishedName Build();
}
}We can add the common cases as methods, and for the uncommon / custom cases folks can use the Add method that takes an Oid.
Alternatives
Can be built yourself using AsnWriter, but, that feels a bit too close to the metal for my taste.
Activity
Dotnet-GitSync-Bot commentedon Nov 16, 2020
I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.
vcsjones commentedon Nov 16, 2020
/cc @bartonjs
bartonjs commentedon Nov 16, 2020
A couple of questions:
What does this do?
If it's
E = test@example.org, CN = example.orgthat probably matches user expectations from the .NET API perspective, but that technically means that "Add" is "Push", since the Name property prints reversed. Conversely,CN = example.org, E = test@example.orgprobably makes sense to someone who understands X.500 names more than .NET.My initial thought was that Build and BuildReversed were both warranted, but I think that this is probably something that someone can easily debug one way or the other.
Is there validation?
https://www.itu.int/ITU-T/formal-language/itu-t/x/x520/2008/SelectedAttributeTypes.html says that CountryName (C / 2.5.4.6) is a 2 character PrintableString that corresponds to an ISO 3166 country code. How much, if any, of that is validated?
"日本"is two characters, but it falls outside the PrintableString character set. "ZQ" is two characters, and a PrintableString, but not a current two letter ISO 3166 identifier.I think my preference would be to restrict
AddCountryNameto two-character PrintableString values (not trying to keep alignment with ISO 3166). I could be wrong, and maybe "no validation" is best.We might also have to name
AddCountryNameAddCountryOrRegionName. It doesn't match what the spec calls it, but it solves the problem with theTWidentifier.Are all of the MultiValue things needed?
Multi-Valued RDNs are pretty rare. Leaving the OID-keyed one as an escape valve seems reasonable, but do the others provide enough value (basically to tests) to warrant the confusion they introduce (namely, that they're useful).
If Multi-Value things are needed, should it be a suffix?
Assuming classic IntelliSense ordering, should the multi-value methods be grouped together, or grouped with their single-value counterpart?
Prefix logical grouping:
Suffix logical grouping:
Should we accept pre-encoded values?
Should we accept
string oidValueas an alternative toOid oid?Probably 😄 It can keep the Oid-typed one, just add overloads.
vcsjones commentedon Nov 16, 2020
That makes sense, I also considered making this more
ICollectionlike withInsertand such... but I tend to agree that once the behavior is understood, it's fairly straight forward.The case of
countryNameis fairly straight forward, so I could see validating it as simple as two letters (so there is no need to debate which country codes are valid...).CountryName I think is also restricted to not having multiple values, but I agree the multi-value could go away anyway.
I like keeping the generic
Addone around for it, but agree that the common case ones can go away.I also think we can make the single value cases accept ROS now.Nope.vcsjones commentedon Nov 16, 2020
A thought: does it need an option to specify how things get encoded? e.g. to force
UTF8StringorBMPString? If we do, does it belong onBuildor for individualAdd*calls?bartonjs commentedon Nov 17, 2020
Something like
?
Clearly my thought was Add. If we build things using the encoded order (the opposite of the .NET strings) the add methods can just immediately go into an AsnWriter (though that presumably closes itself out when you call Build, we'd need to codify if Build closes the builder or you can call Add again and then Build again for a "and one more thing" approach.
If we build them in the friendly way then an AsnWriter can still be used immediately which then Encode()s into a Stack<byte[]> and Reset()s, then Build builds up the macro structure reading from the stack... not hyper-efficient, but not really the end of the world.
Why can't the add methods take
ReadOnlySpan<char>for the value parameter as an overload to the string version?vcsjones commentedon Nov 17, 2020
Taking the
AsnTagseems reasonable and provides the most flexibility. Presumably this would stop you from doing something nonsensical like using a AsnNull tag?I had not planned on having Add immediately write to the
AsnWriterbut to defer it untilEncode. As you have just explained, it seems doable now.vcsjones commentedon Nov 17, 2020
Updated proposal.
The
Oldoverloads looks a bit redundant to me now. I don't really have a preference one way or the other, but if there is strong belief that the ROS overloads are good enough I wouldn't mind omitting them.KalleOlaviNiemitalo commentedon Nov 17, 2020
How would you use that API to add a multi-value RDN like in one of the examples in RFC 4514 section 4:
If
public void AddMultiValue(Oid oid, string[] values, Asn1Tag? stringEncodingType = null)adds a whole RDN, then it cannot be used here, because the attribute types don't match.Perhaps the API could work like this instead (assuming big-endian):
bartonjs commentedon Nov 17, 2020
Rather than have state with Start/End multi, probably just something like
Or just cut multi-valued altogether, because the ordering is weird. The example of
OU=Sales+CN=J. Smith,DC=example,DC=net(which is the .NET default order, aka "Reversed" (RFC 4514 section 2.1 says last-prints-first)) isBecause of how strings display, the calls would actually be (I think)
Since we're maintaining "the API call order matches the string order" it might make sense, but it's definitely a bit of a trip for anyone who knows how the final output is encoded (ok, maybe just me).
vcsjones commentedon Nov 18, 2020
At this point I'm inclined to agree. My motivation for the proposal was
CertificateRequest, which accepts an X500DN as input, whereas most of the places it has been used is as some kind of output.I suppose if someone wanted All The Flexibility they can use
AsnWriterto encode it.38 remaining items
bartonjs commentedon Sep 21, 2021
Video
Looks good as proposed (for the third time!)
vcsjones commentedon Sep 21, 2021
@bartonjs did this get approved and is just missing the label?
bartonjs commentedon Sep 21, 2021
Clicking's hard, mm'kay? 😄
amin1best commentedon Oct 1, 2021
According to the following links:
https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ldap/distinguished-names
https://www.ietf.org/rfc/rfc2253.txt
If possible, I suggest:
AddStreetAddress
AddUserid
In x520 and x509 for each attribute there is a field called LDAP-NAME which I use to build RDN.
x520 Sample


x509 Sample
Isn't there an easier way to add a RDN?
like:
@bartonjs , @vcsjones
bartonjs commentedon Oct 1, 2021
@amin1best
LDAPNameis mostly redundant withoid. It largely informs what a string lookup table for OID to FriendlyName (and vice versa) should be, and/or a string-based lookup in LDAP. At any case, only the OID and string are actually encoded into the X.500 value.Once we take that parameter out, its now an overload redundant to one we already have: