diff --git a/lib/hx509/hxtool-commands.in b/lib/hx509/hxtool-commands.in index 11a4289e6..a75d539c3 100644 --- a/lib/hx509/hxtool-commands.in +++ b/lib/hx509/hxtool-commands.in @@ -858,6 +858,150 @@ command = { argument="certificates..." help = "Create a CRL" } +command = { + option = { + long = "verbose" + short = "v" + type = "flag" + help = "verbose" + } + option = { + long = "end-entity" + type = "flag" + help = "check the first EE certificate in the store" + } + option = { + long = "ca" + type = "flag" + help = "check the first CA certificate in the store" + } + option = { + long = "cert-num" + type = "integer" + default = "-1" + help = "check the nth certificate in the store" + } + option = { + long = "expr" + type = "string" + argument = "expression" + help = "test the first certificate matching expression" + } + option = { + long = "has-email-san" + short = "M" + type = "strings" + argument = "email-address" + help = "check that cert has email SAN" + } + option = { + long = "has-xmpp-san" + type = "strings" + short = "X" + argument = "jabber address" + help = "check that cert has XMPP SAN" + } + option = { + long = "has-ms-upn-san" + short = "U" + type = "strings" + argument = "UPN" + help = "check that cert has UPN SAN" + } + option = { + long = "has-dnsname-san" + short = "D" + type = "strings" + argument = "domainname" + help = "check that cert has domainname SAN" + } + option = { + long = "has-pkinit-san" + short = "P" + type = "strings" + argument = "Kerberos principal name" + help = "check that cert has PKINIT SAN" + } + option = { + long = "has-registeredID-san" + short = "R" + type = "strings" + argument = "OID" + help = "check that cert has registeredID SAN" + } + option = { + long = "has-eku" + short = "E" + type = "strings" + argument = "OID" + help = "check that cert has EKU" + } + option = { + long = "has-ku" + short = "K" + type = "strings" + argument = "key usage element" + help = "check that cert has key usage" + } + option = { + long = "exact" + type = "flag" + help = "check that cert has only given SANs/EKUs/KUs" + } + option = { + long = "valid-now" + short = "n" + type = "flag" + help = "check that current time is in certicate's validity period" + } + option = { + long = "valid-at" + type = "string" + argument = "datetime" + help = "check that the certificate is valid at given time" + } + option = { + long = "not-after-eq" + type = "string" + argument = "datetime" + help = "check that the certificate's notAfter is as given" + } + option = { + long = "not-after-lt" + type = "string" + argument = "datetime" + help = "check that the certificate's notAfter is before the given time" + } + option = { + long = "not-after-gt" + type = "string" + argument = "datetime" + help = "check that the certificate's notAfter is after the given time" + } + option = { + long = "not-before-eq" + type = "string" + argument = "datetime" + help = "check that the certificate's notBefore is as given" + } + option = { + long = "not-before-lt" + type = "string" + argument = "datetime" + help = "check that the certificate's notBefore is before the given time" + } + option = { + long = "not-before-gt" + type = "string" + argument = "datetime" + help = "check that the certificate's notBefore is after the given time" + } + name = "acert" + min_args = "1" + max_args = "1" + argument = "certificate-store" + help = "Assert certificate content" +} command = { name = "help" name = "?" diff --git a/lib/hx509/hxtool.c b/lib/hx509/hxtool.c index 9d5eb383e..fec71fe23 100644 --- a/lib/hx509/hxtool.c +++ b/lib/hx509/hxtool.c @@ -2364,6 +2364,547 @@ hxtool_list_oids(void *opt, int argc, char **argv) return 0; } +static int +acert1_sans_utf8_other(struct acert_options *opt, + struct getarg_strings *wanted, + const char *type, + heim_any *san, + size_t *count) +{ + size_t k, len; + + if (!wanted->num_strings) + return 0; + for (k = 0; k < wanted->num_strings; k++) { + len = strlen(wanted->strings[k]); + if (len == san->length && + strncmp(san->data, wanted->strings[k], len) == 0) { + if (opt->verbose_flag) + fprintf(stderr, "Matched OtherName SAN %s (%s)\n", + wanted->strings[k], type); + (*count)++; + return 0; + } + } + if (opt->verbose_flag) + fprintf(stderr, "Did not match OtherName SAN %s (%s)\n", + wanted->strings[k], type); + return -1; +} + +static int +acert1_sans_other(struct acert_options *opt, + heim_oid *type_id, + heim_any *value, + size_t *count) +{ + heim_any pkinit; + size_t k, match; + const char *type_str = NULL; + char *s = NULL; + int ret; + + (void) der_print_heim_oid_sym(type_id, '.', &s); + type_str = s ? s : ""; + if (der_heim_oid_cmp(type_id, &asn1_oid_id_pkix_on_xmppAddr) == 0) { + ret = acert1_sans_utf8_other(opt, &opt->has_xmpp_san_strings, + s ? s : "xmpp", value, count); + free(s); + return ret; + } + if (der_heim_oid_cmp(type_id, &asn1_oid_id_pkinit_san) != 0) { + if (opt->verbose_flag) + fprintf(stderr, "Ignoring OtherName SAN of type %s\n", type_str); + free(s); + return -1; + } + + free(s); + type_str = s = NULL; + + if (opt->has_pkinit_san_strings.num_strings == 0) + return 0; + + for (k = 0; k < opt->has_pkinit_san_strings.num_strings; k++) { + const char *s2 = opt->has_pkinit_san_strings.strings[k]; + + if ((ret = _hx509_make_pkinit_san(context, s2, &pkinit))) + return ret; + match = (pkinit.length == value->length && + memcmp(pkinit.data, value->data, pkinit.length) == 0); + free(pkinit.data); + if (match) { + if (opt->verbose_flag) + fprintf(stderr, "Matched PKINIT SAN %s\n", s2); + (*count)++; + return 0; + } + } + if (opt->verbose_flag) + fprintf(stderr, "Unexpected PKINIT SAN\n"); + return -1; +} + +static int +acert1_sans(struct acert_options *opt, + Extension *e, + size_t *count, + size_t *found) +{ + heim_printable_string hps; + GeneralNames gns; + size_t i, k, sz; + size_t unwanted = 0; + int ret = 0; + + memset(&gns, 0, sizeof(gns)); + decode_GeneralNames(e->extnValue.data, e->extnValue.length, &gns, &sz); + for (i = 0; (ret == -1 || ret == 0) && i < gns.len; i++) { + GeneralName *gn = &gns.val[i]; + const char *s; + + (*found)++; + if (gn->element == choice_GeneralName_rfc822Name) { + for (k = 0; k < opt->has_email_san_strings.num_strings; k++) { + s = opt->has_email_san_strings.strings[k]; + hps.data = rk_UNCONST(s); + hps.length = strlen(s); + if (der_printable_string_cmp(&gn->u.rfc822Name, &hps) == 0) { + if (opt->verbose_flag) + fprintf(stderr, "Matched e-mail address SAN %s\n", s); + (*count)++; + break; + } + } + if (k && k == opt->has_email_san_strings.num_strings) { + if (opt->verbose_flag) + fprintf(stderr, "Unexpected e-mail address SAN %.*s\n", + (int)gn->u.rfc822Name.length, + (const char *)gn->u.rfc822Name.data); + unwanted++; + } + } else if (gn->element == choice_GeneralName_dNSName) { + for (k = 0; k < opt->has_dnsname_san_strings.num_strings; k++) { + s = opt->has_dnsname_san_strings.strings[k]; + hps.data = rk_UNCONST(s); + hps.length = strlen(s); + if (der_printable_string_cmp(&gn->u.dNSName, &hps) == 0) { + if (opt->verbose_flag) + fprintf(stderr, "Matched dNSName SAN %s\n", s); + (*count)++; + break; + } + } + if (k && k == opt->has_dnsname_san_strings.num_strings) { + if (opt->verbose_flag) + fprintf(stderr, "Unexpected e-mail address SAN %.*s\n", + (int)gn->u.dNSName.length, + (const char *)gn->u.dNSName.data); + unwanted++; + } + } else if (gn->element == choice_GeneralName_registeredID) { + for (k = 0; k < opt->has_registeredID_san_strings.num_strings; k++) { + s = opt->has_registeredID_san_strings.strings[k]; + heim_oid oid; + + memset(&oid, 0, sizeof(oid)); + if ((ret = der_parse_heim_oid(s, NULL, &oid))) + break; + if (der_heim_oid_cmp(&gn->u.registeredID, &oid) == 0) { + der_free_oid(&oid); + if (opt->verbose_flag) + fprintf(stderr, "Matched registeredID SAN %s\n", s); + (*count)++; + break; + } + der_free_oid(&oid); + } + if (k && k == opt->has_dnsname_san_strings.num_strings) { + if (opt->verbose_flag) + fprintf(stderr, "Unexpected registeredID SAN\n"); + unwanted++; + } + } else if (gn->element == choice_GeneralName_otherName) { + ret = acert1_sans_other(opt, &gn->u.otherName.type_id, + &gn->u.otherName.value, count); + } else if (opt->verbose_flag) { + fprintf(stderr, "Unexpected unsupported SAN\n"); + unwanted++; + } + } + free_GeneralNames(&gns); + if (ret == 0 && unwanted && opt->exact_flag) + return -1; + return ret; +} + +static int +acert1_ekus(struct acert_options *opt, + Extension *e, + size_t *count, + size_t *found) +{ + ExtKeyUsage eku; + size_t i, k, sz; + size_t unwanted = 0; + int ret = 0; + + memset(&eku, 0, sizeof(eku)); + decode_ExtKeyUsage(e->extnValue.data, e->extnValue.length, &eku, &sz); + for (i = 0; (ret == -1 || ret == 0) && i < eku.len; i++) { + (*found)++; + for (k = 0; k < opt->has_eku_strings.num_strings; k++) { + const char *s = opt->has_eku_strings.strings[k]; + heim_oid oid; + + memset(&oid, 0, sizeof(oid)); + if ((ret = der_parse_heim_oid(s, NULL, &oid))) + break; + if (der_heim_oid_cmp(&eku.val[i], &oid) == 0) { + der_free_oid(&oid); + if (opt->verbose_flag) + fprintf(stderr, "Matched EKU OID %s\n", s); + (*count)++; + break; + } + der_free_oid(&oid); + } + if (k && k == opt->has_eku_strings.num_strings) { + char *oids = NULL; + + (void) der_print_heim_oid_sym(&eku.val[i], '.', &oids); + if (opt->verbose_flag) + fprintf(stderr, "Unexpected EKU OID %s\n", + oids ? oids : ""); + unwanted++; + } + } + free_ExtKeyUsage(&eku); + if (ret == 0 && unwanted && opt->exact_flag) + return -1; + return ret; +} + +static int +acert1_kus(struct acert_options *opt, + Extension *e, + size_t *count, + size_t *found) +{ + const struct units *u = asn1_KeyUsage_units(); + uint64_t ku_num; + KeyUsage ku; + size_t unwanted = 0; + size_t wanted = opt->has_ku_strings.num_strings; + size_t i, k, sz; + + memset(&ku, 0, sizeof(ku)); + decode_KeyUsage(e->extnValue.data, e->extnValue.length, &ku, &sz); + ku_num = KeyUsage2int(ku); + + /* Validate requested key usage values */ + for (k = 0; k < wanted; k++) { + const char *s = opt->has_ku_strings.strings[k]; + + for (i = 0; u[i].name; i++) + if (strcmp(s, u[i].name) == 0) + break; + + if (u[i].name == NULL) + warnx("Warning: requested key usage %s unknown", s); + } + + for (i = 0; u[i].name; i++) { + if ((u[i].mult & ku_num)) + (*found)++; + for (k = 0; k < wanted; k++) { + const char *s = opt->has_ku_strings.strings[k]; + + if (!(u[i].mult & ku_num) || strcmp(s, u[i].name) != 0) + continue; + + if (opt->verbose_flag) + fprintf(stderr, "Matched key usage %s\n", s); + (*count)++; + break; + } + if ((u[i].mult & ku_num) && k == wanted) { + if (opt->verbose_flag) + fprintf(stderr, "Unexpected key usage %s\n", u[i].name); + unwanted++; + } + } + + return (unwanted && opt->exact_flag) ? -1 : 0; +} + +static time_t +ptime(const char *s) +{ + struct tm at_tm; + char *rest; + int at_s; + + if ((rest = strptime(s, "%Y-%m-%dT%H:%M:%S", &at_tm)) != NULL && + rest[0] == '\0') + return mktime(&at_tm); + if ((rest = strptime(s, "%Y%m%d%H%M%S", &at_tm)) != NULL && rest[0] == '\0') + return mktime(&at_tm); + if ((at_s = parse_time(s, "s")) != -1) + return time(NULL) + at_s; + errx(1, "Could not parse time spec %s", s); +} + +static int +acert1_validity(struct acert_options *opt, hx509_cert cert) +{ + time_t not_before_eq = 0; + time_t not_before_lt = 0; + time_t not_before_gt = 0; + time_t not_after_eq = 0; + time_t not_after_lt = 0; + time_t not_after_gt = 0; + + if (opt->valid_now_flag) { + time_t now = time(NULL); + + if (hx509_cert_get_notBefore(cert) > now) { + if (opt->verbose_flag) + fprintf(stderr, "Certificate not valid yet\n"); + return -1; + } + if (hx509_cert_get_notAfter(cert) < now) { + if (opt->verbose_flag) + fprintf(stderr, "Certificate currently expired\n"); + return -1; + } + } + if (opt->valid_at_string) { + time_t at = ptime(opt->valid_at_string); + + if (hx509_cert_get_notBefore(cert) > at) { + if (opt->verbose_flag) + fprintf(stderr, "Certificate not valid yet at %s\n", + opt->valid_at_string); + return -1; + } + if (hx509_cert_get_notAfter(cert) < at) { + if (opt->verbose_flag) + fprintf(stderr, "Certificate expired before %s\n", + opt->valid_at_string); + return -1; + } + } + + if (opt->not_before_eq_string) + not_before_eq = ptime(opt->not_before_eq_string); + if (opt->not_before_lt_string) + not_before_lt = ptime(opt->not_before_lt_string); + if (opt->not_before_gt_string) + not_before_gt = ptime(opt->not_before_gt_string); + if (opt->not_after_eq_string) + not_after_eq = ptime(opt->not_after_eq_string); + if (opt->not_after_lt_string) + not_after_lt = ptime(opt->not_after_lt_string); + if (opt->not_after_gt_string) + not_after_gt = ptime(opt->not_after_gt_string); + + if ((not_before_eq && hx509_cert_get_notBefore(cert) != not_before_eq) || + (not_before_lt && hx509_cert_get_notBefore(cert) >= not_before_lt) || + (not_before_gt && hx509_cert_get_notBefore(cert) <= not_before_gt)) { + if (opt->verbose_flag) + fprintf(stderr, "Certificate notBefore not as requested\n"); + return -1; + } + if ((not_after_eq && hx509_cert_get_notAfter(cert) != not_after_eq) || + (not_after_lt && hx509_cert_get_notAfter(cert) >= not_after_lt) || + (not_after_gt && hx509_cert_get_notAfter(cert) <= not_after_gt)) { + if (opt->verbose_flag) + fprintf(stderr, "Certificate notAfter not as requested\n"); + return -1; + } + + return 0; +} + +static int +acert1(struct acert_options *opt, size_t cert_num, hx509_cert cert, int *matched) +{ + const heim_oid *misc_exts [] = { + &asn1_oid_id_x509_ce_authorityKeyIdentifier, + &asn1_oid_id_x509_ce_subjectKeyIdentifier, + &asn1_oid_id_x509_ce_basicConstraints, + &asn1_oid_id_x509_ce_nameConstraints, + &asn1_oid_id_x509_ce_certificatePolicies, + &asn1_oid_id_x509_ce_policyMappings, + &asn1_oid_id_x509_ce_issuerAltName, + &asn1_oid_id_x509_ce_subjectDirectoryAttributes, + &asn1_oid_id_x509_ce_policyConstraints, + &asn1_oid_id_x509_ce_cRLDistributionPoints, + &asn1_oid_id_x509_ce_deltaCRLIndicator, + &asn1_oid_id_x509_ce_issuingDistributionPoint, + &asn1_oid_id_x509_ce_inhibitAnyPolicy, + &asn1_oid_id_x509_ce_cRLNumber, + &asn1_oid_id_x509_ce_freshestCRL, + NULL + }; + const Certificate *c; + const Extensions *e; + KeyUsage ku; + size_t matched_elements = 0; + size_t wanted, sans_wanted, ekus_wanted, kus_wanted; + size_t found, sans_found, ekus_found, kus_found; + size_t i, k; + int ret; + + if ((c = _hx509_get_cert(cert)) == NULL) + errx(1, "Could not get Certificate"); + e = c->tbsCertificate.extensions; + + ret = _hx509_cert_get_keyusage(context, cert, &ku); + if (ret && ret != HX509_KU_CERT_MISSING) + hx509_err(context, 1, ret, "Could not get key usage of certificate"); + if (ret == HX509_KU_CERT_MISSING && opt->ca_flag) + return 0; /* want CA cert; this isn't it */ + if (ret == 0 && opt->ca_flag && !ku.keyCertSign) + return 0; /* want CA cert; this isn't it */ + if (ret == 0 && opt->end_entity_flag && ku.keyCertSign) + return 0; /* want EE cert; this isn't it */ + + if (opt->cert_num_integer != -1 && cert_num <= INT_MAX && + opt->cert_num_integer != (int)cert_num) + return 0; + if (opt->cert_num_integer == -1 || opt->cert_num_integer == (int)cert_num) + *matched = 1; + + if (_hx509_cert_get_version(c) < 3) { + warnx("Certificate with version %d < 3 ignored", + _hx509_cert_get_version(c)); + return 0; + } + + sans_wanted = opt->has_email_san_strings.num_strings + + opt->has_xmpp_san_strings.num_strings + + opt->has_ms_upn_san_strings.num_strings + + opt->has_dnsname_san_strings.num_strings + + opt->has_pkinit_san_strings.num_strings + + opt->has_registeredID_san_strings.num_strings; + ekus_wanted = opt->has_eku_strings.num_strings; + kus_wanted = opt->has_ku_strings.num_strings; + wanted = sans_wanted + ekus_wanted + kus_wanted; + found = sans_found = ekus_found = kus_found = 0; + + if (e == NULL) { + if (wanted) + return -1; + return acert1_validity(opt, cert);; + } + + for (i = 0; i < e->len; i++) { + if (der_heim_oid_cmp(&e->val[i].extnID, + &asn1_oid_id_x509_ce_subjectAltName) == 0) { + ret = acert1_sans(opt, &e->val[i], &matched_elements, &sans_found); + if (ret == -1 && sans_wanted == 0 && + (!opt->exact_flag || sans_found == 0)) + ret = 0; + } else if (der_heim_oid_cmp(&e->val[i].extnID, + &asn1_oid_id_x509_ce_extKeyUsage) == 0) { + ret = acert1_ekus(opt, &e->val[i], &matched_elements, &ekus_found); + if (ret == -1 && ekus_wanted == 0 && + (!opt->exact_flag || ekus_found == 0)) + ret = 0; + } else if (der_heim_oid_cmp(&e->val[i].extnID, + &asn1_oid_id_x509_ce_keyUsage) == 0) { + ret = acert1_kus(opt, &e->val[i], &matched_elements, &kus_found); + if (ret == -1 && kus_wanted == 0 && + (!opt->exact_flag || kus_found == 0)) + ret = 0; + } else { + char *oids = NULL; + + for (k = 0; misc_exts[k]; k++) { + if (der_heim_oid_cmp(&e->val[i].extnID, misc_exts[k]) == 0) + break; + } + if (misc_exts[k]) + continue; + + (void) der_print_heim_oid(&e->val[i].extnID, '.', &oids); + warnx("Matching certificate has unexpected certificate " + "extension %s", oids ? oids : ""); + free(oids); + ret = -1; + } + if (ret && ret != -1) + hx509_err(context, 1, ret, "Error checking matching certificate"); + if (ret == -1) + break; + } + if (matched_elements != wanted) + return -1; + found = sans_found + ekus_found + kus_found; + if (matched_elements != found && opt->exact_flag) + return -1; + if (ret) + return ret; + return acert1_validity(opt, cert); +} + +int +acert(struct acert_options *opt, int argc, char **argv) +{ + hx509_cursor cursor = NULL; + hx509_certs certs = NULL; + hx509_cert cert = NULL; + size_t n = 0; + int matched = 0; + int ret; + + if (opt->not_after_eq_string && + (opt->not_after_lt_string || opt->not_after_gt_string)) + errx(1, "--not-after-eq should not be given with --not-after-lt/gt"); + if (opt->not_before_eq_string && + (opt->not_before_lt_string || opt->not_before_gt_string)) + errx(1, "--not-before-eq should not be given with --not-before-lt/gt"); + + if ((ret = hx509_certs_init(context, argv[0], 0, NULL, &certs))) + hx509_err(context, 1, ret, "Could not load certificates from %s", + argv[0]); + + hx509_query *q = NULL; + if (opt->expr_string) { + if ((ret = hx509_query_alloc(context, &q))) + hx509_err(context, 1, ret, "Could not initialize query"); + hx509_query_match_expr(context, q, opt->expr_string); + if ((ret = hx509_certs_find(context, certs, q, &cert)) || !cert) + hx509_err(context, 1, ret, "No matching certificate"); + ret = acert1(opt, -1, cert, &matched); + matched = 1; + } else { + ret = hx509_certs_start_seq(context, certs, &cursor); + while (ret == 0 && + (ret = hx509_certs_next_cert(context, certs, + cursor, &cert)) == 0 && + cert) { + ret = acert1(opt, n++, cert, &matched); + if (matched) + break; + } + if (cursor) + (void) hx509_certs_end_seq(context, certs, cursor); + } + if (!matched && ret) + hx509_err(context, 1, ret, "Could not find certificate"); + if (!matched) + errx(1, "Could not find certificate"); + if (ret == -1) + errx(1, "Matching certificate did not meet requirements"); + if (ret) + hx509_err(context, 1, ret, "Matching certificate did not meet " + "requirements"); + return 0; +} + /* * */