From 901d58bf30b33e89055248037527e9a807c5b389 Mon Sep 17 00:00:00 2001 From: Tom Carroll Date: Thu, 23 Apr 2020 23:15:59 -0700 Subject: [PATCH] Converse the multicert auth protocol Implement the multiple certificate-based authentication protocol. The XML-message protocol may require three rounds of conversations (instead of the existing two): HTTP redirect, announce capabilities and receive multicert challenge, and respond to the multicert challenge and obtain cookie. Signed-off-by: Tom Carroll --- auth.c | 301 ++++++++++++++++++++++++++++++++++++++++- main.c | 30 ++++ openconnect-internal.h | 4 + 3 files changed, 332 insertions(+), 3 deletions(-) diff --git a/auth.c b/auth.c index c07ed15b..6684b8b2 100644 --- a/auth.c +++ b/auth.c @@ -44,6 +44,14 @@ static int cstp_can_gen_tokencode(struct openconnect_info *vpninfo, struct oc_auth_form *form, struct oc_form_opt *opt); +/* multiple certificate-based authentication */ +static int announce_auth_methods(struct openconnect_info *vpninfo, + xmlNodePtr root); +static void parse_multicert_request(struct openconnect_info *vpninfo, + xmlNodePtr node, int *cert_rq); +static int multicert_response(struct openconnect_info *vpninfo, + int cert_rq, const char *challenge, struct oc_text_buf *body); + int openconnect_set_option_value(struct oc_form_opt *opt, const char *value) { if (opt->type == OC_FORM_OPT_SELECT) { @@ -575,7 +583,7 @@ static int parse_xml_response(struct openconnect_info *vpninfo, char *response, continue; } else if (xmlnode_is_named(xml_node, "client-cert-request")) { if (cert_rq) - *cert_rq = 1; + *cert_rq |= CERT_AUTH_REQ_CERT1; else { vpn_progress(vpninfo, PRG_ERR, _("Received when not expected.\n")); @@ -594,6 +602,22 @@ static int parse_xml_response(struct openconnect_info *vpninfo, char *response, ret = parse_host_scan_node(vpninfo, xml_node); } else if (xmlnode_is_named(xml_node, "config")) { parse_config_node(vpninfo, xml_node); + } else if (xmlnode_is_named(xml_node, "multiple-client-cert-request")) { + if (cert_rq) { + *cert_rq |= CERT_AUTH_REQ_CERT1|CERT_AUTH_REQ_CERT2; + parse_multicert_request(vpninfo, xml_node, cert_rq); + } else { + vpn_progress(vpninfo, PRG_ERR, + _("Received when not expected.\n")); + ret = -EINVAL; + } + } else if (xmlnode_is_named(xml_node, "cert-authenticated")) { + /** + * cert-authenticated indicates that the certificate for the + * TLS session is valid. Thus, remove flag for CERT1 request. + */ + if (cert_rq) + *cert_rq &= ~CERT_AUTH_REQ_CERT1; } else { xmlnode_get_text(xml_node, "session-token", &vpninfo->cookie); xmlnode_get_text(xml_node, "error", &form->error); @@ -805,9 +829,14 @@ static int xmlpost_initial_req(struct openconnect_info *vpninfo, if (buf_error(url_buf)) goto bad; - node = xmlNewTextChild(root, NULL, XCAST("group-access"), XCAST(url_buf->data)); + node = xmlNewTextChild(root, NULL, XCAST("group-access"), + XCAST(url_buf->data)); if (!node) goto bad; + + if (announce_auth_methods(vpninfo, root) < 0) + goto bad; + if (cert_fail) { node = xmlNewTextChild(root, NULL, XCAST("client-cert-fail"), NULL); if (!node) @@ -1362,7 +1391,7 @@ newgroup: if (result < 0) goto fail; - if (cert_rq) { + if (cert_rq & CERT_AUTH_REQ_CERT1) { int cert_failed = 0; free_auth_form(form); @@ -1386,7 +1415,26 @@ newgroup: if (result < 0) goto fail; continue; + } else if (cert_rq & CERT_AUTH_REQ_CERT2) { + // load cert + free_auth_form(form); form = NULL; + buf_truncate(request_body); + result = multicert_response(vpninfo, cert_rq, form_buf, + request_body); + if (result < 0) + goto fail; + /** + * Hack!!! + * If we processed a redirect, ntries == 3 once execution + * returns to the top of the for loop. + * This causes the client to attempt GET instead of POST. This + * is not what we want. Thus, we decrement tries by one as + * a work around. + */ + tries -= tries > 0; + continue; } + if (form && form->action) { vpninfo->redirect_url = strdup(form->action); handle_redirect(vpninfo); @@ -1552,3 +1600,250 @@ out: return result; } + +/** + * Multiple certificate-based authentication + * + * Two certificates are employed: a "machine" certificate and a + * "user" certificate. The machine certificate is used to establish + * the TLS session. The user certificate is used to sign a challenge. + * + * An example XML exchange follows: + * CLIENT + * + * + * 4.4.01054 + * + * + * + * ANYCONNECT-MCA + * 136775778 + * multiple-cert + * single-sign-on + * 1506879881148 + * + * + * sha256 + * sha384 + * sha512 + * + * FA4003BD87436B227####snip####C138A08FF724F0100015B863F750914839EE79C86DFE8F0B9A0199E2 + * + * + * + * CLIENT + * + * + * 4.4.01054 + * + * + * ANYCONNECT-MCA + * 608423386 + * multiple-cert + * single-sign-on + * 1506879881148 + * + * + * + * + * MIIG+AYJKoZIhvcNAQcCoIIG6TCCBuU + * yTCCAzwwggIkAgkApaQuJKNF4RowDQYJKoZIhvcNAQELBQAwWTELMAkGA1UEBhMC + * #Snip# + * gSCx8Luo9V76nPjDI8PORurSFVWL9jiGJH0rLakYoGv + * + * FIYur1Dzb4VPThVZtYwxSsCVRBUin/8MwWK+G5u2Phr4fJ + * #snip# + * EYt4G2hQ4hySySYqD4L4iV91uCT5b5Bmr5HZmSqKehg0zrDBjqxx7CLMSf2pSmQnjMwi6D0ygT= + * + * + * + */ + + /** + * Announce our authentication capabilities + */ +int announce_auth_methods(struct openconnect_info *vpninfo, + xmlNodePtr root) +{ + xmlNodePtr node; + + if (vpninfo->cert2 != NULL) { + node = xmlNewChild(root, NULL, XCAST("capabilities"), NULL); + if (node) + node = xmlNewTextChild(node, NULL, XCAST("auth-method"), + XCAST("multiple-cert")); + if (!node) + goto bad; + } + + return 0; + + bad: + + return -ENOMEM; +} + +struct multicert_digest_entry { + const xmlChar *name; + int id; +}; + +static const struct multicert_digest_entry multicert_digests[] = { + { XCAST("sha512"), CERT_AUTH_DIGEST_SHA512 }, + { XCAST("sha384"), CERT_AUTH_DIGEST_SHA384 }, + { XCAST("sha256"), CERT_AUTH_DIGEST_SHA256 }, + { NULL, CERT_AUTH_DIGEST_UNKNOWN }, +}; + +static int multicert_digest_by_name(const xmlChar *name) +{ + const struct multicert_digest_entry *entry; + int id = CERT_AUTH_DIGEST_UNKNOWN; + + for (entry = multicert_digests; entry->name != NULL; entry++) { + if (xmlStrcasecmp(entry->name, name) == 0) { + id = entry->id; + break; + } + } + return id; +} + +static const xmlChar *multicert_digest_by_id(int id) +{ + const struct multicert_digest_entry *entry; + const xmlChar *ret = NULL; + + for (entry = multicert_digests; entry->name != NULL; entry++) { + if (entry->id == id) { + ret = entry->name; + break; + } + } + + return ret; +} + +void parse_multicert_request(struct openconnect_info *vpninfo, + xmlNodePtr node, int *cert_rq) +{ + xmlNodePtr child; + xmlChar *content; + int digest; + + /* node is a multiple-client-cert-request element */ + for (child = node->children; child; child = child->next) { + if (child->type != XML_ELEMENT_NODE) + continue; + + if (xmlStrcmp(child->name, XCAST("hash-algorithm")) != 0) + continue; + + content = xmlNodeGetContent(child); + + digest = multicert_digest_by_name(content); + /* not found */ + if (digest == CERT_AUTH_DIGEST_UNKNOWN) + vpn_progress(vpninfo, PRG_INFO, + _("Algorithm '%s' is unknown.\n"), + (char *)content); + else + *cert_rq |= digest; + xmlFree(content); + } +} + +int multicert_response(struct openconnect_info *vpninfo, + int cert_rq, const char *challenge, struct oc_text_buf *body) +{ + xmlDocPtr doc = NULL; + xmlNodePtr root, auth, node, chain; + const xmlChar *digest_name; + char *identity = NULL; + struct challenge_response resp = {0}; + int ret; + + if ((cert_rq & CERT_AUTH_DIGEST_MASK) == 0) { + vpn_progress(vpninfo, PRG_ERR, + _("Couldn't agree on signature algorithm")); + return xmlpost_initial_req(vpninfo, body, 1); + } + + ret = cert_auth_challenge_response(vpninfo, cert_rq, challenge, + &identity, &resp); + if (ret < 0) + return xmlpost_initial_req(vpninfo, body, 1); + + digest_name = multicert_digest_by_id(resp.digest); + if (digest_name == NULL) + goto bad; + + doc = xmlpost_new_query(vpninfo, "auth-reply", &root); + if (!doc) + goto bad; + + node = xmlNewChild(root, NULL, XCAST("session-token"), NULL); + if (!node) + goto bad; + + node = xmlNewChild(root, NULL, XCAST("session-id"), NULL); + if (!node) + goto bad; + + if (vpninfo->opaque_srvdata != NULL) { + node = xmlCopyNode(vpninfo->opaque_srvdata, 1); + if (!node || !xmlAddChild(root, node)) + goto bad; + } + // key1 ownership is proved by TLS session + auth = xmlNewChild(root, NULL, XCAST("auth"), NULL); + if (!auth) + goto bad; + + chain = xmlNewChild(auth, NULL, XCAST("client-cert-chain"), NULL); + if (!chain || !xmlNewProp(chain, XCAST("cert-store"), XCAST("1M"))) + goto bad; + + if (!xmlNewChild(chain, NULL, XCAST("client-cert-sent-via-protocol"), + NULL)) + goto bad; + // key2 ownership is proved by signing the challenge + chain = xmlNewChild(auth, NULL, XCAST("client-cert-chain"), NULL); + if (!chain || !xmlNewProp(chain, XCAST("cert-store"), XCAST("1U"))) + goto bad; + + node = xmlNewTextChild(chain, NULL, XCAST("client-cert"), + XCAST(identity)); + if (!node || !xmlNewProp(node, XCAST("cert-format"), XCAST("pkcs7"))) + goto bad; + + node = xmlNewTextChild(chain, NULL, + XCAST("client-cert-auth-signature"), XCAST(resp.data)); + if (!node || !xmlNewProp(node, XCAST("hash-algorithm-chosen"), + digest_name)) + goto bad; + + free(identity); free(resp.data); + return xmlpost_complete(doc, body); + + bad: + free(identity); free(resp.data); + xmlpost_complete(doc, NULL); + return -ENOMEM; +} diff --git a/main.c b/main.c index cc3dd91e..a4a21f97 100644 --- a/main.c +++ b/main.c @@ -194,6 +194,9 @@ enum { OPT_PROTOCOL, OPT_PASSTOS, OPT_VERSION, + OPT_MULTICERT_CERT2, + OPT_MULTICERT_KEY2, + OPT_MULTICERT_KEY2_PASSWORD, }; #ifdef __sun__ @@ -285,6 +288,11 @@ static const struct option long_options[] = { #elif defined(OPENCONNECT_OPENSSL) OPTION("openssl-ciphers", 1, OPT_CIPHERSUITES), #endif +#if ENABLE_MULTICERT > 0 + OPTION("cert2", 1, OPT_MULTICERT_CERT2), + OPTION("key2", 1, OPT_MULTICERT_KEY2), + OPTION("key2-password", 1, OPT_MULTICERT_KEY2_PASSWORD), +#endif /* ENABLE_MULTICERT */ OPTION(NULL, 0, 0) }; @@ -832,6 +840,16 @@ static void usage(void) printf(" %s\n", _("(NOTE: Yubikey OATH disabled in this build)")); #endif +#if ENABLE_MULTICERT > 0 + printf("\n%s:\n", _("Multiple certificate-based authentication")); + printf(" --cert2=CERT2 %s\n", + _("User certificate CERT2")); + printf(" --key2=KEY2 %s\n", + _("User key KEY2")); + printf(" --key2-password=PASS2 %s\n", + _("Passpharse PASS2 for CERT2/KEY2")); +#endif /* ENABLE_MULTICERT > 0 */ + printf("\n%s:\n", _("Server validation")); printf(" --servercert=FINGERPRINT %s\n", _("Server's certificate SHA1 fingerprint")); printf(" --no-cert-check %s\n", _("Do not require server SSL cert to be valid")); @@ -1801,6 +1819,18 @@ int main(int argc, char **argv) strncpy(vpninfo->ciphersuite_config, config_arg, sizeof(vpninfo->ciphersuite_config) - 1); break; + case OPT_MULTICERT_CERT2: + free(vpninfo->cert2); + vpninfo->cert2 = dup_config_arg(); + break; + case OPT_MULTICERT_KEY2: + free(vpninfo->key2); + vpninfo->key2 = dup_config_arg(); + break; + case OPT_MULTICERT_KEY2_PASSWORD: + free(vpninfo->key2_password); + vpninfo->key2_password = dup_config_arg(); + break; default: usage(); } diff --git a/openconnect-internal.h b/openconnect-internal.h index b3806136..c97f4aa3 100644 --- a/openconnect-internal.h +++ b/openconnect-internal.h @@ -752,6 +752,10 @@ struct openconnect_info { #define vpn_perror(vpninfo, msg) vpn_progress((vpninfo), PRG_ERR, "%s: %s\n", (msg), strerror(errno)) /* certificate authentication */ +#if !defined(ENABLE_MULTICERT) && defined(OPENCONNECT_GNUTLS) +# define ENABLE_MULTICERT 1 +#endif + typedef enum { CERT_AUTH_REQ_CERT1 = (1U<<0), #define CERT_AUTH_REQ_CERT1 CERT_AUTH_REQ_CERT1 -- 2.50.1