зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1111399, Part 2: Implement RFC822 (email) name constraints, r=keeler
--HG-- extra : rebase_source : 5905e247eee4d3562d741e6e9656dc4c40d821e4
This commit is contained in:
Родитель
c61befa56f
Коммит
1543a46c03
|
@ -115,7 +115,7 @@ ReadGeneralName(Reader& reader,
|
|||
return Success;
|
||||
}
|
||||
|
||||
MOZILLA_PKIX_ENUM_CLASS FallBackToCommonName { No = 0, Yes = 1 };
|
||||
MOZILLA_PKIX_ENUM_CLASS FallBackToSearchWithinSubject { No = 0, Yes = 1 };
|
||||
|
||||
MOZILLA_PKIX_ENUM_CLASS MatchResult
|
||||
{
|
||||
|
@ -127,15 +127,19 @@ MOZILLA_PKIX_ENUM_CLASS MatchResult
|
|||
Result SearchNames(const Input* subjectAltName, Input subject,
|
||||
GeneralNameType referenceIDType,
|
||||
Input referenceID,
|
||||
FallBackToCommonName fallBackToCommonName,
|
||||
FallBackToSearchWithinSubject fallBackToCommonName,
|
||||
/*out*/ MatchResult& match);
|
||||
Result SearchWithinRDN(Reader& rdn,
|
||||
GeneralNameType referenceIDType,
|
||||
Input referenceID,
|
||||
FallBackToSearchWithinSubject fallBackToEmailAddress,
|
||||
FallBackToSearchWithinSubject fallBackToCommonName,
|
||||
/*in/out*/ MatchResult& match);
|
||||
Result SearchWithinAVA(Reader& rdn,
|
||||
GeneralNameType referenceIDType,
|
||||
Input referenceID,
|
||||
FallBackToSearchWithinSubject fallBackToEmailAddress,
|
||||
FallBackToSearchWithinSubject fallBackToCommonName,
|
||||
/*in/out*/ MatchResult& match);
|
||||
void MatchSubjectPresentedIDWithReferenceID(GeneralNameType presentedIDType,
|
||||
Input presentedID,
|
||||
|
@ -162,11 +166,36 @@ MOZILLA_PKIX_ENUM_CLASS IDRole
|
|||
NameConstraint = 2,
|
||||
};
|
||||
|
||||
bool IsValidDNSID(Input hostname, IDRole idRole);
|
||||
MOZILLA_PKIX_ENUM_CLASS Wildcards
|
||||
{
|
||||
AllowWildcards = 0,
|
||||
DisallowWildcards = 1
|
||||
};
|
||||
|
||||
// DNSName constraints implicitly allow subdomain matching when there is no
|
||||
// leading dot ("foo.example.com" matches a constraint of "example.com"), but
|
||||
// RFC822Name constraints only allow subdomain matching when there is a leading
|
||||
// dot ("foo.example.com" does not match "example.com" but does match
|
||||
// ".example.com").
|
||||
MOZILLA_PKIX_ENUM_CLASS DotlessSubdomainMatches
|
||||
{
|
||||
DisallowDotlessSubdomainMatches = 0,
|
||||
AllowDotlessSubdomainMatches = 1
|
||||
};
|
||||
|
||||
bool IsValidDNSID(Input hostname, IDRole idRole, Wildcards allowWildcards);
|
||||
|
||||
Result MatchPresentedDNSIDWithReferenceDNSID(
|
||||
Input presentedDNSID, IDRole referenceDNSIDRole,
|
||||
Input referenceDNSID, /*out*/ bool& matches);
|
||||
Input presentedDNSID,
|
||||
Wildcards allowWildcards,
|
||||
DotlessSubdomainMatches allowDotlessSubdomainMatches,
|
||||
IDRole referenceDNSIDRole,
|
||||
Input referenceDNSID,
|
||||
/*out*/ bool& matches);
|
||||
|
||||
Result MatchPresentedRFC822NameWithReferenceRFC822Name(
|
||||
Input presentedRFC822Name, IDRole referenceRFC822NameRole,
|
||||
Input referenceRFC822Name, /*out*/ bool& matches);
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
|
@ -182,7 +211,10 @@ MatchPresentedDNSIDWithReferenceDNSID(Input presentedDNSID,
|
|||
/*out*/ bool& matches)
|
||||
{
|
||||
return MatchPresentedDNSIDWithReferenceDNSID(
|
||||
presentedDNSID, IDRole::ReferenceID, referenceDNSID, matches);
|
||||
presentedDNSID, Wildcards::AllowWildcards,
|
||||
DotlessSubdomainMatches::AllowDotlessSubdomainMatches,
|
||||
IDRole::ReferenceID,
|
||||
referenceDNSID, matches);
|
||||
}
|
||||
|
||||
// Verify that the given end-entity cert, which is assumed to have been already
|
||||
|
@ -215,13 +247,13 @@ CheckCertHostname(Input endEntityCertDER, Input hostname)
|
|||
uint8_t ipv4[4];
|
||||
if (IsValidReferenceDNSID(hostname)) {
|
||||
rv = SearchNames(subjectAltName, subject, GeneralNameType::dNSName,
|
||||
hostname, FallBackToCommonName::Yes, match);
|
||||
hostname, FallBackToSearchWithinSubject::Yes, match);
|
||||
} else if (ParseIPv6Address(hostname, ipv6)) {
|
||||
rv = SearchNames(subjectAltName, subject, GeneralNameType::iPAddress,
|
||||
Input(ipv6), FallBackToCommonName::No, match);
|
||||
Input(ipv6), FallBackToSearchWithinSubject::No, match);
|
||||
} else if (ParseIPv4Address(hostname, ipv4)) {
|
||||
rv = SearchNames(subjectAltName, subject, GeneralNameType::iPAddress,
|
||||
Input(ipv4), FallBackToCommonName::Yes, match);
|
||||
Input(ipv4), FallBackToSearchWithinSubject::Yes, match);
|
||||
} else {
|
||||
return Result::ERROR_BAD_CERT_DOMAIN;
|
||||
}
|
||||
|
@ -247,11 +279,11 @@ CheckNameConstraints(Input encodedNameConstraints,
|
|||
KeyPurposeId requiredEKUIfPresent)
|
||||
{
|
||||
for (const BackCert* child = &firstChild; child; child = child->childCert) {
|
||||
FallBackToCommonName fallBackToCommonName
|
||||
FallBackToSearchWithinSubject fallBackToCommonName
|
||||
= (child->endEntityOrCA == EndEntityOrCA::MustBeEndEntity &&
|
||||
requiredEKUIfPresent == KeyPurposeId::id_kp_serverAuth)
|
||||
? FallBackToCommonName::Yes
|
||||
: FallBackToCommonName::No;
|
||||
? FallBackToSearchWithinSubject::Yes
|
||||
: FallBackToSearchWithinSubject::No;
|
||||
|
||||
MatchResult match;
|
||||
Result rv = SearchNames(child->GetSubjectAltName(), child->GetSubject(),
|
||||
|
@ -295,7 +327,7 @@ SearchNames(/*optional*/ const Input* subjectAltName,
|
|||
Input subject,
|
||||
GeneralNameType referenceIDType,
|
||||
Input referenceID,
|
||||
FallBackToCommonName fallBackToCommonName,
|
||||
FallBackToSearchWithinSubject fallBackToCommonName,
|
||||
/*out*/ MatchResult& match)
|
||||
{
|
||||
Result rv;
|
||||
|
@ -318,7 +350,6 @@ SearchNames(/*optional*/ const Input* subjectAltName,
|
|||
//
|
||||
// TODO(bug XXXXXXX): Consider dropping support for IP addresses as
|
||||
// identifiers completely.
|
||||
bool hasAtLeastOneDNSNameOrIPAddressSAN = false;
|
||||
|
||||
if (subjectAltName) {
|
||||
Reader altNames;
|
||||
|
@ -349,7 +380,7 @@ SearchNames(/*optional*/ const Input* subjectAltName,
|
|||
}
|
||||
if (presentedIDType == GeneralNameType::dNSName ||
|
||||
presentedIDType == GeneralNameType::iPAddress) {
|
||||
hasAtLeastOneDNSNameOrIPAddressSAN = true;
|
||||
fallBackToCommonName = FallBackToSearchWithinSubject::No;
|
||||
}
|
||||
} while (!altNames.AtEnd());
|
||||
}
|
||||
|
@ -362,8 +393,19 @@ SearchNames(/*optional*/ const Input* subjectAltName,
|
|||
}
|
||||
}
|
||||
|
||||
if (hasAtLeastOneDNSNameOrIPAddressSAN ||
|
||||
fallBackToCommonName != FallBackToCommonName::Yes) {
|
||||
FallBackToSearchWithinSubject fallBackToEmailAddress;
|
||||
if (!subjectAltName &&
|
||||
(referenceIDType == GeneralNameType::rfc822Name ||
|
||||
referenceIDType == GeneralNameType::nameConstraints)) {
|
||||
fallBackToEmailAddress = FallBackToSearchWithinSubject::Yes;
|
||||
} else {
|
||||
fallBackToEmailAddress = FallBackToSearchWithinSubject::No;
|
||||
}
|
||||
|
||||
// Short-circuit the parsing of the subject name if we're not going to match
|
||||
// any names in it
|
||||
if (fallBackToEmailAddress == FallBackToSearchWithinSubject::No &&
|
||||
fallBackToCommonName == FallBackToSearchWithinSubject::No) {
|
||||
return Success;
|
||||
}
|
||||
|
||||
|
@ -436,7 +478,8 @@ SearchNames(/*optional*/ const Input* subjectAltName,
|
|||
return der::NestedOf(subjectReader, der::SEQUENCE, der::SET,
|
||||
der::EmptyAllowed::Yes,
|
||||
bind(SearchWithinRDN, _1, referenceIDType,
|
||||
referenceID, ref(match)));
|
||||
referenceID, fallBackToEmailAddress,
|
||||
fallBackToCommonName, ref(match)));
|
||||
}
|
||||
|
||||
// RelativeDistinguishedName ::=
|
||||
|
@ -449,12 +492,15 @@ Result
|
|||
SearchWithinRDN(Reader& rdn,
|
||||
GeneralNameType referenceIDType,
|
||||
Input referenceID,
|
||||
FallBackToSearchWithinSubject fallBackToEmailAddress,
|
||||
FallBackToSearchWithinSubject fallBackToCommonName,
|
||||
/*in/out*/ MatchResult& match)
|
||||
{
|
||||
do {
|
||||
Result rv = der::Nested(rdn, der::SEQUENCE,
|
||||
bind(SearchWithinAVA, _1, referenceIDType,
|
||||
referenceID, ref(match)));
|
||||
referenceID, fallBackToEmailAddress,
|
||||
fallBackToCommonName, ref(match)));
|
||||
if (rv != Success) {
|
||||
return rv;
|
||||
}
|
||||
|
@ -481,15 +527,10 @@ Result
|
|||
SearchWithinAVA(Reader& rdn,
|
||||
GeneralNameType referenceIDType,
|
||||
Input referenceID,
|
||||
FallBackToSearchWithinSubject fallBackToEmailAddress,
|
||||
FallBackToSearchWithinSubject fallBackToCommonName,
|
||||
/*in/out*/ MatchResult& match)
|
||||
{
|
||||
// id-at OBJECT IDENTIFIER ::= { joint-iso-ccitt(2) ds(5) 4 }
|
||||
// id-at-commonName AttributeType ::= { id-at 3 }
|
||||
// python DottedOIDToCode.py id-at-commonName 2.5.4.3
|
||||
static const uint8_t id_at_commonName[] = {
|
||||
0x55, 0x04, 0x03
|
||||
};
|
||||
|
||||
// AttributeTypeAndValue ::= SEQUENCE {
|
||||
// type AttributeType,
|
||||
// value AttributeValue }
|
||||
|
@ -497,84 +538,126 @@ SearchWithinAVA(Reader& rdn,
|
|||
// AttributeType ::= OBJECT IDENTIFIER
|
||||
//
|
||||
// AttributeValue ::= ANY -- DEFINED BY AttributeType
|
||||
//
|
||||
// DirectoryString ::= CHOICE {
|
||||
// teletexString TeletexString (SIZE (1..MAX)),
|
||||
// printableString PrintableString (SIZE (1..MAX)),
|
||||
// universalString UniversalString (SIZE (1..MAX)),
|
||||
// utf8String UTF8String (SIZE (1..MAX)),
|
||||
// bmpString BMPString (SIZE (1..MAX)) }
|
||||
Reader type;
|
||||
Result rv = der::ExpectTagAndGetValue(rdn, der::OIDTag, type);
|
||||
if (rv != Success) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
// We're only interested in CN attributes.
|
||||
if (!type.MatchRest(id_at_commonName)) {
|
||||
rdn.SkipToEnd();
|
||||
// Try to match the CN as a DNSName or an IPAddress.
|
||||
//
|
||||
// id-at-commonName AttributeType ::= { id-at 3 }
|
||||
//
|
||||
// -- Naming attributes of type X520CommonName:
|
||||
// -- X520CommonName ::= DirectoryName (SIZE (1..ub-common-name))
|
||||
// --
|
||||
// -- Expanded to avoid parameterized type:
|
||||
// X520CommonName ::= CHOICE {
|
||||
// teletexString TeletexString (SIZE (1..ub-common-name)),
|
||||
// printableString PrintableString (SIZE (1..ub-common-name)),
|
||||
// universalString UniversalString (SIZE (1..ub-common-name)),
|
||||
// utf8String UTF8String (SIZE (1..ub-common-name)),
|
||||
// bmpString BMPString (SIZE (1..ub-common-name)) }
|
||||
//
|
||||
// python DottedOIDToCode.py id-at-commonName 2.5.4.3
|
||||
static const uint8_t id_at_commonName[] = {
|
||||
0x55, 0x04, 0x03
|
||||
};
|
||||
if (fallBackToCommonName == FallBackToSearchWithinSubject::Yes &&
|
||||
type.MatchRest(id_at_commonName)) {
|
||||
// We might have previously found a match. Now that we've found another CN,
|
||||
// we no longer consider that previous match to be a match, so "forget" about
|
||||
// it.
|
||||
match = MatchResult::NoNamesOfGivenType;
|
||||
|
||||
uint8_t valueEncodingTag;
|
||||
Input presentedID;
|
||||
rv = der::ReadTagAndGetValue(rdn, valueEncodingTag, presentedID);
|
||||
if (rv != Success) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
// PrintableString is a subset of ASCII that contains all the characters
|
||||
// allowed in CN-IDs except '*'. Although '*' is illegal, there are many
|
||||
// real-world certificates that are encoded this way, so we accept it.
|
||||
//
|
||||
// In the case of UTF8String, we rely on the fact that in UTF-8 the octets in
|
||||
// a multi-byte encoding of a code point are always distinct from ASCII. Any
|
||||
// non-ASCII byte in a UTF-8 string causes us to fail to match. We make no
|
||||
// attempt to detect or report malformed UTF-8 (e.g. incomplete or overlong
|
||||
// encodings of code points, or encodings of invalid code points).
|
||||
//
|
||||
// TeletexString is supported as long as it does not contain any escape
|
||||
// sequences, which are not supported. We'll reject escape sequences as
|
||||
// invalid characters in names, which means we only accept strings that are
|
||||
// in the default character set, which is a superset of ASCII. Note that NSS
|
||||
// actually treats TeletexString as ISO-8859-1. Many certificates that have
|
||||
// wildcard CN-IDs (e.g. "*.example.com") use TeletexString because
|
||||
// PrintableString is defined to not allow '*' and because, at one point in
|
||||
// history, UTF8String was too new to use for compatibility reasons.
|
||||
//
|
||||
// UniversalString and BMPString are also deprecated, and they are a little
|
||||
// harder to support because they are not single-byte ASCII superset
|
||||
// encodings, so we don't bother.
|
||||
if (valueEncodingTag != der::PrintableString &&
|
||||
valueEncodingTag != der::UTF8String &&
|
||||
valueEncodingTag != der::TeletexString) {
|
||||
return Success;
|
||||
}
|
||||
|
||||
if (IsValidPresentedDNSID(presentedID)) {
|
||||
MatchSubjectPresentedIDWithReferenceID(GeneralNameType::dNSName,
|
||||
presentedID, referenceIDType,
|
||||
referenceID, match);
|
||||
} else {
|
||||
// We don't match CN-IDs for IPv6 addresses.
|
||||
// MatchSubjectPresentedIDWithReferenceID ensures that it won't match an
|
||||
// IPv4 address with an IPv6 address, so we don't need to check that
|
||||
// referenceID is an IPv4 address here.
|
||||
uint8_t ipv4[4];
|
||||
if (ParseIPv4Address(presentedID, ipv4)) {
|
||||
MatchSubjectPresentedIDWithReferenceID(GeneralNameType::iPAddress,
|
||||
Input(ipv4), referenceIDType,
|
||||
referenceID, match);
|
||||
}
|
||||
}
|
||||
|
||||
// Regardless of whether there was a match, we keep going in case we find
|
||||
// another CN later. If we do find another one, then this match/mismatch
|
||||
// will be ignored, because we only care about the most specific CN.
|
||||
|
||||
return Success;
|
||||
}
|
||||
|
||||
// We might have previously found a match. Now that we've found another CN,
|
||||
// we no longer consider that previous match to be a match, so "forget" about
|
||||
// it.
|
||||
match = MatchResult::NoNamesOfGivenType;
|
||||
|
||||
uint8_t valueEncodingTag;
|
||||
Input presentedID;
|
||||
rv = der::ReadTagAndGetValue(rdn, valueEncodingTag, presentedID);
|
||||
if (rv != Success) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
// PrintableString is a subset of ASCII that contains all the characters
|
||||
// allowed in CN-IDs except '*'. Although '*' is illegal, there are many
|
||||
// real-world certificates that are encoded this way, so we accept it.
|
||||
// Match an email address against an emailAddress attribute in the
|
||||
// subject.
|
||||
//
|
||||
// In the case of UTF8String, we rely on the fact that in UTF-8 the octets in
|
||||
// a multi-byte encoding of a code point are always distinct from ASCII. Any
|
||||
// non-ASCII byte in a UTF-8 string causes us to fail to match. We make no
|
||||
// attempt to detect or report malformed UTF-8 (e.g. incomplete or overlong
|
||||
// encodings of code points, or encodings of invalid code points).
|
||||
// id-emailAddress AttributeType ::= { pkcs-9 1 }
|
||||
//
|
||||
// TeletexString is supported as long as it does not contain any escape
|
||||
// sequences, which are not supported. We'll reject escape sequences as
|
||||
// invalid characters in names, which means we only accept strings that are
|
||||
// in the default character set, which is a superset of ASCII. Note that NSS
|
||||
// actually treats TeletexString as ISO-8859-1. Many certificates that have
|
||||
// wildcard CN-IDs (e.g. "*.example.com") use TeletexString because
|
||||
// PrintableString is defined to not allow '*' and because, at one point in
|
||||
// history, UTF8String was too new to use for compatibility reasons.
|
||||
// EmailAddress ::= IA5String (SIZE (1..ub-emailaddress-length))
|
||||
//
|
||||
// UniversalString and BMPString are also deprecated, and they are a little
|
||||
// harder to support because they are not single-byte ASCII superset
|
||||
// encodings, so we don't bother.
|
||||
if (valueEncodingTag != der::PrintableString &&
|
||||
valueEncodingTag != der::UTF8String &&
|
||||
valueEncodingTag != der::TeletexString) {
|
||||
return Success;
|
||||
}
|
||||
|
||||
if (IsValidPresentedDNSID(presentedID)) {
|
||||
MatchSubjectPresentedIDWithReferenceID(GeneralNameType::dNSName,
|
||||
// python DottedOIDToCode.py id-emailAddress 1.2.840.113549.1.9.1
|
||||
static const uint8_t id_emailAddress[] = {
|
||||
0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x01
|
||||
};
|
||||
if (fallBackToEmailAddress == FallBackToSearchWithinSubject::Yes &&
|
||||
type.MatchRest(id_emailAddress)) {
|
||||
if (referenceIDType == GeneralNameType::rfc822Name &&
|
||||
match == MatchResult::Match) {
|
||||
// We already found a match; we don't need to match another one
|
||||
return Success;
|
||||
}
|
||||
Input presentedID;
|
||||
rv = der::ExpectTagAndGetValue(rdn, der::IA5String, presentedID);
|
||||
if (rv != Success) {
|
||||
return rv;
|
||||
}
|
||||
return MatchPresentedIDWithReferenceID(GeneralNameType::rfc822Name,
|
||||
presentedID, referenceIDType,
|
||||
referenceID, match);
|
||||
} else {
|
||||
// We don't match CN-IDs for IPv6 addresses.
|
||||
// MatchSubjectPresentedIDWithReferenceID ensures that it won't match an
|
||||
// IPv4 address with an IPv6 address, so we don't need to check that
|
||||
// referenceID is an IPv4 address here.
|
||||
uint8_t ipv4[4];
|
||||
if (ParseIPv4Address(presentedID, ipv4)) {
|
||||
MatchSubjectPresentedIDWithReferenceID(GeneralNameType::iPAddress,
|
||||
Input(ipv4), referenceIDType,
|
||||
referenceID, match);
|
||||
}
|
||||
}
|
||||
|
||||
// We don't match CN-IDs for any other types of names.
|
||||
|
||||
rdn.SkipToEnd();
|
||||
return Success;
|
||||
}
|
||||
|
||||
|
@ -618,7 +701,9 @@ MatchPresentedIDWithReferenceID(GeneralNameType presentedIDType,
|
|||
switch (referenceIDType) {
|
||||
case GeneralNameType::dNSName:
|
||||
rv = MatchPresentedDNSIDWithReferenceDNSID(
|
||||
presentedID, IDRole::ReferenceID, referenceID, foundMatch);
|
||||
presentedID, Wildcards::AllowWildcards,
|
||||
DotlessSubdomainMatches::AllowDotlessSubdomainMatches,
|
||||
IDRole::ReferenceID, referenceID, foundMatch);
|
||||
break;
|
||||
|
||||
case GeneralNameType::iPAddress:
|
||||
|
@ -626,10 +711,14 @@ MatchPresentedIDWithReferenceID(GeneralNameType presentedIDType,
|
|||
rv = Success;
|
||||
break;
|
||||
|
||||
case GeneralNameType::rfc822Name: // fall through
|
||||
case GeneralNameType::rfc822Name:
|
||||
rv = MatchPresentedRFC822NameWithReferenceRFC822Name(
|
||||
presentedID, IDRole::ReferenceID, referenceID, foundMatch);
|
||||
break;
|
||||
|
||||
case GeneralNameType::directoryName:
|
||||
// fall through (At some point, we may add APIs for matching rfc822Name
|
||||
// and/or directoryName names.)
|
||||
// TODO: At some point, we may add APIs for matching DirectoryNames.
|
||||
// fall through
|
||||
|
||||
case GeneralNameType::otherName: // fall through
|
||||
case GeneralNameType::x400Address: // fall through
|
||||
|
@ -769,7 +858,9 @@ CheckPresentedIDConformsToNameConstraintsSubtrees(
|
|||
switch (presentedIDType) {
|
||||
case GeneralNameType::dNSName:
|
||||
rv = MatchPresentedDNSIDWithReferenceDNSID(
|
||||
presentedID, IDRole::NameConstraint, base, matches);
|
||||
presentedID, Wildcards::AllowWildcards,
|
||||
DotlessSubdomainMatches::AllowDotlessSubdomainMatches,
|
||||
IDRole::NameConstraint, base, matches);
|
||||
if (rv != Success) {
|
||||
return rv;
|
||||
}
|
||||
|
@ -793,7 +884,12 @@ CheckPresentedIDConformsToNameConstraintsSubtrees(
|
|||
break;
|
||||
|
||||
case GeneralNameType::rfc822Name:
|
||||
return Result::FATAL_ERROR_LIBRARY_FAILURE; // TODO: implement
|
||||
rv = MatchPresentedRFC822NameWithReferenceRFC822Name(
|
||||
presentedID, IDRole::NameConstraint, base, matches);
|
||||
if (rv != Success) {
|
||||
return rv;
|
||||
}
|
||||
break;
|
||||
|
||||
// RFC 5280 says "Conforming CAs [...] SHOULD NOT impose name
|
||||
// constraints on the x400Address, ediPartyName, or registeredID
|
||||
|
@ -969,16 +1065,20 @@ CheckPresentedIDConformsToNameConstraintsSubtrees(
|
|||
// incorporated into the spec:
|
||||
// https://www.ietf.org/mail-archive/web/pkix/current/msg21192.html
|
||||
Result
|
||||
MatchPresentedDNSIDWithReferenceDNSID(Input presentedDNSID,
|
||||
IDRole referenceDNSIDRole,
|
||||
Input referenceDNSID,
|
||||
/*out*/ bool& matches)
|
||||
MatchPresentedDNSIDWithReferenceDNSID(
|
||||
Input presentedDNSID,
|
||||
Wildcards allowWildcards,
|
||||
DotlessSubdomainMatches allowDotlessSubdomainMatches,
|
||||
IDRole referenceDNSIDRole,
|
||||
Input referenceDNSID,
|
||||
/*out*/ bool& matches)
|
||||
{
|
||||
if (!IsValidPresentedDNSID(presentedDNSID)) {
|
||||
if (!IsValidDNSID(presentedDNSID, IDRole::PresentedID, allowWildcards)) {
|
||||
return Result::ERROR_BAD_DER;
|
||||
}
|
||||
|
||||
if (!IsValidDNSID(referenceDNSID, referenceDNSIDRole)) {
|
||||
if (!IsValidDNSID(referenceDNSID, referenceDNSIDRole,
|
||||
Wildcards::DisallowWildcards)) {
|
||||
return Result::ERROR_BAD_DER;
|
||||
}
|
||||
|
||||
|
@ -1028,7 +1128,8 @@ MatchPresentedDNSIDWithReferenceDNSID(Input presentedDNSID,
|
|||
return NotReached("skipping subdomain failed",
|
||||
Result::FATAL_ERROR_LIBRARY_FAILURE);
|
||||
}
|
||||
} else {
|
||||
} else if (allowDotlessSubdomainMatches ==
|
||||
DotlessSubdomainMatches::AllowDotlessSubdomainMatches) {
|
||||
if (presented.Skip(static_cast<Input::size_type>(
|
||||
presentedDNSID.GetLength() -
|
||||
referenceDNSID.GetLength() - 1)) != Success) {
|
||||
|
@ -1283,6 +1384,163 @@ MatchPresentedDirectoryNameWithConstraint(NameConstraintsSubtrees subtreesType,
|
|||
}
|
||||
}
|
||||
|
||||
// RFC 5280 says:
|
||||
//
|
||||
// The format of an rfc822Name is a "Mailbox" as defined in Section 4.1.2
|
||||
// of [RFC2821]. A Mailbox has the form "Local-part@Domain". Note that a
|
||||
// Mailbox has no phrase (such as a common name) before it, has no comment
|
||||
// (text surrounded in parentheses) after it, and is not surrounded by "<"
|
||||
// and ">". Rules for encoding Internet mail addresses that include
|
||||
// internationalized domain names are specified in Section 7.5.
|
||||
//
|
||||
// and:
|
||||
//
|
||||
// A name constraint for Internet mail addresses MAY specify a
|
||||
// particular mailbox, all addresses at a particular host, or all
|
||||
// mailboxes in a domain. To indicate a particular mailbox, the
|
||||
// constraint is the complete mail address. For example,
|
||||
// "root@example.com" indicates the root mailbox on the host
|
||||
// "example.com". To indicate all Internet mail addresses on a
|
||||
// particular host, the constraint is specified as the host name. For
|
||||
// example, the constraint "example.com" is satisfied by any mail
|
||||
// address at the host "example.com". To specify any address within a
|
||||
// domain, the constraint is specified with a leading period (as with
|
||||
// URIs). For example, ".example.com" indicates all the Internet mail
|
||||
// addresses in the domain "example.com", but not Internet mail
|
||||
// addresses on the host "example.com".
|
||||
|
||||
bool
|
||||
IsValidRFC822Name(Input input)
|
||||
{
|
||||
Reader reader(input);
|
||||
|
||||
// Local-part@.
|
||||
bool startOfAtom = true;
|
||||
for (;;) {
|
||||
uint8_t presentedByte;
|
||||
if (reader.Read(presentedByte) != Success) {
|
||||
return false;
|
||||
}
|
||||
switch (presentedByte) {
|
||||
// atext is defined in https://tools.ietf.org/html/rfc2822#section-3.2.4
|
||||
case 'A': case 'a': case 'N': case 'n': case '0': case '!': case '#':
|
||||
case 'B': case 'b': case 'O': case 'o': case '1': case '$': case '%':
|
||||
case 'C': case 'c': case 'P': case 'p': case '2': case '&': case '\'':
|
||||
case 'D': case 'd': case 'Q': case 'q': case '3': case '*': case '+':
|
||||
case 'E': case 'e': case 'R': case 'r': case '4': case '-': case '/':
|
||||
case 'F': case 'f': case 'S': case 's': case '5': case '=': case '?':
|
||||
case 'G': case 'g': case 'T': case 't': case '6': case '^': case '_':
|
||||
case 'H': case 'h': case 'U': case 'u': case '7': case '`': case '{':
|
||||
case 'I': case 'i': case 'V': case 'v': case '8': case '|': case '}':
|
||||
case 'J': case 'j': case 'W': case 'w': case '9': case '~':
|
||||
case 'K': case 'k': case 'X': case 'x':
|
||||
case 'L': case 'l': case 'Y': case 'y':
|
||||
case 'M': case 'm': case 'Z': case 'z':
|
||||
startOfAtom = false;
|
||||
break;
|
||||
|
||||
case '.':
|
||||
if (startOfAtom) {
|
||||
return false;
|
||||
}
|
||||
startOfAtom = true;
|
||||
break;
|
||||
|
||||
case '@':
|
||||
{
|
||||
if (startOfAtom) {
|
||||
return false;
|
||||
}
|
||||
Input domain;
|
||||
reader.SkipToEnd(domain);
|
||||
return IsValidDNSID(domain, IDRole::PresentedID,
|
||||
Wildcards::DisallowWildcards);
|
||||
}
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Result
|
||||
MatchPresentedRFC822NameWithReferenceRFC822Name(Input presentedRFC822Name,
|
||||
IDRole referenceRFC822NameRole,
|
||||
Input referenceRFC822Name,
|
||||
/*out*/ bool& matches)
|
||||
{
|
||||
if (!IsValidRFC822Name(presentedRFC822Name)) {
|
||||
return Result::ERROR_BAD_DER;
|
||||
}
|
||||
Reader presented(presentedRFC822Name);
|
||||
|
||||
switch (referenceRFC822NameRole)
|
||||
{
|
||||
case IDRole::PresentedID:
|
||||
return Result::FATAL_ERROR_INVALID_ARGS;
|
||||
|
||||
case IDRole::ReferenceID:
|
||||
break;
|
||||
|
||||
case IDRole::NameConstraint:
|
||||
{
|
||||
if (InputContains(referenceRFC822Name, '@')) {
|
||||
// The constraint is of the form "Local-part@Domain".
|
||||
break;
|
||||
}
|
||||
|
||||
// The constraint is of the form "example.com" or ".example.com".
|
||||
|
||||
// Skip past the '@' in the presented ID.
|
||||
for (;;) {
|
||||
uint8_t presentedByte;
|
||||
if (presented.Read(presentedByte) != Success) {
|
||||
return Result::FATAL_ERROR_LIBRARY_FAILURE;
|
||||
}
|
||||
if (presentedByte == '@') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Input presentedDNSID;
|
||||
presented.SkipToEnd(presentedDNSID);
|
||||
|
||||
return MatchPresentedDNSIDWithReferenceDNSID(
|
||||
presentedDNSID, Wildcards::DisallowWildcards,
|
||||
DotlessSubdomainMatches::DisallowDotlessSubdomainMatches,
|
||||
IDRole::NameConstraint, referenceRFC822Name, matches);
|
||||
}
|
||||
|
||||
default:
|
||||
return NotReached("invalid referenceRFC822NameRole",
|
||||
Result::FATAL_ERROR_INVALID_ARGS);
|
||||
}
|
||||
|
||||
if (!IsValidRFC822Name(referenceRFC822Name)) {
|
||||
return Result::ERROR_BAD_DER;
|
||||
}
|
||||
|
||||
Reader reference(referenceRFC822Name);
|
||||
|
||||
for (;;) {
|
||||
uint8_t presentedByte;
|
||||
if (presented.Read(presentedByte) != Success) {
|
||||
matches = reference.AtEnd();
|
||||
return Success;
|
||||
}
|
||||
uint8_t referenceByte;
|
||||
if (reference.Read(referenceByte) != Success) {
|
||||
matches = false;
|
||||
return Success;
|
||||
}
|
||||
if (LocaleInsensitveToLower(presentedByte) !=
|
||||
LocaleInsensitveToLower(referenceByte)) {
|
||||
matches = false;
|
||||
return Success;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We avoid isdigit because it is locale-sensitive. See
|
||||
// http://pubs.opengroup.org/onlinepubs/009695399/functions/tolower.html.
|
||||
inline uint8_t
|
||||
|
@ -1559,19 +1817,21 @@ ParseIPv6Address(Input hostname, /*out*/ uint8_t (&out)[16])
|
|||
bool
|
||||
IsValidReferenceDNSID(Input hostname)
|
||||
{
|
||||
return IsValidDNSID(hostname, IDRole::ReferenceID);
|
||||
return IsValidDNSID(hostname, IDRole::ReferenceID,
|
||||
Wildcards::DisallowWildcards);
|
||||
}
|
||||
|
||||
bool
|
||||
IsValidPresentedDNSID(Input hostname)
|
||||
{
|
||||
return IsValidDNSID(hostname, IDRole::PresentedID);
|
||||
return IsValidDNSID(hostname, IDRole::PresentedID,
|
||||
Wildcards::AllowWildcards);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
bool
|
||||
IsValidDNSID(Input hostname, IDRole idRole)
|
||||
IsValidDNSID(Input hostname, IDRole idRole, Wildcards allowWildcards)
|
||||
{
|
||||
if (hostname.GetLength() > 253) {
|
||||
return false;
|
||||
|
@ -1591,7 +1851,8 @@ IsValidDNSID(Input hostname, IDRole idRole)
|
|||
// Only presented IDs are allowed to have wildcard labels. And, like
|
||||
// Chromium, be stricter than RFC 6125 requires by insisting that a
|
||||
// wildcard label consist only of '*'.
|
||||
bool isWildcard = idRole == IDRole::PresentedID && input.Peek('*');
|
||||
bool isWildcard = allowWildcards == Wildcards::AllowWildcards &&
|
||||
input.Peek('*');
|
||||
bool isFirstByte = !isWildcard;
|
||||
if (isWildcard) {
|
||||
Result rv = input.Skip(1);
|
||||
|
|
|
@ -1804,6 +1804,15 @@ static const NameConstraintParams NAME_CONSTRAINT_PARAMS[] =
|
|||
GeneralSubtree(DNSName("host.example.com")),
|
||||
Result::ERROR_CERT_NOT_IN_NAME_SPACE, Success
|
||||
},
|
||||
{ ByteString(), RFC822Name("a@host.example.com"),
|
||||
GeneralSubtree(RFC822Name("host.example.com")),
|
||||
Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE
|
||||
},
|
||||
{ // This test case is an example from RFC 5280.
|
||||
ByteString(), RFC822Name("a@host1.example.com"),
|
||||
GeneralSubtree(RFC822Name("host.example.com")),
|
||||
Result::ERROR_CERT_NOT_IN_NAME_SPACE, Success
|
||||
},
|
||||
|
||||
// Q: When the name constraint does not start with ".", do subdomain
|
||||
// presented identifiers match it? For example, does the presented
|
||||
|
@ -1813,12 +1822,23 @@ static const NameConstraintParams NAME_CONSTRAINT_PARAMS[] =
|
|||
GeneralSubtree(DNSName( "host.example.com")),
|
||||
Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE
|
||||
},
|
||||
{ // The subdomain matching rule for host names that do not start with "." is
|
||||
// different for RFC822Names than for DNSNames!
|
||||
ByteString(), RFC822Name("a@www.host.example.com"),
|
||||
GeneralSubtree(RFC822Name( "host.example.com")),
|
||||
Result::ERROR_CERT_NOT_IN_NAME_SPACE,
|
||||
Success
|
||||
},
|
||||
|
||||
// Q: When the name constraint does not start with ".", does a
|
||||
// non-subdomain prefix match it? For example, does "bigfoo.bar.com"
|
||||
// match "foo.bar.com"?
|
||||
{ ByteString(), DNSName("bigfoo.bar.com"),
|
||||
GeneralSubtree(DNSName("foo.bar.com")),
|
||||
GeneralSubtree(DNSName( "foo.bar.com")),
|
||||
Result::ERROR_CERT_NOT_IN_NAME_SPACE, Success
|
||||
},
|
||||
{ ByteString(), RFC822Name("a@bigfoo.bar.com"),
|
||||
GeneralSubtree(RFC822Name( "foo.bar.com")),
|
||||
Result::ERROR_CERT_NOT_IN_NAME_SPACE, Success
|
||||
},
|
||||
|
||||
|
@ -1827,19 +1847,41 @@ static const NameConstraintParams NAME_CONSTRAINT_PARAMS[] =
|
|||
// "www.example.com" match a constraint of ".example.com"? Does a
|
||||
// presented ID of "example.com" match a constraint of ".example.com"?
|
||||
{ ByteString(), DNSName("www.example.com"),
|
||||
GeneralSubtree(DNSName(".example.com")),
|
||||
GeneralSubtree(DNSName( ".example.com")),
|
||||
Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE
|
||||
},
|
||||
{ // When there is no Local-part, an RFC822 name constraint's domain may
|
||||
// start with '.', and the semantics are the same as for DNSNames.
|
||||
ByteString(), RFC822Name("a@www.example.com"),
|
||||
GeneralSubtree(RFC822Name( ".example.com")),
|
||||
Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE
|
||||
},
|
||||
{ // When there is a Local-part, an RFC822 name constraint's domain must not
|
||||
// start with '.'.
|
||||
ByteString(), RFC822Name("a@www.example.com"),
|
||||
GeneralSubtree(RFC822Name( "a@.example.com")),
|
||||
Result::ERROR_BAD_DER, Result::ERROR_BAD_DER
|
||||
},
|
||||
{ // Check that we only allow subdomains to match.
|
||||
ByteString(), DNSName("example.com"),
|
||||
ByteString(), DNSName( "example.com"),
|
||||
GeneralSubtree(DNSName(".example.com")),
|
||||
Result::ERROR_CERT_NOT_IN_NAME_SPACE, Success
|
||||
},
|
||||
{ // Check that we only allow subdomains to match.
|
||||
ByteString(), RFC822Name("a@example.com"),
|
||||
GeneralSubtree(RFC822Name(".example.com")),
|
||||
Result::ERROR_CERT_NOT_IN_NAME_SPACE, Success
|
||||
},
|
||||
{ // Check that we don't get confused and consider "b" == "."
|
||||
ByteString(), DNSName("bexample.com"),
|
||||
GeneralSubtree(DNSName(".example.com")),
|
||||
Result::ERROR_CERT_NOT_IN_NAME_SPACE, Success
|
||||
},
|
||||
{ // Check that we don't get confused and consider "b" == "."
|
||||
ByteString(), RFC822Name("a@bexample.com"),
|
||||
GeneralSubtree(RFC822Name( ".example.com")),
|
||||
Result::ERROR_CERT_NOT_IN_NAME_SPACE, Success
|
||||
},
|
||||
|
||||
// Q: Is there a way to prevent subdomain matches?
|
||||
// (This is tested in a different set of tests because it requires a
|
||||
|
@ -1847,7 +1889,7 @@ static const NameConstraintParams NAME_CONSTRAINT_PARAMS[] =
|
|||
|
||||
// Q: Are name constraints allowed to be specified as absolute names?
|
||||
// For example, does a presented ID of "example.com" match a name
|
||||
// constraint of "example.com." and vice versa.
|
||||
// constraint of "example.com." and vice versa?
|
||||
//
|
||||
{ // The DNSName in the constraint is not valid because constraint DNS IDs
|
||||
// are not allowed to be absolute.
|
||||
|
@ -1855,13 +1897,21 @@ static const NameConstraintParams NAME_CONSTRAINT_PARAMS[] =
|
|||
GeneralSubtree(DNSName("example.com.")),
|
||||
Result::ERROR_BAD_DER, Result::ERROR_BAD_DER,
|
||||
},
|
||||
{ ByteString(), RFC822Name("a@example.com"),
|
||||
GeneralSubtree(RFC822Name( "example.com.")),
|
||||
Result::ERROR_BAD_DER, Result::ERROR_BAD_DER,
|
||||
},
|
||||
{ // The DNSName in the SAN is not valid because presented DNS IDs are not
|
||||
// allowed to be absolute.
|
||||
ByteString(), DNSName("example.com."),
|
||||
GeneralSubtree(DNSName("example.com")),
|
||||
Result::ERROR_BAD_DER, Result::ERROR_BAD_DER,
|
||||
},
|
||||
{ // The presented ID is the same length as the constraint, because the
|
||||
{ ByteString(), RFC822Name("a@example.com."),
|
||||
GeneralSubtree(RFC822Name( "example.com")),
|
||||
Result::ERROR_BAD_DER, Result::ERROR_BAD_DER,
|
||||
},
|
||||
{ // The presented DNSName is the same length as the constraint, because the
|
||||
// subdomain is only one character long and because the constraint both
|
||||
// begins and ends with ".". But, it doesn't matter because absolute names
|
||||
// are not allowed for DNSName constraints.
|
||||
|
@ -1869,31 +1919,61 @@ static const NameConstraintParams NAME_CONSTRAINT_PARAMS[] =
|
|||
GeneralSubtree(DNSName(".example.com.")),
|
||||
Result::ERROR_BAD_DER, Result::ERROR_BAD_DER,
|
||||
},
|
||||
{ // The presented DNSName is the same length as the constraint, because the
|
||||
// subdomain is only one character long and because the constraint both
|
||||
// begins and ends with ".".
|
||||
ByteString(), RFC822Name("a@p.example.com"),
|
||||
GeneralSubtree(RFC822Name( ".example.com.")),
|
||||
Result::ERROR_BAD_DER, Result::ERROR_BAD_DER,
|
||||
},
|
||||
{ // Same as previous test case, but using a wildcard presented ID.
|
||||
ByteString(), DNSName("*.example.com"),
|
||||
GeneralSubtree(DNSName(".example.com.")),
|
||||
Result::ERROR_BAD_DER, Result::ERROR_BAD_DER
|
||||
},
|
||||
{ // Same as previous test case, but using a wildcard presented ID, which is
|
||||
// invalid in an RFC822Name.
|
||||
ByteString(), RFC822Name("a@*.example.com"),
|
||||
GeneralSubtree(RFC822Name( ".example.com.")),
|
||||
Result::ERROR_BAD_DER, Result::ERROR_BAD_DER
|
||||
},
|
||||
|
||||
// Q: Are "" and "." valid DNSName constraints? If so, what do they mean?
|
||||
{ ByteString(), DNSName("example.com"),
|
||||
GeneralSubtree(DNSName("")),
|
||||
Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE
|
||||
},
|
||||
{ ByteString(), RFC822Name("a@example.com"),
|
||||
GeneralSubtree(RFC822Name("")),
|
||||
Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE
|
||||
},
|
||||
{ // The malformed (absolute) presented ID does not match.
|
||||
ByteString(), DNSName("example.com."),
|
||||
GeneralSubtree(DNSName("")),
|
||||
Result::ERROR_BAD_DER, Result::ERROR_BAD_DER
|
||||
},
|
||||
{ // Invalid syntax in name constraint.
|
||||
{ ByteString(), RFC822Name("a@example.com."),
|
||||
GeneralSubtree(RFC822Name("")),
|
||||
Result::ERROR_BAD_DER, Result::ERROR_BAD_DER
|
||||
},
|
||||
{ // Invalid syntax in name constraint
|
||||
ByteString(), DNSName("example.com"),
|
||||
GeneralSubtree(DNSName(".")),
|
||||
Result::ERROR_BAD_DER, Result::ERROR_BAD_DER,
|
||||
},
|
||||
{ // Invalid syntax in name constraint
|
||||
ByteString(), RFC822Name("a@example.com"),
|
||||
GeneralSubtree(RFC822Name(".")),
|
||||
Result::ERROR_BAD_DER, Result::ERROR_BAD_DER,
|
||||
},
|
||||
{ ByteString(), DNSName("example.com."),
|
||||
GeneralSubtree(DNSName(".")),
|
||||
Result::ERROR_BAD_DER, Result::ERROR_BAD_DER
|
||||
},
|
||||
{ ByteString(), RFC822Name("a@example.com."),
|
||||
GeneralSubtree(RFC822Name(".")),
|
||||
Result::ERROR_BAD_DER, Result::ERROR_BAD_DER
|
||||
},
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// Basic IP Address constraints (non-CN-ID)
|
||||
|
@ -2053,6 +2133,9 @@ static const NameConstraintParams NAME_CONSTRAINT_PARAMS[] =
|
|||
{ ByteString(), NO_SAN, GeneralSubtree(IPAddress(ipv6_addr_overlong_bytes)),
|
||||
Success, Success
|
||||
},
|
||||
{ ByteString(), NO_SAN, GeneralSubtree(RFC822Name("\0")),
|
||||
Success, Success
|
||||
},
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// Basic CN-ID DNSName constraint tests.
|
||||
|
@ -2191,6 +2274,54 @@ static const NameConstraintParams NAME_CONSTRAINT_PARAMS[] =
|
|||
GeneralSubtree(DNSName("b.example.com")),
|
||||
Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE
|
||||
},
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// Additional RFC822 name constraint tests. There are more tests regarding
|
||||
// the DNSName part of the constraint mixed into the DNSName constraint
|
||||
// tests.
|
||||
|
||||
{ ByteString(), RFC822Name("a@example.com"),
|
||||
GeneralSubtree(RFC822Name("a@example.com")),
|
||||
Success, Result::ERROR_CERT_NOT_IN_NAME_SPACE
|
||||
},
|
||||
|
||||
// Bug 1056773: name constraints that omit Local-part but include '@' are
|
||||
// invalid.
|
||||
{ ByteString(), RFC822Name("a@example.com"),
|
||||
GeneralSubtree(RFC822Name("@example.com")),
|
||||
Result::ERROR_BAD_DER,
|
||||
Result::ERROR_BAD_DER
|
||||
},
|
||||
{ ByteString(), RFC822Name("@example.com"),
|
||||
GeneralSubtree(RFC822Name("@example.com")),
|
||||
Result::ERROR_BAD_DER,
|
||||
Result::ERROR_BAD_DER
|
||||
},
|
||||
{ ByteString(), RFC822Name("example.com"),
|
||||
GeneralSubtree(RFC822Name("@example.com")),
|
||||
Result::ERROR_BAD_DER,
|
||||
Result::ERROR_BAD_DER
|
||||
},
|
||||
{ ByteString(), RFC822Name("a@mail.example.com"),
|
||||
GeneralSubtree(RFC822Name("a@*.example.com")),
|
||||
Result::ERROR_BAD_DER,
|
||||
Result::ERROR_BAD_DER
|
||||
},
|
||||
{ ByteString(), RFC822Name("a@*.example.com"),
|
||||
GeneralSubtree(RFC822Name(".example.com")),
|
||||
Result::ERROR_BAD_DER,
|
||||
Result::ERROR_BAD_DER
|
||||
},
|
||||
{ ByteString(), RFC822Name("@example.com"),
|
||||
GeneralSubtree(RFC822Name(".example.com")),
|
||||
Result::ERROR_BAD_DER,
|
||||
Result::ERROR_BAD_DER
|
||||
},
|
||||
{ ByteString(), RFC822Name("@a.example.com"),
|
||||
GeneralSubtree(RFC822Name(".example.com")),
|
||||
Result::ERROR_BAD_DER,
|
||||
Result::ERROR_BAD_DER
|
||||
},
|
||||
};
|
||||
|
||||
class pkixnames_CheckNameConstraints
|
||||
|
|
Загрузка…
Ссылка в новой задаче