Skip to content

Commit

Permalink
Allow XRootD client to accept subjectAltNames.
Browse files Browse the repository at this point in the history
With this, if the CN doesn't follow the expected matching rules,
then the client will iterate through the listed subjectAltNames
to determine whether the certificate matches the current host.

This includes support for CNs and SANs with wildcards.
  • Loading branch information
bbockelm committed May 19, 2018
1 parent 936a579 commit 8f7d3ae
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 10 deletions.
74 changes: 74 additions & 0 deletions src/XrdCrypto/XrdCryptoX509.cc
Expand Up @@ -252,3 +252,77 @@ int XrdCryptoX509::DumpExtensions(bool)
ABSTRACTMETHOD("XrdCryptoX509::DumpExtensions");
return -1;
}

/**
--_____________________________________________________________________________
* Compare two hostnames and see if they are the same, including wildcards.
*
* For example,
*
* - foo.example.com and foo.example.com are considered equal.
* - bar.example.com and foo.example.com are not equal.
* - *.example.com and foo.example.com are equal.
* - FOO.example.com and foo.EXAMPLE.COM are equal (comparison is not case sensitive).
* - F*.com and foo.com are equal
*
* Returns true if the hostnames are considered a match
*/
bool
XrdCryptoX509::MatchHostnames(const char * match_pattern, const char * hostname)
{
char match_copy[256], host_copy[256];
char *tok1, *tok2;
char *run1, *run2;
int idx;

if ((match_pattern == NULL || hostname == NULL) ||
((strlen(match_pattern) > 255) || strlen(hostname) > 255))
return false;

// Create a lowercase copy of both hostnames
for (idx = 0; match_pattern[idx]; idx++) {
match_copy[idx] = tolower(match_pattern[idx]);
}
match_copy[idx] = '\0';
for (idx = 0; hostname[idx]; idx++) {
host_copy[idx] = tolower(hostname[idx]);
}
host_copy[idx] = '\0';

// Split the strings by '.' character, iterate through each sub-component.
run1 = match_copy;
run2 = host_copy;
for (tok1 = strsep(&run1, "."), tok2 = strsep(&run2, ".");
tok1 && tok2;
tok1 = strsep(&run1, "."), tok2 = strsep(&run2, "."))
{
// Match non-wildcard bits
while (*tok1 && *tok2 && *tok1 == *tok2) {
if (*tok2 == '*')
return false;
if (*tok1 == '*')
break;
tok1++;
tok2++;
}

/**
* At this point, one of the following must be true:
* - We hit a wildcard. In this case, we accept the match as
* long as there is no non-wildcard after it.
* - We hit a character that doesn't match.
* - We hit the of at least one string.
*/
if (*tok1 == '*') {
tok1++;
// Non-wildcard after wildcard -- not acceptable.
if (*tok1 != '\0')
return false;
}
// Only accept the match if both components are at their end.
else if (*tok1 || *tok2) {
return false;
}
}
return !tok1 && !tok2;
}
11 changes: 11 additions & 0 deletions src/XrdCrypto/XrdCryptoX509.hh
Expand Up @@ -103,12 +103,23 @@ public:
virtual const char *SubjectHash(int); // hash
const char *SubjectHash() { return SubjectHash(0); } // hash

// Returns true if the certificate has a subject alt name which matches
// the given hostnem.
virtual bool MatchesSAN(const char * fqdn) = 0;

// Retrieve a given extension if there (in opaque form)
virtual XrdCryptoX509data GetExtension(const char *oid);

// Verify signature
virtual bool Verify(XrdCryptoX509 *ref);

// Compare two hostnames, handling wildcards as appropriate. Necessary
// for support for accepting connections where the remote X509 certificate
// is a wildcard certificate.
//
// Returns true if the FQDN matches the specified pattern
static bool MatchHostnames(const char *match_pattern, const char *fqdn);

private:

static const char *ctype[4]; // Names of types
Expand Down
49 changes: 49 additions & 0 deletions src/XrdCrypto/XrdCryptosslX509.cc
Expand Up @@ -1091,3 +1091,52 @@ int XrdCryptosslX509::Asn1PrintInfo(int tag, int xclass, int constructed, int in
BIO_free(bp);
return(0);
}

