Backport X509 hostname, IP address and email checking code from HEAD.
authorDr. Stephen Henson <steve@openssl.org>
Wed, 19 Dec 2012 15:01:59 +0000 (15:01 +0000)
committerDr. Stephen Henson <steve@openssl.org>
Wed, 19 Dec 2012 15:01:59 +0000 (15:01 +0000)
CHANGES
apps/apps.c
apps/apps.h
apps/x509.c
crypto/x509v3/Makefile
crypto/x509v3/v3_utl.c
crypto/x509v3/x509v3.h
test/Makefile

diff --git a/CHANGES b/CHANGES
index 250512c4730645999ce056fb06eda0fdb46771bb..1110d9e13a654a72798abc5fe38648a8322469b7 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -4,6 +4,15 @@
 
  Changes between 1.0.1 and 1.0.2 [xx XXX xxxx]
 
+  *) Fixes and wildcard matching support to hostname and email checking
+     functions. Add manual page.
+     [Florian Weimer (Red Hat Product Security Team)]
+
+  *) New functions to check a hostname email or IP address against a
+     certificate. Add options x509 utility to print results of checks against
+     a certificate.
+     [Steve Henson]
+
   *) Fix OCSP checking.
      [Rob Stradling <rob.stradling@comodo.com> and Ben Laurie]
 
index 4f12f3a196ab516b97bc75972151ab905a5bf317..fea5b25c1c732ad092d1afe32addd3a0a2d24422 100644 (file)
@@ -2771,6 +2771,35 @@ unsigned char *next_protos_parse(unsigned short *outlen, const char *in)
        }
 #endif  /* !OPENSSL_NO_TLSEXT && !OPENSSL_NO_NEXTPROTONEG */
 
+void print_cert_checks(BIO *bio, X509 *x,
+                               const unsigned char *checkhost,
+                               const unsigned char *checkemail,
+                               const char *checkip)
+       {
+       if (x == NULL)
+               return;
+       if (checkhost)
+               {
+               BIO_printf(bio, "Hostname %s does%s match certificate\n",
+                               checkhost, X509_check_host(x, checkhost, 0, 0)
+                                               ? "" : " NOT");
+               }
+
+       if (checkemail)
+               {
+               BIO_printf(bio, "Email %s does%s match certificate\n",
+                               checkemail, X509_check_email(x, checkemail, 0,
+                                               0) ? "" : " NOT");
+               }
+
+       if (checkip)
+               {
+               BIO_printf(bio, "IP %s does%s match certificate\n",
+                               checkip, X509_check_ip_asc(x, checkip,
+                                               0) ? "" : " NOT");
+               }
+       }
+
 /*
  * Platform-specific sections
  */
index c1ca99da12e8bfa75bdd71c2ceaee5311c4e5e2e..4c9f95a1ce2228c1ca85e0c523e66934d9cab134 100644 (file)
@@ -335,6 +335,11 @@ void jpake_server_auth(BIO *out, BIO *conn, const char *secret);
 unsigned char *next_protos_parse(unsigned short *outlen, const char *in);
 #endif  /* !OPENSSL_NO_TLSEXT && !OPENSSL_NO_NEXTPROTONEG */
 
+void print_cert_checks(BIO *bio, X509 *x,
+                               const unsigned char *checkhost,
+                               const unsigned char *checkemail,
+                               const char *checkip);
+
 #define FORMAT_UNDEF    0
 #define FORMAT_ASN1     1
 #define FORMAT_TEXT     2
index 3863ab968dadb8539712b7e1a40c359a763a15e5..361eca624e24756fd9658846a457b59968b46008 100644 (file)
@@ -207,6 +207,8 @@ int MAIN(int argc, char **argv)
        int need_rand = 0;
        int checkend=0,checkoffset=0;
        unsigned long nmflag = 0, certflag = 0;
+       unsigned char *checkhost = NULL, *checkemail = NULL;
+       char *checkip = NULL;
 #ifndef OPENSSL_NO_ENGINE
        char *engine=NULL;
 #endif
@@ -450,6 +452,21 @@ int MAIN(int argc, char **argv)
                        checkoffset=atoi(*(++argv));
                        checkend=1;
                        }
+               else if (strcmp(*argv,"-checkhost") == 0)
+                       {
+                       if (--argc < 1) goto bad;
+                       checkhost=(unsigned char *)*(++argv);
+                       }
+               else if (strcmp(*argv,"-checkemail") == 0)
+                       {
+                       if (--argc < 1) goto bad;
+                       checkemail=(unsigned char *)*(++argv);
+                       }
+               else if (strcmp(*argv,"-checkip") == 0)
+                       {
+                       if (--argc < 1) goto bad;
+                       checkip=*(++argv);
+                       }
                else if (strcmp(*argv,"-noout") == 0)
                        noout= ++num;
                else if (strcmp(*argv,"-trustout") == 0)
