/* Usage: ./test-wildcard This is a test program for the ssl_post_connect_check function. It generates certificates with a variety of different combinations of commonNames and dNSNames, then checks that matching names are accepted and non-matching names are rejected. The SSL transactions happen over OpenSSL BIO pairs. */ #include #include #include #include #include #include #include #include #include "ncat_core.h" #define KEY_BITS 1024 static int tests_run = 0, tests_passed = 0; /* A length-delimited string. */ struct lstr { size_t len; const char *s; }; /* Make an anonymous struct lstr. */ #define LSTR(s) { sizeof(s) - 1, (s) } /* Variable-length arrays of struct lstr are terminated with a special sentinel value. */ #define LSTR_SENTINEL { -1, NULL } const struct lstr lstr_sentinel = LSTR_SENTINEL; int is_sentinel(const struct lstr *name) { return name->len == -1; } int ssl_post_connect_check(SSL *ssl, const char *hostname); static struct lstr *check(SSL *ssl, const struct lstr names[]); static int ssl_ctx_trust_cert(SSL_CTX *ctx, X509 *cert); static int gen_cert(X509 **cert, EVP_PKEY **key, const struct lstr commonNames[], const struct lstr dNSNames[]); static void print_escaped(const char *s, size_t len); static void print_array(const struct lstr array[]); static int arrays_equal(const struct lstr a[], const struct lstr b[]); /* Returns positive on success, 0 on failure. The various arrays must be NULL-terminated. */ static int test(const struct lstr commonNames[], const struct lstr dNSNames[], const struct lstr test_names[], const struct lstr expected[]) { SSL_CTX *server_ctx, *client_ctx; SSL *server_ssl, *client_ssl; BIO *server_bio, *client_bio; X509 *cert; EVP_PKEY *key; struct lstr *results; int need_accept, need_connect; int passed; tests_run++; ncat_assert(gen_cert(&cert, &key, commonNames, dNSNames) == 1); ncat_assert(BIO_new_bio_pair(&server_bio, 0, &client_bio, 0) == 1); server_ctx = SSL_CTX_new(SSLv23_server_method()); ncat_assert(server_ctx != NULL); client_ctx = SSL_CTX_new(SSLv23_client_method()); ncat_assert(client_ctx != NULL); SSL_CTX_set_verify(client_ctx, SSL_VERIFY_PEER, NULL); SSL_CTX_set_verify_depth(client_ctx, 1); ssl_ctx_trust_cert(client_ctx, cert); server_ssl = SSL_new(server_ctx); ncat_assert(server_ssl != NULL); SSL_set_accept_state(server_ssl); SSL_set_bio(server_ssl, server_bio, server_bio); ncat_assert(SSL_use_certificate(server_ssl, cert) == 1); ncat_assert(SSL_use_PrivateKey(server_ssl, key) == 1); client_ssl = SSL_new(client_ctx); ncat_assert(client_ssl != NULL); SSL_set_connect_state(client_ssl); SSL_set_bio(client_ssl, client_bio, client_bio); passed = 0; need_accept = 1; need_connect = 1; do { int rc, err; if (need_accept) { rc = SSL_accept(server_ssl); err = SSL_get_error(server_ssl, rc); if (rc == 1) { need_accept = 0; } else { if (err != SSL_ERROR_WANT_READ && err != SSL_ERROR_WANT_WRITE) { printf("SSL_accept: %s \n", ERR_error_string(ERR_get_error(), NULL)); goto end; } } } if (need_connect) { rc = SSL_connect(client_ssl); err = SSL_get_error(client_ssl, rc); if (rc == 1) { need_connect = 0; } else { if (err != SSL_ERROR_WANT_READ && err != SSL_ERROR_WANT_WRITE) { printf("SSL_connect: %s \n", ERR_error_string(ERR_get_error(), NULL)); goto end; } } } } while (need_accept || need_connect); results = check(client_ssl, test_names); if (arrays_equal(results, expected)) { tests_passed++; passed = 1; printf("PASS CN"); print_array(commonNames); printf(" DNS"); print_array(dNSNames); printf("\n"); } else { printf("FAIL CN"); print_array(commonNames); printf(" DNS"); print_array(dNSNames); printf("\n"); printf(" got "); print_array(results); printf("\n"); printf("expected "); print_array(expected); printf("\n"); } free(results); end: X509_free(cert); EVP_PKEY_free(key); (void) BIO_destroy_bio_pair(server_bio); SSL_CTX_free(server_ctx); SSL_CTX_free(client_ctx); SSL_free(server_ssl); SSL_free(client_ssl); return passed; } /* Returns a sentinel-terminated malloc-allocated array of names that match ssl with ssl_post_connect_check. */ static struct lstr *check(SSL *ssl, const struct lstr names[]) { const struct lstr *name; struct lstr *results = NULL; size_t size = 0, capacity = 0; if (names == NULL) return NULL; for (name = names; !is_sentinel(name); name++) { if (ssl_post_connect_check(ssl, name->s)) { if (size >= capacity) { capacity = (size + 1) * 2; results = safe_realloc(results, (capacity + 1) * sizeof(results[0])); } results[size++] = *name; } } results = safe_realloc(results, (size + 1) * sizeof(results[0])); results[size] = lstr_sentinel; return results; } /* Make a certificate object trusted by an SSL_CTX. I couldn't find a way to do this directly, so the certificate is written in PEM format to a temporary file and then loaded with SSL_CTX_load_verify_locations. Returns 1 on success and 0 on failure. */ static int ssl_ctx_trust_cert(SSL_CTX *ctx, X509 *cert) { char name[] = "ncat-test-XXXXXX"; int fd; FILE *fp; int rc; fd = mkstemp(name); if (fd == -1) return 0; fp = fdopen(fd, "w"); if (fp == NULL) { close(fd); return 0; } if (PEM_write_X509(fp, cert) == 0) { fclose(fp); return 0; } fclose(fp); rc = SSL_CTX_load_verify_locations(ctx, name, NULL); if (rc == 0) { fprintf(stderr, "SSL_CTX_load_verify_locations: %s \n", ERR_error_string(ERR_get_error(), NULL)); } if (unlink(name) == -1) fprintf(stderr, "unlink(\"%s\"): %s\n", name, strerror(errno)); return rc; } static int set_dNSNames(X509 *cert, const struct lstr dNSNames[]) { STACK_OF(GENERAL_NAME) *gen_names; GENERAL_NAME *gen_name; X509_EXTENSION *ext; const struct lstr *name; if (dNSNames == NULL || is_sentinel(&dNSNames[0])) return 1; /* We break the abstraction here a bit because the normal way of setting a list of values, using an i2v method, uses a stack of CONF_VALUE that doesn't contain the length of each value. We rely on the fact that the internal representation (the "i" in "i2d") for NID_subject_alt_name is STACK_OF(GENERAL_NAME). */ gen_names = sk_GENERAL_NAME_new_null(); if (gen_names == NULL) return 0; for (name = dNSNames; !is_sentinel(name); name++) { gen_name = GENERAL_NAME_new(); if (gen_name == NULL) goto stack_err; gen_name->type = GEN_DNS; gen_name->d.dNSName = M_ASN1_IA5STRING_new(); if (gen_name->d.dNSName == NULL) goto name_err; if (ASN1_STRING_set(gen_name->d.dNSName, name->s, name->len) == 0) goto name_err; if (sk_GENERAL_NAME_push(gen_names, gen_name) == 0) goto name_err; } ext = X509V3_EXT_i2d(NID_subject_alt_name, 0, gen_names); if (ext == NULL) goto stack_err; if (X509_add_ext(cert, ext, -1) == 0) { X509_EXTENSION_free(ext); goto stack_err; } X509_EXTENSION_free(ext); sk_GENERAL_NAME_pop_free(gen_names, GENERAL_NAME_free); return 1; name_err: GENERAL_NAME_free(gen_name); stack_err: sk_GENERAL_NAME_pop_free(gen_names, GENERAL_NAME_free); return 0; } static int gen_cert(X509 **cert, EVP_PKEY **key, const struct lstr commonNames[], const struct lstr dNSNames[]) { RSA *rsa; int rc; *cert = NULL; *key = NULL; /* Generate a private key. */ *key = EVP_PKEY_new(); if (*key == NULL) goto err; do { rsa = RSA_generate_key(KEY_BITS, RSA_F4, NULL, NULL); if (rsa == NULL) goto err; rc = RSA_check_key(rsa); } while (rc == 0); if (rc == -1) goto err; if (EVP_PKEY_assign_RSA(*key, rsa) == 0) { RSA_free(rsa); goto err; } /* Generate a certificate. */ *cert = X509_new(); if (*cert == NULL) goto err; if (X509_set_version(*cert, 2) == 0) /* Version 3. */ goto err; ASN1_INTEGER_set(X509_get_serialNumber(*cert), get_random_u32() & 0x7FFFFFFF); /* Set the commonNames. */ if (commonNames != NULL) { X509_NAME *subj; const struct lstr *name; subj = X509_get_subject_name(*cert); for (name = commonNames; !is_sentinel(name); name++) { if (X509_NAME_add_entry_by_txt(subj, "commonName", MBSTRING_ASC, (unsigned char *) name->s, name->len, -1, 0) == 0) { goto err; } } } /* Set the dNSNames. */ if (set_dNSNames(*cert, dNSNames) == 0) goto err; if (X509_set_issuer_name(*cert, X509_get_subject_name(*cert)) == 0 || X509_gmtime_adj(X509_get_notBefore(*cert), 0) == 0 || X509_gmtime_adj(X509_get_notAfter(*cert), 60) == 0 || X509_set_pubkey(*cert, *key) == 0) { goto err; } /* Sign it. */ if (X509_sign(*cert, *key, EVP_sha1()) == 0) goto err; return 1; err: if (*cert != NULL) X509_free(*cert); if (*key != NULL) EVP_PKEY_free(*key); return 0; } static void print_escaped(const char *s, size_t len) { int c; for ( ; len > 0; len--) { c = (unsigned char) *s++; if (isprint(c) && !isspace(c)) putchar(c); else printf("\\%03o", c); } } static void print_array(const struct lstr array[]) { const struct lstr *p; if (array == NULL) { printf("[]"); return; } printf("["); for (p = array; !is_sentinel(p); p++) { if (p != array) printf(" "); print_escaped(p->s, p->len); } printf("]"); } static int lstr_equal(const struct lstr *a, const struct lstr *b) { return a->len == b->len && memcmp(a->s, b->s, a->len) == 0; } static int arrays_equal(const struct lstr a[], const struct lstr b[]) { if (a == NULL) return b == NULL; if (b == NULL) return a == NULL; while (!is_sentinel(a) && !is_sentinel(b)) { if (!lstr_equal(a, b)) return 0; a++; b++; } return is_sentinel(a) && is_sentinel(b); } /* This is just a constant used to give a fixed length to the arrays that are conceptually variable-length in the test cases. Increase it if some array grows too big. */ #define ARR_LEN 10 const struct lstr test_names[] = { LSTR("a.com"), LSTR("www.a.com"), LSTR("sub.www.a.com"), LSTR("www.example.com"), LSTR("example.co.uk"), LSTR("*.*.com"), LSTR_SENTINEL }; /* These tests just check that matching a single string works properly. */ struct { const struct lstr name[ARR_LEN]; const struct lstr expected[ARR_LEN]; } single_tests[] = { { { LSTR_SENTINEL }, { LSTR_SENTINEL } }, { { LSTR("a.com"), LSTR_SENTINEL }, { LSTR("a.com"), LSTR_SENTINEL } }, { { LSTR("www.a.com"), LSTR_SENTINEL }, { LSTR("www.a.com"), LSTR_SENTINEL } }, { { LSTR("*.a.com"), LSTR_SENTINEL }, { LSTR("www.a.com"), LSTR_SENTINEL } }, { { LSTR("w*.a.com"), LSTR_SENTINEL }, { LSTR_SENTINEL } }, { { LSTR("*w.a.com"), LSTR_SENTINEL }, { LSTR_SENTINEL } }, { { LSTR("www.*.com"), LSTR_SENTINEL }, { LSTR_SENTINEL } }, { { LSTR("*.com"), LSTR_SENTINEL }, { LSTR_SENTINEL } }, { { LSTR("*.com."), LSTR_SENTINEL }, { LSTR_SENTINEL } }, { { LSTR("*.*.com"), LSTR_SENTINEL }, { LSTR_SENTINEL } }, { { LSTR("a.com\0evil.com"), LSTR_SENTINEL }, { LSTR_SENTINEL } }, }; /* These test different combinations of commonName and dNSName. */ struct { const struct lstr common[ARR_LEN]; const struct lstr dns[ARR_LEN]; const struct lstr expected[ARR_LEN]; } double_tests[] = { /* Should not match any commonName if any dNSNames exist. */ { { LSTR("a.com"), LSTR_SENTINEL }, { LSTR("example.co.uk"), LSTR_SENTINEL }, { LSTR("example.co.uk"), LSTR_SENTINEL } }, { { LSTR("a.com"), LSTR_SENTINEL }, { LSTR("b.com"), LSTR_SENTINEL }, { LSTR_SENTINEL } }, /* Should check against all of the dNSNames. */ { { LSTR_SENTINEL }, { LSTR("a.com"), LSTR("example.co.uk"), LSTR("b.com"), LSTR_SENTINEL }, { LSTR("a.com"), LSTR("example.co.uk"), LSTR_SENTINEL } }, }; const struct lstr specificity_test_names[] = { LSTR("a.com"), LSTR("sub.b.com"), LSTR("sub.c.com"), LSTR("sub.d.com"), LSTR("sub.sub.e.com"), LSTR("sub.sub.f.com"), LSTR("sub.sub.g.com"), LSTR_SENTINEL }; /* Validation should check only the "most specific" commonName if multiple exist. This "most specific" term is used in RFCs 2818, 4261, and 5018 at least, but is not defined anywhere that I can find. Let's interpret it as the greatest number of name elements, with wildcard names considered less specific than all non-wildcard names. For ties, the name that comes later is considered more specific. */ struct { const struct lstr patterns[ARR_LEN]; const struct lstr expected_forward; const struct lstr expected_backward; } specificity_tests[] = { { { LSTR("a.com"), LSTR("*.b.com"), LSTR("sub.c.com"), LSTR("sub.d.com"), LSTR("*.sub.e.com"), LSTR("*.sub.f.com"), LSTR("sub.sub.g.com"), LSTR_SENTINEL }, LSTR("sub.sub.g.com"), LSTR("sub.sub.g.com") }, { { LSTR("a.com"), LSTR("*.b.com"), LSTR("sub.c.com"), LSTR("sub.d.com"), LSTR("*.sub.e.com"), LSTR("*.sub.f.com"), LSTR_SENTINEL }, LSTR("sub.d.com"), LSTR("sub.c.com") }, { { LSTR("a.com"), LSTR("*.b.com"), LSTR("sub.c.com"), LSTR("*.sub.e.com"), LSTR("*.sub.f.com"), LSTR_SENTINEL }, LSTR("sub.c.com"), LSTR("sub.c.com") }, { { LSTR("a.com"), LSTR("*.b.com"), LSTR("*.sub.e.com"), LSTR("*.sub.f.com"), LSTR_SENTINEL }, LSTR("a.com"), LSTR("a.com") }, { { LSTR("*.b.com"), LSTR("*.sub.e.com"), LSTR("*.sub.f.com"), LSTR_SENTINEL }, LSTR("sub.sub.f.com"), LSTR("sub.sub.e.com") }, { { LSTR("*.b.com"), LSTR("*.sub.e.com"), LSTR_SENTINEL }, LSTR("sub.sub.e.com"), LSTR("sub.sub.e.com") }, }; #define NELEMS(a) (sizeof(a) / sizeof(a[0])) void reverse(struct lstr a[]) { struct lstr tmp; unsigned int i, j; i = 0; for (j = 0; !is_sentinel(&a[j]); j++) ; if (j == 0) return; j--; while (i < j) { tmp = a[i]; a[i] = a[j]; a[j] = tmp; i++; j--; } } void test_specificity(const struct lstr patterns[], const struct lstr test_names[], const struct lstr expected_forward[], const struct lstr expected_backward[]) { struct lstr scratch[ARR_LEN]; unsigned int i; for (i = 0; i < ARR_LEN && !is_sentinel(&patterns[i]); i++) scratch[i] = patterns[i]; ncat_assert(i < ARR_LEN); scratch[i] = lstr_sentinel; test(scratch, NULL, test_names, expected_forward); reverse(scratch); test(scratch, NULL, test_names, expected_backward); return; } int main(void) { unsigned int i; SSL_library_init(); ERR_load_crypto_strings(); SSL_load_error_strings(); /* Test single pattens in both the commonName and dNSName positions. */ for (i = 0; i < NELEMS(single_tests); i++) test(single_tests[i].name, NULL, test_names, single_tests[i].expected); for (i = 0; i < NELEMS(single_tests); i++) test(NULL, single_tests[i].name, test_names, single_tests[i].expected); for (i = 0; i < NELEMS(double_tests); i++) { test(double_tests[i].common, double_tests[i].dns, test_names, double_tests[i].expected); } for (i = 0; i < NELEMS(specificity_tests); i++) { struct lstr expected_forward[2], expected_backward[2]; /* Put the expected names in arrays for the test. */ expected_forward[0] = specificity_tests[i].expected_forward; expected_forward[1] = lstr_sentinel; expected_backward[0] = specificity_tests[i].expected_backward; expected_backward[1] = lstr_sentinel; test_specificity(specificity_tests[i].patterns, specificity_test_names, expected_forward, expected_backward); } printf("%d / %d tests passed.\n", tests_passed, tests_run); return tests_passed == tests_run ? 0 : 1; }