diff options
Diffstat (limited to 'open-vm-tools/vgauth/serviceImpl/saml.cpp')
-rw-r--r-- | open-vm-tools/vgauth/serviceImpl/saml.cpp | 1262 |
1 files changed, 1262 insertions, 0 deletions
diff --git a/open-vm-tools/vgauth/serviceImpl/saml.cpp b/open-vm-tools/vgauth/serviceImpl/saml.cpp new file mode 100644 index 00000000..2531e173 --- /dev/null +++ b/open-vm-tools/vgauth/serviceImpl/saml.cpp @@ -0,0 +1,1262 @@ +/********************************************************* + * Copyright (C) 2011-2015 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.cpp + * + * Code for authenticating users based on SAML tokens. + */ + +#include <iostream> +#include <memory> +#include <sstream> +#include <vector> + +#undef WIN32_LEAN_AND_MEAN // XSEC unconditionally redefines this +#include <xsec/dsig/DSIGKeyInfoX509.hpp> +#include <xsec/dsig/DSIGReference.hpp> +#include <xsec/dsig/DSIGReferenceList.hpp> +#include <xsec/framework/XSECEnv.hpp> +#include <xsec/framework/XSECException.hpp> +#include <xsec/framework/XSECProvider.hpp> +#include <xsec/utils/XSECDOMUtils.hpp> + +#include <xercesc/dom/DOMNode.hpp> +#include <xercesc/framework/MemBufInputSource.hpp> +#include <xercesc/framework/MemoryManager.hpp> +#include <xercesc/framework/XMLGrammarPool.hpp> +#include <xercesc/framework/XMLGrammarPoolImpl.hpp> +#include <xercesc/parsers/XercesDOMParser.hpp> +#include <xercesc/sax/ErrorHandler.hpp> +#include <xercesc/sax/SAXParseException.hpp> +#include <xercesc/util/PlatformUtils.hpp> +#include <xercesc/util/XMLString.hpp> +#include <xercesc/validators/common/Grammar.hpp> + +/* + * XXX + * + * Optimization idea: stash a hash (SHA512) of a valid token, and bypass + * the full assertion process when we see that token again. The expiration + * date of the token must also be saved off (and beware the time skew issue). + * + * Note that there's some extra complexity here: + * + * 1 - AddAlias sets up a cert/user mapping + * 2 - a SAML token is used (and cached) using this cert/user combo + * 3 - RemoveAlias removes the combo + * 4 - the cached token still works + * + * So the cache should only bypass the token validation, not the certificate + * check in ServiceVerifyAndCheckTrustCertChainForSubject() + * + * Also TBD is how much this buys us in the real world. With short + * token lifetimes, its less interesting. Its also possible that + * it will have no measureable affect because the token verification + * will be lost in the noise of the API plumbing from VC->hostd->VMX->tools. + * + * The security folks have signed off on this, so long as we store only + * in memory. + * + */ + +/* + * XXX + * + * We should be a lot smarter about this, but this gets QE + * moving. + */ +#define SAML_TOKEN_PREFIX "saml:" +#define SAML_TOKEN_SSO_PREFIX "saml2:" + +extern "C" { +#include "prefs.h" +#include "serviceInt.h" +} +#include "samlInt.hpp" + + +/** + * Error handler used to log warnings from the XML parser. + */ + +class SAMLErrorHandler : public ErrorHandler { +public: + static void + printWarning(const SAXParseException &e, + const char *msg) + { + SAMLStringWrapper nativeMsg(e.getMessage()); + + /* + * XXX + * + * These functions were inlined on older compilers but are exported + * from libstdc++.so on newer compilers (4.4.3). Avoid using them to + * avoid the newer dependency. + * + * _ZNSo9_M_insertIyEERSoT_@@GLIBCXX_3.4.9 + * std::basic_ostream<char, std::char_traits<char> >& + * std::basic_ostream<char, std::char_traits<char> >:: + * _M_insert<unsigned long long>(unsigned long long) + * aka: operator<<(uint64_t) + * + * _ZSt16__ostream_insertIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_i@@GLIBCXX_3.4.9 + * std::basic_ostream<char, std::char_traits<char> >& + * std::__ostream_insert<char, std::char_traits<char> > + * (std::basic_ostream<char, std::char_traits<char> >&, + * char const*, int) + * aka: operator<<(std::string) + * + */ + Debug("SAML: %s: %s (line=%d, col=%d)\n", + msg, nativeMsg.c_str(), + (int) e.getLineNumber(), (int) e.getColumnNumber()); + +#ifdef avoid_this_usage + /* + * I'm tired of defining format modifier macros, so let's use + * stringstream to handle e.getLineNumber()'s return type. + */ + + std::stringstream ss; + + ss << msg << ": " << nativeMsg.c_str() << "" << " (line=" << + e.getLineNumber() << ", col=" << e.getColumnNumber() << ")"; + + Debug("SAML: %s.\n", ss.str().c_str()); +#endif + } + + void + warning(const SAXParseException &e) + { + printWarning(e, "warning"); + } + + void + error(const SAXParseException &e) + { + printWarning(e, "error"); + } + + void + fatalError (const SAXParseException &e) + { + printWarning(e, "fatal error"); + } + + void + resetErrors() + { + } +}; + + + +/** + * The XML schema files needed to perform validating parsing of the + * SAML assertions. Note: the order is important, since schemas need + * to be loaded before any schema that depends on them, so don't change + * the order. + */ +static const char *schemas[] = { + "xml.xsd", + "XMLSchema.xsd", + "xmldsig-core-schema.xsd", + "xenc-schema.xsd", + "saml-schema-assertion-2.0.xsd", +}; + + +/** + * An in-memory cache for XML schemas. + */ +static XMLGrammarPool *pool = NULL; + +static int clockSkewAdjustment = VGAUTH_PREF_DEFAULT_CLOCK_SKEW_SECS; + +static bool SAMLLoadSchema(XercesDOMParser &parser, + const SAMLGlibString &schemaDir, + const char *filename); +static DOMDocument *SAMLValidateSchemaAndParse(XercesDOMParser &parser, + const char *xmlText); + +static bool SAMLCheckSubject(const DOMDocument *doc, + SAMLTokenData &token); + +static bool SAMLCheckConditions(const DOMDocument *doc, + SAMLTokenData &token); + +static bool SAMLCheckTimeAttr(const DOMElement *elem, const char *attrName, + bool beforeNow); + +static bool SAMLCheckAudience(const XMLCh *audience); + +static bool SAMLCheckSignature(DOMDocument *doc, + vector<string> &certs); + +static bool SAMLCheckReference(const DOMDocument *doc, DSIGSignature *sig); + +static DOMElement *SAMLFindChildByName(const DOMElement *elem, + const char *name); + +static auto_ptr<DSIGKeyInfoX509> SAMLFindKey(const XSECEnv &secEnv, + const DOMElement *sigElem); + + +/* + ****************************************************************************** + * SAML_Init -- */ /** + * + * Performs any initialization needed for SAML processing. + * + * @return VGAUTH_E_OK on success, VGAuthError on failure + * + ****************************************************************************** + */ + +VGAuthError +SAML_Init() +{ + try { + XMLPlatformUtils::Initialize(); + XSECPlatformUtils::Initialise(); + + auto_ptr<XMLGrammarPool> myPool = SAMLCreateAndPopulateGrammarPool(); + if (NULL == myPool.get()) { + return VGAUTH_E_FAIL; + } + + pool = myPool.release(); + + clockSkewAdjustment = 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__, clockSkewAdjustment); + + return VGAUTH_E_OK; + } catch (const XMLException& e) { + SAMLStringWrapper msg(e.getMessage()); + + Warning("Failed to initialize Xerces: %s.\n", msg.c_str()); + return VGAUTH_E_FAIL; + } catch (...) { + // We're called from C code, so don't let any exceptions out. + Warning("%s: Unexpected exception.\n", __FUNCTION__); + return VGAUTH_E_FAIL; + } +} + + +/* + ****************************************************************************** + * SAMLCreateAndPopulateGrammarPool -- */ /** + * + * Creates a grammar pool that is populates with cached grammars representing + * the XML schemas needed for SAML validation. + * + * @return A heap allocated grammar pool (must be freed with operator + * delete) or NULL on failure. + * + ****************************************************************************** + */ + +auto_ptr<XMLGrammarPool> +SAMLCreateAndPopulateGrammarPool() +{ + auto_ptr<XMLGrammarPool> newPool(new XMLGrammarPoolImpl(XMLPlatformUtils::fgMemoryManager)); + + /* + * Create a parser instance to load all the schemas, so they can + * be cached for later. In addition to making parsing faster, we + * need to cache them so that Xerces does not try to download + * schemas from the web when one is referenced or imported by another + * schema. + */ + XercesDOMParser parser(NULL, XMLPlatformUtils::fgMemoryManager, + newPool.get()); + + gchar *dir = Pref_GetString(gPrefs, VGAUTH_PREF_SAML_SCHEMA_DIR, + VGAUTH_PREF_GROUP_NAME_SERVICE, NULL); + if (NULL == dir) { +#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. + */ + dir = 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__, dir, newDir); + + g_free(dir); + dir = newDir; + } +#else + /* + * XXX -- clean this up to make a better default for Linux. + */ + dir = g_build_filename(gInstallDir, "..", "schemas", NULL); +#endif + } + Log("%s: Using '%s' for SAML schemas\n", __FUNCTION__, dir); + SAMLGlibString schemaDir(dir); + + for (unsigned int i = 0; i < G_N_ELEMENTS(schemas); i++) { + if (!SAMLLoadSchema(parser, schemaDir, schemas[i])) { + return auto_ptr<XMLGrammarPool>(NULL); + } + } + + return newPool; +} + + +/* + ****************************************************************************** + * SAML_Shutdown -- */ /** + * + * Performs any clean-up of resources needed for SAML processing. + * + ****************************************************************************** + */ + +void +SAML_Shutdown() +{ + try { + delete pool; + pool = NULL; + XSECPlatformUtils::Terminate(); + XMLPlatformUtils::Terminate(); + } catch (...) { + // We're called from C code, so don't let any exceptions out. + Warning("%s: Unexpected exception.\n", __FUNCTION__); + } +} + + +/* + ****************************************************************************** + * SAML_Reload -- */ /** + * + * Reload any in-memory state used by the SAML module. + * + ****************************************************************************** + */ + +void +SAML_Reload() +{ + ASSERT(pool != NULL); + + auto_ptr<XMLGrammarPool> myPool = SAMLCreateAndPopulateGrammarPool(); + if (NULL == myPool.get()) { + Warning("%s: Failed to reload SAML state. Using old settings.\n", + __FUNCTION__); + return; + } + + delete pool; + pool = myPool.release(); +} + + +/* + ****************************************************************************** + * SAMLLoadSchema -- */ /** + * + * Loads a schema into the grammar pool used by the given parser. + * + * @param[in] parser The parser to load the schema with. + * @param[in] schemaDir The full path to the directory containing the schema. + * @param[in] filename The name of the XML schema file. + * + * @return true if the schema file was successfully loaded, false otherwise. + * + ****************************************************************************** + */ + +static bool +SAMLLoadSchema(XercesDOMParser &parser, + const SAMLGlibString &schemaDir, + const char *filename) +{ + SAMLGlibString schemaPath(g_build_filename(schemaDir.c_str(), filename, + NULL)); + Grammar *g = parser.loadGrammar(schemaPath.c_str(), + Grammar::SchemaGrammarType, true); + if (g == NULL) { + /* + * The parser complains even with official schemas, so we don't + * normally set an error handler. However, this should not fail since + * we control these files, so try again with logging, so we can see + * what went wrong. + */ + SAMLErrorHandler errorHandler; + parser.setErrorHandler(&errorHandler); + + g = parser.loadGrammar(schemaPath.c_str(), Grammar::SchemaGrammarType, + true); + + Warning("Failed to load XML Schema from %s.\n", schemaPath.c_str()); + return false; + } + + return true; +} + + +/* + ****************************************************************************** + * 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. + * @param[out] verifySi The subjectInfo associeatd with the entry + * in the ID provider 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, + char **userNameOut, + char **subjNameOut, + ServiceAliasInfo **verifyAi) +{ + try { + vector<string> certs; + VGAuthError err; + SAMLTokenData token; + + err = SAMLVerifyAssertion(xmlText, token, certs); + if (VGAUTH_E_OK != err) { + return err; + } + + return err; + } catch (XSECException &e) { + SAMLStringWrapper msg(e.getMsg()); + + Warning("XSec exception while verifying assertion: %s.\n", msg.c_str()); + return VGAUTH_E_FAIL; + } catch (const XMLException& e) { + SAMLStringWrapper msg(e.getMessage()); + + Warning("Xerces exception while verifying assertion: %s.\n", + msg.c_str()); + return VGAUTH_E_FAIL; + } catch (...) { + // We're called from C code, so don't let any exceptions out. + Warning("Unexpected exception.\n"); + return VGAUTH_E_FAIL; + } +} + + +/* + ****************************************************************************** + * 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. + * @param[out] verifySi The subjectInfo associeatd with the entry + * in the ID provider 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) +{ + *userNameOut = NULL; + *subjNameOut = NULL; + *verifyAi = NULL; + + try { + vector<string> certs; + VGAuthError err; + SAMLTokenData token; + char **pemCerts; + ServiceSubject subj; + int i; + + err = SAMLVerifyAssertion(xmlText, token, certs); + if (VGAUTH_E_OK != err) { + return err; + } + + pemCerts = (char **) g_malloc0(sizeof(char *) * certs.size()); + for (i = 0; i < (int) certs.size(); i++) { + pemCerts[i] = g_strdup(certs[i].c_str()); + } + subj.type = SUBJECT_TYPE_NAMED; + if (subjNameOut) { + *subjNameOut = g_strdup(token.subjectName.c_str()); + } + subj.name = g_strdup(token.subjectName.c_str()); + err = ServiceVerifyAndCheckTrustCertChainForSubject((int) certs.size(), + (const char **) pemCerts, + userName, + &subj, + userNameOut, + verifyAi); + Debug("%s: ServiceVerifyAndCheckTrustCertChainForSubject() returned "VGAUTHERR_FMT64"\n", __FUNCTION__, err); + + for (i = 0; i < (int) certs.size(); i++) { + g_free(pemCerts[i]); + } + g_free(pemCerts); + g_free(subj.name); + return err; + } catch (XSECException &e) { + SAMLStringWrapper msg(e.getMsg()); + + Warning("XSec exception while verifying assertion: %s.\n", msg.c_str()); + return VGAUTH_E_FAIL; + } catch (const XMLException& e) { + SAMLStringWrapper msg(e.getMessage()); + + Warning("Xerces exception while verifying assertion: %s.\n", + msg.c_str()); + return VGAUTH_E_FAIL; + } catch (...) { + // We're called from C code, so don't let any exceptions out. + Warning("Unexpected exception.\n"); + return VGAUTH_E_FAIL; + } +} + + +/* + ****************************************************************************** + * SAMLVerifyAssertion -- */ /** + * + * Performs the following checks to validate a SAML assertion. + * 1) Checks that the XML document is well formed according to the SAML 2.0 + * Assertion XML schema. + * 2) Check that the assertion is signed by a certificate contained within + * the assertion. + * 3) TODO: Check that the assertion contains a Subject element, and that + * Subject element should contain a SubjectConfirmation element. The + * SubjectConfirmation method must be "bearer" + * ("urn:oasis:names:tc:SAML:2.0:cm:bearer"). + * 4) The Conditions element for the assertion must be met in terms of + * any "NotBefore" or "NotOnOrAfter" information. + * The chain of certs used to verify the signature will be returned via @a + * certs. + * + * @param[in] xmlText + * @param[out] token The interesting bits extracted from the xmlText. + * @param[out] certs If the SAML assertion is verified, then this will + * contain the certificate chain for the issuer. + * Each certificate will be base64 encoded (but without + * the PEM-style bookends), with the issuer's cert + * at element 0. + * + * @return VGAUTH_E_OK on success, VGAuthError on failure + * + ****************************************************************************** + */ + +VGAuthError +SAMLVerifyAssertion(const char *xmlText, + SAMLTokenData &token, + vector<string> &certs) +{ + XercesDOMParser parser(NULL, XMLPlatformUtils::fgMemoryManager, pool); + SAMLErrorHandler errorHandler; + SecurityManager sm; + + parser.setErrorHandler(&errorHandler); + + // prevent the billion laughs attack -- put a limit on entity expansions + sm.setEntityExpansionLimit(100); + parser.setSecurityManager(&sm); + + DOMDocument *doc = SAMLValidateSchemaAndParse(parser, xmlText); + if (NULL == doc) { + return VGAUTH_E_AUTHENTICATION_DENIED; + } + + const DOMElement *s = SAMLFindChildByName(doc->getDocumentElement(), + SAML_TOKEN_PREFIX"Subject"); + if (NULL == s) { + Debug("Couldn't find " SAML_TOKEN_PREFIX " in token\n"); + s = SAMLFindChildByName(doc->getDocumentElement(), + SAML_TOKEN_SSO_PREFIX"Subject"); + if (NULL == s) { + Debug("Couldn't find " SAML_TOKEN_SSO_PREFIX " in token\n"); + Warning("No recognized tags in token; punting\n"); + return VGAUTH_E_AUTHENTICATION_DENIED; + } else { + Debug("Found " SAML_TOKEN_SSO_PREFIX " in token\n"); + token.isSSOToken = true; + token.ns = SAML_TOKEN_SSO_PREFIX; + } + } else { + Debug("Found " SAML_TOKEN_PREFIX " in token\n"); + token.isSSOToken = false; + token.ns = SAML_TOKEN_PREFIX; + } + + if (!SAMLCheckSubject(doc, token)) { + return VGAUTH_E_AUTHENTICATION_DENIED; + } + + if (!SAMLCheckConditions(doc, token)) { + return VGAUTH_E_AUTHENTICATION_DENIED; + } + + if (!SAMLCheckSignature(doc, certs)) { + return VGAUTH_E_AUTHENTICATION_DENIED; + } + + return VGAUTH_E_OK; +} + + +/* + ****************************************************************************** + * SAMLValidateSchemaAndParse -- */ /** + * + * Checks that the XML document is well formed according to the SAML 2.0 + * Assertion XML schema. + * + * @param[in] parser The parser to use with the XML document. + * @param[in] xmlText The text of the SAML assertion. + * + * @return A pointer to a DOMDocument instance that represents the parsed + * SAML assertion or NULL if the document was not valid. The memory + * used by the DOMDocument is owned by the parser. + * + ****************************************************************************** + */ + +static DOMDocument * +SAMLValidateSchemaAndParse(XercesDOMParser &parser, + const char *xmlText) +{ + parser.setDoNamespaces(true); + parser.setDoSchema(true); + parser.setValidationScheme(AbstractDOMParser::Val_Always); + parser.useCachedGrammarInParse(true); + + MemBufInputSource in(reinterpret_cast<const XMLByte *>(xmlText), + strlen(xmlText), "VGAuthSamlAssertion"); + + parser.parse(in); + + xsecsize_t errorCount = parser.getErrorCount(); + if (errorCount > 0) { + Debug("Encountered %u errors while parsing SAML assertion.\n", + (unsigned int) errorCount); + return NULL; + } + + DOMDocument *doc = parser.getDocument(); + ASSERT(doc != NULL); + + return doc; +} + + +/* + ****************************************************************************** + * SAMLCheckSubject -- */ /** + * + * 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: + * <saml:Subject> + * <saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"> + * scott@example.org + * </saml:NameID> + * <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"> + * <saml:SubjectConfirmationData NotOnOrAfter="2011-12-08T00:42:10Z"> + * </saml:SubjectConfirmationData> + * </saml:SubjectConfirmation> + * </saml:Subject> + * + * @param[in] doc The DOM representation of the SAML assertions. + * @param[in/out] token Information about the token to be populated. + * + * @return true if the conditions in at least one SubjectConfirmation is met, + * false otherwise. + * + ****************************************************************************** + */ + +static bool +SAMLCheckSubject(const DOMDocument *doc, + SAMLTokenData &token) +{ + const DOMElement *subject; + char *name = g_strdup_printf("%sSubject", + token.ns.c_str()); + subject = SAMLFindChildByName(doc->getDocumentElement(), name); + g_free(name); + + if (NULL == subject) { + // Should not happen, since this is required element in the schema. + Log("%s: Missing subject element!\n", __FUNCTION__); +// ASSERT(0); + return false; + } + + const DOMElement *nameID; + name = g_strdup_printf("%sNameID", token.ns.c_str()); + nameID = SAMLFindChildByName(subject, name); + g_free(name); + if (NULL == nameID) { + /* + * The schema allows BaseID, NameID, or EncryptedID. The library code + * for the SSO server only supports NameID. EncryptedID is really + * complicated (and we don't have decryption keys, so let's not + * support it for now. + */ + + Log("%s: No NameID element for the subject.\n", __FUNCTION__); + return false; + } + + token.subjectName = SAMLStringWrapper(nameID->getTextContent()).c_str(); + Debug("%s: subjectName: '%s'\n", __FUNCTION__, token.subjectName.c_str()); + + /* + * TODO: Investigate: NameID elements can have a NameQualifier attribute. + * This smells like a domain name, and we might want to include it with + * subject name (<NameQualifier>\subjectName). + */ + + /* + * Find all the SubjectConfirmation nodes and see if at least one can be + * verified. + */ + + name = g_strdup_printf("%sSubjectConfirmation", token.ns.c_str()); + XMLT scName(name); + g_free(name); + for (DOMElement *child = subject->getFirstElementChild(); child != NULL; + child = child->getNextElementSibling()) { + + if (!XMLString::equals(child->getNodeName(), scName.getUnicodeStr())) { + continue; + } + + const XMLCh *method = child->getAttribute(MAKE_UNICODE_STRING("Method")); + if ((NULL == method) || (0 == *method)) { + // Should not happen, since this is a required attribute. + ASSERT(0); + Debug("%s: Missing confirmation method.\n", __FUNCTION__); + continue; + } + + if (!XMLString::equals( + MAKE_UNICODE_STRING("urn:oasis:names:tc:SAML:2.0:cm:bearer"), + method)) { + Debug("%s: Non-bearer confirmation method in token", __FUNCTION__); + continue; + } + + const DOMElement *subjConfirmData; + name = g_strdup_printf("%sSubjectConfirmationData", token.ns.c_str()); + subjConfirmData = SAMLFindChildByName(child, name); + g_free(name); + if (NULL != subjConfirmData) { + if (!SAMLCheckTimeAttr(subjConfirmData, "NotBefore", true) || + !SAMLCheckTimeAttr(subjConfirmData, "NotOnOrAfter", false)) { + Debug("%s: subjConfirmData time check failed\n", __FUNCTION__); + continue; + } + + const XMLCh *recipient; + recipient = subjConfirmData->getAttribute( + MAKE_UNICODE_STRING("Recipient")); + /* + * getAttribute() returns a 0-length string, not NULL, if it can't + * find what it wants. + */ + if ((0 != XMLString::stringLen(recipient)) && + !SAMLCheckAudience(recipient)) { + Debug("%s: failed recipient check\n", __FUNCTION__); + continue; + } + } + + return true; + } + + Debug("%s: Could not verify using any SubjectConfirmation elements\n", + __FUNCTION__); + return false; +} + + +/* + ****************************************************************************** + * SAMLCheckConditions -- */ /** + * + * 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: + * <saml:Conditions NotBefore="2011-12-08T00:41:10Z" + * NotOnOrAfter="2011-12-08T00:42:10Z"> + * <saml:AudienceRestriction> + * <saml:Audience>https://sp.example.com/SAML2</saml:Audience> + * </saml:AudienceRestriction> + * </saml:Conditions> + * + * @param[in] doc The DOM representation of the SAML assertions. + * + * @return true if the conditions are met; false otherwise. + * + ****************************************************************************** + */ + +static bool +SAMLCheckConditions(const DOMDocument *doc, + SAMLTokenData &token) +{ + /* + * There should be at most one Conditions element and the schema checking + * done by the parser should enforce that. + */ + char *name = g_strdup_printf("%sConditions", token.ns.c_str()); + const DOMElement *conditions = SAMLFindChildByName(doc->getDocumentElement(), + name); + g_free(name); + if (NULL == conditions) { + // Conditions are optional. + return true; + } + + if (!SAMLCheckTimeAttr(conditions, "NotBefore", true) || + !SAMLCheckTimeAttr(conditions, "NotOnOrAfter", false)) { + return false; + } + + /* + * <Condition> 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. + */ + name = g_strdup_printf("%sCondition", token.ns.c_str()); + if (SAMLFindChildByName(conditions, name) != NULL) { + Log("%s: Unrecognized condition found!\n", __FUNCTION__); + g_free(name); + return false; + } + g_free(name); + + /* + * <AudienceRestriction> 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. + * Some searching online shows people using http://<hostname>/ as the + * URI, but let's wait until we get some feedback from the SSO team. + * TODO: Validate it using SAMLCheckAudience(). + */ + + /* + * <OneTimeUse> element is specified to disallow caching. We don't + * cache, so it doesn't affect out validation. + * However, we need to communicate it to clients so they do not cache. + */ + name = g_strdup_printf("%sOneTimeUse", token.ns.c_str()); + token.oneTimeUse = (SAMLFindChildByName(conditions, name) + != NULL); + g_free(name); + + /* + * <ProxyRestriction> only applies if a service wants to make their own + * assertions based on a SAML assertion. That should not apply here. + */ + + return true; +} + + +/* + ****************************************************************************** + * SAMLCheckTimeAttr -- */ /** + * + * Checks that the given attribute with the given name is a timestamp and + * compares it against the current time. + * + * @param[in] elem The element containing the attribute. + * @param[in] attrName The name of the attribute. + * @param[in] beforeNow Whether the condition given by the attribute + * requires that the timestamp be before now (true) + * or after (false). + * + ****************************************************************************** + */ + +static bool +SAMLCheckTimeAttr(const DOMElement *elem, + const char *attrName, + bool beforeNow) +{ + const XMLCh *timeAttr = elem->getAttribute(MAKE_UNICODE_STRING(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. + */ + return true; + } + + SAMLStringWrapper timeStr(timeAttr); + GTimeVal time; + + if (!g_time_val_from_iso8601(timeStr.c_str(), &time)) { + Log("%s: Could not parse %s value (%s).\n", __FUNCTION__, attrName, + timeStr.c_str()); + return false; + } + + GTimeVal now; + g_get_current_time(&now); + + GTimeVal *before; + GTimeVal *after; + + if (beforeNow) { + before = &time; + after = &now; + } else { + before = &now; + after = &time; + } + + /* + * If the time delta is within our clock skew range, let it through. + * Ignore the micros since we're adjusting anyways for clock + * skew. + */ + glong diff; + + diff = abs(before->tv_sec - after->tv_sec); + if (diff > clockSkewAdjustment) { + Debug("%s: FAILED SAML assertion (timeStamp %s, delta %d) %s.\n", + __FUNCTION__, timeStr.c_str(), (int) diff, + beforeNow ? "is not yet valid" : "has expired"); + return false; + } + + return true; +} + + +/* + ****************************************************************************** + * SAMLCheckAudience -- */ /** + * + * 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. + * + ****************************************************************************** + */ + +static bool +SAMLCheckAudience(const XMLCh *audience) +{ + bool ret; + + /* + * XXX This should be much better. Ideally it should check that it refers + * to the hostname of a URL or matches some kind of URN. Also, this is + * where the VC UUID can be used when running in a VM. + * We should accept: + * URL: <scheme_name>://<host_name>[/stuff] + * URN: urn:vmware:vgauth:<vc_domain_name>:<vc_vm_uuid>:[vgauth_client_app_name] + * Glib has a basic URL and we should use it. + * TODO: Need a RpcIn call into the VMX to get the VC UUID, since it is not + * currently exposed. (Could be NamespaceDB, but then need to make a separate + * workflow for pushing the VC UUIDs out to VMs.) + */ + + ret = strstr(SAMLStringWrapper(audience).c_str(), + g_get_host_name()) != NULL; + Debug("%s: audience check: token: '%s', host: '%s' ? %d\n", + __FUNCTION__, + SAMLStringWrapper(audience).c_str(), + g_get_host_name(), ret); + return ret; +} + + +/* + ****************************************************************************** + * SAMLCheckSignature -- */ /** + * + * Finds the signature in the SAML assertion, then extracts the X509 + * from that, then checks that the signature is valid. + * + * @param[in] doc The document of which to check the signature. + * @param[out] certs The base64 encoded certificates present in the + * signature. + * + * @return true if the signature if valid, false otherwise. + * + ****************************************************************************** + */ + +static bool +SAMLCheckSignature(DOMDocument *doc, + vector<string> &certs) +{ + DOMElement *sigElem = SAMLFindChildByName(doc->getDocumentElement(), + "ds:Signature"); + if (NULL == sigElem) { + Debug("%s: No top level signature found.\n", __FUNCTION__); + return false; + } + + XSECEnv secEnv(doc); + + auto_ptr<DSIGKeyInfoX509> keyInfo = SAMLFindKey(secEnv, sigElem); + if (keyInfo.get() == NULL) { + Debug("%s: No X509 data found as part of the signature.\n", + __FUNCTION__); + return false; + } + + if (keyInfo->getCertificateListSize() == 0) { + Debug("%s: No X509 certificates found in the signature\n", __FUNCTION__); + return false; + } + + const XSECCryptoX509 *x509 = keyInfo->getCertificateCryptoItem(0); + ASSERT(NULL != x509); + + XSECProvider prov; + DSIGSignature *sig = prov.newSignatureFromDOM(doc, sigElem); + + sig->load(); + sig->setSigningKey(x509->clonePublicKey()); + + if (!SAMLCheckReference(doc, sig)) { + return false; + } + + if (!sig->verify()) { + Debug("%s: Signature check failed: %s.\n", __FUNCTION__, + SAMLStringWrapper(sig->getErrMsgs()).c_str()); + return false; + } + + for (int i = 0; i < keyInfo->getCertificateListSize(); i++) { + const XSECCryptoX509 *cert = keyInfo->getCertificateCryptoItem(i); + certs.push_back(string(cert->getDEREncodingSB().rawCharBuffer())); + } + + return true; +} + + +/* + ****************************************************************************** + * SAMLCheckReference -- */ /** + * + * Checks that the given signature refers to (and thus was computed over) + * the root element of the document. This ensures that the entire document + * is protected/endorsed by the signature. + * See the SAML Core specification, section 5.4.2. + * + * @param[in] doc The document in which contains the signature. + * @param[in] sig The signature + * + * @return true if the signature refers to the whole document, or false + * otherwise. + * + ****************************************************************************** + */ + +static bool +SAMLCheckReference(const DOMDocument *doc, + DSIGSignature *sig) +{ + DOMElement *rootElem = doc->getDocumentElement(); + + const XMLCh *id = rootElem->getAttribute(MAKE_UNICODE_STRING("ID")); + if (NULL == id) { + Debug("%s: NULL ID attribute.\n", __FUNCTION__); + return false; + } + + XMLSize_t idLen = XMLString::stringLen(id); + if (0 == idLen) { + Debug("%s: Root element has no or an empty ID attribute.\n", + __FUNCTION__); + return false; + } + + /* + * At least one reference should contain a URI that refers to the root + * element. To do so, that URI should be "#" followed by the value of + * the ID element of the root node; for example if the ID is "SAML" the + * URI must be "#SAML". + * + * TODO: The vmacore implementation of SAML parsing, used by clients + * validating tokens, allows for multiple references and considers if + * at least one matches. However, the SAML spec (section 5.4.2) requires + * that there be only one reference element in the signature. Currently + * we follow the vmacore behavior. + */ + + XMLT uriPrefix("#"); + XMLSize_t prefixLen = XMLString::stringLen(uriPrefix.getUnicodeStr()); + + DSIGReferenceList *references = sig->getReferenceList(); + DSIGReferenceList::size_type numReferences = references->getSize(); + for (DSIGReferenceList::size_type i = 0; i < numReferences; i++) { + DSIGReference *ref = references->item(i); + const XMLCh *uri = ref->getURI(); + + if (XMLString::startsWith(uri, uriPrefix.getUnicodeStr()) && + XMLString::equals(id, uri + prefixLen)) { + return true; + } + } + + Debug("%s: No matching reference found in the signature for ID '%s'.\n", + __FUNCTION__, SAMLStringWrapper(id).c_str()); + return false; +} + + +/* + ****************************************************************************** + * SAMLFindChildByName -- */ /** + * + * Finds the first element that is a child of the given element which + * matches the given node name. + * + * TODO: Investigate using getLocalName() and getNamespaceURI() to + * identify the child, since in "ds:Signature" "ds" is an alias to as longer + * URI, and that URI should be used instead (it's more stable). + * + * @param[in] elem The element to search the children of. + * @param[in] name The name of the child element + * + * @return A pointer to the DOMElement matching the name, or NULL if + * no such element is found. + * + ****************************************************************************** + */ + +static DOMElement * +SAMLFindChildByName(const DOMElement *elem, + const char *name) +{ + XMLT sigNodeName(name); + DOMElement *childElem; + + for (childElem = elem->getFirstElementChild(); + childElem != NULL; childElem = childElem->getNextElementSibling()) { + if (XMLString::equals(childElem->getNodeName(), + sigNodeName.getUnicodeStr())) { + break; + } + } + + return childElem; +} + + +/* + ****************************************************************************** + * SAMLFindKey -- */ /** + * + * Finds the first ds:X509Data element under the given ds:Signature element. + * + * @param[in] secEnv A XSEC environment to create the object from. + * @param[in] sigElem The root element of the signuture. + * + * @return A pointer to a DSIGKeyInfoX509 object, which must be freed using + * operator delete, or NULL if no ds:X509Data element is found. + * + ****************************************************************************** + */ + +static auto_ptr<DSIGKeyInfoX509> +SAMLFindKey(const XSECEnv &secEnv, + const DOMElement *sigElem) +{ + DOMNodeList *keyInfos = + sigElem->getElementsByTagName(MAKE_UNICODE_STRING("ds:X509Data")); + + if (keyInfos->getLength() == 0) { + return auto_ptr<DSIGKeyInfoX509>(NULL); + } + + auto_ptr<DSIGKeyInfoX509> keyInfo(new DSIGKeyInfoX509(&secEnv, + keyInfos->item(0))); + + keyInfo->load(); + + return keyInfo; +} |