@@ -1044,6 +1061,8 @@ bad:
                goto end;
                }
 
+       print_cert_checks(STDout, x, checkhost, checkemail, checkip);
+
        if (noout)
                {
                ret=0;
index 556ef351bf888cac13c72af59043e7065943e636..3ac379a7fa7da03ab3e9948bd3225efd8a34194d 100644 (file)
@@ -13,7 +13,7 @@ AR=           ar r
 CFLAGS= $(INCLUDES) $(CFLAG)
 
 GENERAL=Makefile README
-TEST=
+TEST=v3nametest.c
 APPS=
 
 LIB=$(TOP)/libcrypto.a
index e0302345400af1dec202ac6674e4ba9a98271f9f..de43c2fb3b2eb74430a7e7dd28e6539b4a6f5f77 100644 (file)
@@ -568,6 +568,305 @@ void X509_email_free(STACK_OF(OPENSSL_STRING) *sk)
        sk_OPENSSL_STRING_pop_free(sk, str_free);
 }
 
+typedef int (*equal_fn)(const unsigned char *pattern, size_t pattern_len,
+                       const unsigned char *subject, size_t subject_len);
+
+/* Compare while ASCII ignoring case. */
+static int equal_nocase(const unsigned char *pattern, size_t pattern_len,
+                       const unsigned char *subject, size_t subject_len)
+       {
+       if (pattern_len != subject_len)
+               return 0;
+       while (pattern_len)
+               {
+               unsigned char l = *pattern;
+               unsigned char r = *subject;
+               /* The pattern must not contain NUL characters. */
+               if (l == 0)
+                       return 0;
+               if (l != r)
+                       {
+                       if ('A' <= l && l <= 'Z')
+                               l = (l - 'A') + 'a';
+                       if ('A' <= r && r <= 'Z')
+                               r = (r - 'A') + 'a';
+                       if (l != r)
+                               return 0;
+                       }
+               ++pattern;
+               ++subject;
+               --pattern_len;
+               }
+       return 1;
+       }
+
+/* Compare using memcmp. */
+static int equal_case(const unsigned char *pattern, size_t pattern_len,
+                     const unsigned char *subject, size_t subject_len)
+{
+       /* The pattern must not contain NUL characters. */
+       if (memchr(pattern, '\0', pattern_len) != NULL)
+               return 0;
+       if (pattern_len != subject_len)
+               return 0;
+       return !memcmp(pattern, subject, pattern_len);
+}
+
+/* RFC 5280, section 7.5, requires that only the domain is compared in
+   a case-insensitive manner. */
+static int equal_email(const unsigned char *a, size_t a_len,
+                      const unsigned char *b, size_t b_len)
+       {
+       size_t i = a_len;
+       if (a_len != b_len)
+               return 0;
+       /* We search backwards for the '@' character, so that we do
+          not have to deal with quoted local-parts.  The domain part
+          is compared in a case-insensitive manner. */
+       while (i > 0)
+               {
+               --i;
+               if (a[i] == '@' || b[i] == '@')
+                       {
+                       if (!equal_nocase(a + i, a_len - i,
+                                         b + i, a_len - i))
+                               return 0;
+                       break;
+                       }
+               }
+       if (i == 0)
+               i = a_len;
+       return equal_case(a, i, b, i);
+       }
+
+/* Compare the prefix and suffix with the subject, and check that the
+   characters in-between are valid. */
+static int wildcard_match(const unsigned char *prefix, size_t prefix_len,
+                         const unsigned char *suffix, size_t suffix_len,
+                         const unsigned char *subject, size_t subject_len)
+       {
+       const unsigned char *wildcard_start;
+       const unsigned char *wildcard_end;
+       const unsigned char *p;
+       if (subject_len < prefix_len + suffix_len)
+               return 0;
+       if (!equal_nocase(prefix, prefix_len, subject, prefix_len))
+               return 0;
+       wildcard_start = subject + prefix_len;
+       wildcard_end = subject + (subject_len - suffix_len);
+       if (!equal_nocase(wildcard_end, suffix_len, suffix, suffix_len))
+               return 0;
+       /* The wildcard must match at least one character. */
+       if (wildcard_start == wildcard_end)
+               return 0;
+       /* Check that the part matched by the wildcard contains only
+          permitted characters and only matches a single label. */
+       for (p = wildcard_start; p != wildcard_end; ++p)
+               if (!(('0' <= *p && *p <= '9') ||
+                     ('A' <= *p && *p <= 'Z') ||
+                     ('a' <= *p && *p <= 'z') ||
+                     *p == '-'))
+                       return 0;
+       return 1;
+       }
+
+/* Checks if the memory region consistens of [0-9A-Za-z.-]. */
+static int valid_domain_characters(const unsigned char *p, size_t len)
+       {
+       while (len)
+               {
+               if (!(('0' <= *p && *p <= '9') ||
+                     ('A' <= *p && *p <= 'Z') ||
+                     ('a' <= *p && *p <= 'z') ||
+                     *p == '-' || *p == '.'))
+                       return 0;
+               ++p;
+               --len;
+               }
+       return 1;
+       }
+
+/* Find the '*' in a wildcard pattern.  If no such character is found
+   or the pattern is otherwise invalid, returns NULL. */
+static const unsigned char *wildcard_find_star(const unsigned char *pattern,
+                                              size_t pattern_len)
+       {
+       const unsigned char *star = memchr(pattern, '*', pattern_len);
+       size_t dot_count = 0;
+       const unsigned char *suffix_start;
+       size_t suffix_length;
+       if (star == NULL)
+               return NULL;
+       suffix_start = star + 1;
+       suffix_length = (pattern + pattern_len) - (star + 1);
+       if (!(valid_domain_characters(pattern, star - pattern) &&
+             valid_domain_characters(suffix_start, suffix_length)))
+               return NULL;
+       /* Check that the suffix matches at least two labels. */
+       while (suffix_length)
+               {
+               if (*suffix_start == '.')
+                       ++dot_count;
+               ++suffix_start;
+               --suffix_length;
+               }
+       if (dot_count < 2)
+               return NULL;
+       return star;
+       }
+
+/* Compare using wildcards. */
+static int equal_wildcard(const unsigned char *pattern, size_t pattern_len,
+                         const unsigned char *subject, size_t subject_len)
+       {
+       const unsigned char *star = wildcard_find_star(pattern, pattern_len);
+       if (star == NULL)
+               return equal_nocase(pattern, pattern_len,
+                                   subject, subject_len);
+       return wildcard_match(pattern, star - pattern,
+                             star + 1, (pattern + pattern_len) - star - 1,
+                             subject, subject_len);
+       }
+
+/* Compare an ASN1_STRING to a supplied string. If they match
+ * return 1. If cmp_type > 0 only compare if string matches the
+ * type, otherwise convert it to UTF8.
+ */
+
+static int do_check_string(ASN1_STRING *a, int cmp_type, equal_fn equal,
+                               const unsigned char *b, size_t blen)
+       {
+       if (!a->data || !a->length)
+               return 0;
+       if (cmp_type > 0)
+               {
+               if (cmp_type != a->type)
+                       return 0;
+               if (cmp_type == V_ASN1_IA5STRING)
+                       return equal(a->data, a->length, b, blen);
+               if (a->length == (int)blen && !memcmp(a->data, b, blen))
+                       return 1;
+               else
+                       return 0;
+               }
+       else
+               {
+               int astrlen, rv;
+               unsigned char *astr;
+               astrlen = ASN1_STRING_to_UTF8(&astr, a);
+               if (astrlen < 0)
+                       return -1;
+               rv = equal(astr, astrlen, b, blen);
+               OPENSSL_free(astr);
+               return rv;
+               }
+       }
+
+static int do_x509_check(X509 *x, const unsigned char *chk, size_t chklen,
+                                       unsigned int flags, int check_type)
+       {
+       GENERAL_NAMES *gens = NULL;
+       X509_NAME *name = NULL;
+       int i;
+       int cnid;
+       int alt_type;
+       equal_fn equal;
+       if (check_type == GEN_EMAIL)
+               {
+               cnid = NID_pkcs9_emailAddress;
+               alt_type = V_ASN1_IA5STRING;
+               equal = equal_email;
+               }
+       else if (check_type == GEN_DNS)
+               {
+               cnid = NID_commonName;
+               alt_type = V_ASN1_IA5STRING;
+               if (flags & X509_CHECK_FLAG_NO_WILDCARDS)
+                       equal = equal_nocase;
+               else
+                       equal = equal_wildcard;
+               }
+       else
+               {
+               cnid = 0;
+               alt_type = V_ASN1_OCTET_STRING;
+               equal = equal_case;
+               }
+
+       if (chklen == 0)
+               chklen = strlen((const char *)chk);
+
+       gens = X509_get_ext_d2i(x, NID_subject_alt_name, NULL, NULL);
+       if (gens)
+               {
+               int rv = 0;
+               for (i = 0; i < sk_GENERAL_NAME_num(gens); i++)
+                       {
+                       GENERAL_NAME *gen;
+                       ASN1_STRING *cstr;
+                       gen = sk_GENERAL_NAME_value(gens, i);
+                       if(gen->type != check_type)
+                               continue;
+                       if (check_type == GEN_EMAIL)
+                               cstr = gen->d.rfc822Name;
+                       else if (check_type == GEN_DNS)
+                               cstr = gen->d.dNSName;
+                       else
+                               cstr = gen->d.iPAddress;
+                       if (do_check_string(cstr, alt_type, equal, chk, chklen))
+                               {
+                               rv = 1;
+                               break;
+                               }
+                       }
+               GENERAL_NAMES_free(gens);
+               if (rv)
+                       return 1;
+               if (!(flags & X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT) || !cnid)
+                       return 0;
+               }
+       i = -1;
+       name = X509_get_subject_name(x);
+       while((i = X509_NAME_get_index_by_NID(name, cnid, i)) >= 0)
+               {
+               X509_NAME_ENTRY *ne;
+               ASN1_STRING *str;
+               ne = X509_NAME_get_entry(name, i);
+               str = X509_NAME_ENTRY_get_data(ne);
+               if (do_check_string(str, -1, equal, chk, chklen))
+                       return 1;
+               }
+       return 0;
+       }
+
+int X509_check_host(X509 *x, const unsigned char *chk, size_t chklen,
+                                       unsigned int flags)
+       {
+       return do_x509_check(x, chk, chklen, flags, GEN_DNS);
+       }
+
+int X509_check_email(X509 *x, const unsigned char *chk, size_t chklen,
+                                       unsigned int flags)
+       {
+       return do_x509_check(x, chk, chklen, flags, GEN_EMAIL);
+       }
+
+int X509_check_ip(X509 *x, const unsigned char *chk, size_t chklen,
+                                       unsigned int flags)
+       {
+       return do_x509_check(x, chk, chklen, flags, GEN_IPADD);
+       }
+
+int X509_check_ip_asc(X509 *x, const char *ipasc, unsigned int flags)
+       {
+       unsigned char ipout[16];
+       int iplen;
+       iplen = a2i_ipadd(ipout, ipasc);
+       if (iplen == 0)
+               return -2;
+       return do_x509_check(x, ipout, (size_t)iplen, flags, GEN_IPADD);
+       }
+
 /* Convert IP addresses both IPv4 and IPv6 into an 
  * OCTET STRING compatible with RFC3280.
  */
index 34909475ae6e9677a203066e50d08e9b3fc5af23..e5cce114a3d29bc6cfa5973d7b2cdcfd54a2ccb0 100644 (file)
@@ -698,6 +698,20 @@ STACK_OF(OPENSSL_STRING) *X509_get1_email(X509 *x);
 STACK_OF(OPENSSL_STRING) *X509_REQ_get1_email(X509_REQ *x);
 void X509_email_free(STACK_OF(OPENSSL_STRING) *sk);
 STACK_OF(OPENSSL_STRING) *X509_get1_ocsp(X509 *x);
+/* Flags for X509_check_* functions */
+
+/* Always check subject name for host match even if subject alt names present */
+#define X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT   0x1
+/* Disable wild-card matching for dnsName fields and common name. */
+#define X509_CHECK_FLAG_NO_WILDCARDS   0x2
+
+int X509_check_host(X509 *x, const unsigned char *chk, size_t chklen,
+                                       unsigned int flags);
+int X509_check_email(X509 *x, const unsigned char *chk, size_t chklen,
+                                       unsigned int flags);
+int X509_check_ip(X509 *x, const unsigned char *chk, size_t chklen,
+                                       unsigned int flags);
+int X509_check_ip_asc(X509 *x, const char *ipasc, unsigned int flags);
 
 ASN1_OCTET_STRING *a2i_IPADDRESS(const char *ipasc);
 ASN1_OCTET_STRING *a2i_IPADDRESS_NC(const char *ipasc);
index 331c721c9b28c41ec9752440117929f03c1103ca..37b2b85649ffbbb4b814fe2e2c575a8aed8595bf 100644 (file)
@@ -62,6 +62,7 @@ EVPTEST=      evp_test
 IGETEST=       igetest
 JPAKETEST=     jpaketest
 SRPTEST=       srptest
+V3NAMETEST=    v3nametest
 ASN1TEST=      asn1test
 
 TESTS=         alltests
@@ -74,7 +75,7 @@ EXE=  $(BNTEST)$(EXE_EXT) $(ECTEST)$(EXE_EXT)  $(ECDSATEST)$(EXE_EXT) $(ECDHTEST)
        $(RANDTEST)$(EXE_EXT) $(DHTEST)$(EXE_EXT) $(ENGINETEST)$(EXE_EXT) \
        $(BFTEST)$(EXE_EXT) $(CASTTEST)$(EXE_EXT) $(SSLTEST)$(EXE_EXT) $(EXPTEST)$(EXE_EXT) $(DSATEST)$(EXE_EXT) $(RSATEST)$(EXE_EXT) \
        $(EVPTEST)$(EXE_EXT) $(IGETEST)$(EXE_EXT) $(JPAKETEST)$(EXE_EXT) $(SRPTEST)$(EXE_EXT) \
