From: Oliver Kurth Date: Fri, 15 Sep 2017 18:23:05 +0000 (-0700) Subject: Add xmlsec1 to open-vm-tools X-Git-Tag: stable-10.2.0~517 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=d1ec74708ae1ad1cfb84eada2e6eb70624637c6b;p=thirdparty%2Fopen-vm-tools.git Add xmlsec1 to open-vm-tools Add the new files to open vm tools and add a switch to turn it on. xml-seccurity-c remains the default for now. Also clean up the service Makefile, which was using the client lib (which also dragged in stuff it shouldn't have) for common code instead of building it in. --- diff --git a/open-vm-tools/configure.ac b/open-vm-tools/configure.ac index 201ff797e..26cc33b4d 100644 --- a/open-vm-tools/configure.ac +++ b/open-vm-tools/configure.ac @@ -433,8 +433,32 @@ fi AC_ARG_ENABLE([vgauth], [AS_HELP_STRING([--disable-vgauth], [do not build vgauth.])], - [], - [enable_vgauth=yes]) + [ + enable_vgauth="$enableval" + use_xmlsec1=no + ], + [ + enable_vgauth=yes + use_xmlsec1=no + ]) + +AC_ARG_ENABLE([xmlsec1], + [AS_HELP_STRING([--enable-xmlsec1], + [build vgauth with xmlsec1 instead of xml-security-c (off by default).])], + [use_xmlsec1="$enableval"], + [use_xmlsec1=no]) + +AC_ARG_ENABLE([xml-security-c], + [AS_HELP_STRING([--disable-xml-security-c], + [build vgauth with xml-security-c instead of xmlsec1 (on by default).])], + [ + if test "$enableval" = "yes"; then + use_xmlsec1="no"; + else + use_xmlsec1="yes" + fi + ], + []) if test "$enable_vgauth" = "yes" ; then AC_ARG_ENABLE([caf], @@ -471,6 +495,19 @@ AC_ARG_WITH([xerces], [enable_vgauth=no], [with_xerces=yes]) +AC_ARG_WITH([xmlsec1], + [AS_HELP_STRING([--without-xmlsec1], + [compiles without xmlsec1 support (disables vgauth).])], + [enable_vgauth=no], + [with_xmlsec1=yes]) + +AC_ARG_WITH([xml2], + [AS_HELP_STRING([--without-xml2], + [compiles without xml2 support (disables vgauth).])], + [enable_vgauth=no], + [with_xml2=yes]) + + # Make sure we are building with openssl 1.0.1 and above so that # we use only TLSv1_2. @@ -489,7 +526,29 @@ fi if test "$enable_vgauth" = "yes"; then CPPFLAGS="$CPPFLAGS -DUSE_VGAUTH" - AC_VMW_DEFAULT_FLAGS([XERCES]) +if test "$use_xmlsec1" = "yes"; then + AC_VMW_DEFAULT_FLAGS([XML2]) + AC_VMW_CHECK_LIB([xml2], + [XML2], + [], + [], + [], + [], + [], + [], + [AC_VMW_LIB_ERROR([XML2], [xml2])]) + + AC_VMW_DEFAULT_FLAGS([XMLSEC1]) + AC_VMW_CHECK_LIB([xmlsec1], + [XMLSEC1], + [], + [xmlsec1-config], + [], + [xmlsec/xmlsec.h], + [xmlSecCheckVersion], + [], + [AC_VMW_LIB_ERROR([XMLSEC1], [xmlsec1])]) +else AC_VMW_CHECK_LIB([xerces-c], [XERCES], [], @@ -510,6 +569,7 @@ if test "$enable_vgauth" = "yes"; then [], [], [AC_VMW_LIB_ERROR([XMLSECURITY], [xml-security-c])]) +fi else enable_caf=no fi @@ -1241,6 +1301,7 @@ AM_CONDITIONAL(USE_PRINTF_WRAPPERS, test "$bsdPrintfWrappers" = "yes") AM_CONDITIONAL(ENABLE_DEPLOYPKG, test "$enable_deploypkg" = "yes") AM_CONDITIONAL(ENABLE_GRABBITMQPROXY, test "$enable_grabbitmqproxy" = "yes") AM_CONDITIONAL(ENABLE_VGAUTH, test "$enable_vgauth" = "yes") +AM_CONDITIONAL(USE_XMLSEC1, test "$use_xmlsec1" = "yes") AM_CONDITIONAL(ENABLE_CAF, test "$enable_caf" = "yes") AM_CONDITIONAL(HAVE_VSOCK, test "$os" = "linux") AM_CONDITIONAL(HAVE_MKDTEMP, test "$have_mkdtemp" = "yes") diff --git a/open-vm-tools/vgauth/service/Makefile.am b/open-vm-tools/vgauth/service/Makefile.am index 7fedee1f6..8c51d9f2a 100644 --- a/open-vm-tools/vgauth/service/Makefile.am +++ b/open-vm-tools/vgauth/service/Makefile.am @@ -30,11 +30,23 @@ VGAuthService_SOURCES += ../serviceImpl/filePosix.c VGAuthService_SOURCES += ../serviceImpl/netPosix.c VGAuthService_SOURCES += ../serviceImpl/proto.c VGAuthService_SOURCES += ../serviceImpl/random.c +if USE_XMLSEC1 +VGAuthService_SOURCES += ../serviceImpl/saml-xmlsec1.c +else VGAuthService_SOURCES += ../serviceImpl/saml-xml-security-c.cpp +endif VGAuthService_SOURCES += ../serviceImpl/service.c VGAuthService_SOURCES += ../serviceImpl/ticket.c VGAuthService_SOURCES += ../serviceImpl/verify.c +VGAuthService_SOURCES += ../common/audit.c +VGAuthService_SOURCES += ../common/certverify.c +VGAuthService_SOURCES += ../common/i18n.c +VGAuthService_SOURCES += ../common/prefs.c +VGAuthService_SOURCES += ../common/usercheck.c +VGAuthService_SOURCES += ../common/VGAuthLog.c +VGAuthService_SOURCES += ../common/VGAuthUtil.c + VGAuthServicedir = /etc/vmware-tools/vgauth/schemas VGAuthService_SCRIPTS = VGAuthService_SCRIPTS += ../serviceImpl/schemas/datatypes.dtd @@ -49,26 +61,30 @@ VGAuthService_SCRIPTS += ../serviceImpl/schemas/xml.xsd VGAuthService_SCRIPTS += ../serviceImpl/schemas/catalog.xml VGAuthService_CPPFLAGS = -VGAuthService_CPPFLAGS += -DVMTOOLS_USE_GLIB VGAuthService_CPPFLAGS += @GLIB2_CPPFLAGS@ +if USE_XMLSEC1 +VGAuthService_CPPFLAGS += @XMLSEC1_CPPFLAGS@ +else VGAuthService_CPPFLAGS += @XERCES_CPPFLAGS@ VGAuthService_CPPFLAGS += @XMLSECURITY_CPPFLAGS@ +endif VGAuthService_CPPFLAGS += @SSL_CPPFLAGS@ -VGAuthService_CPPFLAGS += -I$(top_srcdir)/libvmtools VGAuthService_CPPFLAGS += -I$(top_srcdir)/vgauth/public VGAuthService_CPPFLAGS += -I$(top_srcdir)/vgauth/common VGAuthService_CPPFLAGS += -I$(top_srcdir)/vgauth/serviceImpl VGAuthService_LDADD = -VGAuthService_LDADD += @VMTOOLS_LIBS@ VGAuthService_LDADD += @GLIB2_LIBS@ VGAuthService_LDADD += @GTHREAD_LIBS@ -VGAuthService_LDADD += ../lib/libvgauth.la +if USE_XMLSEC1 +VGAuthService_LDADD += @XMLSEC1_LIBS@ +else VGAuthService_LDADD += @XERCES_LIBS@ VGAuthService_LDADD += @XMLSECURITY_LIBS@ -VGAuthService_LDADD += @SSL_LIBS@ VGAuthService_LDADD += -lxerces-c VGAuthService_LDADD += -lxml-security-c +endif +VGAuthService_LDADD += @SSL_LIBS@ VGAuthService_LDADD += -lssl VGAuthService_LDADD += -lcrypto diff --git a/open-vm-tools/vgauth/serviceImpl/saml-xmlsec1.c b/open-vm-tools/vgauth/serviceImpl/saml-xmlsec1.c new file mode 100644 index 000000000..e74ffff5c --- /dev/null +++ b/open-vm-tools/vgauth/serviceImpl/saml-xmlsec1.c @@ -0,0 +1,1554 @@ +/********************************************************* + * Copyright (C) 2016 VMware, Inc. All rights reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation version 2.1 and no later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the Lesser GNU General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + *********************************************************/ + +/** + * @file saml-xmlsec1.cpp + * + * Code for authenticating users based on SAML tokens. + */ + + +/* + * XXX TODO + * + * - Reference verify or confirm its done by sig check + * + * - refactor to share code with xml-security-c verifiier + * - refactor to share with test/sign code + * make things static and tweak names as necessary + * - testing + * - valgrind + * + */ + + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include "prefs.h" +#include "serviceInt.h" + +static int gClockSkewAdjustment = VGAUTH_PREF_DEFAULT_CLOCK_SKEW_SECS; +static xmlSchemaPtr gParsedSchemas = NULL; +static xmlSchemaValidCtxtPtr gSchemaValidateCtx = NULL; + +#define CATALOG_FILENAME "catalog.xml" +#define SAML_SCHEMA_FILENAME "saml-schema-assertion-2.0.xsd" + + +/* + ****************************************************************************** + * XmlErrorHandler -- */ /** + * + * Error handler for xml2. + * + * @param[in] ctx Context (unused). + * @param[in] msg The error message in printf format. + * @param[in] ... Any args for the msg. + * + * XXX This probably wants to live in shared code. + * + ****************************************************************************** + */ + +static void +XmlErrorHandler(void *ctx, + const char *msg, + ...) +{ + gchar msgStr[1024]; + va_list argPtr; + va_start(argPtr, msg); + vsnprintf(msgStr, sizeof msgStr, msg, argPtr); + va_end(argPtr); + + /* + * XXX are any of these either debug or fatal? treat all as + * warning for now. + */ + g_warning("XML Error: %s", msgStr); +} + + +/* + ****************************************************************************** + * XmlSecErrorHandler -- */ /** + * + * Error handler for xmlsec. + * + * @param[in] file The name of the file generating the error. + * @param[in] line The line number generating the error. + * @param[in] func The function generating the error. + * @param[in] errorObject The error specific object. + * @param[in] errorSubject The error specific subject. + * @param[in] reason The error code. + * @param[in] msg The additional error message. + * + * XXX This probably wants to live in shared code. + * + ****************************************************************************** + */ + +static void +XmlSecErrorHandler(const char *file, + int line, + const char *func, + const char *errorObject, + const char *errorSubject, + int reason, + const char *msg) +{ + /* + * XXX are any of these either debug or fatal? treat all as + * warning for now. + */ + g_warning("XMLSec Error: %s:%s(line %d) object %s" + " subject %s reason: %d, msg: %s", + file, func, line, + errorObject ? errorObject : "", + errorSubject ? errorSubject : "", + reason, msg); +} + + +/* + ****************************************************************************** + * LoadCatalogAndSchema -- */ /** + * + * Loads the schemas for validation. + * + * Using a catalog here ala xmllint. Another option would be an + * additional schema acting like a catalog. + * + * @param[in] catPath Path to the catalog file. + * @param[in] schemaPath Path to the SAML schema file. + * + * return TRUE on success + ****************************************************************************** + * + */ + +gboolean +LoadCatalogAndSchema(void) +{ + int ret; + gboolean retVal = FALSE; + xmlSchemaParserCtxtPtr ctx = NULL; + gchar *catalogPath = NULL; + gchar *schemaPath = NULL; + gchar *schemaDir = NULL; + + schemaDir = Pref_GetString(gPrefs, + VGAUTH_PREF_SAML_SCHEMA_DIR, + VGAUTH_PREF_GROUP_NAME_SERVICE, + NULL); + + if (NULL == schemaDir) { +#ifdef _WIN32 + /* + * To make life easier for the Windows installer, assume + * the schema directory is next to the executable. Also + * check in ../ in case we're in a dev environment. + */ + schemaDir = g_build_filename(gInstallDir, "schemas", NULL); + if (!(g_file_test(dir, G_FILE_TEST_EXISTS) && + g_file_test(dir, G_FILE_TEST_IS_DIR))) { + + gchar *newDir = g_build_filename(gInstallDir, "..", "schemas", NULL); + + Debug("%s: schemas not found in Windows install loc '%s'," + " trying dev location of '%s'\n", __FUNCTION__, schemaDir, newDir); + + g_free(schemaDir); + schemaDir = newDir; + } +#else + /* + * XXX -- clean this up to make a better default for Linux. + */ + schemaDir = g_build_filename(gInstallDir, "..", "schemas", NULL); +#endif + } + Log("%s: Using '%s' for SAML schemas\n", __FUNCTION__, schemaDir); + catalogPath = g_build_filename(schemaDir, CATALOG_FILENAME, NULL); + schemaPath = g_build_filename(schemaDir, SAML_SCHEMA_FILENAME, NULL); + + xmlInitializeCatalog(); + + /* + * xmlLoadCatalog() just adds to the default catalog, and won't return an + * error if it doesn't exist so long as a default catalog is set. + * + * So sanity check its existence. + */ + if (!g_file_test(catalogPath, G_FILE_TEST_EXISTS)) { + g_warning("Error: catalog file not found at \"%s\"\n", catalogPath); + retVal = FALSE; + goto done; + } + ret = xmlLoadCatalog(catalogPath); + if (ret < 0) { + g_warning("Error: Failed to load catalog at \"%s\"\n", catalogPath); + retVal = FALSE; + goto done; + } + + ctx = xmlSchemaNewParserCtxt(schemaPath); + if (NULL == ctx) { + g_warning("Failed to create schema parser context\n"); + retVal = FALSE; + goto done; + } + + xmlSchemaSetParserErrors(ctx, + (xmlSchemaValidityErrorFunc) XmlErrorHandler, + (xmlSchemaValidityErrorFunc) XmlErrorHandler, + NULL); + gParsedSchemas = xmlSchemaParse(ctx); + if (NULL == gParsedSchemas) { + /* + * This shouldn't happen. Means somebody mucked with our + * schemas. + */ + g_warning("Error: Failed to parse schemas\n"); + retVal = FALSE; + goto done; + } + + /* + * Set up the validaton context for later use. + */ + gSchemaValidateCtx = xmlSchemaNewValidCtxt(gParsedSchemas); + if (NULL == gSchemaValidateCtx) { + g_warning("Failed to create schema validation context\n"); + retVal = FALSE; + goto done; + } + xmlSchemaSetValidErrors(gSchemaValidateCtx, + XmlErrorHandler, + XmlErrorHandler, + NULL); + + retVal = TRUE; +done: + if (NULL != ctx) { + xmlSchemaFreeParserCtxt(ctx); + } + g_free(catalogPath); + g_free(schemaPath); + g_free(schemaDir); + + return retVal; +} + + +/* + ****************************************************************************** + * FreeSchemas -- */ /** + * + * Frees global schema data. + ****************************************************************************** + * + */ + +void +FreeSchemas(void) +{ + if (NULL != gSchemaValidateCtx) { + xmlSchemaFreeValidCtxt(gSchemaValidateCtx); + gSchemaValidateCtx = NULL; + } + if (NULL != gParsedSchemas) { + xmlSchemaFree(gParsedSchemas); + gParsedSchemas = NULL; + } +} + + +/* + ****************************************************************************** + * LoadPrefs -- */ /** + * + * Loads any preferences SAML cares about. + ****************************************************************************** + * + */ + +static void +LoadPrefs(void) +{ + gClockSkewAdjustment = Pref_GetInt(gPrefs, VGAUTH_PREF_CLOCK_SKEW_SECS, + VGAUTH_PREF_GROUP_NAME_SERVICE, + VGAUTH_PREF_DEFAULT_CLOCK_SKEW_SECS); + Log("%s: Allowing %d of clock skew for SAML date validation\n", + __FUNCTION__, gClockSkewAdjustment); +} + + +/* + ****************************************************************************** + * SAML_Init -- */ /** + * + * Performs any initialization needed for SAML processing. + * + * @return VGAUTH_E_OK on success, VGAuthError on failure + * + ****************************************************************************** + */ + +VGAuthError +SAML_Init(void) +{ + int ret; + + /* + * Init the xml parser + */ + xmlInitParser(); + + /* + * Verify the xml2 version -- if this is too old + * its fatal, so we may want to use a different check. + */ + LIBXML_TEST_VERSION + + /* + * Tell libxml to do ID/REF lookups + * Tell libxml to complete attributes with defaults from the DTDs + */ + xmlLoadExtDtdDefaultValue = XML_DETECT_IDS | XML_COMPLETE_ATTRS; + xmlSubstituteEntitiesDefault(1); + + + /* set up the xml2 error handler */ + xmlSetGenericErrorFunc(NULL, XmlErrorHandler); + + /* + * Load schemas + */ + if (!LoadCatalogAndSchema()) { + g_warning("Failed to load schemas\n"); + return VGAUTH_E_FAIL; + } + + /* init xmlsec */ + ret = xmlSecInit(); + if (ret < 0) { + g_warning("xmlSecInit() failed %d\n", ret); + return VGAUTH_E_FAIL; + } + + /* + * version check xmlsec1 + */ + // XXX may want to make this non-fatal + if (xmlSecCheckVersion() != 1) { + g_warning("Error: xmlsec1 lib version mismatch\n"); + return VGAUTH_E_FAIL; + } + + /* + * init the xmlsec1 crypto app layer + */ + ret = xmlSecCryptoAppInit(NULL); + if (ret < 0) { + g_warning("xmlSecCryptoAppInit() failed %d\n", ret); + return VGAUTH_E_FAIL; + } + + /* + * Do crypto-engine specific initialization + */ + ret = xmlSecCryptoInit(); + if (ret < 0) { + g_warning("xmlSecCryptoInit() failed %d\n", ret); + return VGAUTH_E_FAIL; + } + + /* + * set up the error callback + */ + xmlSecErrorsSetCallback(XmlSecErrorHandler); + + /* + * Load prefs + */ + LoadPrefs(); + + return VGAUTH_E_OK; +} + + +/* + ****************************************************************************** + * SAML_Shutdown -- */ /** + * + * Performs any clean-up of resources allocated by SAML code. + * + ****************************************************************************** + */ + +void +SAML_Shutdown() +{ + FreeSchemas(); + xmlSecCryptoShutdown(); + xmlSecCryptoAppShutdown(); + xmlSecShutdown(); + +#if 0 + /* + * This is not thread safe: + * http://0pointer.de/blog/projects/beware-of-xmlCleanupParser + * and should only be called just before exit() + * Because of this, our symbol-checker hates it: See PR 407137 + */ + xmlCleanupParser(); +#endif +} + + +/* + ****************************************************************************** + * SAML_Reload -- */ /** + * + * Reload any in-memory state used by the SAML module. + * + ****************************************************************************** + */ + +void +SAML_Reload() +{ + FreeSchemas(); + LoadPrefs(); + LoadCatalogAndSchema(); +} + + +/* + ****************************************************************************** + * FreeCertArray -- */ /** + * + * Frees a simple array of pemCert. + * + * @param[in] num Number of certs in array. + * @param[in] certs Array of certs to free. + * + ****************************************************************************** + */ +static void +FreeCertArray(int num, + gchar **certs) +{ + int i; + + for (i = 0; i < num; i++) { + g_free(certs[i]); + } + g_free(certs); +} + + +/* + ****************************************************************************** + * FindAttrValue -- */ /** + * + * Returns the value of a attribute in an XML node. + * + * @param[in] node XML subtree node. + * @param[in] attrName Name of the attribute. + * + * @return Attribute value if exists. The caller must free this with xmlFree(). + * + ****************************************************************************** + */ + +static xmlChar * +FindAttrValue(const xmlNodePtr node, + const gchar *attrName) +{ + xmlAttrPtr attr; + xmlChar *name; + + /* + * Find the attribute + */ + attr = xmlHasProp(node, attrName); + if ((attr == NULL) || (attr->children == NULL)) { + return NULL; + } + + /* + * get the attribute value + */ + name = xmlNodeListGetString(node->doc, attr->children, 1); + + return name; +} + + +/* + ****************************************************************************** + * RegisterID -- */ /** + * + * Register the document ID with the xml parser. + * + * This needs to be done if the document ID doesn't use the standard. + * Otherwise the signing fails when setting up the reference. + * SAML likes using 'ID' intead of the default 'xml:id', so + * this is needed for both signing and verification. + * + * This is a no-op if the schemas have been loaded since they + * set it up. + * + * See xmlsec1 FAQ 3.2 + * + * Based on https://www.aleksey.com/pipermail/xmlsec/2003/001768.html + * + * @param[in] node The XML node on which to set the ID. + * @param[in] idName The name of the ID. + * + * @return TRUE on success. + ****************************************************************************** + */ + +gboolean +RegisterID(xmlNodePtr node, + const xmlChar *idName) +{ + xmlAttrPtr attr; + xmlAttrPtr tmp; + xmlChar *name; + + /* + * find pointer to id attribute + */ + attr = xmlHasProp(node, idName); + if ((attr == NULL) || (attr->children == NULL)) { + return FALSE; + } + + /* + * get the attribute (id) value + */ + name = xmlNodeListGetString(node->doc, attr->children, 1); + if (name == NULL) { + return FALSE; + } + + /* + * check that we don't have the id already registered + */ + tmp = xmlGetID(node->doc, name); + if (tmp != NULL) { + xmlFree(name); + /* no-op if its already there */ + return TRUE; + } + + /* + * finally register id + */ + xmlAddID(NULL, node->doc, name, attr); + + xmlFree(name); + return TRUE; +} + + +/* + ****************************************************************************** + * FindNodeByName -- */ /** + * + * Searches under the specified node for one with a matching name. + * + * @param[in] root XML subtree root under which to search. + * @param[in] nodeName Name of node to find. + * + * @return matching xmlNodePtr or NULL. Caller should not free this node. + * + ****************************************************************************** + */ + +static xmlNodePtr +FindNodeByName(xmlNodePtr root, + char *nodeName) +{ + xmlNodePtr cur; + + cur = root->children; + while (cur != NULL) { + if (cur->type == XML_ELEMENT_NODE) { + if (xmlStrEqual(nodeName, cur->name)) { + break; + } + } + cur = cur->next; + } + + return cur; +} + + +/* + ****************************************************************************** + * FindAllNodesByName -- */ /** + * + * Searches under the specified node for all with a matching name. + * + * @param[in] root XML subtree root under which to search. + * @param[in] nodeName Name of node to find. + * @param[out] nodeName Array of matches. + * + * @return Number of matching nodes. Caller needs to free the array + * of Nodes, but not the nodes themselves. + * + ****************************************************************************** + */ + +static int +FindAllNodesByName(xmlNodePtr root, + char *nodeName, + xmlNodePtr **nodes) +{ + xmlNodePtr cur; + xmlNodePtr *list = NULL; + int count = 0; + + cur = root->children; + while (cur != NULL) { + if (cur->type == XML_ELEMENT_NODE) { + if (xmlStrEqual(nodeName, cur->name)) { + list = g_realloc_n(list, + sizeof(xmlNodePtr), + count + 1); + list[count++] = cur; + } + } + cur = cur->next; + } + *nodes = list; + + return count; +} + + +/* + ****************************************************************************** + * ValidateDoc -- */ /** + * + * Validates the XML document against the schema. + * + * @param[in] doc Parsed XML document. + * + ****************************************************************************** + */ + +gboolean +ValidateDoc(xmlDocPtr doc) +{ + int ret; + + ret = xmlSchemaValidateDoc(gSchemaValidateCtx, doc); + if (ret < 0) { + g_warning("Failed to validate doc against schema\n"); + } + + return (ret == 0) ? TRUE : FALSE; +} + + +/* + ****************************************************************************** + * CheckTimeAttr -- */ /** + * + * Checks that the given attribute with the given name is a timestamp and + * compares it against the current time. + * + * @param[in] node The node containing the attribute. + * @param[in] attrName The name of the attribute. + * @param[in] notBefore Whether the condition given by the attribute + * should be in the past or 'now' (TRUE). + * + ****************************************************************************** + */ + +gboolean +CheckTimeAttr(const xmlNodePtr node, + const gchar *attrName, + gboolean notBefore) +{ + xmlChar *timeAttr; + GTimeVal attrTime; + GTimeVal now; + glong diff; + gboolean retVal; + + timeAttr = FindAttrValue(node, attrName); + if ((NULL == timeAttr) || (0 == *timeAttr)) { + /* + * The presence of all time restrictions in SAML are optional, so if + * the attribute is not present, that is fine. + */ + retVal = TRUE; + goto done; + } + + if (!g_time_val_from_iso8601(timeAttr, &attrTime)) { + g_warning("%s: Could not parse %s value (%s).\n", __FUNCTION__, attrName, + timeAttr); + retVal = FALSE; + goto done; + } + + g_get_current_time(&now); + + /* + * Check the difference, doing the math so that a positive + * value is bad. Ignore the micros field since precision + * is unnecessary here because we see unsynced clocks in + * the real world. + */ + if (notBefore) { + // expect time <= now + diff = attrTime.tv_sec - now.tv_sec; + } else { + // expect now <= time + diff = now.tv_sec - attrTime.tv_sec; + } + + /* + * A negative value is fine, a postive value + * greater than the clock skew range is bad. + */ + if (diff > gClockSkewAdjustment) { + g_debug("%s: FAILED SAML assertion (timeStamp %s, delta %d) %s.\n", + __FUNCTION__, timeAttr, (int) diff, + notBefore ? "is not yet valid" : "has expired"); + retVal = FALSE; + goto done; + } + + retVal = TRUE; + +done: + if (timeAttr) { + xmlFree(timeAttr); + } + return retVal; +} + + +/* + ****************************************************************************** + * CheckAudience -- */ /** + * + * Checks whether the given audience URI refers to this machine. + * + * @param[in] audience An audience URI that a token is targetted for. + * + * @return TRUE if the audience URI refers to this machine, FALSE otherwise. + * + ****************************************************************************** + */ + +gboolean +CheckAudience(const xmlChar *audience) +{ + gboolean ret; + + /* + * Our SSO server doesn't set Recipient, so this only gets used by test code + * whch uses a simple hostname check. + * + * Something like a VC UUID might be more accurate in a virtual + * machine. + */ + + ret = strstr(audience, g_get_host_name()) != NULL; + g_debug("%s: audience check: token: '%s', host: '%s' ? %d\n", + __FUNCTION__, + audience, g_get_host_name(), ret); + return ret; +} + + +/* + ****************************************************************************** + * VerifySubject -- */ /** + * + * Extracts the name of the subject and enforces any conditions in + * SubjectConfirmation elements. + * Subjects are described in section 2.4 of the SAML Core specification. + * + * Example Subject XML: + * + * + * scott@example.org + * + * + * + * + * + * + * + * @param[in] doc The parsed SAML token. + * @param[out] subjectRet The Subject NameId. Should be g_free()d by + * caller. + * + * @return TRUE if the conditions in at least one SubjectConfirmation is met, + * FALSE otherwise. + * + ****************************************************************************** + */ + +gboolean +VerifySubject(xmlDocPtr doc, + gchar **subjectRet) +{ + xmlNodePtr subjNode; + xmlNodePtr nameIDNode; + xmlNodePtr child; + gchar *subjectVal = NULL; + gboolean retCode = FALSE; + gboolean validSubjectFound = FALSE; + xmlChar *tmp; + + if (NULL != subjectRet) { + *subjectRet = NULL; + } + + subjNode = FindNodeByName(xmlDocGetRootElement(doc), "Subject"); + if (NULL == subjNode) { + g_warning("No Subject node found\n"); + goto done; + } + + /* + * Pull out the NameID for later checks elsewhere. + */ + nameIDNode = FindNodeByName(subjNode, "NameID"); + if (NULL == nameIDNode) { + g_warning("%s: NameID not found in Subject\n", __FUNCTION__); + goto done; + } + tmp = xmlNodeGetContent(nameIDNode); + subjectVal = g_strdup(tmp); + xmlFree(tmp); + + /* + * Find all the SubjectConfirmation nodes and see if at least one + * can be validated. + */ + // XXX cloning xml-security-c here by using a loop. Might + // be cleaner to use FindAllNodeByName() + for (child = subjNode->children; child != NULL; child = child->next) { + xmlChar *method; + xmlNodePtr subjConfirmData; + + if (child->type == XML_ELEMENT_NODE) { + if (!xmlStrEqual(child->name, "SubjectConfirmation")) { + continue; + } + method = FindAttrValue(child, "Method"); + if ((NULL == method) || (0 == *method)) { + // should not happen since this is required + g_warning("%s: Missing SubjectConfirmation method\n", __FUNCTION__); + xmlFree(method); + goto done; + } + if (!xmlStrEqual(method, "urn:oasis:names:tc:SAML:2.0:cm:bearer")) { + g_warning("%s: method %s not bearer\n", __FUNCTION__, method); + xmlFree(method); + continue; + } + xmlFree(method); + + subjConfirmData = FindNodeByName(child, "SubjectConfirmationData"); + if (NULL != subjConfirmData) { + xmlChar *recipient; + + if (!CheckTimeAttr(subjConfirmData, "NotBefore", TRUE) || + !CheckTimeAttr(subjConfirmData, "NotOnOrAfter", FALSE)) { + g_warning("%s: subjConfirmData time check failed\n", + __FUNCTION__); + continue; + } + + /* + * Recipient isn't always there. + */ + recipient = FindAttrValue(subjConfirmData, "Recipient"); + if ((NULL != recipient) && (0 != *recipient) && + !CheckAudience(recipient)) { + g_debug("%s: failed recipient check\n", __FUNCTION__); + xmlFree(recipient); + continue; + } + xmlFree(recipient); + } + + /* + * passed all the checks, we have a match so kick out + */ + validSubjectFound = TRUE; + break; + } + } + + if (validSubjectFound && (NULL != subjectRet)) { + *subjectRet = subjectVal; + } else { + g_free(subjectVal); + } + retCode = validSubjectFound; +done: + return retCode; +} + + +/* + ****************************************************************************** + * VerifyConditions -- */ /** + * + * Enforces conditions specified by the "saml:Conditions" element + * under the root element. + * Conditions are described in section 2.5 of the SAML Core specification. + * + * Example Conditions XML: + * + * + * https://sp.example.com/SAML2 + * + * + * + * @param[in] doc The parsed SAML token. + * + * @return TRUE if the conditions are met; FALSE otherwise. + * + ****************************************************************************** + */ + +gboolean +VerifyConditions(xmlDocPtr doc) +{ + xmlNodePtr condNode; + + /* + * There should be at most one Conditions element and the schema checking + * done by the parser should enforce that. + */ + condNode = FindNodeByName(xmlDocGetRootElement(doc), "Conditions"); + if (NULL == condNode) { + // Conditions are optional. + g_debug("%s: No Conditions found, accepting\n", __FUNCTION__); + return TRUE; + } + + if (!CheckTimeAttr(condNode, "NotBefore", TRUE) || + !CheckTimeAttr(condNode, "NotOnOrAfter", FALSE)) { + g_warning("%s: Time Conditions failed!\n", __FUNCTION__); + return FALSE; + } + + /* + * is a generic element, intended as an extension point. + * We don't know about any. According to the general processng rules, if + * we find a condition we don't know about, the result of the validation + * is "indeterminate" and we should reject the assertion. + */ + if (FindNodeByName(condNode, "Condition") != NULL) { + g_warning("%s: Unrecognized condition found!\n", __FUNCTION__); + return FALSE; + } + + /* + * defines a set a URIs that describe what + * audience the assertioned is addressed to or intended for. + * But it's very generic. From the spec (section 2.5.1.4): + * A URI reference that identifies an intended audience. The URI + * reference MAY identify a document that describes the terms and + * conditions of audience membership. It MAY also contain the unique + * identifier URI from a SAML name identifier that describes a system + * entity. + * + * Our SSO server doesn't set it, so no point in checking it. + */ + +#if 0 + // XXX nothing looks at this + /* + * element is specified to disallow caching. We don't + * cache, so it doesn't affect our validation. + * However, we need to communicate it to clients so they do not cache. + */ + oneTimeUse = (FindChildByName(condNode, "OneTimeUse") + != NULL); +#endif + + /* + * only applies if a service wants to make their own + * assertions based on a SAML assertion. That should not apply here. + */ + + return TRUE; +} + + +/* + ****************************************************************************** + * VerifyReference -- */ /** + * + * Verifies that the Reference in an XML signtaure is valid. + * + * XXX This is done in the xml-security-c version, but doesn't seem + * to be necessary in xmlsec1, since it appears to do it itself. + * xmlsec1 also puts the interesting data off in a semi-hidden list, + * suggesting its not useful to callers. + * + * @param[in] doc Parsed XML document. + * @param[in] dsigCtx Digitial signature context. + * + * @return TRUE on sucecss. + * + ****************************************************************************** + */ + +static gboolean +VerifyReference(xmlDocPtr doc, + xmlSecDSigCtxPtr dsigCtx) +{ + xmlNodePtr root; + xmlChar *idAttr; + gboolean retCode = FALSE; + + root = xmlDocGetRootElement(doc); + idAttr = xmlGetProp(root, "ID"); + + if (NULL == idAttr) { + g_warning("No ID attribute found\n"); + goto done; + } + + // XXX if this needs to be done, then the place to look is + // the signedInfoReferences in the dsigCtx. + // this appears to be filled in after the xmlSecDSigCtxVerify() + // (unlike in xml-security-c) + + retCode = TRUE; +done: + if (NULL != idAttr) { + xmlFree(idAttr); + } + return retCode; +} + + +/* + ****************************************************************************** + * BuildCertChain -- */ /** + * + * Pulls the certs out of the parsed SAML token, adds them to the + * key manager, and returns them as a list. + * + * @param[in] x509Node x509 data node. + * @param[in] mgr KeyManager + * @param[out] numCerts Number of certs being returned. + * @param[out] certChain Array containing the certs in OpenSSL PEM + * format. Array and contents must be g_free()d + * by caller. + * + * @return TRUE on success. + * + ****************************************************************************** + */ + +static gboolean +BuildCertChain(xmlNodePtr x509Node, + xmlSecKeysMngrPtr mgr, + int *numCerts, + gchar ***certChain) +{ + gboolean bRet = FALSE; + xmlNodePtr *x509CertNodes = NULL; + int num; + int i; + int ret; + gchar **certList = NULL; + + num = FindAllNodesByName(x509Node, + (char *) xmlSecNodeX509Certificate, + &x509CertNodes); + if (num == 0) { + g_warning("Missing x509 certificate node(s)\n"); + goto done; + } + + certList = g_malloc0_n(num + 1, sizeof(gchar *)); + + for (i = 0; i < num; i++) { + gchar *pemCert = NULL; + xmlChar *base64Cert; + + base64Cert = xmlNodeGetContent(x509CertNodes[i]); + if (NULL == base64Cert) { + g_warning("Missing x509 certificate base64 data\n"); + goto done; + } + + /* + * Turn the raw base64 into PEM. Thanks for being so anal, + * OpenSSL. + */ + + // XXX make this use CertVerify_EncodePEMForSSL() instead, + // since it handles corner cases (the newline after the + // base64 may exist). But that means changes to more files, + // so do this later + pemCert = g_strdup_printf("-----BEGIN CERTIFICATE-----\n" + "%s" + "\n-----END CERTIFICATE-----\n", + base64Cert); + + xmlFree(base64Cert); + + /* + * Add cert to the keymanager. + */ + ret = xmlSecCryptoAppKeysMngrCertLoadMemory(mgr, + pemCert, + strlen(pemCert), + xmlSecKeyDataFormatPem, + xmlSecKeyDataTypeTrusted); + if (ret < 0) { + g_warning("Failed to add cert to key manager\n"); + goto done; + } + + // add pemCert to the returned list + certList[i] = pemCert; + } + + bRet = TRUE; + *numCerts = num; + *certChain = certList; + +done: + if (!bRet) { + FreeCertArray(num, certList); + } + g_free(x509CertNodes); + + return bRet; +} + + +/* + ****************************************************************************** + * VerifySignature -- */ /** + * + * Verifies the signature on an XML document. + * + * @param[in] doc Parsed XML document. + * @param[out] numCerts Number of certs in the token. + * @param[out] certChain Certs in the token. Caller should g_free() array and + * contents. + * + * @return TRUE on success. + * + ****************************************************************************** + */ + +gboolean +VerifySignature(xmlDocPtr doc, + int *numCerts, + gchar ***certChain) +{ + xmlNodePtr dsigNode; + xmlNodePtr keyInfoNode; + xmlNodePtr x509Node; + xmlSecDSigCtxPtr dsigCtx = NULL; + xmlSecKeysMngrPtr mgr = NULL; + int ret; + int num = 0; + gchar **certList = NULL; + gboolean bRet; + gboolean retCode = FALSE; + + *numCerts = 0; + *certChain = NULL; + + /* + * First pull out the signature to get to the x509 cert. + */ + dsigNode = xmlSecFindNode(xmlDocGetRootElement(doc), + xmlSecNodeSignature, xmlSecDSigNs); + if (NULL == dsigNode) { + g_warning("Missing signature node\n"); + goto done; + } + + keyInfoNode = xmlSecFindNode(dsigNode, xmlSecNodeKeyInfo, + xmlSecDSigNs); + if (NULL == keyInfoNode) { + g_warning("Missing KeyInfo node\n"); + goto done; + } + + x509Node = xmlSecFindNode(keyInfoNode, xmlSecNodeX509Data, + xmlSecDSigNs); + if (NULL == x509Node) { + g_warning("Missing x509 node\n"); + goto done; + } + + /* + * Make a key manager to hold the certs. + */ + mgr = xmlSecKeysMngrCreate(); + if (mgr == NULL) { + g_warning("Failed to create key manager"); + goto done; + } + + ret = xmlSecCryptoAppDefaultKeysMngrInit(mgr); + if (ret < 0) { + g_warning("Failed to init key manager\n"); + goto done; + } + + + /* + * Get the cert chain from the token. + * + * Unlike xml-security-c, xmlsec1 wants to validate the cert + * chain in the token so it needs the full chain, not just + * the public key from the first cert. + * + * Also save it off for later use by the alias store check. + */ + bRet = BuildCertChain(x509Node, mgr, &num, &certList); + if (FALSE == bRet) { + g_warning("Failed to add cert to key manager\n"); + goto done; + } + + /* + * Create a signature context with the key manager + */ + dsigCtx = xmlSecDSigCtxCreate(mgr); + if (NULL == dsigCtx) { + g_warning("Missing signature node\n"); + goto done; + } + + /* + * The vgauth service code expects the id to be "ID". xmlSec + * won't handle the URI ref in the signature unless we + * + * a) use 'xml:id' (the default) instead of "ID" + * or + * b) register the ID + * + * We can't control what the SSO server does, so its "b". + */ + bRet = RegisterID(xmlDocGetRootElement(doc), "ID"); + if (bRet == FALSE) { + g_warning("failed to register ID\n"); + goto done; + } + + /* + * Verify signature. This just returns if the signature code worked + * or not, not if the signature is correct. + */ + ret = xmlSecDSigCtxVerify(dsigCtx, dsigNode); + if (ret < 0) { + g_warning("Signature verify failed\n"); + goto done; + } + + // verify the Reference + // + // XXX these don't seem to exist until after the Verify. + // The one we want is in the list of dsigCtx->signedInfoReferences + // + // XXX is this even necessary, or has it already been handled + // by the Verify? + // + // the xml-security-c version does this before the verify, + // but the data needed isn't accessible in xmlsec1 until after + // the verify. + // + bRet = VerifyReference(doc, dsigCtx); + if (FALSE == bRet) { + g_warning("Failed to verify Reference\n"); + goto done; + } + + /* + * Check status to verify the signature is correct. + * + */ + if (dsigCtx->status != xmlSecDSigStatusSucceeded) { + g_warning("Signature is INVALID\n"); + goto done; + } + + retCode = TRUE; + *numCerts = num; + *certChain = certList; +done: + if (!retCode) { + FreeCertArray(num, certList); + } + if (dsigCtx) { + xmlSecDSigCtxDestroy(dsigCtx); + } + if (mgr) { + xmlSecKeysMngrDestroy(mgr); + } + + return retCode; +} + + +/* + ****************************************************************************** + * VerifySAMLToken -- */ /** + * + * Verifies a XML text as a SAML token. + * Parses the XML, then verifies Subject, Conditions and Signature. + * + * @param[in] token Text of SAML token. + * @param[out] subject Subject of SAML token, Caller must g_free(). + * @param[out] numCerts Number of certs in the token. + * @param[out] certChain Certs in the token. Caller should g_free() array and + * contents. + * + * @return matching TRUE on success. + * + ****************************************************************************** + */ + +static gboolean +VerifySAMLToken(const gchar *token, + gchar **subject, + int *numCerts, + gchar ***certChain) +{ + xmlDocPtr doc = NULL; + int retCode = FALSE; + gboolean bRet; + /* + * If we want to set extra options, use this path. + */ +#if PARSE_WITH_OPTIONS + xmlParserCtxtPtr parseCtx = NULL; + + parseCtx = xmlCreateDocParserCtxt(token); + + /* + * Don't allow extra stuff to be pulled off the net. + * The schema validation should prevent this from getting + * through, but it might still be nice to prevent network issues + * from slowing things down. + */ + xmlCtxtUseOptions(parseCtx, XML_PARSE_NONET); + doc = xmlCtxtReadMemory(parseCtx, + token, + strlen(token), + NULL, NULL, 0); +#else + doc = xmlParseMemory(token, strlen(token)); +#endif + if ((NULL == doc) || (xmlDocGetRootElement(doc) == NULL)) { + g_warning("Failed to parse document\n"); + goto done; + } + + bRet = ValidateDoc(doc); + if (FALSE == bRet) { + g_warning("Failed to validate token against schema\n"); + goto done; + } + + bRet = VerifySubject(doc, subject); + if (FALSE == bRet) { + g_warning("Failed to verify Subject node\n"); + goto done; + } + + bRet = VerifyConditions(doc); + if (FALSE == bRet) { + g_warning("Failed to verify Conditions\n"); + goto done; + } + + bRet = VerifySignature(doc, numCerts, certChain); + if (FALSE == bRet) { + g_warning("Failed to verify Signature\n"); + goto done; + } + + retCode = TRUE; +done: +#if PARSE_WITH_OPTIONS + if (NULL != parseCtx) { + xmlFreeParserCtxt(parseCtx); + } +#endif + if (!retCode && (NULL != subject)) { + g_free(*subject); + *subject = NULL; + } + if (doc) { + xmlFreeDoc(doc); + } + + return retCode; +} + + +/* + ****************************************************************************** + * SAML_VerifyBearerToken -- */ /** + * + * Determines whether the SAML bearer token can be used to authenticate. + * A token consists of a single SAML assertion. + * + * This is currently only used from the test code. + * + * @param[in] xmlText The text of the SAML assertion. + * @param[in] userName Optional username to authenticate as. + * @param[out] userNameOut The user that the token has authenticated as. + * @param[out] subjNameOut The subject in the token. Caller must g_free(). + * @param[out] verifyAi The alias info associated with the entry + * in the alias store used to verify the + * SAML cert. + * + * @return VGAUTH_E_OK on success, VGAuthError on failure + * + ****************************************************************************** + */ + +VGAuthError +SAML_VerifyBearerToken(const char *xmlText, + const char *userName, // UNUSED + char **userNameOut, // UNUSED + char **subjNameOut, + ServiceAliasInfo **verifyAi) // UNUSED +{ + gboolean ret; + gchar **certChain = NULL; + int num = 0; + + ret = VerifySAMLToken(xmlText, + subjNameOut, + &num, + &certChain); + + // clean up -- this code doesn't look at the chain + FreeCertArray(num, certChain); + + // XXX errors may need some work + return (ret == TRUE) ? VGAUTH_E_OK : VGAUTH_E_AUTHENTICATION_DENIED; +} + + +/* + ****************************************************************************** + * SAML_VerifyBearerTokenAndChain -- */ /** + * + * Determines whether the SAML bearer token can be used to authenticate. + * A token consists of a single SAML assertion. + * The token must first be verified, then the certificate chain used + * verify it must be checked against the appropriate certificate store. + * + * @param[in] xmlText The text of the SAML assertion. + * @param[in] userName Optional username to authenticate as. + * @param[out] userNameOut The user that the token has authenticated as. + * @param[out] subjNameOut The subject in the token. Caller must g_free(). + * @param[out] verifyAi The alias info associated with the entry + * in the alias store used to verify the + * SAML cert. + * + * @return VGAUTH_E_OK on success, VGAuthError on failure + * + ****************************************************************************** + */ + +VGAuthError +SAML_VerifyBearerTokenAndChain(const char *xmlText, + const char *userName, + char **userNameOut, + char **subjNameOut, + ServiceAliasInfo **verifyAi) +{ + VGAuthError err; + gboolean bRet; + int num; + gchar **certChain = NULL; + ServiceSubject subj; + + *userNameOut = NULL; + *subjNameOut = NULL; + *verifyAi = NULL; + + bRet = VerifySAMLToken(xmlText, + subjNameOut, + &num, + &certChain); + + if (FALSE == bRet) { + return VGAUTH_E_AUTHENTICATION_DENIED; + } + + subj.type = SUBJECT_TYPE_NAMED; + subj.name = *subjNameOut; + err = ServiceVerifyAndCheckTrustCertChainForSubject(num, + (const char **) certChain, + userName, + &subj, + userNameOut, + verifyAi); + g_debug("%s: ServiceVerifyAndCheckTrustCertChainForSubject() " + "returned "VGAUTHERR_FMT64"\n", __FUNCTION__, err); + FreeCertArray(num, certChain); + + return err; +} +