//____________________________________________________________________________
bool XrdCryptosslX509::MatchesSAN(const char *fqdn)
{
EPNAME("MatchesSAN");

// Statically allocated array for hostname lengths. RFC1035 limits
// valid lengths to 255 characters.
char san_fqdn[256];

if (!fqdn)
return false;

// Only an EEC is usable as a host certificate.
if (type != kEEC)
return false;

GENERAL_NAMES *gens = static_cast<GENERAL_NAMES *>(X509_get_ext_d2i(cert,
NID_subject_alt_name, NULL, NULL));
if (!gens)
return false;

bool success = false;
for (int idx = 0; idx < sk_GENERAL_NAME_num(gens); idx++) {
GENERAL_NAME *gen;
ASN1_STRING *cstr;
gen = sk_GENERAL_NAME_value(gens, idx);
if (gen->type != GEN_DNS)
continue;
cstr = gen->d.dNSName;
if (ASN1_STRING_type(cstr) != V_ASN1_IA5STRING)
continue;
int san_fqdn_len = ASN1_STRING_length(cstr);
if (san_fqdn_len > 255)
continue;
memcpy(san_fqdn, ASN1_STRING_data(cstr), san_fqdn_len);
san_fqdn[san_fqdn_len] = '\0';
if (strlen(san_fqdn) != static_cast<size_t>(san_fqdn_len)) // Avoid embedded null's.
continue;
DEBUG("Comparing SAN " << san_fqdn << " with " << fqdn);
if (MatchHostnames(san_fqdn, fqdn)) {
DEBUG("SAN " << san_fqdn << " matches with " << fqdn);
success = true;
break;
}
}
sk_GENERAL_NAME_pop_free(gens, GENERAL_NAME_free);
return success;
}
3 changes: 3 additions & 0 deletions src/XrdCrypto/XrdCryptosslX509.hh
Expand Up @@ -98,6 +98,9 @@ public:
const char *SubjectHash(int = 0); // get hash of subject name
const char *IssuerHash(int = 0); // get hash of issuer name

// Check SANs
virtual bool MatchesSAN(const char *);

// Retrieve a given extension if there (in opaque form)
XrdCryptoX509data GetExtension(const char *oid);

Expand Down
20 changes: 10 additions & 10 deletions src/XrdSecgsi/XrdSecProtocolgsi.cc
Expand Up @@ -3048,7 +3048,10 @@ int XrdSecProtocolgsi::ClientDoCert(XrdSutBuffer *br, XrdSutBuffer **bm,
}
//
// Verify server identity
if (!ServerCertNameOK(hs->Chain->End()->Subject(), emsg)) {
// First, check the DN. On failure, we will iterate through
// the alternate names.
if (!ServerCertNameOK(hs->Chain->End()->Subject(), emsg) &&
!hs->Chain->End()->MatchesSAN(Entity.host)) {
return -1;
}
//
Expand Down Expand Up @@ -5154,6 +5157,7 @@ XrdSecgsiVOMS_t XrdSecProtocolgsi::LoadVOMSFun(const char *plugin,
return ep;
}


//_____________________________________________________________________________
bool XrdSecProtocolgsi::ServerCertNameOK(const char *subject, XrdOucString &emsg)
{
Expand All @@ -5174,16 +5178,12 @@ bool XrdSecProtocolgsi::ServerCertNameOK(const char *subject, XrdOucString &emsg

// Always check if the server CN is in the standard form "[*/]<target host name>[/*]"
if (Entity.host) {
if (srvcn != (const char *) Entity.host) {
int ih = srvcn.find((const char *) Entity.host);
if (ih == 0 || (ih > 0 && srvcn[ih-1] == '/')) {
ih += strlen(Entity.host);
if (ih >= srvcn.length() ||
srvcn[ih] == '\0' || srvcn[ih] == '/') allowed = 1;
}
} else {
allowed = 1;
size_t ih = srvcn.find("/");
if (ih != std::string::npos) {
srvcn.erasefromstart(ih + 1);
}
allowed = XrdCryptoX509::MatchHostnames(srvcn.c_str(), Entity.host);

// Update the error msg, if the case
if (!allowed) {
if (emsg.length() <= 0) {
Expand Down

0 comments on commit 8f7d3ae

Please sign in to comment.