-       $(ASN1TEST)$(EXE_EXT)
+       $(ASN1TEST)$(EXE_EXT) $(V3NAMETEST)$(EXE_EXT)
 
 # $(METHTEST)$(EXE_EXT)
 
@@ -86,7 +87,7 @@ OBJ=  $(BNTEST).o $(ECTEST).o  $(ECDSATEST).o $(ECDHTEST).o $(IDEATEST).o \
        $(MDC2TEST).o $(RMDTEST).o \
        $(RANDTEST).o $(DHTEST).o $(ENGINETEST).o $(CASTTEST).o \
        $(BFTEST).o  $(SSLTEST).o  $(DSATEST).o  $(EXPTEST).o $(RSATEST).o \
-       $(EVPTEST).o $(IGETEST).o $(JPAKETEST).o $(ASN1TEST).o
+       $(EVPTEST).o $(IGETEST).o $(JPAKETEST).o $(ASN1TEST).o $(V3NAMETEST).o
 SRC=   $(BNTEST).c $(ECTEST).c  $(ECDSATEST).c $(ECDHTEST).c $(IDEATEST).c \
        $(MD2TEST).c  $(MD4TEST).c $(MD5TEST).c \
        $(HMACTEST).c $(WPTEST).c \
@@ -94,7 +95,8 @@ SRC=  $(BNTEST).c $(ECTEST).c  $(ECDSATEST).c $(ECDHTEST).c $(IDEATEST).c \
        $(DESTEST).c $(SHATEST).c $(SHA1TEST).c $(MDC2TEST).c $(RMDTEST).c \
        $(RANDTEST).c $(DHTEST).c $(ENGINETEST).c $(CASTTEST).c \
        $(BFTEST).c  $(SSLTEST).c $(DSATEST).c   $(EXPTEST).c $(RSATEST).c \
-       $(EVPTEST).c $(IGETEST).c $(JPAKETEST).c $(SRPTEST).c $(ASN1TEST).c
+       $(EVPTEST).c $(IGETEST).c $(JPAKETEST).c $(SRPTEST).c $(ASN1TEST).c \
+       $(V3NAMETEST).c
 
 EXHEADER= 
 HEADER=        $(EXHEADER)
@@ -137,7 +139,7 @@ alltests: \
        test_enc test_x509 test_rsa test_crl test_sid \
        test_gen test_req test_pkcs7 test_verify test_dh test_dsa \
        test_ss test_ca test_engine test_evp test_ssl test_tsa test_ige \
-       test_jpake test_srp test_cms test_ocsp
+       test_jpake test_srp test_cms test_ocsp test_v3name
 
 test_evp:
        ../util/shlib_wrap.sh ./$(EVPTEST) evptests.txt
@@ -322,6 +324,10 @@ test_ocsp:
        @echo "Test OCSP"
        @sh ./tocsp
 
+test_v3name: $(V3NAMETEST)$(EXE_EXT)
+       @echo "Test X509v3_check_*"
+       ../util/shlib_wrap.sh ./$(V3NAMETEST)
+
 lint:
        lint -DLINT $(INCLUDES) $(SRC)>fluff
 
@@ -473,6 +479,9 @@ $(ASN1TEST)$(EXE_EXT): $(ASN1TEST).o $(DLIBCRYPTO)
 $(SRPTEST)$(EXE_EXT): $(SRPTEST).o $(DLIBCRYPTO)
        @target=$(SRPTEST); $(BUILD_CMD)
 
+$(V3NAMETEST)$(EXE_EXT): $(V3NAMETEST).o $(DLIBCRYPTO)
+       @target=$(V3NAMETEST); $(BUILD_CMD)
+
 #$(AESTEST).o: $(AESTEST).c
 #      $(CC) -c $(CFLAGS) -DINTERMEDIATE_VALUE_KAT -DTRACE_KAT_MCT $(AESTEST).c