-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
Make it safer and easier to build an X500DistinguishedName #44738
Comments
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. |
/cc @bartonjs |
A couple of questions: What does this do?X509DistinguishedNameBuilder builder = new X509DistinguishedNameBuilder();
builder.AddEmail("[email protected]");
builder.AddCommonName("example.org");
string whatAmI = builder.Build().Name; If it's 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? I think my preference would be to restrict We might also have to name 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: public void AddDomainComponent(string domainComponent);
public void AddEmailAddress(string emailAddress);
public void AddMultiValueDomainComponent(string[] domainComponents);
public void AddMultiValueEmailAddress(string[] emailAddresses); Suffix logical grouping: public void AddDomainComponent(string domainComponent);
public void AddDomainComponentMultiValued(string[] domainComponents);
public void AddEmailAddress(string emailAddress);
public void AddEmailAddressMultiValued(string[] emailAddresses); Should we accept pre-encoded values?public void Add(Oid oid, byte[] encodedValue);
public void AddEncoded(Oid oid, byte[] encodedValue); Should we accept
|
That makes sense, I also considered making this more
The case of 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 |
A thought: does it need an option to specify how things get encoded? e.g. to force |
Something like /// <param name="stringEncodingType">An optional ASN.1 tag for the string encoding type to use for the value. The default value uses the preferred encoding for the OID, when known, or UTF-8.</param>
/// <exception cref="WhateverAsnWriterThrows"><paramref name="value"/> cannot be validly encoded under the specified encoding type.</exception>
public void Add(Oid oid, string value, Asn1Tag? stringEncodingType = null); ?
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 |
Taking the
I had not planned on having Add immediately write to the |
Updated proposal. The |
How would you use that API to add a multi-value RDN like in one of the examples in RFC 4514 section 4:
If Perhaps the API could work like this instead (assuming big-endian): X509DistinguishedNameBuilder builder = new X509DistinguishedNameBuilder();
builder.AddDomainComponent("net");
builder.AddDomainComponent("example");
builder.StartMultiValueRdn();
builder.AddOrganizationUnitName("Sales");
builder.AddCommonName("J. Smith");
builder.EndMultiValueRdn();
builder.Build(); |
Tagging subscribers to this area: @bartonjs, @vcsjones, @krwq, @jeffhandley Issue Details
|
Rather than have state with Start/End multi, probably just something like // Easy CN+CN
public void AddMultiValue(string oidValue, string[] values, Asn1Tag? stringEncodingType = null);
// OU+CN with common/default encoding
public void AddMultiValue(IEnumerable<(string OidValue, string Value)> valueSegments, Asn1Tag? stringEncodingType = null);
// OU with IA5String and CN with UTF8String, 'cuz why not?
public void AddMultiValue(IEnumerable<(string OidValue, string Value, Asn1Tag? StringEncodingType)> valueSegments); Or just cut multi-valued altogether, because the ordering is weird. The example of
Because of how strings display, the calls would actually be (I think) X509DistinguishedNameBuilder builder = new X509DistinguishedNameBuilder();
builder.AddMultiValue(new[] { (Oids.OU, "Sales"), (Oids.CN, "Smith") });
builder.AddDomainComponent("example");
builder.AddDomainComponent("net");
return builder.Build().Name; 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). |
At this point I'm inclined to agree. My motivation for the proposal was I suppose if someone wanted All The Flexibility they can use |
Removed from proposal. |
A couple of musings from this morning.
|
Updated, but note that "al" is only in "OrganizationalUnitName", not "OrganizationName" The rest of the feedback seems sensible. |
Yep, that's what I meant. Apparently I didn't have enough coffee-power to type Unit. At least I linked to the right thing 😄. |
namespace System.Security.Cryptography.X509Certificates
{
public sealed class X500DistinguishedNameBuilder
{
public void AddEmailAddress(string emailAddress, Asn1Tag? stringEncodingType = null);
public void AddDomainComponent(string domainComponent, Asn1Tag? stringEncodingType = null);
public void AddLocalityName(string localityName, Asn1Tag? stringEncodingType = null);
public void AddCommonName(string commonName, Asn1Tag? stringEncodingType = null);
public void AddCountryOrRegion(string twoLetterCode, Asn1Tag? stringEncodingType = null);
public void AddOrganizationName(string organizationName, Asn1Tag? stringEncodingType = null);
public void AddOrganizationalUnitName(string organizationalUnitName, Asn1Tag? stringEncodingType = null);
public void AddStateOrProvinceName(string stateOrProvinceName, Asn1Tag? stringEncodingType = null);
public void Add(Oid oid, string value, Asn1Tag? stringEncodingType = null);
public void Add(string oidValue, string value, Asn1Tag? stringEncodingType = null);
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();
}
} |
@bartonjs Spent a bit of lunch time trying to get something done for this one since I thought it would be quick but it raised a few questions.
|
@bartonjs Another wall I hit, this API proposal exposes types from Okay, so I go and add the ref project like so: <ProjectReference Include="..\..\System.Formats.Asn1\ref\System.Formats.Asn1.csproj" /> New build error:
So the S.F.Asn1 project is netstandard and net461 only. I am guessing that has something to do with this, but I am now stuck. |
Hm. We were only accepting the Asn1Tag as an encoding type hint anyways, right? Maybe we want to do partial class X500DistinguishedNameBuilder
{
public enum EncodingType
{
Default,
Utf8String,
IA5String,
etc
}
public void AddCommonName(string commonName, EncodingType stringEncodingType = default);
} to avoid exposing System.Formats.Asn1 through the X509Certificates ref. |
When the RDN type has a specific encoding we shouldn't have the extra parameter. If someone wants to build a bad string they can do AddEncoded.
Well, what's worse is we're prepending. So I think we're really just keeping a public void Build()
{
// throw if nothing was added?
AsnWriter writer = new AsnWriter(DER);
using (writer.PushSequence())
{
foreach (stuff in _stack)
{
using (writer.PushSet())
using (writer.PushSequence())
{
writer.WriteObjectIdentifier(oidValue);
writer.WriteCharacterString(...);
}
}
}
return writer.Build();
}
private string DebuggerDisplay()
{
return _display ??= (_stack.Count == 0 ? "" : new X500DistinguishedName(Build()).Name;
}
private string PushRdn(string oidValue, string rdnValue, ...)
{
_display = null;
...
} |
@bartonjs Okay. It seems that this still needs a bit of work. If you don't mind, I think this should go back to [api-needs-work] and I will get a proposal diff up as soon as I get a clearer understanding of the different RDN encoding requirements. Apologies for this needing around round of review. |
namespace System.Security.Cryptography.X509Certificates
{
public sealed partial class X500DistinguishedNameBuilder
{
public void AddEmailAddress(string emailAddress);
public void AddDomainComponent(string domainComponent);
public void AddLocalityName(string localityName);
public void AddCommonName(string commonName);
public void AddCountryOrRegion(string twoLetterCode);
public void AddOrganizationName(string organizationName);
public void AddOrganizationalUnitName(string organizationalUnitName);
public void AddStateOrProvinceName(string stateOrProvinceName);
public void Add(string oidValue, string value, int stringEncodingType = 0);
public void Add(Oid oid, string value, int stringEncodingType = 0);
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();
}
} |
@bartonjs I'm probably going to miss this for .NET 6 so I can wrap up other things. Unless you have objections this should be moved to .NET 7. |
@bartonjs Okay, for .NET 7 we can go back to the original proposal as the original issue does not seem to exist since we added a configuration to System.Formats.Asn1 package for .NET 6 in addition to .NET Standard. To jog everyone's memory: the first proposal's public methods had I would propose we go back to the first approved API. If we do, do we need a 3rd API approval, or does the first one stick? |
Well, we wouldn't need the full AsnTag value, right, just a UniversalTagNumber, right? (Since it would throw for anything other than a Universal) |
Y..es. So. Another proposal then: namespace System.Security.Cryptography.X509Certificates
{
public sealed partial class X500DistinguishedNameBuilder
{
public void AddEmailAddress(string emailAddress);
public void AddDomainComponent(string domainComponent);
public void AddLocalityName(string localityName);
public void AddCommonName(string commonName);
public void AddCountryOrRegion(string twoLetterCode);
public void AddOrganizationName(string organizationName);
public void AddOrganizationalUnitName(string organizationalUnitName);
public void AddStateOrProvinceName(string stateOrProvinceName);
- public void Add(string oidValue, string value, int stringEncodingType = 0);
- public void Add(Oid oid, string value, int stringEncodingType = 0);
+ public void Add(string oidValue, string value, UniversalTagNumber? stringEncodingType = null);
+ public void Add(Oid oid, string value, UniversalTagNumber? stringEncodingType = null);
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();
}
} The |
The original proposal (which took a tag) was nullable. Presumably we thought it was useful to have the default value for known tag types (a copy-enumerator, once we ever get around to making an enumerator?) |
We could make it a nullable value type again. We got rid of the optionals / nullable on the convince helpers where I saw them being the most useful. I suppose we could make |
Looks good as proposed (for the third time!) namespace System.Security.Cryptography.X509Certificates
{
public sealed partial class X500DistinguishedNameBuilder
{
public void AddEmailAddress(string emailAddress);
public void AddDomainComponent(string domainComponent);
public void AddLocalityName(string localityName);
public void AddCommonName(string commonName);
public void AddCountryOrRegion(string twoLetterCode);
public void AddOrganizationName(string organizationName);
public void AddOrganizationalUnitName(string organizationalUnitName);
public void AddStateOrProvinceName(string stateOrProvinceName);
public void Add(string oidValue, string value, UniversalTagNumber? stringEncodingType = null);
public void Add(Oid oid, string value, UniversalTagNumber? stringEncodingType = null);
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();
}
} |
@bartonjs did this get approved and is just missing the label? |
Clicking's hard, mm'kay? 😄 |
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:
Isn't there an easier way to add a RDN? public void Add(Oid oid, string LDAPName, string value); |
@amin1best Once we take that parameter out, its now an overload redundant to one we already have: public void Add(Oid oid, string value, UniversalTagNumber? stringEncodingType = null); |
What / Why
The
CertificateRequest
functionality takes anX500DistingishedName
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: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 anX500DistinguishedName
.Proposal
Update from before:
Previous proposal:
We can add the common cases as methods, and for the uncommon / custom cases folks can use the
Add
method that takes anOid
.Alternatives
Can be built yourself using
AsnWriter
, but, that feels a bit too close to the metal for my taste.The text was updated successfully, but these errors were encountered: