From 8af2d79d35a2a8223c19ed99a49d184e92447927 Mon Sep 17 00:00:00 2001 From: Nicolas Williams Date: Tue, 16 Jul 2019 14:51:59 -0500 Subject: [PATCH] hx509: Add missing CSR extension request support This is necessary in order to add proper support for CSRs in kx509, where the KDC can examine all requested KUs/EKUs/SANs, check authorization, and issue a certificate with all those extensions if authorized. This is the convention used by OpenSSL, of encoding all the KU, EKUs, and SANs being requested as Extensions as they would appear in the TBSCertificate, then putting those in as a single Attribute in the CSR's Attributes list with attribute OID {id-pkcs-9, 14}. - expose all hx509_request_*() functions - finish support in hx509_request_parse*() for KU, EKU, and SAN CSR attributes - finish support in hx509_request_to_pkcs10() for encoding all requested KU, EKU, and SAN extensions as a CSR extReq (extension request) - add hx509_request_add_*() support for: - id-pkinit-san and ms-upn-pkinit-san - XMPP (Jabber) SAN - registeredID (useless but trivial) - add hxtool request-create options for all supported SANs - add hxtool request-create options for KeyUsage - add hxtool request-create options for ExtKeyUsage - add hxtool request-print support for all these things - fix bugs in existing id-pkinit-san handling Possible future improvements - add HX509_TRACE env var and support (it would be nice to be able to observe why some certificate is rejected, or not matched in a query) - add testing that CSR creating and printing round-trip for all KUs, EKUs, and SANs (probably in tests/kdc/check-pkinit.in) - add testing that OpenSSL can print a CSR made by hxtool and vice-versa - hxtool ca: add KU sanity checking (via hx509_ca_sign() and/or friends) (don't allow encrypt for signing-only algs) (don't allow encrypt for RSA at all, or for RSA with small e exponents) - hxtool request-print: warn about all unknown attributes and extensions - hxtool ca: MAYBE add support for adding requested extensions from the --req=CSR ("Maybe" because CA operators should really verify and authorize all requested attributes, and should acknowledge that they have, and the simplest way to do this is to make them add all the corresponding CLI arguments to the hxtool ca command, but too, that is error-prone, thus it's not clear yet which approach is best. Perhaps interactively prompt for yes/no for each attribute.) - add additional SAN types: - iPAddress (useless?) - dNSSrv (useful!) - directoryName (useless, but trivial) - uniformResourceIdentifier (useful) - it would be nice if the ASN.1 compiler could generate print functions..., and/or even better, to-JSON functions - it would be nice if we had a known-OID db, including the names of the types they refer to in certificate extensions, otherName SANs and CSR attributes, then we could generate a CSR and certificate printer for all known options even when they are not supported by the rest of Heimdal - and we could also get friendly names for OIDs, and we could resolve their arc names - longer term, we could also stand to add some ASN.1 information object system functionality, just enough to make lib/hx509/asn1_print awesome by being able to automatically decode all heim_any and OCTET STRING content (better than its current --inner option) --- lib/asn1/pkcs10.asn1 | 1 - lib/asn1/pkcs9.asn1 | 1 + lib/asn1/rfc2459.asn1 | 4 +- lib/hx509/ca.c | 218 ++++--- lib/hx509/hxtool-commands.in | 25 + lib/hx509/hxtool.c | 64 +- lib/hx509/libhx509-exports.def | 29 +- lib/hx509/name.c | 125 +++- lib/hx509/req.c | 1117 ++++++++++++++++++++++++++++---- lib/hx509/test_name.c | 88 ++- lib/hx509/test_req.in | 80 ++- lib/hx509/version-script.map | 26 +- lib/roken/parse_units.c | 2 +- 13 files changed, 1554 insertions(+), 226 deletions(-) diff --git a/lib/asn1/pkcs10.asn1 b/lib/asn1/pkcs10.asn1 index f3fe37b1b..14685bc7d 100644 --- a/lib/asn1/pkcs10.asn1 +++ b/lib/asn1/pkcs10.asn1 @@ -7,7 +7,6 @@ IMPORTS Name, SubjectPublicKeyInfo, Attribute, AlgorithmIdentifier FROM rfc2459; - CertificationRequestInfo ::= SEQUENCE { version INTEGER { pkcs10-v1(0) }, subject Name, diff --git a/lib/asn1/pkcs9.asn1 b/lib/asn1/pkcs9.asn1 index 50bf9dd1c..43b4a9dfd 100644 --- a/lib/asn1/pkcs9.asn1 +++ b/lib/asn1/pkcs9.asn1 @@ -14,6 +14,7 @@ id-pkcs9-contentType OBJECT IDENTIFIER ::= {id-pkcs-9 3 } id-pkcs9-messageDigest OBJECT IDENTIFIER ::= {id-pkcs-9 4 } id-pkcs9-signingTime OBJECT IDENTIFIER ::= {id-pkcs-9 5 } id-pkcs9-countersignature OBJECT IDENTIFIER ::= {id-pkcs-9 6 } +id-pkcs9-extReq OBJECT IDENTIFIER ::= {id-pkcs-9 14} id-pkcs-9-at-friendlyName OBJECT IDENTIFIER ::= {id-pkcs-9 20} id-pkcs-9-at-localKeyId OBJECT IDENTIFIER ::= {id-pkcs-9 21} diff --git a/lib/asn1/rfc2459.asn1 b/lib/asn1/rfc2459.asn1 index 05214b866..b7deb63ff 100644 --- a/lib/asn1/rfc2459.asn1 +++ b/lib/asn1/rfc2459.asn1 @@ -185,9 +185,11 @@ DirectoryString ::= CHOICE { bmpString BMPString } +AttributeValues ::= SET OF AttributeValue + Attribute ::= SEQUENCE { type AttributeType, - value SET OF -- AttributeValue -- heim_any + value AttributeValues } AttributeTypeAndValue ::= SEQUENCE { diff --git a/lib/hx509/ca.c b/lib/hx509/ca.c index f81641eef..6aa464eab 100644 --- a/lib/hx509/ca.c +++ b/lib/hx509/ca.c @@ -604,6 +604,145 @@ hx509_ca_tbs_add_san_otherName(hx509_context context, return add_GeneralNames(&tbs->san, &gn); } +static +int +dequote_strndup(hx509_context context, const char *in, size_t len, char **out) +{ + size_t i, k; + char *s; + + *out = NULL; + if ((s = malloc(len + 1)) == NULL) { + hx509_set_error_string(context, 0, ENOMEM, "malloc: out of memory"); + return ENOMEM; + } + + for (k = i = 0; i < len; i++) { + if (in[i] == '\\') { + switch (in[++i]) { + case 't': s[k++] = '\t'; break; + case 'b': s[k++] = '\b'; break; + case 'n': s[k++] = '\n'; break; + case '0': + for (i++; i < len; i++) { + if (in[i] == '\0') + break; + if (in[i++] == '\\' && in[i] == '0') + continue; + hx509_set_error_string(context, 0, + HX509_PARSING_NAME_FAILED, + "embedded NULs not supported in " + "PKINIT SANs"); + free(s); + return HX509_PARSING_NAME_FAILED; + } + break; + case '\0': + hx509_set_error_string(context, 0, + HX509_PARSING_NAME_FAILED, + "trailing unquoted backslashes not " + "allowed in PKINIT SANs"); + free(s); + return HX509_PARSING_NAME_FAILED; + default: s[k++] = in[i]; break; + } + } else { + s[k++] = in[i]; + } + } + s[k] = '\0'; + + *out = s; + return 0; +} + +int +_hx509_make_pkinit_san(hx509_context context, + const char *principal, + heim_octet_string *os) +{ + KRB5PrincipalName p; + size_t size; + int ret; + + os->data = NULL; + os->length = 0; + memset(&p, 0, sizeof(p)); + + /* Parse principal */ + { + const char *str, *str_start; + size_t n, i; + + /* Count number of components */ + n = 1; + for (str = principal; *str != '\0' && *str != '@'; str++) { + if (*str == '\\') { + if (str[1] == '\0') { + ret = HX509_PARSING_NAME_FAILED; + hx509_set_error_string(context, 0, ret, + "trailing \\ in principal name"); + goto out; + } + str++; + } else if(*str == '/') { + n++; + } else if(*str == '@') { + break; + } + } + if (*str != '@') { + /* Note that we allow the realm to be empty */ + ret = HX509_PARSING_NAME_FAILED; + hx509_set_error_string(context, 0, ret, "Missing @ in principal"); + goto out; + }; + + p.principalName.name_string.val = + calloc(n, sizeof(*p.principalName.name_string.val)); + if (p.principalName.name_string.val == NULL) { + ret = ENOMEM; + hx509_set_error_string(context, 0, ret, "malloc: out of memory"); + goto out; + } + p.principalName.name_string.len = n; + p.principalName.name_type = KRB5_NT_PRINCIPAL; + + for (i = 0, str_start = str = principal; *str != '\0'; str++) { + if (*str=='\\') { + str++; + } else if(*str == '/') { + /* Note that we allow components to be empty */ + ret = dequote_strndup(context, str_start, str - str_start, + &p.principalName.name_string.val[i++]); + if (ret) + goto out; + str_start = str + 1; + } else if(*str == '@') { + ret = dequote_strndup(context, str_start, str - str_start, + &p.principalName.name_string.val[i++]); + if (ret == 0) + ret = dequote_strndup(context, str + 1, strlen(str + 1), &p.realm); + if (ret) + goto out; + break; + } + } + } + + ASN1_MALLOC_ENCODE(KRB5PrincipalName, os->data, os->length, &p, &size, ret); + if (ret) { + hx509_set_error_string(context, 0, ret, "Out of memory"); + goto out; + } + if (size != os->length) + _hx509_abort("internal ASN.1 encoder error"); + +out: + free_KRB5PrincipalName(&p); + return ret; +} + /** * Add Kerberos Subject Alternative Name to the to-be-signed * certificate object. The principal string is a UTF8 string. @@ -623,84 +762,13 @@ hx509_ca_tbs_add_san_pkinit(hx509_context context, const char *principal) { heim_octet_string os; - KRB5PrincipalName p; - size_t size; int ret; - char *s = NULL; - memset(&p, 0, sizeof(p)); - - /* parse principal */ - { - const char *str; - char *q; - int n; - - /* count number of component */ - n = 1; - for(str = principal; *str != '\0' && *str != '@'; str++){ - if(*str=='\\'){ - if(str[1] == '\0' || str[1] == '@') { - ret = HX509_PARSING_NAME_FAILED; - hx509_set_error_string(context, 0, ret, - "trailing \\ in principal name"); - goto out; - } - str++; - } else if(*str == '/') - n++; - } - p.principalName.name_string.val = - calloc(n, sizeof(*p.principalName.name_string.val)); - if (p.principalName.name_string.val == NULL) { - ret = ENOMEM; - hx509_set_error_string(context, 0, ret, "malloc: out of memory"); - goto out; - } - p.principalName.name_string.len = n; - - p.principalName.name_type = KRB5_NT_PRINCIPAL; - q = s = strdup(principal); - if (q == NULL) { - ret = ENOMEM; - hx509_set_error_string(context, 0, ret, "malloc: out of memory"); - goto out; - } - p.realm = strrchr(q, '@'); - if (p.realm == NULL) { - ret = HX509_PARSING_NAME_FAILED; - hx509_set_error_string(context, 0, ret, "Missing @ in principal"); - goto out; - }; - *p.realm++ = '\0'; - - n = 0; - while (q) { - p.principalName.name_string.val[n++] = q; - q = strchr(q, '/'); - if (q) - *q++ = '\0'; - } - } - - ASN1_MALLOC_ENCODE(KRB5PrincipalName, os.data, os.length, &p, &size, ret); - if (ret) { - hx509_set_error_string(context, 0, ret, "Out of memory"); - goto out; - } - if (size != os.length) - _hx509_abort("internal ASN.1 encoder error"); - - ret = hx509_ca_tbs_add_san_otherName(context, - tbs, - &asn1_oid_id_pkinit_san, - &os); + ret = _hx509_make_pkinit_san(context, principal, &os); + if (ret == 0) + ret = hx509_ca_tbs_add_san_otherName(context, tbs, + &asn1_oid_id_pkinit_san, &os); free(os.data); -out: - if (p.principalName.name_string.val) - free (p.principalName.name_string.val); - if (s) - free(s); return ret; } diff --git a/lib/hx509/hxtool-commands.in b/lib/hx509/hxtool-commands.in index 64f59363d..11a4289e6 100644 --- a/lib/hx509/hxtool-commands.in +++ b/lib/hx509/hxtool-commands.in @@ -471,11 +471,36 @@ command = { type = "strings" help = "Email address in SubjectAltName" } + option = { + long = "jid" + type = "strings" + help = "XMPP (Jabber) address in SubjectAltName" + } option = { long = "dnsname" type = "strings" help = "Hostname or domainname in SubjectAltName" } + option = { + long = "kerberos" + type = "strings" + help = "Kerberos principal name as SubjectAltName" + } + option = { + long = "ms-kerberos" + type = "strings" + help = "Kerberos principal name as SubjectAltName (Microsoft variant)" + } + option = { + long = "registered" + type = "strings" + help = "Registered object ID as SubjectAltName" + } + option = { + long = "dn" + type = "strings" + help = "Directory name as SubjectAltName" + } option = { long = "type" type = "string" diff --git a/lib/hx509/hxtool.c b/lib/hx509/hxtool.c index ac748506c..9d5eb383e 100644 --- a/lib/hx509/hxtool.c +++ b/lib/hx509/hxtool.c @@ -1370,29 +1370,65 @@ request_create(struct request_create_options *opt, int argc, char **argv) char *s; hx509_name_to_string(name, &s); printf("%s\n", s); + free(s); } hx509_name_free(&name); } for (i = 0; i < opt->email_strings.num_strings; i++) { - ret = _hx509_request_add_email(context, req, - opt->email_strings.strings[i]); + ret = hx509_request_add_email(context, req, + opt->email_strings.strings[i]); if (ret) hx509_err(context, 1, ret, "hx509_request_add_email"); } + for (i = 0; i < opt->jid_strings.num_strings; i++) { + ret = hx509_request_add_xmpp_name(context, req, + opt->jid_strings.strings[i]); + if (ret) + hx509_err(context, 1, ret, "hx509_request_add_xmpp_name"); + } + for (i = 0; i < opt->dnsname_strings.num_strings; i++) { - ret = _hx509_request_add_dns_name(context, req, - opt->dnsname_strings.strings[i]); + ret = hx509_request_add_dns_name(context, req, + opt->dnsname_strings.strings[i]); if (ret) hx509_err(context, 1, ret, "hx509_request_add_dns_name"); } + for (i = 0; i < opt->kerberos_strings.num_strings; i++) { + ret = hx509_request_add_pkinit(context, req, + opt->kerberos_strings.strings[i]); + if (ret) + hx509_err(context, 1, ret, "hx509_request_add_pkinit"); + } + + for (i = 0; i < opt->ms_kerberos_strings.num_strings; i++) { + ret = hx509_request_add_ms_upn_name(context, req, + opt->ms_kerberos_strings.strings[i]); + if (ret) + hx509_err(context, 1, ret, "hx509_request_add_ms_upn_name"); + } + + for (i = 0; i < opt->registered_strings.num_strings; i++) { + heim_oid oid; + + ret = der_parse_heim_oid(opt->registered_strings.strings[i], NULL, + &oid); + if (ret) + hx509_err(context, 1, ret, "OID parse error"); + ret = hx509_request_add_registered(context, req, &oid); + der_free_oid(&oid); + if (ret) + hx509_err(context, 1, ret, "hx509_request_add_registered"); + } + for (i = 0; i < opt->eku_strings.num_strings; i++) { heim_oid oid; parse_oid(opt->eku_strings.strings[i], NULL, &oid); - ret = _hx509_request_add_eku(context, req, &oid); + ret = hx509_request_add_eku(context, req, &oid); + der_free_oid(&oid); if (ret) hx509_err(context, 1, ret, "hx509_request_add_eku"); } @@ -1409,12 +1445,12 @@ request_create(struct request_create_options *opt, int argc, char **argv) if (ret) hx509_err(context, 1, ret, "hx509_request_set_SubjectPublicKeyInfo"); - ret = _hx509_request_to_pkcs10(context, - req, - signer, - &request); + ret = hx509_request_to_pkcs10(context, + req, + signer, + &request); if (ret) - hx509_err(context, 1, ret, "_hx509_request_to_pkcs10"); + hx509_err(context, 1, ret, "hx509_request_to_pkcs10"); hx509_private_key_free(&signer); hx509_request_free(&req); @@ -1440,7 +1476,7 @@ request_print(struct request_print_options *opt, int argc, char **argv) if (ret) hx509_err(context, 1, ret, "parse_request: %s", argv[i]); - ret = _hx509_request_print(context, req, stdout); + ret = hx509_request_print(context, req, stdout); hx509_request_free(&req); if (ret) hx509_err(context, 1, ret, "Failed to print file %s", argv[i]); @@ -1901,6 +1937,11 @@ hxtool_ca(struct certificate_sign_options *opt, int argc, char **argv) if (opt->req_string) { hx509_request req; + /* + * XXX Extract the CN and other attributes we want to preserve from the + * requested subjectName and then set them in the hx509_env for the + * template. + */ ret = hx509_request_parse(context, opt->req_string, &req); if (ret) hx509_err(context, 1, ret, "parse_request: %s", opt->req_string); @@ -1910,6 +1951,7 @@ hxtool_ca(struct certificate_sign_options *opt, int argc, char **argv) ret = hx509_request_get_SubjectPublicKeyInfo(context, req, &spki); if (ret) hx509_err(context, 1, ret, "get spki"); + /* XXX Add option to extract */ hx509_request_free(&req); } diff --git a/lib/hx509/libhx509-exports.def b/lib/hx509/libhx509-exports.def index 846435f32..621d047de 100644 --- a/lib/hx509/libhx509-exports.def +++ b/lib/hx509/libhx509-exports.def @@ -12,31 +12,46 @@ EXPORTS _hx509_generate_private_key_free _hx509_generate_private_key_init _hx509_generate_private_key_is_ca + _hx509_make_pkinit_san _hx509_map_file_os _hx509_name_from_Name hx509_private_key2SPKI hx509_private_key_free _hx509_private_key_ref - _hx509_request_add_dns_name - _hx509_request_add_eku - _hx509_request_add_email + hx509_request_add_GeneralName + hx509_request_add_dns_name + hx509_request_add_eku + hx509_request_add_email + hx509_request_add_ms_upn_name + hx509_request_add_pkinit + hx509_request_add_registered + hx509_request_add_xmpp_name _hx509_private_key_export _hx509_private_key_exportable _hx509_private_key_get_internal _hx509_private_key_oid _hx509_private_key_ref hx509_request_free + hx509_request_get_dns_name_san + hx509_request_get_eku + hx509_request_get_email_san + hx509_request_get_exts + hx509_request_get_ku + hx509_request_get_ms_upn_san + hx509_request_get_name + hx509_request_get_pkinit_san hx509_request_get_SubjectPublicKeyInfo + hx509_request_get_xmpp_san hx509_request_get_name hx509_request_init hx509_request_parse hx509_request_parse_der - _hx509_request_print + hx509_request_print hx509_request_set_SubjectPublicKeyInfo -; _hx509_request_set_email + hx509_request_add_email hx509_request_set_name - _hx509_request_to_pkcs10 - _hx509_request_to_pkcs10 + hx509_request_set_ku + hx509_request_to_pkcs10 _hx509_unmap_file_os _hx509_write_file hx509_bitstring_print diff --git a/lib/hx509/name.c b/lib/hx509/name.c index 2dccc6453..69782cd69 100644 --- a/lib/hx509/name.c +++ b/lib/hx509/name.c @@ -1001,6 +1001,102 @@ hx509_name_is_null_p(const hx509_name name) name->der_name.u.rdnSequence.len == 0; } +/* + * This necessarily duplicates code from libkrb5, and has to unless we move + * common code here or to lib/roken for it. We do have slightly different + * needs (e.g., we want space quoted, and we want to indicate whether we saw + * trailing garbage, we have no need for flags, no special realm treatment, + * etc) than the corresponding code in libkrb5, so for now we duplicate this + * code. + * + * The relevant RFCs here are RFC1964 for the string representation of Kerberos + * principal names, and RFC4556 for the KRB5PrincipalName ASN.1 type (Kerberos + * lacks such a type because on the wire the name and realm are sent + * separately as a form of cheap compression). + * + * Note that we cannot handle embedded NULs because of Heimdal's representation + * of ASN.1 strings as C strings. + */ +struct rk_strpool * +_hx509_unparse_kerberos_name(struct rk_strpool *strpool, heim_any *value) +{ + static const char comp_quotable_chars[] = " \n\t\b\\/@"; + static const char realm_quotable_chars[] = " \n\t\b\\@"; + KRB5PrincipalName kn; + const char *s; + size_t i, k, len, plen; + int extra_bits; + int need_slash = 0; + int ret; + + ret = decode_KRB5PrincipalName(value->data, value->length, &kn, &len); + if (ret) + return rk_strpoolprintf(strpool, "length != len); + + for (i = 0; i < kn.principalName.name_string.len; i++) { + s = kn.principalName.name_string.val[i]; + len = strlen(s); + + if (need_slash) + strpool = rk_strpoolprintf(strpool, "/"); + need_slash = 1; + + for (k = 0; k < len; s += plen, k += plen) { + char c; + + plen = strcspn(s, comp_quotable_chars); + if (plen) + strpool = rk_strpoolprintf(strpool, "%.*s", (int)plen, s); + if (k + plen >= len) + continue; + switch ((c = s[plen++])) { + case '\n': strpool = rk_strpoolprintf(strpool, "\\n"); break; + case '\t': strpool = rk_strpoolprintf(strpool, "\\t"); break; + case '\b': strpool = rk_strpoolprintf(strpool, "\\b"); break; + /* default -> '@', ' ', '\\', or '/' */ + default: strpool = rk_strpoolprintf(strpool, "\\%c", c); break; + } + } + } + strpool = rk_strpoolprintf(strpool, "@"); + s = kn.realm; + len = strlen(kn.realm); + for (k = 0; k < len; s += plen, k += plen) { + char c; + + plen = strcspn(s, realm_quotable_chars); + if (plen) + strpool = rk_strpoolprintf(strpool, "%.*s", (int)plen, s); + if (k + plen >= len) + continue; + switch ((c = s[plen++])) { + case '\n': strpool = rk_strpoolprintf(strpool, "\\n"); break; + case '\t': strpool = rk_strpoolprintf(strpool, "\\t"); break; + case '\b': strpool = rk_strpoolprintf(strpool, "\\b"); break; + /* default -> '@', ' ', or '\\' */ + default: strpool = rk_strpoolprintf(strpool, "\\%c", c); break; + } + } + if (extra_bits) + strpool = rk_strpoolprintf(strpool, " "); + free_KRB5PrincipalName(&kn); + return strpool; +} + +struct rk_strpool * +hx509_unparse_utf8_string_name(struct rk_strpool *strpool, heim_any *value) +{ + PKIXXmppAddr us; + size_t size; + + if (decode_PKIXXmppAddr(value->data, value->length, &us, &size)) + return rk_strpoolprintf(strpool, ""); + strpool = rk_strpoolprintf(strpool, "%s", us); + free_PKIXXmppAddr(&us); + return strpool; +} + /** * Unparse the hx509 name in name into a string. * @@ -1025,17 +1121,34 @@ hx509_general_name_unparse(GeneralName *name, char **str) hx509_oid_sprint(&name->u.otherName.type_id, &oid); if (oid == NULL) return ENOMEM; - strpool = rk_strpoolprintf(strpool, "otherName: %s", oid); + strpool = rk_strpoolprintf(strpool, "otherName: %s ", oid); + if (der_heim_oid_cmp(&name->u.otherName.type_id, + &asn1_oid_id_pkinit_san) == 0) { + strpool = _hx509_unparse_kerberos_name(strpool, + &name->u.otherName.value); + } else if (der_heim_oid_cmp(&name->u.otherName.type_id, + &asn1_oid_id_pkix_on_xmppAddr) == 0) { + strpool = rk_strpoolprintf(strpool, "xmppAddr "); + strpool = hx509_unparse_utf8_string_name(strpool, + &name->u.otherName.value); + } else if (der_heim_oid_cmp(&name->u.otherName.type_id, + &asn1_oid_id_pkinit_ms_san) == 0) { + strpool = rk_strpoolprintf(strpool, "pkinitMsSan "); + strpool = hx509_unparse_utf8_string_name(strpool, + &name->u.otherName.value); + } else { + strpool = rk_strpoolprintf(strpool, "u.rfc822Name.length, (char *)name->u.rfc822Name.data); break; case choice_GeneralName_dNSName: - strpool = rk_strpoolprintf(strpool, "dNSName: %.*s\n", + strpool = rk_strpoolprintf(strpool, "dNSName: %.*s", (int)name->u.dNSName.length, (char *)name->u.dNSName.data); break; @@ -1095,10 +1208,8 @@ hx509_general_name_unparse(GeneralName *name, char **str) default: return EINVAL; } - if (strpool == NULL) + if (strpool == NULL || + (*str = rk_strpoolcollect(strpool)) == NULL) return ENOMEM; - - *str = rk_strpoolcollect(strpool); - return 0; } diff --git a/lib/hx509/req.c b/lib/hx509/req.c index dadea7eb2..1a9e9f61c 100644 --- a/lib/hx509/req.c +++ b/lib/hx509/req.c @@ -37,14 +37,22 @@ struct hx509_request_data { hx509_name name; SubjectPublicKeyInfo key; + KeyUsage ku; ExtKeyUsage eku; GeneralNames san; }; -/* +/** + * Allocate and initialize an hx509_request structure representing a PKCS#10 + * certificate signing request. * + * @param context An hx509 context. + * @param req Where to put the new hx509_request object. + * + * @return An hx509 error code, see hx509_get_error_string(). + * + * @ingroup hx509_request */ - HX509_LIB_FUNCTION int HX509_LIB_CALL hx509_request_init(hx509_context context, hx509_request *req) { @@ -55,19 +63,41 @@ hx509_request_init(hx509_context context, hx509_request *req) return 0; } +/** + * Free a certificate signing request object. + * + * @param req A pointer to the hx509_request to free. + * + * @ingroup hx509_request + */ HX509_LIB_FUNCTION void HX509_LIB_CALL -hx509_request_free(hx509_request *req) +hx509_request_free(hx509_request *reqp) { - if ((*req)->name) - hx509_name_free(&(*req)->name); - free_SubjectPublicKeyInfo(&(*req)->key); - free_ExtKeyUsage(&(*req)->eku); - free_GeneralNames(&(*req)->san); - memset(*req, 0, sizeof(**req)); - free(*req); - *req = NULL; + hx509_request req = *reqp; + + *reqp = NULL; + if (req == NULL) + return; + if (req->name) + hx509_name_free(&req->name); + free_SubjectPublicKeyInfo(&req->key); + free_ExtKeyUsage(&req->eku); + free_GeneralNames(&req->san); + memset(req, 0, sizeof(*req)); + free(req); } +/** + * Set the subjectName of the CSR. + * + * @param context An hx509 context. + * @param req The hx509_request to alter. + * @param name The subjectName. + * + * @return An hx509 error code, see hx509_get_error_string(). + * + * @ingroup hx509_request + */ HX509_LIB_FUNCTION int HX509_LIB_CALL hx509_request_set_name(hx509_context context, hx509_request req, @@ -83,6 +113,17 @@ hx509_request_set_name(hx509_context context, return 0; } +/** + * Get the subject name requested by a CSR. + * + * @param context An hx509 context. + * @param req The hx509_request object. + * @param name Where to put the name. + * + * @return An hx509 error code, see hx509_get_error_string(). + * + * @ingroup hx509_request + */ HX509_LIB_FUNCTION int HX509_LIB_CALL hx509_request_get_name(hx509_context context, hx509_request req, @@ -95,6 +136,17 @@ hx509_request_get_name(hx509_context context, return hx509_name_copy(context, req->name, name); } +/** + * Set the subject public key requested by a CSR. + * + * @param context An hx509 context. + * @param req The hx509_request object. + * @param key The public key. + * + * @return An hx509 error code, see hx509_get_error_string(). + * + * @ingroup hx509_request + */ HX509_LIB_FUNCTION int HX509_LIB_CALL hx509_request_set_SubjectPublicKeyInfo(hx509_context context, hx509_request req, @@ -104,6 +156,17 @@ hx509_request_set_SubjectPublicKeyInfo(hx509_context context, return copy_SubjectPublicKeyInfo(key, &req->key); } +/** + * Get the subject public key requested by a CSR. + * + * @param context An hx509 context. + * @param req The hx509_request object. + * @param key Where to put the key. + * + * @return An hx509 error code, see hx509_get_error_string(). + * + * @ingroup hx509_request + */ HX509_LIB_FUNCTION int HX509_LIB_CALL hx509_request_get_SubjectPublicKeyInfo(hx509_context context, hx509_request req, @@ -112,10 +175,57 @@ hx509_request_get_SubjectPublicKeyInfo(hx509_context context, return copy_SubjectPublicKeyInfo(&req->key, key); } +/** + * Set the key usage requested by a CSR. + * + * @param context An hx509 context. + * @param req The hx509_request object. + * @param ku The key usage. + * + * @return An hx509 error code, see hx509_get_error_string(). + * + * @ingroup hx509_request + */ HX509_LIB_FUNCTION int HX509_LIB_CALL -_hx509_request_add_eku(hx509_context context, - hx509_request req, - const heim_oid *oid) +hx509_request_set_ku(hx509_context context, hx509_request req, KeyUsage ku) +{ + req->ku = ku; + return 0; +} + +/** + * Get the key usage requested by a CSR. + * + * @param context An hx509 context. + * @param req The hx509_request object. + * @param ku Where to put the key usage. + * + * @return An hx509 error code, see hx509_get_error_string(). + * + * @ingroup hx509_request + */ +HX509_LIB_FUNCTION int HX509_LIB_CALL +hx509_request_get_ku(hx509_context context, hx509_request req, KeyUsage *ku) +{ + *ku = req->ku; + return 0; +} + +/** + * Add an extended key usage OID to a CSR. + * + * @param context An hx509 context. + * @param req The hx509_request object. + * @param oid The EKU OID. + * + * @return An hx509 error code, see hx509_get_error_string(). + * + * @ingroup hx509_request + */ +HX509_LIB_FUNCTION int HX509_LIB_CALL +hx509_request_add_eku(hx509_context context, + hx509_request req, + const heim_oid *oid) { void *val; int ret; @@ -134,10 +244,112 @@ _hx509_request_add_eku(hx509_context context, return 0; } +/** + * Add a GeneralName (Jabber ID) subject alternative name to a CSR. + * + * XXX Make this take a heim_octet_string, not a GeneralName*. + * + * @param context An hx509 context. + * @param req The hx509_request object. + * @param gn The GeneralName object. + * + * @return An hx509 error code, see hx509_get_error_string(). + * + * @ingroup hx509_request + */ HX509_LIB_FUNCTION int HX509_LIB_CALL -_hx509_request_add_dns_name(hx509_context context, - hx509_request req, - const char *hostname) +hx509_request_add_GeneralName(hx509_context context, + hx509_request req, + const GeneralName *gn) +{ + return add_GeneralNames(&req->san, gn); +} + +static int +add_utf8_other_san(hx509_context context, + GeneralNames *gns, + const heim_oid *oid, + const char *s) +{ + const PKIXXmppAddr us = (const PKIXXmppAddr)(uintptr_t)s; + GeneralName gn; + size_t size; + int ret; + + gn.element = choice_GeneralName_otherName; + gn.u.otherName.type_id.length = 0; + gn.u.otherName.type_id.components = 0; + gn.u.otherName.value.data = NULL; + gn.u.otherName.value.length = 0; + ret = der_copy_oid(oid, &gn.u.otherName.type_id); + if (ret == 0) + ASN1_MALLOC_ENCODE(PKIXXmppAddr, gn.u.otherName.value.data, + gn.u.otherName.value.length, &us, &size, ret); + if (ret == 0 && size != gn.u.otherName.value.length) + _hx509_abort("internal ASN.1 encoder error"); + if (ret == 0) + ret = add_GeneralNames(gns, &gn); + free_GeneralName(&gn); + if (ret) + hx509_set_error_string(context, 0, ret, "Out of memory"); + return ret; +} + +/** + * Add an xmppAddr (Jabber ID) subject alternative name to a CSR. + * + * @param context An hx509 context. + * @param req The hx509_request object. + * @param jid The XMPP address. + * + * @return An hx509 error code, see hx509_get_error_string(). + * + * @ingroup hx509_request + */ +HX509_LIB_FUNCTION int HX509_LIB_CALL +hx509_request_add_xmpp_name(hx509_context context, + hx509_request req, + const char *jid) +{ + return add_utf8_other_san(context, &req->san, &asn1_oid_id_pkix_on_xmppAddr, + jid); +} + +/** + * Add a Microsoft UPN subject alternative name to a CSR. + * + * @param context An hx509 context. + * @param req The hx509_request object. + * @param hostname The XMPP address. + * + * @return An hx509 error code, see hx509_get_error_string(). + * + * @ingroup hx509_request + */ +HX509_LIB_FUNCTION int HX509_LIB_CALL +hx509_request_add_ms_upn_name(hx509_context context, + hx509_request req, + const char *upn) +{ + return add_utf8_other_san(context, &req->san, &asn1_oid_id_pkinit_ms_san, + upn); +} + +/** + * Add a dNSName (hostname) subject alternative name to a CSR. + * + * @param context An hx509 context. + * @param req The hx509_request object. + * @param hostname The fully-qualified hostname. + * + * @return An hx509 error code, see hx509_get_error_string(). + * + * @ingroup hx509_request + */ +HX509_LIB_FUNCTION int HX509_LIB_CALL +hx509_request_add_dns_name(hx509_context context, + hx509_request req, + const char *hostname) { GeneralName name; @@ -149,33 +361,243 @@ _hx509_request_add_dns_name(hx509_context context, return add_GeneralNames(&req->san, &name); } +/** + * Add an rfc822Name (e-mail address) subject alternative name to a CSR. + * + * @param context An hx509 context. + * @param req The hx509_request object. + * @param email The e-mail address. + * + * @return An hx509 error code, see hx509_get_error_string(). + * + * @ingroup hx509_request + */ HX509_LIB_FUNCTION int HX509_LIB_CALL -_hx509_request_add_email(hx509_context context, - hx509_request req, - const char *email) +hx509_request_add_email(hx509_context context, + hx509_request req, + const char *email) { GeneralName name; memset(&name, 0, sizeof(name)); name.element = choice_GeneralName_rfc822Name; - name.u.dNSName.data = rk_UNCONST(email); - name.u.dNSName.length = strlen(email); + name.u.rfc822Name.data = rk_UNCONST(email); + name.u.rfc822Name.length = strlen(email); return add_GeneralNames(&req->san, &name); } - - +/** + * Add a registeredID (OID) subject alternative name to a CSR. + * + * @param context An hx509 context. + * @param req The hx509_request object. + * @param oid The OID. + * + * @return An hx509 error code, see hx509_get_error_string(). + * + * @ingroup hx509_request + */ HX509_LIB_FUNCTION int HX509_LIB_CALL -_hx509_request_to_pkcs10(hx509_context context, - const hx509_request req, - const hx509_private_key signer, - heim_octet_string *request) +hx509_request_add_registered(hx509_context context, + hx509_request req, + heim_oid *oid) +{ + GeneralName name; + int ret; + + memset(&name, 0, sizeof(name)); + name.element = choice_GeneralName_registeredID; + ret = der_copy_oid(oid, &name.u.registeredID); + if (ret) + return ret; + ret = add_GeneralNames(&req->san, &name); + free_GeneralName(&name); + return ret; +} + +/** + * Add a Kerberos V5 principal subject alternative name to a CSR. + * + * @param context An hx509 context. + * @param req The hx509_request object. + * @param princ The Kerberos principal name. + * + * @return An hx509 error code, see hx509_get_error_string(). + * + * @ingroup hx509_request + */ +HX509_LIB_FUNCTION int HX509_LIB_CALL +hx509_request_add_pkinit(hx509_context context, + hx509_request req, + const char *princ) +{ + KRB5PrincipalName kn; + GeneralName gn; + int ret; + + memset(&kn, 0, sizeof(kn)); + memset(&gn, 0, sizeof(gn)); + gn.element = choice_GeneralName_otherName; + gn.u.otherName.type_id.length = 0; + gn.u.otherName.type_id.components = 0; + gn.u.otherName.value.data = NULL; + gn.u.otherName.value.length = 0; + ret = der_copy_oid(&asn1_oid_id_pkinit_san, &gn.u.otherName.type_id); + if (ret == 0) + ret = _hx509_make_pkinit_san(context, princ, &gn.u.otherName.value); + if (ret == 0) + ret = add_GeneralNames(&req->san, &gn); + free_GeneralName(&gn); + return ret; +} + +/* XXX Add DNSSRV and other SANs */ + +static int +get_exts(hx509_context context, + const hx509_request req, + Extensions *exts) +{ + uint64_t ku_num; + size_t size; + int ret = 0; + + exts->val = NULL; + exts->len = 0; + + if ((ku_num = KeyUsage2int(req->ku))) { + Extension e; + + memset(&e, 0, sizeof(e)); + /* The critical field needs to be made DEFAULT FALSE... */ + if ((e.critical = malloc(sizeof(*e.critical))) == NULL) + ret = ENOMEM; + if (ret == 0) + *e.critical = 1; + if (ret == 0) + ASN1_MALLOC_ENCODE(KeyUsage, e.extnValue.data, e.extnValue.length, + &req->ku, &size, ret); + if (ret == 0) + ret = der_copy_oid(&asn1_oid_id_x509_ce_keyUsage, &e.extnID); + if (ret == 0) + ret = add_Extensions(exts, &e); + free_Extension(&e); + } + if (ret == 0 && req->eku.len) { + Extension e; + + memset(&e, 0, sizeof(e)); + if ((e.critical = malloc(sizeof(*e.critical))) == NULL) + ret = ENOMEM; + if (ret == 0) + *e.critical = 1; + if (ret == 0) + ASN1_MALLOC_ENCODE(ExtKeyUsage, + e.extnValue.data, e.extnValue.length, + &req->eku, &size, ret); + if (ret == 0) + ret = der_copy_oid(&asn1_oid_id_x509_ce_extKeyUsage, &e.extnID); + if (ret == 0) + ret = add_Extensions(exts, &e); + free_Extension(&e); + } + if (ret == 0 && req->san.len) { + Extension e; + + memset(&e, 0, sizeof(e)); + /* + * SANs are critical when the subject Name is empty. + * + * The empty DN check could probably stand to be a function we export. + */ + e.critical = NULL; + if (req->name && + req->name->der_name.element == choice_Name_rdnSequence && + req->name->der_name.u.rdnSequence.len == 0) { + + if ((e.critical = malloc(sizeof(*e.critical))) == NULL) + ret = ENOMEM; + if (ret == 0) { + *e.critical = 1; + } + } + if (ret == 0) + ASN1_MALLOC_ENCODE(GeneralNames, + e.extnValue.data, e.extnValue.length, + &req->san, + &size, ret); + if (ret == 0) + ret = der_copy_oid(&asn1_oid_id_x509_ce_subjectAltName, &e.extnID); + if (ret == 0) + ret = add_Extensions(exts, &e); + free_Extension(&e); + } + + return ret; +} + +/** + * Get the KU/EKUs/SANs set on a request as a DER-encoding of Extensions. + * + * @param context An hx509 context. + * @param req The hx509_request object. + * @param exts_der Where to put the DER-encoded Extensions. + * + * @return An hx509 error code, see hx509_get_error_string(). + * + * @ingroup hx509_request + */ +HX509_LIB_FUNCTION int HX509_LIB_CALL +hx509_request_get_exts(hx509_context context, + const hx509_request req, + heim_octet_string *exts_der) +{ + Extensions exts; + size_t size; + int ret; + + exts_der->data = NULL; + exts_der->length = 0; + ret = get_exts(context, req, &exts); + if (ret == 0 && exts_der->data && exts_der->length) + ASN1_MALLOC_ENCODE(Extensions, exts_der->data, exts_der->length, + &exts, &size, ret); + free_Extensions(&exts); + return ret; +} + +/* XXX Add PEM */ + +/** + * Encode a CSR. + * + * @param context An hx509 context. + * @param req The hx509_request object. + * @param signer The private key corresponding to the CSR's subject public key. + * @param request Where to put the DER-encoded CSR. + * + * @return An hx509 error code, see hx509_get_error_string(). + * + * @ingroup hx509_request + */ +HX509_LIB_FUNCTION int HX509_LIB_CALL +hx509_request_to_pkcs10(hx509_context context, + const hx509_request req, + const hx509_private_key signer, + heim_octet_string *request) { CertificationRequest r; - heim_octet_string data, os; - int ret; + Extensions exts; + heim_octet_string data; size_t size; + int ret; + + request->data = NULL; + request->length = 0; + + data.length = 0; + data.data = NULL; if (req->name == NULL) { hx509_set_error_string(context, 0, EINVAL, @@ -184,59 +606,85 @@ _hx509_request_to_pkcs10(hx509_context context, } memset(&r, 0, sizeof(r)); - memset(request, 0, sizeof(*request)); + /* Setup CSR */ r.certificationRequestInfo.version = pkcs10_v1; - ret = copy_Name(&req->name->der_name, &r.certificationRequestInfo.subject); - if (ret) - goto out; - ret = copy_SubjectPublicKeyInfo(&req->key, - &r.certificationRequestInfo.subjectPKInfo); - if (ret) - goto out; - r.certificationRequestInfo.attributes = - calloc(1, sizeof(*r.certificationRequestInfo.attributes)); - if (r.certificationRequestInfo.attributes == NULL) { - ret = ENOMEM; - goto out; + if (ret == 0) + ret = copy_SubjectPublicKeyInfo(&req->key, + &r.certificationRequestInfo.subjectPKInfo); + + /* Encode extReq attribute with requested Certificate Extensions */ + + if (ret == 0) + ret = get_exts(context, req, &exts); + if (ret == 0 && exts.len) { + Attribute *a; + heim_any extns; + + r.certificationRequestInfo.attributes = + calloc(1, sizeof(r.certificationRequestInfo.attributes[0])); + if (r.certificationRequestInfo.attributes == NULL) + ret = ENOMEM; + if (ret == 0) { + r.certificationRequestInfo.attributes[0].len = 1; + r.certificationRequestInfo.attributes[0].val = + calloc(1, sizeof(r.certificationRequestInfo.attributes[0].val[0])); + if (r.certificationRequestInfo.attributes[0].val == NULL) + ret = ENOMEM; + if (ret == 0) + a = r.certificationRequestInfo.attributes[0].val; + } + if (ret == 0) + ASN1_MALLOC_ENCODE(Extensions, extns.data, extns.length, + &exts, &size, ret); + if (ret == 0) + ret = der_copy_oid(&asn1_oid_id_pkcs9_extReq, &a->type); + if (ret == 0) + ret = add_AttributeValues(&a->value, &extns); + free_heim_any(&extns); } - ASN1_MALLOC_ENCODE(CertificationRequestInfo, data.data, data.length, - &r.certificationRequestInfo, &size, ret); - if (ret) - goto out; - if (data.length != size) + /* Encode CSR body for signing */ + if (ret == 0) + ASN1_MALLOC_ENCODE(CertificationRequestInfo, data.data, data.length, + &r.certificationRequestInfo, &size, ret); + if (ret == 0 && data.length != size) abort(); - ret = _hx509_create_signature(context, - signer, - _hx509_crypto_default_sig_alg, - &data, - &r.signatureAlgorithm, - &os); + /* Self-sign CSR body */ + if (ret == 0) { + ret = _hx509_create_signature_bitstring(context, signer, + _hx509_crypto_default_sig_alg, + &data, + &r.signatureAlgorithm, + &r.signature); + } free(data.data); - if (ret) - goto out; - r.signature.data = os.data; - r.signature.length = os.length * 8; - ASN1_MALLOC_ENCODE(CertificationRequest, data.data, data.length, - &r, &size, ret); - if (ret) - goto out; - if (data.length != size) + /* Encode CSR */ + if (ret == 0) + ASN1_MALLOC_ENCODE(CertificationRequest, request->data, request->length, + &r, &size, ret); + if (ret == 0 && request->length != size) abort(); - *request = data; - -out: free_CertificationRequest(&r); - return ret; } +/** + * Parse an encoded CSR and verify its self-signature. + * + * @param context An hx509 context. + * @param der The DER-encoded CSR. + * @param req Where to put request object. + * + * @return An hx509 error code, see hx509_get_error_string(). + * + * @ingroup hx509_request + */ HX509_LIB_FUNCTION int HX509_LIB_CALL hx509_request_parse_der(hx509_context context, heim_octet_string *der, @@ -244,75 +692,164 @@ hx509_request_parse_der(hx509_context context, { CertificationRequestInfo *rinfo = NULL; CertificationRequest r; - Certificate c; - hx509_name subject = NULL; hx509_cert signer = NULL; - size_t size; + Extensions exts; + size_t i, size; int ret; + memset(&exts, 0, sizeof(exts)); + + /* Initial setup and decoding of CSR */ + ret = hx509_request_init(context, req); + if (ret) + return ret; ret = decode_CertificationRequest(der->data, der->length, &r, &size); if (ret) { hx509_set_error_string(context, 0, ret, "Failed to decode CSR"); + free(*req); return ret; } rinfo = &r.certificationRequestInfo; + + /* + * Setup a 'signer' for verifying the self-signature for proof of + * possession. + * + * Sadly we need a "certificate" here because _hx509_verify_signature_*() + * functions want one as a signer even though all the verification + * functions that use the signer argument only ever use the spki of the + * signer certificate. + * + * FIXME Change struct signature_alg's verify_signature's prototype to use + * an spki instead of an hx509_cert as the signer! The we won't have + * to do this. + */ if (ret == 0) { - /* - * Sadly we need an hx509_cert here because _hx509_verify_signature_*() - * functions want one as a signer even though all the verification - * functions that use the signer argument always only use the spki of - * the signer certificate. - * - * XXX Change struct signature_alg's verify_signature's prototype to - * use an spki instead of an hx509_cert as the signer! The we - * won't have to do this. - */ + Certificate c; memset(&c, 0, sizeof(c)); c.tbsCertificate.subjectPublicKeyInfo = rinfo->subjectPKInfo; if ((signer = hx509_cert_init(context, &c, NULL)) == NULL) ret = ENOMEM; - } /* Verify the signature */ - if (ret == 0) { - heim_octet_string *d = &r.certificationRequestInfo._save; - - ret = _hx509_verify_signature_bitstring(context, signer, - &r.signatureAlgorithm, d, - &r.signature); - if (ret) - hx509_set_error_string(context, 0, ret, - "CSR signature verification failed"); - } - - /* Build and populate the hx509_request */ if (ret == 0) - ret = hx509_request_init(context, req); + ret = _hx509_verify_signature_bitstring(context, signer, + &r.signatureAlgorithm, + &rinfo->_save, + &r.signature); + if (ret) + hx509_set_error_string(context, 0, ret, + "CSR signature verification failed"); + hx509_cert_free(signer); + + /* Populate the hx509_request */ if (ret == 0) ret = hx509_request_set_SubjectPublicKeyInfo(context, *req, &rinfo->subjectPKInfo); if (ret == 0) - ret = _hx509_name_from_Name(&rinfo->subject, &subject); - if (ret == 0) - ret = hx509_request_set_name(context, *req, subject); + ret = _hx509_name_from_Name(&rinfo->subject, &(*req)->name); - /* - * XXX Extract EKUs and SANs from the CSR's attributes. That means looking - * for an attr of OID asn1_oid_id_x509_ce_subjectAltName - * (id-ce-subjectAltName) and decoding their values. See RFC5912. - */ + /* Extract KUs, EKUs, and SANs from the CSR's attributes */ + if (ret || !rinfo->attributes || !rinfo->attributes[0].len) + goto out; - if (subject) - hx509_name_free(&subject); - if (signer) - hx509_cert_free(signer); + for (i = 0; ret == 0 && i < rinfo->attributes[0].len; i++) { + Attribute *a = &rinfo->attributes[0].val[i]; + heim_any *av = NULL; + + /* We only support Extensions request attributes */ + if (der_heim_oid_cmp(&a->type, &asn1_oid_id_pkcs9_extReq) != 0) { + char *oidstr = NULL; + + /* + * We need an HX509_TRACE facility for this sort of warning. + * + * We'd put the warning in the context and then allow the caller to + * extract and reset the warning. + * + * FIXME + */ + der_print_heim_oid(&a->type, '.', &oidstr); + warnx("Unknown or unsupported CSR attribute %s", + oidstr ? oidstr : ""); + free(oidstr); + continue; + } + if (!a->value.val) + continue; + + av = a->value.val; + ret = decode_Extensions(av->data, av->length, &exts, NULL); + if (ret) { + hx509_set_error_string(context, 0, ret, + "CSR signature verification failed " + "due to invalid extReq attribute"); + goto out; + } + } + for (i = 0; ret == 0 && i < exts.len; i++) { + const char *what = ""; + Extension *e = &exts.val[i]; + + if (der_heim_oid_cmp(&e->extnID, + &asn1_oid_id_x509_ce_keyUsage) == 0) { + ret = decode_KeyUsage(e->extnValue.data, e->extnValue.length, + &(*req)->ku, NULL); + what = "keyUsage"; + } else if (der_heim_oid_cmp(&e->extnID, + &asn1_oid_id_x509_ce_extKeyUsage) == 0) { + ret = decode_ExtKeyUsage(e->extnValue.data, e->extnValue.length, + &(*req)->eku, NULL); + what = "extKeyUsage"; + } else if (der_heim_oid_cmp(&e->extnID, + &asn1_oid_id_x509_ce_subjectAltName) == 0) { + ret = decode_GeneralNames(e->extnValue.data, e->extnValue.length, + &(*req)->san, NULL); + what = "subjectAlternativeName"; + } else { + char *oidstr = NULL; + + /* + * We need an HX509_TRACE facility for this sort of warning. + * + * We'd put the warning in the context and then allow the caller to + * extract and reset the warning. + * + * FIXME + */ + der_print_heim_oid(&e->extnID, '.', &oidstr); + warnx("Unknown or unsupported CSR extension request %s", + oidstr ? oidstr : ""); + free(oidstr); + } + if (ret) { + hx509_set_error_string(context, 0, ret, + "CSR signature verification failed " + "due to invalid %s extension", what); + break; + } + } + +out: + free_CertificationRequest(&r); if (ret) hx509_request_free(req); free_CertificationRequest(&r); return ret; } +/** + * Parse an encoded CSR and verify its self-signature. + * + * @param context An hx509 context. + * @param csr The name of a store containing the CSR ("PKCS10:/path/to/file") + * @param req Where to put request object. + * + * @return An hx509 error code, see hx509_get_error_string(). + * + * @ingroup hx509_request + */ HX509_LIB_FUNCTION int HX509_LIB_CALL hx509_request_parse(hx509_context context, const char *csr, @@ -342,12 +879,325 @@ hx509_request_parse(hx509_context context, return ret; } - +/** + * Iterate EKUs in a CSR. + * + * @param context An hx509 context. + * @param req The hx509_request object. + * @param out A pointer to a char * variable where the OID will be placed + * (caller must free with free()) + * @param cursor An index of EKU (0 for the first); on return it's incremented + * or set to -1 when no EKUs remain. + * + * @return An hx509 error code, see hx509_get_error_string(). + * + * @ingroup hx509_request + */ HX509_LIB_FUNCTION int HX509_LIB_CALL -_hx509_request_print(hx509_context context, hx509_request req, FILE *f) +hx509_request_get_eku(hx509_context context, + hx509_request req, + char **out, + int *cursor) { + size_t i; + + *out = NULL; + if (*cursor < 0) + return 0; + i = (size_t)cursor; + if (i >= req->eku.len) + return 0; /* XXX */ + if (i + 1 < req->eku.len) + (*cursor)++; + else + *cursor = -1; + return der_print_heim_oid(&req->eku.val[i], '.', out); +} + +ssize_t +find_san1(hx509_context context, + hx509_request req, + size_t i, + int kind, + const heim_oid *other_name_oid) +{ + if (i >= req->san.len) + return -1; + do { + GeneralName *san = &req->san.val[i]; + + if (i == INT_MAX) + return -1; + if (san->element == kind && kind != choice_GeneralName_otherName) + return i; + if (san->element == kind && kind == choice_GeneralName_otherName && + der_heim_oid_cmp(&san->u.otherName.type_id, other_name_oid) == 0) + return i; + } while (i++ < req->san.len); + return -1; +} + +ssize_t +find_san(hx509_context context, + hx509_request req, + int *cursor, + int kind, + const heim_oid *other_name_oid) +{ + ssize_t ret; + + if (*cursor < 0) + return -1; + ret = find_san1(context, req, (size_t)*cursor, kind, other_name_oid); + if (ret < 0 || ret >= INT_MAX) + *cursor = -1; + else + *cursor = find_san1(context, req, (size_t)*cursor + 1, kind, + other_name_oid); + return ret; +} + +static int +get_utf8_otherName_san(hx509_context context, + hx509_request req, + const heim_oid *oid, + char **out, + int *cursor) +{ + struct rk_strpool *pool; + ssize_t idx; + size_t i; + + *out = NULL; + if (*cursor < 0) + return 0; + idx = find_san(context, req, cursor, choice_GeneralName_otherName, oid); + if (idx < 0) + return -1; + i = (size_t)idx; + + pool = hx509_unparse_utf8_string_name(NULL, + &req->san.val[i].u.otherName.value); + if (pool == NULL || + (*out = rk_strpoolcollect(pool)) == NULL) + return ENOMEM; + return 0; +} + +/* XXX Add hx509_request_get_san() that also outputs the SAN type */ + +/** + * Iterate XMPP SANs in a CSR. + * + * @param context An hx509 context. + * @param req The hx509_request object. + * @param out A pointer to a char * variable where the Jabber address will be + * placed (caller must free with free()) + * @param cursor An index of SAN (0 for the first); on return it's incremented + * or set to -1 when no SANs remain. + * + * @return An hx509 error code, see hx509_get_error_string(). + * + * @ingroup hx509_request + */ +HX509_LIB_FUNCTION int HX509_LIB_CALL +hx509_request_get_xmpp_san(hx509_context context, + hx509_request req, + char **out, + int *cursor) +{ + return get_utf8_otherName_san(context, req, &asn1_oid_id_pkix_on_xmppAddr, + out, cursor); +} + +/** + * Iterate MS UPN SANs in a CSR. + * + * @param context An hx509 context. + * @param req The hx509_request object. + * @param out A pointer to a char * variable where the UPN will be placed + * (caller must free with free()) + * @param cursor An index of SAN (0 for the first); on return it's incremented + * or set to -1 when no SANs remain. + * + * @return An hx509 error code, see hx509_get_error_string(). + * + * @ingroup hx509_request + */ +HX509_LIB_FUNCTION int HX509_LIB_CALL +hx509_request_get_ms_upn_san(hx509_context context, + hx509_request req, + char **out, + int *cursor) +{ + return get_utf8_otherName_san(context, req, &asn1_oid_id_pkinit_ms_san, + out, cursor); +} + +/** + * Iterate e-mail SANs in a CSR. + * + * @param context An hx509 context. + * @param req The hx509_request object. + * @param out A pointer to a char * variable where the e-mail address will be + * placed (caller must free with free()) + * @param cursor An index of SAN (0 for the first); on return it's incremented + * or set to -1 when no SANs remain. + * + * @return An hx509 error code, see hx509_get_error_string(). + * + * @ingroup hx509_request + */ +HX509_LIB_FUNCTION int HX509_LIB_CALL +hx509_request_get_email_san(hx509_context context, + hx509_request req, + char **out, + int *cursor) +{ + ssize_t idx; + size_t i; + + *out = NULL; + if (*cursor < 0) + return 0; + idx = find_san(context, req, cursor, choice_GeneralName_rfc822Name, NULL); + if (idx < 0) + return -1; + i = (size_t)idx; + + *out = strndup(req->san.val[i].u.rfc822Name.data, + req->san.val[i].u.rfc822Name.length); + if (*out == NULL) + return ENOMEM; + return 0; +} + +/** + * Iterate dNSName (DNS domainname/hostname) SANs in a CSR. + * + * @param context An hx509 context. + * @param req The hx509_request object. + * @param out A pointer to a char * variable where the domainname will be + * placed (caller must free with free()) + * @param cursor An index of SAN (0 for the first); on return it's incremented + * or set to -1 when no SANs remain. + * + * @return An hx509 error code, see hx509_get_error_string(). + * + * @ingroup hx509_request + */ +HX509_LIB_FUNCTION int HX509_LIB_CALL +hx509_request_get_dns_name_san(hx509_context context, + hx509_request req, + char **out, + int *cursor) +{ + ssize_t idx; + size_t i; + + *out = NULL; + if (*cursor < 0) + return 0; + idx = find_san(context, req, cursor, choice_GeneralName_dNSName, NULL); + if (idx < 0) + return -1; + i = (size_t)idx; + + *out = strndup(req->san.val[i].u.dNSName.data, + req->san.val[i].u.dNSName.length); + if (*out == NULL) + return ENOMEM; + return 0; +} + +/** + * Iterate Kerberos principal name (PKINIT) SANs in a CSR. + * + * @param context An hx509 context. + * @param req The hx509_request object. + * @param out A pointer to a char * variable where the principal name will be + * placed (caller must free with free()) + * @param cursor An index of SAN (0 for the first); on return it's incremented + * or set to -1 when no SANs remain. + * + * @return An hx509 error code, see hx509_get_error_string(). + * + * @ingroup hx509_request + */ +HX509_LIB_FUNCTION int HX509_LIB_CALL +hx509_request_get_pkinit_san(hx509_context context, + hx509_request req, + char **out, + int *cursor) +{ + struct rk_strpool *pool; + ssize_t idx; + size_t i; + + *out = NULL; + if (*cursor < 0) + return 0; + idx = find_san(context, req, cursor, choice_GeneralName_otherName, + &asn1_oid_id_pkinit_san); + if (idx < 0) + return -1; + i = (size_t)idx; + + pool = _hx509_unparse_kerberos_name(NULL, + &req->san.val[i].u.otherName.value); + if (pool == NULL || + (*out = rk_strpoolcollect(pool)) == NULL) + return ENOMEM; + return 0; +} + +/* XXX More SAN types */ + +/** + * Display a CSR. + * + * @param context An hx509 context. + * @param req The hx509_request object. + * @param f A FILE * to print the CSR to. + * + * @return An hx509 error code, see hx509_get_error_string(). + * + * @ingroup hx509_request + */ +HX509_LIB_FUNCTION int HX509_LIB_CALL +hx509_request_print(hx509_context context, hx509_request req, FILE *f) +{ + uint64_t ku_num; int ret; + /* + * It's really unformatunate that we can't reuse more of the + * lib/hx509/print.c infrastructure here, as it's too focused on + * Certificates. + * + * For that matter, it's really annoying that CSRs don't more resemble + * Certificates. Indeed, an ideal CSR would look like this: + * + * CSRInfo ::= { + * desiredTbsCertificate TBSCertificate, + * attributes [1] SEQUENCE OF Attribute OPTIONAL, + * } + * CSR :: = { + * csrInfo CSRInfo, + * sigAlg AlgorithmIdentifier, + * signature BIT STRING + * } + * + * with everything related to the desired certificate in + * desiredTbsCertificate and anything not related to the CSR's contents in + * the 'attributes' field. + * + * That wouldn't allow one to have optional desired TBSCertificate + * features, but hey. One could express "gimme all or gimme nothing" as an + * attribute, or "gimme what you can", then check what one got. + */ + fprintf(f, "PKCS#10 CertificationRequest:\n"); + if (req->name) { char *subject; ret = hx509_name_to_string(req->name, &subject); @@ -355,10 +1205,55 @@ _hx509_request_print(hx509_context context, hx509_request req, FILE *f) hx509_set_error_string(context, 0, ret, "Failed to print name"); return ret; } - fprintf(f, "name: %s\n", subject); + fprintf(f, " name: %s\n", subject); free(subject); } + /* XXX Use hx509_request_get_ku() accessor */ + if ((ku_num = KeyUsage2int(req->ku))) { + const struct units *u; + const char *first = " "; - return 0; + fprintf(f, " key usage:"); + for (u = asn1_KeyUsage_units(); u->name; ++u) { + if ((ku_num & u->mult)) { + fprintf(f, "%s%s", first, u->name); + first = ", "; + ku_num &= ~u->mult; + } + } + if (ku_num) + fprintf(f, "%s", first); + fprintf(f, "\n"); + } + /* XXX Use new hx509_request_get_eku() accessor! */ + if (req->eku.len) { + const char *first = " "; + size_t i; + + fprintf(f, " eku:"); + for (i = 0; i< req->eku.len; i++) { + char *oidstr = NULL; + + der_print_heim_oid(&req->eku.val[i], '.', &oidstr); + fprintf(f, "%s{%s}", first, oidstr); + free(oidstr); + first = ", "; + } + fprintf(f, "\n"); + } + /* XXX Use new hx509_request_get_*_san() accessors! */ + if (req->san.len) { + size_t i; + + for (i = 0; i < req->san.len; i++) { + GeneralName *san = &req->san.val[i]; + char *s = NULL; + + hx509_general_name_unparse(san, &s); + fprintf(f, " san: %s\n", s ? s : ""); + free(s); + } + } + + return ret; } - diff --git a/lib/hx509/test_name.c b/lib/hx509/test_name.c index 9d21a7f65..7fd6c22b8 100644 --- a/lib/hx509/test_name.c +++ b/lib/hx509/test_name.c @@ -349,6 +349,74 @@ test_compare(hx509_context context) return 0; } +static int +test_pkinit_san(hx509_context context, const char *p, const char *realm, ...) +{ + KRB5PrincipalName kn; + GeneralName gn; + va_list ap; + size_t i, sz; + char *round_trip; + int ret; + + memset(&kn, 0, sizeof(kn)); + memset(&gn, 0, sizeof(gn)); + + ret = _hx509_make_pkinit_san(context, p, &gn.u.otherName.value); + if (ret == 0) + ret = decode_KRB5PrincipalName(gn.u.otherName.value.data, + gn.u.otherName.value.length, &kn, &sz); + if (ret) + return ret; + if (strcmp(realm, kn.realm)) + return ret; + + va_start(ap, realm); + for (i = 0; i < kn.principalName.name_string.len; i++) { + const char *s = va_arg(ap, const char *); + + if (s == NULL || strcmp(kn.principalName.name_string.val[i], s)) + return 1; + } + if (va_arg(ap, const char *) != NULL) + return 1; + va_end(ap); + + gn.element = choice_GeneralName_otherName; + gn.u.otherName.type_id.length = 0; + gn.u.otherName.type_id.components = 0; + ret = der_copy_oid(&asn1_oid_id_pkinit_san, &gn.u.otherName.type_id); + if (ret == 0) + ret = hx509_general_name_unparse(&gn, &round_trip); + if (ret) + return 1; + if (strncmp(round_trip, "otherName: 1.3.6.1.5.2.2 ", + sizeof("otherName: 1.3.6.1.5.2.2 ") - 1)) + return 1; + if (ret || strcmp(round_trip + sizeof("otherName: 1.3.6.1.5.2.2 ") - 1, p)) + return 1; + free_KRB5PrincipalName(&kn); + free_GeneralName(&gn); + free(round_trip); + return 0; +} + +static int +test_pkinit_san_fail(hx509_context context, const char *p) +{ + heim_octet_string os; + KRB5PrincipalName kn; + int ret; + + memset(&kn, 0, sizeof(kn)); + ret = _hx509_make_pkinit_san(context, p, &os); + if (ret == 0) { + free(os.data); + return 1; + } + return 0; +} + int main(int argc, char **argv) @@ -376,7 +444,25 @@ main(int argc, char **argv) ret += test_compare(context); + ret += test_pkinit_san(context, "foo@BAR.H5L.SE", + "BAR.H5L.SE", "foo", NULL); + ret += test_pkinit_san(context, "foo\\ bar@BAR.H5L.SE", + "BAR.H5L.SE", "foo bar", NULL); + ret += test_pkinit_san(context, "foo\\/bar@BAR.H5L.SE", + "BAR.H5L.SE", "foo/bar", NULL); + ret += test_pkinit_san(context, "foo/bar@BAR.H5L.SE", + "BAR.H5L.SE", "foo", "bar", NULL); + ret += test_pkinit_san(context, "foo\\tbar@BAR.H5L.SE", + "BAR.H5L.SE", "foo\tbar", NULL); + ret += test_pkinit_san(context, "foo\\nbar@BAR.H5L.SE", + "BAR.H5L.SE", "foo\nbar", NULL); + ret += test_pkinit_san(context, "foo@\\ BAR.H5L.SE", + " BAR.H5L.SE", "foo", NULL); + ret += test_pkinit_san(context, "foo@\\nBAR.H5L.SE", + "\nBAR.H5L.SE", "foo", NULL); + ret += test_pkinit_san_fail(context, "foo\\0bar@BAR.H5L.SE"); + hx509_context_free(&context); - return ret; + return !!ret; } diff --git a/lib/hx509/test_req.in b/lib/hx509/test_req.in index 49919d918..5d8d56bb5 100644 --- a/lib/hx509/test_req.in +++ b/lib/hx509/test_req.in @@ -50,14 +50,84 @@ fi ${hxtool} request-create \ --subject="CN=Love,DC=it,DC=su,DC=se" \ - --key=FILE:$srcdir/data/key.der \ - request.out || exit 1 + --key="FILE:$srcdir/data/key.der" \ + "${objdir}/request.out" || exit 1 ${hxtool} request-print \ PKCS10:request.out > /dev/null || exit 1 ${hxtool} request-create \ --subject="CN=Love,DC=it,DC=su,DC=se" \ - --dnsname=nutcracker.it.su.se \ - --key=FILE:$srcdir/data/key.der \ - request.out || exit 1 + --eku=1.2.3.4.5.6.7 --eku=1.2.3.4.5.6.8 \ + --registered=1.2.3.4.5.6.9 --eku=1.2.3.4.5.6.10 \ + --dnsname=nutcracker.test.h5l.se \ + --dnsname=foo.nutcracker.test.h5l.se \ + --kerberos=HTTP/foo.nutcracker.it.su.se@TEST.H5L.SE \ + --kerberos=host/foo.nutcracker.it.su.se@TEST.H5L.SE \ + --email=foo@test.h5l.se \ + --key="FILE:$srcdir/data/key.der" \ + "${objdir}/request.out" || exit 1 + +cat > "$objdir/expected" < "${objdir}/actual" || exit 1 + +diff "$objdir/expected" "${objdir}/actual" || exit 1 + +if openssl version > /dev/null && + openssl req -inform DER -in "${objdir}/request.out" -text | + grep 'Version: 0'; then + v=0 + k= +else + v=1 + k="RSA " +fi + +# Check that OpenSSL can parse our request: +cat > "$objdir/expected" <, othername:, Registered ID:1.2.3.4.5.6.9 + Signature Algorithm: sha256WithRSAEncryption +EOF + +if openssl version > /dev/null; then + openssl req -inform DER -in "${objdir}/request.out" -text | head -25 > "${objdir}/actual" + diff -w "${objdir}/expected" "${objdir}/actual" || exit 1 +fi + diff --git a/lib/hx509/version-script.map b/lib/hx509/version-script.map index 9abc06910..67f917fcd 100644 --- a/lib/hx509/version-script.map +++ b/lib/hx509/version-script.map @@ -14,6 +14,7 @@ HEIMDAL_X509_1.2 { _hx509_generate_private_key_free; _hx509_generate_private_key_init; _hx509_generate_private_key_is_ca; + _hx509_make_pkinit_san; _hx509_map_file_os; _hx509_name_from_Name; _hx509_private_key_export; @@ -21,12 +22,16 @@ HEIMDAL_X509_1.2 { _hx509_private_key_get_internal; _hx509_private_key_oid; _hx509_private_key_ref; - _hx509_request_add_dns_name; - _hx509_request_add_eku; - _hx509_request_add_email; - _hx509_request_print; - _hx509_request_set_email; - _hx509_request_to_pkcs10; + hx509_request_add_GeneralName; + hx509_request_add_dns_name; + hx509_request_add_eku; + hx509_request_add_email; + hx509_request_add_ms_upn_name; + hx509_request_add_pkinit; + hx509_request_add_registered; + hx509_request_add_xmpp_name; + hx509_request_print; + hx509_request_to_pkcs10; _hx509_unmap_file_os; _hx509_write_file; hx509_bitstring_print; @@ -206,12 +211,21 @@ HEIMDAL_X509_1.2 { hx509_query_match_option; hx509_query_statistic_file; hx509_query_unparse_stats; + hx509_request_get_dns_name_san; + hx509_request_get_eku; + hx509_request_get_email_san; + hx509_request_get_exts; + hx509_request_get_ku; + hx509_request_get_ms_upn_san; hx509_request_get_name; + hx509_request_get_pkinit_san; hx509_request_get_SubjectPublicKeyInfo; + hx509_request_get_xmpp_san; hx509_request_free; hx509_request_init; hx509_request_parse; hx509_request_parse_der; + hx509_request_set_ku; hx509_request_set_name; hx509_request_set_SubjectPublicKeyInfo; hx509_revoke_add_crl; diff --git a/lib/roken/parse_units.c b/lib/roken/parse_units.c index 08adf96cf..7f60cc16c 100644 --- a/lib/roken/parse_units.c +++ b/lib/roken/parse_units.c @@ -210,7 +210,7 @@ unparse_something (int num, const struct units *units, char *s, size_t len, tmp = (*print) (s, len, divisor, u->name, num); if (tmp < 0) return tmp; - if (tmp > (int) len) { + if ((size_t)tmp > len) { len = 0; s = NULL; } else {