From 0c4e974da5e77a5c7b793b22a68080955c81ccaf Mon Sep 17 00:00:00 2001 From: Tom Carroll Date: Fri, 1 Jan 2021 13:31:58 -0800 Subject: [PATCH 1/7] Add multicert fields to openconnect_info struct Add multicert cert, key, and password to openconnect_info. Update command line handling. Signed-off-by: Tom Carroll --- library.c | 5 +++++ main.c | 37 +++++++++++++++++++++++++++++++++++++ openconnect-internal.h | 4 ++++ 3 files changed, 46 insertions(+) diff --git a/library.c b/library.c index 18afe89d5..2739b33f4 100644 --- a/library.c +++ b/library.c @@ -651,6 +651,11 @@ void openconnect_vpninfo_free(struct openconnect_info *vpninfo) free(cache); } + if (vpninfo->multicert_key != vpninfo->multicert_cert) + free(vpninfo->multicert_key); + free(vpninfo->multicert_cert); + free_pass(&vpninfo->multicert_key_password); + free(vpninfo->localname); free(vpninfo->useragent); free(vpninfo->authgroup); diff --git a/main.c b/main.c index dc44d2384..b23dbdf79 100644 --- a/main.c +++ b/main.c @@ -208,6 +208,9 @@ enum { OPT_PROTOCOL, OPT_PASSTOS, OPT_VERSION, + OPT_MULTICERT_CERT, + OPT_MULTICERT_KEY, + OPT_MULTICERT_KEY_PASSWORD, }; #ifdef __sun__ @@ -300,6 +303,9 @@ static const struct option long_options[] = { #elif defined(OPENCONNECT_OPENSSL) OPTION("openssl-ciphers", 1, OPT_CIPHERSUITES), #endif + OPTION("cert2", 1, OPT_MULTICERT_CERT), + OPTION("key2", 1, OPT_MULTICERT_KEY), + OPTION("key2-password", 1, OPT_MULTICERT_KEY_PASSWORD), OPTION(NULL, 0, 0) }; @@ -957,6 +963,14 @@ static void usage(void) printf(" --allow-insecure-crypto %s\n", _("Allow use of the ancient, insecure 3DES and RC4 ciphers")); printf(" %s\n", _("(and attempt to override OS crypto policies)")); + printf("\n%s:\n", _("Multiple certificate 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")); + printf("\n"); helpmessage(); @@ -1405,6 +1419,17 @@ static int autocomplete(int argc, char **argv) case OPT_DTLS12_CIPHERS: /* --dtls12-ciphers */ break; + case OPT_MULTICERT_CERT: /* --cert2 */ + case OPT_MULTICERT_KEY: /* --key2 */ + if (!strncmp(comp_opt + prefixlen, "pkcs11:", 7)) { + /* We could do clever things here... */ + return 0; /* .. but we don't. */ + } + autocomplete_special("FILENAME", comp_opt, prefixlen, "!*.@(pem|der|p12|crt)"); + break; + /* disable password autocomplete */ + case OPT_MULTICERT_KEY_PASSWORD: /* --key2-password */ + break; default: fprintf(stderr, _("Unhandled autocomplete for option %d '--%s'. Please report.\n"), opt, long_options[longidx].name); @@ -2006,6 +2031,18 @@ int main(int argc, char **argv) vpninfo->ciphersuite_config = dup_config_arg(); break; + case OPT_MULTICERT_CERT: + free(vpninfo->multicert_cert); + vpninfo->multicert_cert = dup_config_arg(); + break; + case OPT_MULTICERT_KEY: + free(vpninfo->multicert_key); + vpninfo->multicert_key = dup_config_arg(); + break; + case OPT_MULTICERT_KEY_PASSWORD: + free(vpninfo->multicert_key_password); + vpninfo->multicert_key_password = dup_config_arg(); + break; default: usage(); } diff --git a/openconnect-internal.h b/openconnect-internal.h index 0a0d8a9e9..78dd6fec9 100644 --- a/openconnect-internal.h +++ b/openconnect-internal.h @@ -501,6 +501,10 @@ struct openconnect_info { int no_http_keepalive; int dump_http_traffic; + char *multicert_cert; + char *multicert_key; + char *multicert_key_password; + int token_mode; int token_bypassed; int token_tries; -- GitLab From 734b6085f01c5051761266c9c26b0a987f3f2b0e Mon Sep 17 00:00:00 2001 From: Tom Carroll Date: Sun, 3 Jan 2021 22:14:54 -0800 Subject: [PATCH 2/7] Add constants and string maps. Signed-off-by: Tom Carroll --- Makefile.am | 2 +- multicert.c | 76 ++++++++++++++++++++++++++++++++++++++++++ openconnect-internal.h | 48 +++++++++++++++++++++++++- 3 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 multicert.c diff --git a/Makefile.am b/Makefile.am index fc6fa4477..3dac90080 100644 --- a/Makefile.am +++ b/Makefile.am @@ -35,7 +35,7 @@ openconnect_LDADD = libopenconnect.la $(SSL_LIBS) $(LIBXML2_LIBS) \ if OPENCONNECT_WIN32 openconnect_SOURCES += openconnect.rc endif -library_srcs = ssl.c http.c http-auth.c auth-common.c auth-html.c library.c compat.c lzs.c mainloop.c script.c ntlm.c digest.c mtucalc.c openconnect-internal.h +library_srcs = ssl.c http.c http-auth.c auth-common.c auth-html.c library.c compat.c lzs.c mainloop.c script.c ntlm.c digest.c mtucalc.c openconnect-internal.h multicert.c lib_srcs_cisco = auth.c cstp.c lib_srcs_juniper = oncp.c lzo.c auth-juniper.c lib_srcs_pulse = pulse.c diff --git a/multicert.c b/multicert.c new file mode 100644 index 000000000..b65cd9c0a --- /dev/null +++ b/multicert.c @@ -0,0 +1,76 @@ +#include + +#include "openconnect-internal.h" + +#define NELEM(array) (sizeof(array)/sizeof(array[0])) +#define U8STREQ(a,b) ((a)&&(b)&&strcmp((const char*)(a), (const char*)(b))==0) + +typedef unsigned char u8; + +static const struct { + multicert_signhash_algorithm_t id; + const u8 *name; +} digest_table[MULTICERT_SIGNHASH_MAX + 1] = { + [ MULTICERT_SIGNHASH_SHA256 ] = { MULTICERT_SIGNHASH_SHA256, (u8*) "sha256" }, + [ MULTICERT_SIGNHASH_SHA384 ] = { MULTICERT_SIGNHASH_SHA384, (u8*) "sha384" }, + [ MULTICERT_SIGNHASH_SHA512 ] = { MULTICERT_SIGNHASH_SHA512, (u8*) "sha512" }, +}; + +static const struct { + multicert_cert_format_t id; + const u8* name; +} cert_format_table[MULTICERT_CERT_FORMAT_MAX + 1] = { + [ MULTICERT_CERT_FORMAT_PKCS7 ] = { MULTICERT_CERT_FORMAT_PKCS7, (u8*) "pkcs7" }, +}; + +const u8* multicert_signhash_get_name(int id) +{ + size_t i; + + if (id > 0 && (size_t) id < NELEM(digest_table)) { + i = (size_t) id; + if (digest_table[i].id) + return digest_table[i].name; + } + return NULL; +} + +multicert_signhash_algorithm_t multicert_signhash_get_id(const u8 *name) +{ + size_t i; + + if (name) { + for (i = 1; i < NELEM(digest_table); i++) { + const u8 *digest_name = digest_table[i].name; + if (U8STREQ(digest_name, name)) + return digest_table[i].id; + } + } + return MULTICERT_SIGNHASH_UNKNOWN; +} + +const u8* multicert_cert_format_get_name(int id) +{ + size_t i; + + if (id > 0 && (size_t) id < NELEM(cert_format_table)) { + i = (size_t) id; + if (cert_format_table[i].id) + return cert_format_table[i].name; + } + return NULL; +} + +multicert_cert_format_t multicert_cert_format_get_id(const u8 *name) +{ + size_t i; + + if (name) { + for (i = 1; i < NELEM(cert_format_table); i++) { + const u8 *format_name = cert_format_table[i].name; + if (U8STREQ(format_name, name)) + return cert_format_table[i].id; + } + } + return MULTICERT_CERT_FORMAT_UNKNOWN; +} diff --git a/openconnect-internal.h b/openconnect-internal.h index 78dd6fec9..8332f72bd 100644 --- a/openconnect-internal.h +++ b/openconnect-internal.h @@ -196,7 +196,7 @@ struct pkt { #define DTLS_CONNECTED 5 /* Transport connected but not yet enabled */ #define DTLS_ESTABLISHED 6 /* Data path fully established */ -/* Not to be confused with OC_PROTO_xxx flags which are library-visible */ +/* Not to be confused with MULTICERT_PROTO_xxx flags which are library-visible */ #define PROTO_ANYCONNECT 0 #define PROTO_NC 1 #define PROTO_GPST 2 @@ -818,6 +818,52 @@ struct openconnect_info { #define MAX_IV_SIZE 16 #define MAX_ESP_PAD 17 /* Including the next-header field */ +/* multiple certificate authentication */ +typedef enum { + MULTICERT_SIGNHASH_UNKNOWN = 0, +#define MULTICERT_SIGNHASH_NONE MULTICERT_SIGNHASH_NONE + MULTICERT_SIGNHASH_SHA256 = 1, +#define MULTICERT_SIGNHASH_SHA256 MULTICERT_SIGNHASH_SHA256 + MULTICERT_SIGNHASH_SHA384 = 2, +#define MULTICERT_SIGNHASH_SHA384 MULTICERT_SIGNHASH_SHA384 + MULTICERT_SIGNHASH_SHA512 = 3, +#define MULTICERT_SIGNHASH_SHA512 MULTICERT_SIGNHASH_SHA512 + MULTICERT_SIGNHASH_MAX = MULTICERT_SIGNHASH_SHA512 +} multicert_signhash_algorithm_t; + +#define MULTICERT_SIGNHASH_FLAG(v) ((v)?(1<<((v)-1)):0) + +typedef enum { + MULTICERT_CERT_FORMAT_UNKNOWN = 0, +#define MULTICERT_CERT_FORMAT_UNKNOWN MULTICERT_CERT_FORMAT_UNKNOWN + MULTICERT_CERT_FORMAT_PKCS7 = 1, +#define MULTICERT_CERT_FORMAT_PKCS7 MULTICERT_CERT_FORMAT_PKCS7 + MULTICERT_CERT_FORMAT_MAX = MULTICERT_CERT_FORMAT_PKCS7 +} multicert_cert_format_t; + +const unsigned char* multicert_signhash_get_name(int id); +multicert_signhash_algorithm_t multicert_signhash_get_id(const unsigned char *name); +const unsigned char* multicert_cert_format_get_name(int id); +multicert_cert_format_t multicert_cert_format_get_id(const unsigned char *name); + +struct multicert_client_cert +{ + multicert_cert_format_t format; + struct oc_text_buf *data; +}; + +struct multicert_client_signature +{ + multicert_signhash_algorithm_t algorithm; + struct oc_text_buf *data; +}; + +int multicert_compute_response(struct openconnect_info *vpninfo, + unsigned int digests, + const unsigned char *chdata, size_t chdata_len, + struct multicert_client_cert *cert, + struct multicert_client_signature *signature); + #define vpn_progress(_v, lvl, ...) do { \ if ((_v)->verbose >= (lvl)) \ (_v)->progress((_v)->cbdata, lvl, __VA_ARGS__); \ -- GitLab From 1ed2d5c692ebaaf310cc228f2e5bdfe68ca506e5 Mon Sep 17 00:00:00 2001 From: Tom Carroll Date: Sun, 3 Jan 2021 23:51:10 -0800 Subject: [PATCH 3/7] Converse the multiple certificate authentication (mulitcert) 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 | 409 ++++++++++++++++++++++++++++++++++++++++- gnutls.c | 9 + openconnect-internal.h | 6 + openssl.c | 9 + 4 files changed, 424 insertions(+), 9 deletions(-) diff --git a/auth.c b/auth.c index 4a63a0179..5c4e675ae 100644 --- a/auth.c +++ b/auth.c @@ -38,12 +38,33 @@ #include "openconnect-internal.h" +enum { + CERT1_REQUESTED = (1<<0), + CERT1_AUTHENTICATED = (1<<1), + CERT2_REQUESTED = (1<<2), +}; + +struct cert_request +{ + unsigned int state:16; + unsigned int hashes:16; +}; + static int xmlpost_append_form_opts(struct openconnect_info *vpninfo, struct oc_auth_form *form, struct oc_text_buf *body); 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_multicert_capability(struct openconnect_info *vpninfo, + xmlNodePtr root); +static void parse_multicert_request(struct openconnect_info *vpninfo, + xmlNodePtr node, struct cert_request *cert_rq); +static int prepare_multicert_response(struct openconnect_info *vpninfo, + struct cert_request 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) { @@ -549,20 +570,24 @@ static void parse_config_node(struct openconnect_info *vpninfo, xmlNode *xml_nod * < 0, on error * = 0, on success; *form is populated */ -static int parse_xml_response(struct openconnect_info *vpninfo, char *response, - struct oc_auth_form **formp, int *cert_rq) +static int parse_xml_response(struct openconnect_info *vpninfo, + char *response,struct oc_auth_form **formp, + struct cert_request *cert_rq) { struct oc_auth_form *form; xmlDocPtr xml_doc; xmlNode *xml_node; + int old_cert_rq_state = 0; int ret; if (*formp) { free_auth_form(*formp); *formp = NULL; } - if (cert_rq) - *cert_rq = 0; + if (cert_rq) { + old_cert_rq_state = cert_rq->state; + *cert_rq = (struct cert_request) { 0 }; + } if (!response) { vpn_progress(vpninfo, PRG_DEBUG, @@ -598,12 +623,28 @@ 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->state |= CERT1_REQUESTED; else { vpn_progress(vpninfo, PRG_ERR, _("Received when not expected.\n")); ret = -EINVAL; } + } else if (xmlnode_is_named(xml_node, "multiple-client-cert-request")) { + if (cert_rq) { + cert_rq->state |= CERT1_REQUESTED|CERT2_REQUESTED; + 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->state |= CERT1_AUTHENTICATED; } else if (xmlnode_is_named(xml_node, "auth")) { xmlnode_get_prop(xml_node, "id", &form->auth_id); ret = parse_auth_node(vpninfo, xml_node, form); @@ -627,7 +668,14 @@ static int parse_xml_response(struct openconnect_info *vpninfo, char *response, xml_node = xml_node->next; } - if (!form->auth_id && (!cert_rq || !*cert_rq)) { + if (old_cert_rq_state && form->error) { + vpn_progress(vpninfo, PRG_ERR, + _("Server reported certificate error: %s.\n"), form->error); + ret = -EINVAL; + goto out; + } + + if (!form->auth_id && (!cert_rq || !cert_rq->state)) { vpn_progress(vpninfo, PRG_ERR, _("XML response has no \"auth\" node\n")); ret = -EINVAL; @@ -824,6 +872,10 @@ static int xmlpost_initial_req(struct openconnect_info *vpninfo, node = xmlNewTextChild(root, NULL, XCAST("group-access"), XCAST(url)); if (!node) goto bad; + + if (announce_multicert_capability(vpninfo, root) < 0) + goto bad; + if (cert_fail) { node = xmlNewTextChild(root, NULL, XCAST("client-cert-fail"), NULL); if (!node) @@ -1292,7 +1344,8 @@ int cstp_obtain_cookie(struct openconnect_info *vpninfo) const char *method = "POST"; char *orig_host = NULL, *orig_path = NULL, *form_path = NULL; int orig_port = 0; - int cert_rq, cert_sent = !vpninfo->cert; + struct cert_request cert_rq = { 0 }; + int cert_sent = !vpninfo->cert; int newgroup_attempts = 5; if (!vpninfo->xmlpost) @@ -1325,7 +1378,14 @@ newgroup: orig_port = vpninfo->port; for (tries = 0; ; tries++) { - if (tries == 3) { + /** + * Multiple certificate authentication requires an additional exchange. + * tries == 0: redirect + * tries == 1: !cert_sent + initial_req + * tries == 2: cert_sent + initial_req + * tries == 3: challenge response + */ + if (tries == 3 + !!(cert_rq.state&CERT2_REQUESTED)) { fail: if (vpninfo->xmlpost) { no_xmlpost: @@ -1380,7 +1440,8 @@ newgroup: if (result < 0) goto fail; - if (cert_rq) { + if ((cert_rq.state&CERT1_REQUESTED) && + !(cert_rq.state&CERT1_AUTHENTICATED)) { int cert_failed = 0; free_auth_form(form); @@ -1404,6 +1465,14 @@ newgroup: if (result < 0) goto fail; continue; + } else if (cert_rq.state&CERT2_REQUESTED) { + free_auth_form(form); form = NULL; + buf_truncate(request_body); + result = prepare_multicert_response(vpninfo, cert_rq, form_buf, + request_body); + if (result < 0) + goto fail; + continue; } if (form && form->action) { vpninfo->redirect_url = strdup(form->action); @@ -1573,3 +1642,325 @@ out: return result; } + +/** + * Multiple certificate 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= + * + * + * + */ +static int to_base64(struct oc_text_buf **result, + const void *data, size_t data_len) +{ + const uint8_t *dp = data; + struct oc_text_buf *buf; + size_t result_len; + int ret; + + *result = NULL; + + /** + * Check for potential overlow. This computes an approximation + * of the upper bound + */ + if (data_len > (INT_MAX / 4) * 3 - (INT_MAX / 64) * 3 / 4) + return -E2BIG; + /* a 3-byte block is expanded to 4-byte block */ + result_len = ((data_len + 2) / 3) * 4; + /** + * Line feed every 64 characters. No feed needed on last line + * */ + if (data_len > 0) + result_len += (result_len - 1) / 64; + + buf = buf_alloc(); + if (!buf) + return -ENOMEM; + + ret = buf_ensure_space(buf, result_len); + if (ret < 0) + goto out; + + /** + * PEM imposes a 64-character line length + */ + while (data_len > 48) { + buf_append_base64(buf, dp, 48); + buf_append(buf, "\n"); + dp += 48; data_len -= 48; + } + + if (data_len > 0) + buf_append_base64(buf, dp, data_len); + + ret = buf_error(buf); + if (ret < 0) + goto out; + + *result = buf; + buf = NULL; + +out: + buf_free(buf); + return ret; +} + + /** + * Announce multicert capability + */ +int announce_multicert_capability(struct openconnect_info *vpninfo, + xmlNodePtr root) +{ + xmlNodePtr node; + + if (vpninfo->multicert_cert) { + 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; +} + +/** + * parse request + */ +void parse_multicert_request(struct openconnect_info *vpninfo, + xmlNodePtr node, struct cert_request *cert_rq) +{ + xmlNodePtr child; + xmlChar *content; + multicert_signhash_algorithm_t hash; + unsigned int oldhashes = 0; + + /* 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); + if (content == NULL) + continue; + + hash = multicert_signhash_get_id(content); + /* hash was not found */ + if (hash == MULTICERT_SIGNHASH_UNKNOWN) { + vpn_progress(vpninfo, PRG_INFO, + _("Unsupported hash algorithm '%s' requested.\n"), + (char*) content); + goto next; + } + + oldhashes = cert_rq->hashes; + cert_rq->hashes |= MULTICERT_SIGNHASH_FLAG(hash); + if (oldhashes == cert_rq->hashes) + vpn_progress(vpninfo, PRG_INFO, + _("Duplicate hash algorithm '%s' requested.\n"), + (char*) content); + + next: + xmlFree(content); + } +} + +#define BUF_DATA(bp) ((bp)->data) +#define BUF_SIZE(bp) ((bp)->pos) + +static int post_multicert_response(struct openconnect_info *vpninfo, + const xmlChar *format, const xmlChar *cert, + const xmlChar *hash, const xmlChar *signature, + struct oc_text_buf *body) +{ + static const xmlChar *strnull = XCAST("(null)"); + xmlDocPtr doc; + xmlNodePtr root, auth, node, chain; + + 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"), cert ? cert : strnull); + if (!node || !xmlNewProp(node, XCAST("cert-format"), format ? format : strnull)) + goto bad; + + node = xmlNewTextChild(chain, NULL, + XCAST("client-cert-auth-signature"), + signature ? signature : strnull); + if (!node || !xmlNewProp(node, XCAST("hash-algorithm-chosen"), hash ? hash : strnull)) + goto bad; + + return xmlpost_complete(doc, body); + + bad: + xmlpost_complete(doc, NULL); + return -ENOMEM; + +} + +int prepare_multicert_response(struct openconnect_info *vpninfo, + struct cert_request cert_rq, const char *challenge, + struct oc_text_buf *body) +{ + struct multicert_client_cert cert = { 0 }; + struct multicert_client_signature signature = { 0 }; + struct oc_text_buf *certtext = NULL, *signtext = NULL; + const xmlChar *format, *hash; + int ret; + + if (!vpninfo->multicert_cert) { + /* This is a fail safe; we should never get here */ + vpn_progress(vpninfo, PRG_ERR, + _("Multiple-certificate authentication requires a second certificate; none were configured.\n")); + goto error; + } + + if (!cert_rq.hashes) { + vpn_progress(vpninfo, PRG_ERR, + _("Multiple-certificate authentication signature hash algorithm negotiation failed.\n")); + goto error; + } + + ret = multicert_compute_response(vpninfo, cert_rq.hashes, + (unsigned char *) challenge, strlen(challenge), + &cert, &signature); + if (ret < 0) + goto error; + + format = multicert_cert_format_get_name(cert.format); + hash = multicert_signhash_get_name(signature.algorithm); + if (!format || !hash) { + vpn_progress(vpninfo, PRG_ERR, + _("Failed to translate to text.\n")); + goto error; + } + + if (to_base64(&certtext, BUF_DATA(cert.data), BUF_SIZE(cert.data)) < 0 || + to_base64(&signtext, BUF_DATA(signature.data), BUF_SIZE(signature.data)) < 0) { + vpn_progress(vpninfo, PRG_ERR, + _("Error encoding the challenge response.\n")); + goto error; + } + + ret = post_multicert_response(vpninfo, + format, XCAST(BUF_DATA(certtext)), + hash, XCAST(BUF_DATA(signtext)), body); + buf_free(certtext); + buf_free(signtext); + buf_free(cert.data); + buf_free(signature.data); + return ret; + +error: + buf_free(certtext); + buf_free(signtext); + buf_free(cert.data); + buf_free(signature.data); + return xmlpost_initial_req(vpninfo, body, 1); +} diff --git a/gnutls.c b/gnutls.c index 0efd10912..7cf854ff3 100644 --- a/gnutls.c +++ b/gnutls.c @@ -2736,3 +2736,12 @@ void destroy_eap_ttls(struct openconnect_info *vpninfo, void *sess) { gnutls_deinit(sess); } + +int multicert_compute_response(struct openconnect_info *vpninfo, + unsigned int digests, + const unsigned char *chdata, size_t chdata_len, + struct multicert_client_cert *ccert, + struct multicert_client_signature *csignature) +{ + return -ENOSYS; +} diff --git a/openconnect-internal.h b/openconnect-internal.h index 8332f72bd..1ac385ce4 100644 --- a/openconnect-internal.h +++ b/openconnect-internal.h @@ -858,6 +858,12 @@ struct multicert_client_signature struct oc_text_buf *data; }; +int multicert_compute_response(struct openconnect_info *vpninfo, + unsigned int digests, + const unsigned char *chdata, size_t chdata_len, + struct multicert_client_cert *cert, + struct multicert_client_signature *signature); + int multicert_compute_response(struct openconnect_info *vpninfo, unsigned int digests, const unsigned char *chdata, size_t chdata_len, diff --git a/openssl.c b/openssl.c index aef7c4f9d..6e558b5e0 100644 --- a/openssl.c +++ b/openssl.c @@ -2217,3 +2217,12 @@ void destroy_eap_ttls(struct openconnect_info *vpninfo, void *ttls) /* Leave the BIO_METH for now. It may get reused and we don't want to * have to call BIO_get_new_index() more times than is necessary */ } + +int multicert_compute_response(struct openconnect_info *vpninfo, + unsigned int digests, + const unsigned char *chdata, size_t chdata_len, + struct multicert_client_cert *ccert, + struct multicert_client_signature *csignature) +{ + return -ENOSYS; +} -- GitLab From 7e9afd1c38ca8070479533af9e2b090dc49a7a48 Mon Sep 17 00:00:00 2001 From: Tom Carroll Date: Fri, 1 Jan 2021 23:30:19 -0800 Subject: [PATCH 4/7] Expose privkey needed for multicert challenge signing. Signed-off-by: Tom Carroll --- gnutls.c | 37 +++++++++++++++++++++++++++++++++---- openconnect-internal.h | 2 ++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/gnutls.c b/gnutls.c index 7cf854ff3..e3bdd8b58 100644 --- a/gnutls.c +++ b/gnutls.c @@ -603,7 +603,6 @@ static int get_cert_name(gnutls_x509_crt_t cert, char *name, size_t namelen) return 0; } -#if defined(HAVE_P11KIT) || defined(HAVE_TROUSERS) || defined(HAVE_TSS2) || defined (HAVE_GNUTLS_SYSTEM_KEYS) /* We have to convert the array of X509 certificates to gnutls_pcert_st for ourselves. There's no function that takes a gnutls_privkey_t as the key and gnutls_x509_crt_t certificates. */ @@ -639,11 +638,40 @@ static int assign_privkey(struct openconnect_info *vpninfo, free_pcerts: for (i = 0 ; i < nr_certs; i++) gnutls_pcert_deinit(pcerts + i); + pkey = NULL; } + vpninfo->https_pkey = pkey; free(pcerts); return err; } +static int set_x509_key(struct openconnect_info *vpninfo, + gnutls_x509_privkey_t x509_key, + gnutls_x509_crt_t *certs, + unsigned int nr_certs, + uint8_t *free_certs) +{ + gnutls_privkey_t pkey; + int err; + + err = gnutls_privkey_init(&pkey); + if (err < 0) + return err; + + err = gnutls_privkey_import_x509(pkey, x509_key, + GNUTLS_PRIVKEY_IMPORT_COPY); + if (err < 0) + goto free_pkey; + + err = assign_privkey(vpninfo, pkey, certs, nr_certs, free_certs); + if (err < 0) + free_pkey: + gnutls_privkey_deinit(pkey); + + return err; +} + +#if defined(HAVE_P11KIT) || defined(HAVE_TROUSERS) || defined(HAVE_TSS2) || defined (HAVE_GNUTLS_SYSTEM_KEYS) static int verify_signed_data(gnutls_pubkey_t pubkey, gnutls_privkey_t privkey, gnutls_digest_algorithm_t dig, const gnutls_datum_t *data, const gnutls_datum_t *sig) @@ -1784,9 +1812,8 @@ static int load_certificate(struct openconnect_info *vpninfo) } } else #endif /* P11KIT || TROUSERS || TSS2 || SYSTEM_KEYS */ - err = gnutls_certificate_set_x509_key(vpninfo->https_cred, - supporting_certs, - nr_supporting_certs, key); + err = set_x509_key(vpninfo, key, supporting_certs, + nr_supporting_certs, free_supporting_certs); if (err) { vpn_progress(vpninfo, PRG_ERR, @@ -2421,6 +2448,8 @@ void openconnect_close_https(struct openconnect_info *vpninfo, int final) if (final && vpninfo->https_cred) { gnutls_certificate_free_credentials(vpninfo->https_cred); vpninfo->https_cred = NULL; + /* https_pkey is owned by https_cred */ + vpninfo->https_pkey = NULL; #ifdef HAVE_TROUSERS release_tpm1_ctx(vpninfo); #endif diff --git a/openconnect-internal.h b/openconnect-internal.h index 1ac385ce4..42a9be5f5 100644 --- a/openconnect-internal.h +++ b/openconnect-internal.h @@ -578,6 +578,8 @@ struct openconnect_info { gnutls_session_t https_sess; gnutls_session_t eap_ttls_sess; gnutls_certificate_credentials_t https_cred; + /* https_pkey is owned by https_cred; do not deinit */ + gnutls_privkey_t https_pkey; gnutls_psk_client_credentials_t psk_cred; char local_cert_md5[MD5_SIZE * 2 + 1]; /* For CSD */ #ifdef HAVE_TROUSERS -- GitLab From 6a3e878ac02228c6dcf576b008848d8d4075d858 Mon Sep 17 00:00:00 2001 From: Tom Carroll Date: Wed, 6 Jan 2021 11:31:34 -0800 Subject: [PATCH 5/7] Factor out trust store functionality. Reuse trust store functionality to load the second credential. Signed-off-by: Tom Carroll --- gnutls.c | 161 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 88 insertions(+), 73 deletions(-) diff --git a/gnutls.c b/gnutls.c index e3bdd8b58..5394b08da 100644 --- a/gnutls.c +++ b/gnutls.c @@ -2143,6 +2143,80 @@ static int verify_peer(gnutls_session_t session) return err; } +static int load_trusts(struct openconnect_info *vpninfo) +{ + int err; + + if (!vpninfo->https_cred) + return -EINVAL; + + if (!vpninfo->no_system_trust) + gnutls_certificate_set_x509_system_trust(vpninfo->https_cred); + + gnutls_certificate_set_verify_function(vpninfo->https_cred, + verify_peer); + +#ifdef ANDROID_KEYSTORE + if (vpninfo->cafile && !strncmp(vpninfo->cafile, "keystore:", 9)) { + gnutls_datum_t datum; + unsigned int nr_certs; + + err = load_datum(vpninfo, &datum, vpninfo->cafile); + if (err < 0) { + gnutls_certificate_free_credentials(vpninfo->https_cred); + vpninfo->https_cred = NULL; + return err; + } + + /* For GnuTLS 3.x We should use gnutls_x509_crt_list_import2() */ + nr_certs = count_x509_certificates(&datum); + if (nr_certs) { + gnutls_x509_crt_t *certs; + int i; + + certs = calloc(nr_certs, sizeof(*certs)); + if (!certs) { + vpn_progress(vpninfo, PRG_ERR, + _("Failed to allocate memory for cafile certs\n")); + gnutls_free(datum.data); + return -ENOMEM; + } + err = gnutls_x509_crt_list_import(certs, &nr_certs, &datum, + GNUTLS_X509_FMT_PEM, 0); + gnutls_free(datum.data); + if (err >= 0) { + nr_certs = err; + err = gnutls_certificate_set_x509_trust(vpninfo->https_cred, + certs, nr_certs); + } + for (i = 0; i < nr_certs; i++) + gnutls_x509_crt_deinit(certs[i]); + free(certs); + if (err < 0) { + /* From crt_list_import or set_x509_trust */ + vpn_progress(vpninfo, PRG_ERR, + _("Failed to read certs from cafile: %s\n"), + gnutls_strerror(err)); + return -EINVAL; + } + } + } else +#endif + if (vpninfo->cafile) { + err = gnutls_certificate_set_x509_trust_file(vpninfo->https_cred, + vpninfo->cafile, + GNUTLS_X509_FMT_PEM); + if (err < 0) { + vpn_progress(vpninfo, PRG_ERR, + _("Failed to open CA file '%s': %s\n"), + vpninfo->cafile, gnutls_strerror(err)); + return -EINVAL; + } + } + + return 0; +} + int openconnect_open_https(struct openconnect_info *vpninfo) { int ssl_sock = -1; @@ -2166,80 +2240,21 @@ int openconnect_open_https(struct openconnect_info *vpninfo) return ssl_sock; if (!vpninfo->https_cred) { - gnutls_certificate_allocate_credentials(&vpninfo->https_cred); - if (!vpninfo->no_system_trust) - gnutls_certificate_set_x509_system_trust(vpninfo->https_cred); - - gnutls_certificate_set_verify_function(vpninfo->https_cred, - verify_peer); - -#ifdef ANDROID_KEYSTORE - if (vpninfo->cafile && !strncmp(vpninfo->cafile, "keystore:", 9)) { - gnutls_datum_t datum; - unsigned int nr_certs; - - err = load_datum(vpninfo, &datum, vpninfo->cafile); - if (err < 0) { - gnutls_certificate_free_credentials(vpninfo->https_cred); - vpninfo->https_cred = NULL; - return err; - } - - /* For GnuTLS 3.x We should use gnutls_x509_crt_list_import2() */ - nr_certs = count_x509_certificates(&datum); - if (nr_certs) { - gnutls_x509_crt_t *certs; - int i; - - certs = calloc(nr_certs, sizeof(*certs)); - if (!certs) { - vpn_progress(vpninfo, PRG_ERR, - _("Failed to allocate memory for cafile certs\n")); - gnutls_free(datum.data); - gnutls_certificate_free_credentials(vpninfo->https_cred); - vpninfo->https_cred = NULL; - closesocket(ssl_sock); - return -ENOMEM; - } - err = gnutls_x509_crt_list_import(certs, &nr_certs, &datum, - GNUTLS_X509_FMT_PEM, 0); - gnutls_free(datum.data); - if (err >= 0) { - nr_certs = err; - err = gnutls_certificate_set_x509_trust(vpninfo->https_cred, - certs, nr_certs); - } - for (i = 0; i < nr_certs; i++) - gnutls_x509_crt_deinit(certs[i]); - free(certs); - if (err < 0) { - /* From crt_list_import or set_x509_trust */ - vpn_progress(vpninfo, PRG_ERR, - _("Failed to read certs from cafile: %s\n"), - gnutls_strerror(err)); - gnutls_certificate_free_credentials(vpninfo->https_cred); - vpninfo->https_cred = NULL; - closesocket(ssl_sock); - return -EINVAL; - } - } - } else -#endif - if (vpninfo->cafile) { - err = gnutls_certificate_set_x509_trust_file(vpninfo->https_cred, - vpninfo->cafile, - GNUTLS_X509_FMT_PEM); - if (err < 0) { - vpn_progress(vpninfo, PRG_ERR, - _("Failed to open CA file '%s': %s\n"), - vpninfo->cafile, gnutls_strerror(err)); - gnutls_certificate_free_credentials(vpninfo->https_cred); - vpninfo->https_cred = NULL; - closesocket(ssl_sock); - return -EINVAL; - } + err = gnutls_certificate_allocate_credentials(&vpninfo->https_cred); + if (err < 0) { + vpn_progress(vpninfo, PRG_ERR, + _("gnutls_certificate_allocate_credentials error: %s.\n"), + gnutls_strerror(err)); + closesocket(ssl_sock); + return -ENOMEM; + } + err = load_trusts(vpninfo); + if (err < 0) { + gnutls_certificate_free_credentials(vpninfo->https_cred); + vpninfo->https_cred = NULL; + closesocket(ssl_sock); + return err; } - if (vpninfo->cert) { err = load_certificate(vpninfo); if (err) { -- GitLab From bc7835ae3301593e8ceafa7c811924c31d2c140f Mon Sep 17 00:00:00 2001 From: Tom Carroll Date: Wed, 6 Jan 2021 17:47:39 -0800 Subject: [PATCH 6/7] gnutls crypto implementation for signing multicert challenge. Signed-off-by: Tom Carroll --- gnutls.c | 558 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 554 insertions(+), 4 deletions(-) diff --git a/gnutls.c b/gnutls.c index 5394b08da..29242e97f 100644 --- a/gnutls.c +++ b/gnutls.c @@ -640,11 +640,18 @@ static int assign_privkey(struct openconnect_info *vpninfo, gnutls_pcert_deinit(pcerts + i); pkey = NULL; } + vpninfo->https_pkey = pkey; free(pcerts); return err; } +/** + * Set the credential's key and cert. + * + * Interface and semantics similar to gnutls_certificate_set_x509_key(). + * Calls assign_privkey() to actually do the work. + */ static int set_x509_key(struct openconnect_info *vpninfo, gnutls_x509_privkey_t x509_key, gnutls_x509_crt_t *certs, @@ -658,6 +665,9 @@ static int set_x509_key(struct openconnect_info *vpninfo, if (err < 0) return err; + gnutls_privkey_set_pin_function(pkey, gnutls_pin_callback, + vpninfo); + err = gnutls_privkey_import_x509(pkey, x509_key, GNUTLS_PRIVKEY_IMPORT_COPY); if (err < 0) @@ -2781,11 +2791,551 @@ void destroy_eap_ttls(struct openconnect_info *vpninfo, void *sess) gnutls_deinit(sess); } +/** + * multiple-client certificate authentication + */ +static void assert_log(struct openconnect_info *vpninfo, + const char *file, int line, const char *func, + const char *assertion) +{ + vpn_progress(vpninfo, PRG_ERR, + _("Assertion '%s' failed %s[%s]:%d.\n"), + assertion, + file, func, line); +} + +static int debug_log(struct openconnect_info *vpninfo, + const char *file, int line, const char *func, + int err, const char *msg, ...) +{ + vpn_progress(vpninfo, PRG_DEBUG, + "%s %s[%s]:%d: %s.\n", + _(msg), file, func, line, + gnutls_strerror(err)); + return err; +} + +#define assert_return(expr, r) \ + do { \ + if (!(expr)) { \ + assert_log(vpninfo, __FILE__, __LINE__, \ + __func__, #expr); \ + return (r); \ + } \ + } while(0) + +#define debug(Err,Msg) (((Err)<0)? \ + debug_log((vpninfo),__FILE__,__LINE__,__func__,(Err),(Msg)): \ + (Err)) + +struct credentialx { + gnutls_x509_crt_t *certs; + unsigned int ncerts; + gnutls_privkey_t key; + struct openconnect_info *vpninfo__; +}; + +static int err_to_application_error(int err) +{ + if (err >= 0) + return 0; + + switch (err) + { + case GNUTLS_E_MEMORY_ERROR: + return -ENOMEM; + case GNUTLS_E_CONSTRAINT_ERROR: + case GNUTLS_E_ILLEGAL_PARAMETER: + case GNUTLS_E_INVALID_REQUEST: + case GNUTLS_E_UNSUPPORTED_SIGNATURE_ALGORITHM: + return -EINVAL; + } + + /** + * Use a synthetic errno to signal GnuTLS errors that don't + * map to POSIX errno + */ + return err > INT_MIN + 1024 ? err - 1024 : INT_MIN; +} + +static int to_text_buf(struct oc_text_buf **bufp, + const gnutls_datum_t *datum) +{ + struct oc_text_buf *buf; + + *bufp = NULL; + if (!(datum->size <= INT_MAX)) + return GNUTLS_E_MEMORY_ERROR; + + buf = buf_alloc(); + if (!buf) + return GNUTLS_E_MEMORY_ERROR; + + buf_append_bytes(buf, datum->data, (int) datum->size); + if (buf_error(buf) < 0) + goto fail; + + *bufp = buf; + return 0; + +fail: + buf_free(buf); + return GNUTLS_E_MEMORY_ERROR; +} + +static void free_crt_list(gnutls_x509_crt_t *cert_list, + unsigned int cert_list_size) +{ + if (cert_list) { + size_t ncerts = cert_list_size; + while (ncerts--) + gnutls_x509_crt_deinit(cert_list[ncerts]); + gnutls_free(cert_list); + } +} + +/** + * Obtains a X.509 certificate list that has been stored in crd. + * + * GnuTLS >= 3.4 has gnutls_certificate_get_x509_crt. Here we use + * older API to do the same. + */ +static int get_x509_crt(gnutls_certificate_credentials_t crd, + gnutls_x509_crt_t **cert_list, + unsigned int *cert_list_size) +{ + gnutls_datum_t cert; + gnutls_x509_crt_t *crts = NULL, *new_crts; + size_t max_crts = 4; + size_t ncrts = 0; + size_t i; + int err; + +grow_list: + /** + * Grow the list by 4 elements. Plus one for NULL terminator. + * Assume won't overflow. + */ + new_crts = gnutls_realloc(crts, + (max_crts + 4 + 1) * sizeof crts[0]); + if (!new_crts) { + err = GNUTLS_E_MEMORY_ERROR; + goto fail; + } + + max_crts += 4; + crts = new_crts; + + for (i = ncrts; i < max_crts; i++) { + + err = gnutls_certificate_get_crt_raw(crd, 0, i, &cert); + if (err < 0) + break; + + err = gnutls_x509_crt_init(&crts[i]); + if (err < 0) + break; + + ncrts++; + + err = gnutls_x509_crt_import(crts[i], &cert, + GNUTLS_X509_FMT_DER); + if (err < 0) + break; + + /* cert is a reference to the data; don't free */ + } + + if (i >= max_crts) + goto grow_list; + + if (err != GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE) + goto fail; + + crts[ncrts] = NULL; + *cert_list = crts; + *cert_list_size = ncrts; + + return 0; + +fail: + free_crt_list(crts, ncrts); + *cert_list = NULL; + *cert_list_size = 0; + return err; +} + +static int load_credentialx(struct openconnect_info *vpninfo, + struct credentialx **credxp) +{ + struct credentialx *credx; + struct openconnect_info *info; + gnutls_x509_crt_t *certs = NULL; + unsigned int ncerts = 0; + int ret, err; + + if (!vpninfo->multicert_cert) + return -EINVAL; + + info = openconnect_vpninfo_new("load_credentialx", + vpninfo->validate_peer_cert, + NULL, + vpninfo->process_auth_form, + vpninfo->progress, vpninfo->cbdata); + if (!info) + return -ENOMEM; + /* copy certificate-related elements from vpninto to info */ + if ((vpninfo->cafile && + (info->cafile = strdup(vpninfo->cafile)) == NULL) || + (info->cert = strdup(vpninfo->multicert_cert)) == NULL) { + ret = -ENOMEM; + goto fail; + } + + if (vpninfo->multicert_key && + strcmp(vpninfo->multicert_key, vpninfo->multicert_cert) != 0) + info->sslkey = strdup(vpninfo->multicert_key); + else + info->sslkey = info->cert; + if (!info->sslkey) { + ret = -ENOMEM; + goto fail; + } + /* Don't make a copy of the password. */ + info->cert_password = vpninfo->multicert_key_password; + vpninfo->multicert_key_password = NULL; + /* copy remaining relevant parameters */ + info->nopasswd = vpninfo->nopasswd; + info->no_system_trust = vpninfo->no_system_trust; + /* Initialize credential */ + err = gnutls_certificate_allocate_credentials(&info->https_cred); + if (err < 0) { + debug(err, "gnutls_certificate_allocate_credentials"); + ret = -ENOMEM; + goto fail; + } + + ret = load_trusts(info); + if (ret < 0) + goto fail; + + ret = load_certificate(info); + if (ret < 0) + goto fail; + + err = get_x509_crt(info->https_cred, &certs, &ncerts); + if (err < 0) { + debug(err, "get_x509_crt"); + ret = -ENOMEM; + goto fail; + } + + credx = gnutls_calloc(1, sizeof *credx); + if (!credx) { + ret = -ENOMEM; + goto fail; + } + + credx->certs = certs; + credx->ncerts = ncerts; + credx->key = info->https_pkey; + credx->vpninfo__ = info; + *credxp = credx; + + return 0; + +fail: + free_crt_list(certs, ncerts); + openconnect_vpninfo_free(info); + *credxp = NULL; + return ret; +} + +static void free_credentialx(struct openconnect_info *vpninfo, + struct credentialx *credx) +{ + if (credx) { + free_crt_list(credx->certs, credx->ncerts); + openconnect_vpninfo_free(credx->vpninfo__); + free(credx); + } +} + +static int check_key_purpose(struct openconnect_info *vpninfo, + struct credentialx *credx) +{ +#ifndef GNUTLS_KP_ANY +# define GNUTLS_KP_ANY "2.5.29.37.0" +#endif +#ifndef GNUTLS_KP_TLS_WWW_CLIENT +# define GNUTLS_KP_TLS_WWW_CLIENT "1.3.6.1.5.5.7.3.2" +#endif +#ifndef GNUTLS_KP_MS_SMART_CARD_LOGON +# define GNUTLS_KP_MS_SMART_CARD_LOGON "1.3.6.1.4.1.311.20.2.2" +#endif + +#define MAX_OID 128 + char oid[MAX_OID]; + gnutls_x509_crt_t crt; + unsigned int usage = 0, critical; + size_t kp, oidsize; + int err; + + /** + * extendedKeyUsage of either any, clientAuth, or msSmartcardLogin + * satisfy authentication purposes. + */ + crt = credx->certs[0]; + for (kp = 0; ; kp++) { + oidsize = sizeof oid; + err = gnutls_x509_crt_get_key_purpose_oid(crt, kp, + oid, &oidsize, + &critical); + if (err == GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE) { + /* EOF */ + break; + } else if (err == GNUTLS_E_SHORT_MEMORY_BUFFER) { + /** + * The oids we are concerned with have length less than + * MAX_OID + */ + debug(err, "gnutls_x509_crt_get_key_purpose_oid"); + continue; + } else if (err < 0) { + debug(err, "gnutls_x509_crt_get_key_purpose_oid"); + return -1; + } + + if (strcmp(oid, GNUTLS_KP_ANY) == 0 || + strcmp(oid, GNUTLS_KP_TLS_WWW_CLIENT) == 0 || + strcmp(oid, GNUTLS_KP_MS_SMART_CARD_LOGON) == 0) + return 1; + } + + /** + * The certificate does not specify extendedKeyUsage; try + * keyUsage. + */ + if (kp == 0) { + /** + * keyUsage of digitalSignature, nonRepudiation, or + * both satisfy authenticatio.n + */ + err = gnutls_x509_crt_get_key_usage(crt, &usage, &critical); + if (err < 0 && err != GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE) + debug(err, "gnutls_x509_crt_get_key_usage"); + if (err < 0) + usage = 0; + + if (usage& + (GNUTLS_KEY_DIGITAL_SIGNATURE|GNUTLS_KEY_NON_REPUDIATION)) + return 1; + } + + /** + * extendedKeyUsage, keyUsage, or both are specified, but + * purposes are incompatible for authentication. + */ + if (kp > 0 || usage != 0) { + vpn_progress(vpninfo, PRG_INFO, + _("The certificate specifies key usages " + "incompatible with authentication.\n")); + return 0; + } + + /** + * Found neither keyUsage nor extendedKeyUsage, defaults to any + * purpose. + */ + vpn_progress(vpninfo, PRG_INFO, + _("Certificate doesn't specify key usage.\n")); + + return 1; +} + +static int export_cert(struct openconnect_info *vpninfo, + struct multicert_client_cert *cert, + struct credentialx *credx) +{ + gnutls_pkcs7_t pkcs7 = NULL; + gnutls_datum_t data = { 0 }; + struct oc_text_buf *buf; + size_t i, ncerts; + int err; + + assert_return(cert, GNUTLS_E_INVALID_REQUEST); + assert_return(credx, GNUTLS_E_INVALID_REQUEST); + + err = gnutls_pkcs7_init(&pkcs7); + if (err < 0) + return err; + + for (i = 0, ncerts = credx->ncerts; i < ncerts; i++) { + err = gnutls_pkcs7_set_crt(pkcs7, credx->certs[i]); + if (err < 0) + goto out; + } + + err = gnutls_pkcs7_export2(pkcs7, GNUTLS_X509_FMT_DER, &data); + if (err < 0) + goto out; + + err = to_text_buf(&buf, &data); + if (err < 0) + goto out; + + cert->format = MULTICERT_CERT_FORMAT_PKCS7; + cert->data = buf; + + err = 0; + +out: + gnutls_free(data.data); + gnutls_pkcs7_deinit(pkcs7); + return err; +} + +static int do_sign_challenge(struct oc_text_buf **signature, + gnutls_privkey_t key, + gnutls_pk_algorithm_t pk, + gnutls_digest_algorithm_t hash, + const gnutls_datum_t *data) +{ + gnutls_sign_algorithm_t sign; + gnutls_datum_t tmp; + int err; + + *signature = NULL; + + sign = gnutls_pk_to_sign(pk, hash); + +#if GNUTLS_VERSION_NUMBER >= 0x030600 + err = gnutls_privkey_sign_data2(key, + sign, + /* flag */ 0, data, &tmp); +#else + err = gnutls_privkey_sign_data(key, + gnutls_sign_get_hash_algorithm(sign), + /* flag */ 0, data, &tmp); +#endif + if (err < 0) + return err; + + err = to_text_buf(signature, &tmp); + gnutls_free(tmp.data); + + return err; +} + +static int sign_challenge(struct openconnect_info *vpninfo, + struct multicert_client_signature *signature, + struct credentialx *credx, + unsigned int hashes, + const gnutls_datum_t *data) +{ + const struct { + multicert_signhash_algorithm_t id; + gnutls_digest_algorithm_t hash; + } algorithm_map[] = { + { MULTICERT_SIGNHASH_SHA512, GNUTLS_DIG_SHA512 }, + { MULTICERT_SIGNHASH_SHA384, GNUTLS_DIG_SHA384 }, + { MULTICERT_SIGNHASH_SHA256, GNUTLS_DIG_SHA256 }, + { MULTICERT_SIGNHASH_UNKNOWN, GNUTLS_DIG_UNKNOWN }, + }; + struct oc_text_buf *sign_data = NULL; + multicert_signhash_algorithm_t hash; + gnutls_pk_algorithm_t pk; + size_t i; + int err; + + assert_return(signature, GNUTLS_E_INVALID_REQUEST); + assert_return(credx, GNUTLS_E_INVALID_REQUEST); + assert_return(data, GNUTLS_E_INVALID_REQUEST); + + err = gnutls_x509_crt_get_pk_algorithm(credx->certs[0], NULL); + if (err < 0) + return err; + + pk = err; + + for (i = 0; + (hash = algorithm_map[i].id) != MULTICERT_SIGNHASH_UNKNOWN; + i++) { + if ((hashes & MULTICERT_SIGNHASH_FLAG(hash)) == 0) + continue; + + err = do_sign_challenge(&sign_data, credx->key, + pk, algorithm_map[i].hash, + data); + + debug(err, "do_sign_challenge"); + + if (err != GNUTLS_E_INVALID_REQUEST && + err != GNUTLS_E_CONSTRAINT_ERROR) + break; + } + + if (hash == MULTICERT_SIGNHASH_UNKNOWN) + err = GNUTLS_E_UNSUPPORTED_SIGNATURE_ALGORITHM; + + if (err < 0) + return err; + + signature->algorithm = hash; + signature->data = sign_data; + + return 0; +} + int multicert_compute_response(struct openconnect_info *vpninfo, - unsigned int digests, + unsigned int hashes, const unsigned char *chdata, size_t chdata_len, - struct multicert_client_cert *ccert, - struct multicert_client_signature *csignature) + struct multicert_client_cert *psigner, + struct multicert_client_signature *psignature) { - return -ENOSYS; + const gnutls_datum_t data = { (void *)chdata, chdata_len }; + struct credentialx *credx = NULL; + struct multicert_client_cert signer = { 0 }; + struct multicert_client_signature signature = { 0 }; + int ret, err; + + assert_return(hashes&(MULTICERT_SIGNHASH_MAX|(MULTICERT_SIGNHASH_FLAG(MULTICERT_SIGNHASH_MAX)-1)), + -EINVAL); + assert_return(chdata && chdata_len > 0, -EINVAL); + assert_return(psigner, -EINVAL); + assert_return(psignature, -EINVAL); + + ret = load_credentialx(vpninfo, &credx); + if (ret < 0) { + vpn_progress(vpninfo, PRG_ERR, + _("Error loading multiple-certificate credential. Aborting.\n")); + return ret; + } + + check_key_purpose(vpninfo, credx); + + err = export_cert(vpninfo, &signer, credx); + if (err < 0) { + vpn_progress(vpninfo, PRG_ERR, + _("Error exporting multiple-certificate signer's certificate chain: (%d) %s.\n"), + err, gnutls_strerror(err)); + goto cleanup; + } + + err = sign_challenge(vpninfo, &signature, credx, hashes, &data); + if (err < 0) { + vpn_progress(vpninfo, PRG_ERR, + _("Error signing multiple-certificate challenge: (%d) %s.\n"), + err, gnutls_strerror(err)); + goto cleanup; + } + + *psigner = signer; signer.data = NULL; + *psignature = signature; signature.data = NULL; + err = 0; + +cleanup: + buf_free(signer.data); buf_free(signature.data); + free_credentialx(vpninfo, credx); + return err_to_application_error(err); } -- GitLab From cead085c9a0a42eaf3f86bb938c9a63fd057cb80 Mon Sep 17 00:00:00 2001 From: Tom Carroll Date: Wed, 12 May 2021 00:40:30 -0700 Subject: [PATCH 7/7] Signing test Signed-off-by: Tom Carroll --- multicert.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/multicert.c b/multicert.c index b65cd9c0a..7bab8a31c 100644 --- a/multicert.c +++ b/multicert.c @@ -1,5 +1,7 @@ #include +TEST + #include "openconnect-internal.h" #define NELEM(array) (sizeof(array)/sizeof(array[0])) -- GitLab