From 88d66de2aa06a8ad04c94b4f5ae792eb22dd98d1 Mon Sep 17 00:00:00 2001 From: Scott Cantor Date: Sun, 4 Sep 2011 01:33:44 +0000 Subject: [PATCH] https://issues.shibboleth.net/jira/browse/CPPXT-70 --- cpp-xmltooling.sln | 2 - xmltooling/Makefile.am | 4 + xmltooling/XMLToolingConfig.cpp | 36 +- xmltooling/XMLToolingConfig.h | 32 +- xmltooling/internal.h | 7 + xmltooling/security/AbstractPKIXTrustEngine.h | 7 + xmltooling/security/OpenSSLPathValidator.h | 68 +++ xmltooling/security/PKIXPathValidatorParams.h | 108 ++++ xmltooling/security/PathValidator.h | 87 ++++ .../security/impl/AbstractPKIXTrustEngine.cpp | 501 +++---------------- xmltooling/security/impl/PKIXPathValidator.cpp | 546 +++++++++++++++++++++ xmltooling/soap/SOAPTransport.h | 2 - xmltooling/soap/impl/CURLSOAPTransport.cpp | 15 - xmltooling/soap/impl/SOAPClient.cpp | 37 ++ xmltooling/util/Threads.h | 9 + xmltooling/xmltooling.vcxproj | 4 + xmltooling/xmltooling.vcxproj.filters | 12 + 17 files changed, 1022 insertions(+), 455 deletions(-) create mode 100644 xmltooling/security/OpenSSLPathValidator.h create mode 100644 xmltooling/security/PKIXPathValidatorParams.h create mode 100644 xmltooling/security/PathValidator.h create mode 100644 xmltooling/security/impl/PKIXPathValidator.cpp diff --git a/cpp-xmltooling.sln b/cpp-xmltooling.sln index f90a032..be5fd75 100644 --- a/cpp-xmltooling.sln +++ b/cpp-xmltooling.sln @@ -12,7 +12,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Misc", "Misc", "{61BF324D-2 doxygen.cfg = doxygen.cfg m4\doxygen.m4 = m4\doxygen.m4 Makefile.am = Makefile.am - xmltooling.spec.in = xmltooling.spec.in EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Schemas", "Schemas", "{23EF5C29-2A13-4F73-99D1-96B8120F148E}" @@ -75,7 +74,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Doc", "Doc", "{847EC34E-006 doc\LICENSE.txt = doc\LICENSE.txt doc\LOG4CPP.LICENSE = doc\LOG4CPP.LICENSE doc\Makefile.am = doc\Makefile.am - doc\NOTICE.txt = doc\NOTICE.txt doc\OPENSSL.LICENSE = doc\OPENSSL.LICENSE doc\README.txt = doc\README.txt EndProjectSection diff --git a/xmltooling/Makefile.am b/xmltooling/Makefile.am index e6e946a..8b987bd 100644 --- a/xmltooling/Makefile.am +++ b/xmltooling/Makefile.am @@ -78,6 +78,9 @@ secinclude_HEADERS = \ security/KeyInfoCredentialContext.h \ security/KeyInfoResolver.h \ security/OpenSSLCredential.h \ + security/OpenSSLPathValidator.h \ + security/PathValidator.h \ + security/PKIXPathValidatorParams.h \ security/SecurityHelper.h \ security/SignatureTrustEngine.h \ security/TrustEngine.h \ @@ -143,6 +146,7 @@ xmlsec_sources = \ security/impl/InlineKeyResolver.cpp \ security/impl/KeyInfoResolver.cpp \ security/impl/OpenSSLCryptoX509CRL.cpp \ + security/impl/PKIXPathValidator.cpp \ security/impl/SecurityHelper.cpp \ security/impl/StaticPKIXTrustEngine.cpp \ security/impl/TrustEngine.cpp \ diff --git a/xmltooling/XMLToolingConfig.cpp b/xmltooling/XMLToolingConfig.cpp index f5b1e40..740e100 100644 --- a/xmltooling/XMLToolingConfig.cpp +++ b/xmltooling/XMLToolingConfig.cpp @@ -36,9 +36,11 @@ #include "security/OpenSSLCryptoX509CRL.h" #include "security/CredentialResolver.h" #include "security/KeyInfoResolver.h" +#include "security/PathValidator.h" #include "signature/KeyInfo.h" #include "signature/Signature.h" #include "soap/SOAP.h" +#include "soap/SOAPTransport.h" #include "util/NDC.h" #include "util/PathResolver.h" #include "util/ReplayCache.h" @@ -453,16 +455,20 @@ bool XMLToolingInternalConfig::init() REGISTER_XMLTOOLING_EXCEPTION_FACTORY(EncryptionException,xmlencryption); registerKeyInfoClasses(); registerEncryptionClasses(); - registerKeyInfoResolvers(); registerCredentialResolvers(); + registerKeyInfoResolvers(); + registerPathValidators(); registerTrustEngines(); registerXMLAlgorithms(); - registerSOAPTransports(); - initSOAPTransports(); - registerStorageServices(); m_keyInfoResolver = KeyInfoResolverManager.newPlugin(INLINE_KEYINFO_RESOLVER,nullptr); #endif +#ifndef XMLTOOLING_LITE + registerStorageServices(); +#endif + registerSOAPTransports(); + initSOAPTransports(); + m_pathResolver = new PathResolver(); m_urlEncoder = new URLEncoder(); @@ -522,10 +528,14 @@ void XMLToolingInternalConfig::term() XMLToolingException::deregisterFactories(); AttributeExtensibleXMLObject::deregisterIDAttributes(); -#ifndef XMLTOOLING_NO_XMLSEC - StorageServiceManager.deregisterFactories(); termSOAPTransports(); SOAPTransportManager.deregisterFactories(); + +#ifndef XMLTOOLING_LITE + StorageServiceManager.deregisterFactories(); +#endif + +#ifndef XMLTOOLING_NO_XMLSEC TrustEngineManager.deregisterFactories(); CredentialResolverManager.deregisterFactories(); KeyInfoResolverManager.deregisterFactories(); @@ -569,6 +579,9 @@ void XMLToolingInternalConfig::term() delete m_validatingPool; m_validatingPool=nullptr; + for_each(m_namedLocks.begin(), m_namedLocks.end(), cleanup_pair()); + m_namedLocks.clear(); + #ifndef XMLTOOLING_NO_XMLSEC delete m_xsecProvider; m_xsecProvider=nullptr; @@ -594,6 +607,17 @@ void XMLToolingInternalConfig::unlock() m_lock->unlock(); } +Mutex& XMLToolingInternalConfig::getNamedMutex(const char* name) +{ + Locker glock(this); + map::const_iterator m = m_namedLocks.find(name); + if (m != m_namedLocks.end()) + return *(m->second); + Mutex* newlock = Mutex::create(); + m_namedLocks[name] = newlock; + return *newlock; +} + bool XMLToolingInternalConfig::load_library(const char* path, void* context) { #ifdef _DEBUG diff --git a/xmltooling/XMLToolingConfig.h b/xmltooling/XMLToolingConfig.h index 26e01a9..928ce71 100644 --- a/xmltooling/XMLToolingConfig.h +++ b/xmltooling/XMLToolingConfig.h @@ -41,6 +41,7 @@ namespace xmltooling { + class XMLTOOL_API Mutex; class XMLTOOL_API ParserPool; class XMLTOOL_API PathResolver; class XMLTOOL_API TemplateEngine; @@ -52,6 +53,7 @@ namespace xmltooling { #ifndef XMLTOOLING_NO_XMLSEC class XMLTOOL_API CredentialResolver; class XMLTOOL_API KeyInfoResolver; + class XMLTOOL_API PathValidator; class XMLTOOL_API TrustEngine; class XMLTOOL_API XSECCryptoX509CRL; #endif @@ -156,6 +158,15 @@ namespace xmltooling { */ virtual ParserPool& getValidatingParser() const=0; + /** + * Returns a reference to a named mutex. + *

The first access to a given name will create the object. + * + * @param name name of mutex to access + * @return reference to a mutex object + */ + virtual Mutex& getNamedMutex(const char* name)=0; + #ifndef XMLTOOLING_NO_XMLSEC /** * Returns the global KeyInfoResolver instance. @@ -254,6 +265,13 @@ namespace xmltooling { */ unsigned int clock_skew_secs; +#ifndef XMLTOOLING_LITE + /** + * Manages factories for StorageService plugins. + */ + PluginManager StorageServiceManager; +#endif + #ifndef XMLTOOLING_NO_XMLSEC /** * Returns an X.509 CRL implementation object. @@ -261,14 +279,19 @@ namespace xmltooling { virtual XSECCryptoX509CRL* X509CRL() const=0; /** + * Manages factories for CredentialResolver plugins. + */ + PluginManager CredentialResolverManager; + + /** * Manages factories for KeyInfoResolver plugins. */ PluginManager KeyInfoResolverManager; /** - * Manages factories for CredentialResolver plugins. + * Manages factories for PathValidator plugins. */ - PluginManager CredentialResolverManager; + PluginManager PathValidatorManager; /** * Manages factories for TrustEngine plugins. @@ -276,11 +299,6 @@ namespace xmltooling { PluginManager TrustEngineManager; /** - * Manages factories for StorageService plugins. - */ - PluginManager StorageServiceManager; - - /** * Maps an XML Signature/Encryption algorithm identifier to a library-specific * key algorithm and size for use in resolving credentials. * diff --git a/xmltooling/internal.h b/xmltooling/internal.h index d441ad4..caf1be0 100644 --- a/xmltooling/internal.h +++ b/xmltooling/internal.h @@ -44,7 +44,10 @@ #include "XMLToolingConfig.h" #include "util/ParserPool.h" +#include +#include #include + #ifndef XMLTOOLING_NO_XMLSEC #include #endif @@ -95,6 +98,9 @@ namespace xmltooling { Lockable* lock(); void unlock(); + // named mutexes to limit lock scope + Mutex& getNamedMutex(const char* name); + // configuration bool load_library(const char* path, void* context=nullptr); bool log_config(const char* config=nullptr); @@ -126,6 +132,7 @@ namespace xmltooling { private: int m_initCount; Mutex* m_lock; + std::map m_namedLocks; std::vector m_libhandles; ParserPool* m_parserPool; ParserPool* m_validatingPool; diff --git a/xmltooling/security/AbstractPKIXTrustEngine.h b/xmltooling/security/AbstractPKIXTrustEngine.h index 2437e2d..b828fbe 100644 --- a/xmltooling/security/AbstractPKIXTrustEngine.h +++ b/xmltooling/security/AbstractPKIXTrustEngine.h @@ -36,6 +36,7 @@ namespace xmltooling { + class XMLTOOL_API OpenSSLPathValidator; class XMLTOOL_API XSECCryptoX509CRL; /** @@ -54,6 +55,7 @@ namespace xmltooling { *

  • checkRevocation attribute (off, entityOnly, fullChain) *
  • policyMappingInhibit attribute (boolean) *
  • anyPolicyInhibit attribute (boolean) + *
  • &t;PathValidator> element (zero or more) *
  • <TrustedName> element (zero or more) *
  • <PolicyOID> element (zero or more) * @@ -62,6 +64,9 @@ namespace xmltooling { */ AbstractPKIXTrustEngine(const xercesc::DOMElement* e=nullptr); + /** Plugins used to perform path validation. */ + std::vector m_pathValidators; + /** Controls revocation checking, currently limited to CRLs and supports "off", "entityOnly", "fullChain". */ std::string m_checkRevocation; @@ -198,6 +203,8 @@ namespace xmltooling { CredentialCriteria* criteria=nullptr, const std::vector* inlineCRLs=nullptr ) const; + + friend class XMLTOOL_DLLLOCAL PKIXParams; }; }; diff --git a/xmltooling/security/OpenSSLPathValidator.h b/xmltooling/security/OpenSSLPathValidator.h new file mode 100644 index 0000000..cd365ed --- /dev/null +++ b/xmltooling/security/OpenSSLPathValidator.h @@ -0,0 +1,68 @@ +/** + * Licensed to the University Corporation for Advanced Internet + * Development, Inc. (UCAID) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + * + * UCAID licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the + * License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + */ + +/** + * @file xmltooling/security/OpenSSLPathValidator.h + * + * Extended PathValidator interface that adds validation + * using OpenSSL data types directly for efficiency. + */ + +#if !defined(__xmltooling_opensslpathval_h__) && !defined(XMLTOOLING_NO_XMLSEC) +#define __xmltooling_opensslpathval_h__ + +#include + +#include + + +namespace xmltooling { + + /** + * Extended PathValidator interface that adds validation + * using OpenSSL data types directly for efficiency. + */ + class XMLTOOL_API OpenSSLPathValidator : public PathValidator + { + MAKE_NONCOPYABLE(OpenSSLPathValidator); + protected: + OpenSSLPathValidator(); + + public: + virtual ~OpenSSLPathValidator(); + + /** + * Validates an end-entity certificate. + * + * @param certEE end-entity certificate + * @param certChain the complete untrusted certificate chain + * @param params plugin-specific parameters to the validation process + * @return true iff validaton succeeds + */ + virtual bool validate( + X509* certEE, + STACK_OF(X509)* certChain, + const PathValidatorParams& params + ) const=0; + + }; +}; + +#endif /* __xmltooling_opensslpathval_h__ */ diff --git a/xmltooling/security/PKIXPathValidatorParams.h b/xmltooling/security/PKIXPathValidatorParams.h new file mode 100644 index 0000000..e460e4e --- /dev/null +++ b/xmltooling/security/PKIXPathValidatorParams.h @@ -0,0 +1,108 @@ +/** + * Licensed to the University Corporation for Advanced Internet + * Development, Inc. (UCAID) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + * + * UCAID licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the + * License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + */ + +/** + * @file xmltooling/security/PKIXPathValidatorParams.h + * + * PKIX-specific parameters to a PathValidator. + */ + +#if !defined(__xmltooling_pkixvalparam_h__) && !defined(XMLTOOLING_NO_XMLSEC) +#define __xmltooling_pkixvalparam_h__ + +#include + +#include +#include + +namespace xmltooling { + + class XMLTOOL_API XSECCryptoX509CRL; + + /** + * PKIX-specific parameters to a PathValidator. + */ + class XMLTOOL_API PKIXPathValidatorParams : public PathValidator::PathValidatorParams + { + protected: + PKIXPathValidatorParams(); + + public: + virtual ~PKIXPathValidatorParams(); + + /** + * Returns the allowable trust chain verification depth. + * + * @return allowable trust chain verification depth + */ + virtual int getVerificationDepth() const=0; + + /** + * Checks whether the any policy OID should be processed + * if it is included in a certificate. + * + * @return true iff the any policy OID should *not* be processed + */ + virtual bool isAnyPolicyInhibited() const=0; + + /** + * Checks if policy mapping is inhibited. + * + * @return true iff policy mapping should not be allowed + */ + virtual bool isPolicyMappingInhibited() const=0; + + /** + * Returns a set of policy OIDs. + * + * @return set of policy OIDs + */ + virtual const std::set& getPolicies() const=0; + + /** + * Returns a set of trust anchors. + * + * @return set of trust anchors + */ + virtual const std::vector& getTrustAnchors() const=0; + + enum revocation_t { + REVOCATION_OFF = 0, + REVOCATION_ENTITYONLY = 1, + REVOCATION_FULLCHAIN = 2 + }; + + /** + * Returns the type of revocation checking to perform. + * + * @return revocation checking option + */ + virtual revocation_t getRevocationChecking() const=0; + + /** + * Returns a set of CRLs. + * + * @return set of CRLs + */ + virtual const std::vector& getCRLs() const=0; + }; +}; + +#endif /* __xmltooling_pkixvalparam_h__ */ diff --git a/xmltooling/security/PathValidator.h b/xmltooling/security/PathValidator.h new file mode 100644 index 0000000..736eeb3 --- /dev/null +++ b/xmltooling/security/PathValidator.h @@ -0,0 +1,87 @@ +/** + * Licensed to the University Corporation for Advanced Internet + * Development, Inc. (UCAID) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + * + * UCAID licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the + * License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + */ + +/** + * @file xmltooling/security/PathValidator.h + * + * Plugin interface to certificate path validation. + */ + +#if !defined(__xmltooling_pathval_h__) && !defined(XMLTOOLING_NO_XMLSEC) +#define __xmltooling_pathval_h__ + +#include + +class XSECCryptoX509; + +namespace xmltooling { + + /** + * Plugin interface to certificate path validation, independent of context. + *

    This interface assumes that the end-entity certificate is "correctly" + * bound to a party, and solely addresses the validity of that certificate. + */ + class XMLTOOL_API PathValidator + { + MAKE_NONCOPYABLE(PathValidator); + protected: + PathValidator(); + + public: + virtual ~PathValidator(); + + /** + * Marker interface for plugin-specific parameters into the validation + * process. + */ + class XMLTOOL_API PathValidatorParams { + MAKE_NONCOPYABLE(PathValidatorParams); + protected: + PathValidatorParams(); + + public: + virtual ~PathValidatorParams(); + }; + + /** + * Validates an end-entity certificate. + * + * @param certEE end-entity certificate + * @param certChain the complete untrusted certificate chain + * @param params plugin-specific parameters to the validation process + * @return true iff validaton succeeds + */ + virtual bool validate( + XSECCryptoX509* certEE, + const std::vector& certChain, + const PathValidatorParams& params + ) const=0; + }; + + /** + * Registers PathValidator classes into the runtime. + */ + void XMLTOOL_API registerPathValidators(); + + /** PathValidator based on PKIX. */ + #define PKIX_PATHVALIDATOR "PKIX" +}; + +#endif /* __xmltooling_pathval_h__ */ diff --git a/xmltooling/security/impl/AbstractPKIXTrustEngine.cpp b/xmltooling/security/impl/AbstractPKIXTrustEngine.cpp index 5d7a598..dd450b6 100644 --- a/xmltooling/security/impl/AbstractPKIXTrustEngine.cpp +++ b/xmltooling/security/impl/AbstractPKIXTrustEngine.cpp @@ -34,6 +34,8 @@ #include "security/CredentialResolver.h" #include "security/KeyInfoResolver.h" #include "security/OpenSSLCryptoX509CRL.h" +#include "security/OpenSSLPathValidator.h" +#include "security/PKIXPathValidatorParams.h" #include "security/SecurityHelper.h" #include "security/X509Credential.h" #include "signature/SignatureValidator.h" @@ -41,8 +43,6 @@ #include "util/PathResolver.h" #include -#include -#include #include #include @@ -52,434 +52,65 @@ using namespace xmltooling; using namespace std; -namespace { - static int XMLTOOL_DLLLOCAL error_callback(int ok, X509_STORE_CTX* ctx) +namespace xmltooling { + // Adapter between TrustEngine and PathValidator + class XMLTOOL_DLLLOCAL PKIXParams : public PKIXPathValidatorParams { - if (!ok) - Category::getInstance("OpenSSL").error("path validation failure: %s", X509_verify_cert_error_string(ctx->error)); - return ok; - } - - static string XMLTOOL_DLLLOCAL X509_NAME_to_string(X509_NAME* n) - { - string s; - BIO* b = BIO_new(BIO_s_mem()); - X509_NAME_print_ex(b,n,0,XN_FLAG_RFC2253); - BIO_flush(b); - BUF_MEM* bptr=nullptr; - BIO_get_mem_ptr(b, &bptr); - if (bptr && bptr->length > 0) { - s.append(bptr->data, bptr->length); - } - BIO_free(b); - return s; - } - - static time_t XMLTOOL_DLLLOCAL getCRLTime(const ASN1_TIME *a) - { - struct tm t; - memset(&t, 0, sizeof(t)); - // RFC 5280, sections 5.1.2.4 and 5.1.2.5 require thisUpdate and nextUpdate - // to be encoded as UTCTime until 2049, and RFC 5280 section 4.1.2.5.1 - // further restricts the format to "YYMMDDHHMMSSZ" ("even where the number - // of seconds is zero"). - // As long as OpenSSL doesn't provide any API to convert ASN1_TIME values - // time_t, we therefore have to parse it ourselves, unfortunately. - if (sscanf((const char*)a->data, "%2d%2d%2d%2d%2d%2dZ", - &t.tm_year, &t.tm_mon, &t.tm_mday, - &t.tm_hour, &t.tm_min, &t.tm_sec) == 6) { - if (t.tm_year <= 50) { - // RFC 5280, section 4.1.2.5.1 - t.tm_year += 100; - } - t.tm_mon--; -#if defined(HAVE_TIMEGM) - return timegm(&t); -#else - // Windows, and hopefully most others...? - return mktime(&t) - timezone; -#endif - } - return (time_t)-1; - } - - static bool XMLTOOL_DLLLOCAL isFreshCRL(XSECCryptoX509CRL *c, Category* log=nullptr) - { - // eventually, these should be made configurable - #define MIN_SECS_REMAINING 86400 - #define MIN_PERCENT_REMAINING 10 - if (c) { - const X509_CRL* crl = static_cast(c)->getOpenSSLX509CRL(); - time_t thisUpdate = getCRLTime(X509_CRL_get_lastUpdate(crl)); - time_t nextUpdate = getCRLTime(X509_CRL_get_nextUpdate(crl)); - time_t now = time(nullptr); - - if (thisUpdate < 0 || nextUpdate < 0) { - // we failed to parse at least one of the fields (they were not encoded - // as required by RFC 5280, actually) - time_t exp = now + MIN_SECS_REMAINING; - if (log) { - log->warn("isFreshCRL (issuer '%s'): improperly encoded thisUpdate or nextUpdate field - falling back to simple time comparison", - (X509_NAME_to_string(X509_CRL_get_issuer(crl))).c_str()); - } - return (X509_cmp_time(X509_CRL_get_nextUpdate(crl), &exp) > 0) ? true : false; - } - else { - if (log && log->isDebugEnabled()) { - log->debug("isFreshCRL (issuer '%s'): %.0f seconds until nextUpdate (%3.2f%% elapsed since thisUpdate)", - (X509_NAME_to_string(X509_CRL_get_issuer(crl))).c_str(), - difftime(nextUpdate, now), (difftime(now, thisUpdate) * 100) / difftime(nextUpdate, thisUpdate)); - } - - // consider it recent enough if there are at least MIN_SECS_REMAINING - // to the nextUpdate, and at least MIN_PERCENT_REMAINING of its - // overall "validity" are remaining to the nextUpdate - return (now + MIN_SECS_REMAINING < nextUpdate) && - ((difftime(nextUpdate, now) * 100) / difftime(nextUpdate, thisUpdate) > MIN_PERCENT_REMAINING); - } - } - return false; - } - - static XSECCryptoX509CRL* XMLTOOL_DLLLOCAL getRemoteCRLs(const char* cdpuri, Category& log) { - // This is a temporary CRL cache implementation to avoid breaking binary compatibility - // for the library. Caching can't rely on any member objects within the TrustEngine, - // including locks, so we're using the global library lock for the time being. - // All other state is kept in the file system. - - // minimum number of seconds between re-attempting a download from one particular CRLDP - #define MIN_RETRY_WAIT 60 - - // The filenames for the CRL cache are based on a hash of the CRL location. - string cdpfile = SecurityHelper::doHash("SHA1", cdpuri, strlen(cdpuri)) + ".crl"; - XMLToolingConfig::getConfig().getPathResolver()->resolve(cdpfile, PathResolver::XMLTOOLING_RUN_FILE); - string cdpstaging = cdpfile + ".tmp"; - string tsfile = cdpfile + ".ts"; - - time_t now = time(nullptr); - vector crls; - - try { - // While holding the lock, check for a cached copy of the CRL, and remove "expired" ones. - Locker glock(&XMLToolingConfig::getConfig()); -#ifdef WIN32 - struct _stat stat_buf; - if (_stat(cdpfile.c_str(), &stat_buf) == 0) { -#else - struct stat stat_buf; - if (stat(cdpfile.c_str(), &stat_buf) == 0) { -#endif - SecurityHelper::loadCRLsFromFile(crls, cdpfile.c_str()); - if (crls.empty() || crls.front()->getProviderName() != DSIGConstants::s_unicodeStrPROVOpenSSL || - X509_cmp_time(X509_CRL_get_nextUpdate(static_cast(crls.front())->getOpenSSLX509CRL()), &now) < 0) { - for_each(crls.begin(), crls.end(), xmltooling::cleanup()); - crls.clear(); - remove(cdpfile.c_str()); // may as well delete the local copy - remove(tsfile.c_str()); - log.info("deleting cached CRL from %s with nextUpdate field in the past", cdpuri); - } + const AbstractPKIXTrustEngine& m_trust; + const AbstractPKIXTrustEngine::PKIXValidationInfoIterator& m_pkixInfo; + vector m_crls; + public: + PKIXParams( + const AbstractPKIXTrustEngine& t, + const AbstractPKIXTrustEngine::PKIXValidationInfoIterator& pkixInfo, + const vector* inlineCRLs + ) : m_trust(t), m_pkixInfo(pkixInfo) { + if (inlineCRLs && !inlineCRLs->empty()) { + m_crls = *inlineCRLs; + m_crls.insert(m_crls.end(), pkixInfo.getCRLs().begin(), pkixInfo.getCRLs().end()); } } - catch (exception& ex) { - log.error("exception loading cached copy of CRL from %s: %s", cdpuri, ex.what()); - } - if (crls.empty() || !isFreshCRL(crls.front(), &log)) { - bool updateTimestamp = true; - try { - // If we get here, the cached copy didn't exist yet, or it's time to refresh. - // To limit the rate of unsuccessful attempts when a CRLDP is unreachable, - // we remember the timestamp of the last attempt (both successful/unsuccessful). - // We store this in the file system because of the binary compatibility issue. - time_t ts = 0; - try { - Locker glock(&XMLToolingConfig::getConfig()); - ifstream tssrc(tsfile.c_str()); - if (tssrc) - tssrc >> ts; - } - catch (exception&) { - ts = 0; - } + virtual ~PKIXParams() {} - if (difftime(now, ts) > MIN_RETRY_WAIT) { - SOAPTransport::Address addr("AbstractPKIXTrustEngine", cdpuri, cdpuri); - string scheme(addr.m_endpoint, strchr(addr.m_endpoint,':') - addr.m_endpoint); - auto_ptr soap(XMLToolingConfig::getConfig().SOAPTransportManager.newPlugin(scheme.c_str(), addr)); - soap->send(); - istream& msg = soap->receive(); - Locker glock(&XMLToolingConfig::getConfig()); - ofstream out(cdpstaging.c_str(), fstream::trunc|fstream::binary); - out << msg.rdbuf(); - out.close(); - SecurityHelper::loadCRLsFromFile(crls, cdpstaging.c_str()); - if (crls.empty() || crls.front()->getProviderName() != DSIGConstants::s_unicodeStrPROVOpenSSL || - X509_cmp_time(X509_CRL_get_nextUpdate(static_cast(crls.front())->getOpenSSLX509CRL()), &now) < 0) { - // The "new" CRL wasn't usable, so get rid of it. - for_each(crls.begin(), crls.end(), xmltooling::cleanup()); - crls.clear(); - remove(cdpstaging.c_str()); - log.error("ignoring CRL retrieved from %s with nextUpdate field in the past", cdpuri); - } - else { - // "Commit" the new CRL. Note that we might add a CRL which doesn't pass - // isFreshCRL, but that's preferrable over adding none at all. - log.info("CRL refreshed from %s", cdpuri); - remove(cdpfile.c_str()); - if (rename(cdpstaging.c_str(), cdpfile.c_str()) != 0) - log.error("unable to rename CRL staging file"); - } - } - else { - updateTimestamp = false; // don't update if we're within the backoff window - } - } - catch (exception& ex) { - log.error("exception downloading/caching CRL from %s: %s", cdpuri, ex.what()); - } - - if (updateTimestamp) { - // update the timestamp file - Locker glock(&XMLToolingConfig::getConfig()); - ofstream tssink(tsfile.c_str(), fstream::trunc); - tssink << now; - tssink.close(); - } + int getVerificationDepth() const { + return m_pkixInfo.getVerificationDepth(); } - - if (crls.empty()) - return nullptr; - for_each(crls.begin() + 1, crls.end(), xmltooling::cleanup()); - return crls.front(); - } - - static bool XMLTOOL_DLLLOCAL validate( - X509* EE, - STACK_OF(X509)* untrusted, - AbstractPKIXTrustEngine::PKIXValidationInfoIterator* pkixInfo, - bool useCRL, - bool fullCRLChain, - const vector* inlineCRLs=nullptr, - bool policyMappingInhibit=false, - bool anyPolicyInhibit=false, - const set* policyOIDs=nullptr - ) - { - Category& log=Category::getInstance(XMLTOOLING_LOGCAT".TrustEngine"); - - // First we build a stack of CA certs. These objects are all referenced in place. - log.debug("supplying PKIX Validation information"); - - // We need this for CRL support. - X509_STORE* store=X509_STORE_new(); - if (!store) { - log_openssl(); - return false; + bool isAnyPolicyInhibited() const { + return m_trust.m_anyPolicyInhibit; } - - // PKIX policy checking (cf. RFCs 3280/5280 section 6) - if (policyMappingInhibit || anyPolicyInhibit || (policyOIDs && !policyOIDs->empty())) { -#if (OPENSSL_VERSION_NUMBER < 0x00908000L) - log.error("PKIX policy checking option is configured, but OpenSSL version is less than 0.9.8"); - X509_STORE_free(store); - return false; -#else - unsigned long pflags = 0; - X509_VERIFY_PARAM *vpm = X509_VERIFY_PARAM_new(); - if (!vpm) { - log_openssl(); - X509_STORE_free(store); - return false; - } - - // populate the "user-initial-policy-set" input variable - if (policyOIDs && !policyOIDs->empty()) { - for (set::const_iterator o=policyOIDs->begin(); o!=policyOIDs->end(); o++) { - ASN1_OBJECT *oid = OBJ_txt2obj(o->c_str(), 1); - if (oid && X509_VERIFY_PARAM_add0_policy(vpm, oid)) { - log.debug("OID (%s) added to set of acceptable policies", o->c_str()); - } - else { - log_openssl(); - log.error("unable to parse/configure policy OID value (%s)", o->c_str()); - if (oid) - ASN1_OBJECT_free(oid); - X509_VERIFY_PARAM_free(vpm); - X509_STORE_free(store); - return false; - } - } - // when the user has supplied at least one policy OID, he obviously wants to check - // for an explicit policy ("initial-explicit-policy") - pflags |= X509_V_FLAG_EXPLICIT_POLICY; - } - - // "initial-policy-mapping-inhibit" input variable - if (policyMappingInhibit) - pflags |= X509_V_FLAG_INHIBIT_MAP; - // "initial-any-policy-inhibit" input variable - if (anyPolicyInhibit) - pflags |= X509_V_FLAG_INHIBIT_ANY; - - if (!X509_VERIFY_PARAM_set_flags(vpm, pflags) || !X509_STORE_set1_param(store, vpm)) { - log_openssl(); - log.error("unable to set PKIX policy checking parameters"); - X509_VERIFY_PARAM_free(vpm); - X509_STORE_free(store); - return false; - } - - X509_VERIFY_PARAM_free(vpm); -#endif + bool isPolicyMappingInhibited() const { + return m_trust.m_policyMappingInhibit; } - - // This contains the state of the validate operation. - int count=0; - X509_STORE_CTX ctx; - - // AFAICT, EE and untrusted are passed in but not owned by the ctx. -#if (OPENSSL_VERSION_NUMBER >= 0x00907000L) - if (X509_STORE_CTX_init(&ctx,store,EE,untrusted)!=1) { - log_openssl(); - log.error("unable to initialize X509_STORE_CTX"); - X509_STORE_free(store); - return false; + const set& getPolicies() const { + return m_trust.m_policyOIDs; } -#else - X509_STORE_CTX_init(&ctx,store,EE,untrusted); -#endif - - STACK_OF(X509)* CAstack = sk_X509_new_null(); - const vector& CAcerts = pkixInfo->getTrustAnchors(); - for (vector::const_iterator i=CAcerts.begin(); i!=CAcerts.end(); ++i) { - if ((*i)->getProviderName()==DSIGConstants::s_unicodeStrPROVOpenSSL) { - sk_X509_push(CAstack,static_cast(*i)->getOpenSSLX509()); - ++count; - } + const vector& getTrustAnchors() const { + return m_pkixInfo.getTrustAnchors(); } - log.debug("supplied (%d) CA certificate(s)", count); - - // Seems to be most efficient to just pass in the CA stack. - X509_STORE_CTX_trusted_stack(&ctx,CAstack); - X509_STORE_CTX_set_depth(&ctx,100); // we check the depth down below - X509_STORE_CTX_set_verify_cb(&ctx,error_callback); - - // Do a first pass verify. If CRLs aren't used, this is the only pass. - int ret=X509_verify_cert(&ctx); - if (ret==1) { - // Now see if the depth was acceptable by counting the number of intermediates. - int depth=sk_X509_num(ctx.chain)-2; - if (pkixInfo->getVerificationDepth() < depth) { - log.error( - "certificate chain was too long (%d intermediates, only %d allowed)", - (depth==-1) ? 0 : depth, - pkixInfo->getVerificationDepth() - ); - ret=0; - } + PKIXPathValidatorParams::revocation_t getRevocationChecking() const { + if (m_trust.m_checkRevocation.empty() || m_trust.m_checkRevocation == "off") + return PKIXPathValidatorParams::REVOCATION_OFF; + else if (m_trust.m_checkRevocation == "entityOnly") + return PKIXPathValidatorParams::REVOCATION_ENTITYONLY; + else if (m_trust.m_checkRevocation == "fullChain") + return PKIXPathValidatorParams::REVOCATION_FULLCHAIN; + return PKIXPathValidatorParams::REVOCATION_OFF; } - - if (useCRL) { -#if (OPENSSL_VERSION_NUMBER >= 0x00907000L) - // When we add CRLs, we have to be sure the nextUpdate hasn't passed, because OpenSSL won't accept - // the CRL in that case. If we end up not adding a CRL for a particular link in the chain, the - // validation will fail (if the fullChain option was set). - set crlissuers; - time_t now = time(nullptr); - if (inlineCRLs) { - for (vector::const_iterator j=inlineCRLs->begin(); j!=inlineCRLs->end(); ++j) { - if ((*j)->getProviderName()==DSIGConstants::s_unicodeStrPROVOpenSSL && - (X509_cmp_time(X509_CRL_get_nextUpdate(static_cast(*j)->getOpenSSLX509CRL()), &now) > 0)) { - // owned by store - X509_STORE_add_crl(store, X509_CRL_dup(static_cast(*j)->getOpenSSLX509CRL())); - string crlissuer(X509_NAME_to_string(X509_CRL_get_issuer(static_cast(*j)->getOpenSSLX509CRL()))); - if (!crlissuer.empty()) { - log.debug("added inline CRL issued by (%s)", crlissuer.c_str()); - crlissuers.insert(crlissuer); - } - } - } - } - const vector& crls = pkixInfo->getCRLs(); - for (vector::const_iterator j=crls.begin(); j!=crls.end(); ++j) { - if ((*j)->getProviderName()==DSIGConstants::s_unicodeStrPROVOpenSSL && - (X509_cmp_time(X509_CRL_get_nextUpdate(static_cast(*j)->getOpenSSLX509CRL()), &now) > 0)) { - // owned by store - X509_STORE_add_crl(store, X509_CRL_dup(static_cast(*j)->getOpenSSLX509CRL())); - string crlissuer(X509_NAME_to_string(X509_CRL_get_issuer(static_cast(*j)->getOpenSSLX509CRL()))); - if (!crlissuer.empty()) { - log.debug("added CRL issued by (%s)", crlissuer.c_str()); - crlissuers.insert(crlissuer); - } - } - } - - for (int i = 0; i < sk_X509_num(untrusted); ++i) { - X509 *cert = sk_X509_value(untrusted, i); - string crlissuer(X509_NAME_to_string(X509_get_issuer_name(cert))); - if (crlissuers.count(crlissuer)) { - // We already have a CRL for this cert, so skip CRLDP processing for this one. - continue; - } - - bool foundUsableCDP = false; - STACK_OF(DIST_POINT)* dps = (STACK_OF(DIST_POINT)*)X509_get_ext_d2i(cert, NID_crl_distribution_points, nullptr, nullptr); - for (int ii = 0; !foundUsableCDP && ii < sk_DIST_POINT_num(dps); ++ii) { - DIST_POINT* dp = sk_DIST_POINT_value(dps, ii); - if (!dp->distpoint || dp->distpoint->type != 0) - continue; - for (int iii = 0; !foundUsableCDP && iii < sk_GENERAL_NAME_num(dp->distpoint->name.fullname); ++iii) { - GENERAL_NAME* gen = sk_GENERAL_NAME_value(dp->distpoint->name.fullname, iii); - // Only consider HTTP URIs, and stop after the first one we find. -#ifdef HAVE_STRCASECMP - if (gen->type == GEN_URI && (!strncasecmp((const char*)gen->d.ia5->data, "http:", 5))) { -#else - if (gen->type == GEN_URI && (!strnicmp((const char*)gen->d.ia5->data, "http:", 5))) { -#endif - const char* cdpuri = (const char*)gen->d.ia5->data; - auto_ptr crl(getRemoteCRLs(cdpuri, log)); - if (crl.get() && crl->getProviderName()==DSIGConstants::s_unicodeStrPROVOpenSSL && - (isFreshCRL(crl.get()) || (ii == sk_DIST_POINT_num(dps)-1 && iii == sk_GENERAL_NAME_num(dp->distpoint->name.fullname)-1))) { - // owned by store - X509_STORE_add_crl(store, X509_CRL_dup(static_cast(crl.get())->getOpenSSLX509CRL())); - log.debug("added CRL issued by (%s)", crlissuer.c_str()); - crlissuers.insert(crlissuer); - foundUsableCDP = true; - } - } - } - } - sk_DIST_POINT_free(dps); - } - - // Do a second pass verify with CRLs in place. - X509_STORE_CTX_set_flags(&ctx, fullCRLChain ? (X509_V_FLAG_CRL_CHECK|X509_V_FLAG_CRL_CHECK_ALL) : (X509_V_FLAG_CRL_CHECK)); - ret=X509_verify_cert(&ctx); -#else - log.warn("CRL checking is enabled, but OpenSSL version is too old"); - ret = 0; -#endif + const vector& getCRLs() const { + return m_crls.empty() ? m_pkixInfo.getCRLs() : m_crls; } + }; - // Clean up... - X509_STORE_CTX_cleanup(&ctx); - X509_STORE_free(store); - sk_X509_free(CAstack); - - if (ret==1) { - log.debug("successfully validated certificate chain"); - return true; - } - - return false; - } static XMLCh fullCRLChain[] = UNICODE_LITERAL_12(f,u,l,l,C,R,L,C,h,a,i,n); static XMLCh checkRevocation[] = UNICODE_LITERAL_15(c,h,e,c,k,R,e,v,o,c,a,t,i,o,n); static XMLCh policyMappingInhibit[] = UNICODE_LITERAL_20(p,o,l,i,c,y,M,a,p,p,i,n,g,I,n,h,i,b,i,t); static XMLCh anyPolicyInhibit[] = UNICODE_LITERAL_16(a,n,y,P,o,l,i,c,y,I,n,h,i,b,i,t); + static XMLCh _PathValidator[] = UNICODE_LITERAL_13(P,a,t,h,V,a,l,i,d,a,t,o,r); static XMLCh PolicyOID[] = UNICODE_LITERAL_9(P,o,l,i,c,y,O,I,D); static XMLCh TrustedName[] = UNICODE_LITERAL_11(T,r,u,s,t,e,d,N,a,m,e); + static XMLCh type[] = UNICODE_LITERAL_4(t,y,p,e); }; AbstractPKIXTrustEngine::PKIXValidationInfoIterator::PKIXValidationInfoIterator() @@ -518,12 +149,43 @@ AbstractPKIXTrustEngine::AbstractPKIXTrustEngine(const xercesc::DOMElement* e) m_trustedNames.insert(v.get()); } } + else if (XMLString::equals(c->getLocalName(), _PathValidator)) { + try { + string t = XMLHelper::getAttrString(c, nullptr, type); + if (!t.empty()) { + Category::getInstance(XMLTOOLING_LOGCAT".TrustEngine.PKIX").info( + "building PathValidator of type %s", t.c_str() + ); + PathValidator* pv = XMLToolingConfig::getConfig().PathValidatorManager.newPlugin(t.c_str(), c); + OpenSSLPathValidator* ospv = dynamic_cast(pv); + if (!ospv) { + delete pv; + throw XMLSecurityException("PathValidator doesn't support OpenSSL interface."); + } + m_pathValidators.push_back(ospv); + } + } + catch (exception& ex) { + Category::getInstance(XMLTOOLING_LOGCAT".TrustEngine.PKIX").error( + "error building PathValidator: %s", ex.what() + ); + } + } c = XMLHelper::getNextSiblingElement(c); } + + if (m_pathValidators.empty()) { + m_pathValidators.push_back( + dynamic_cast( + XMLToolingConfig::getConfig().PathValidatorManager.newPlugin(PKIX_PATHVALIDATOR, e) + ) + ); + } } AbstractPKIXTrustEngine::~AbstractPKIXTrustEngine() { + for_each(m_pathValidators.begin(), m_pathValidators.end(), xmltooling::cleanup()); } bool AbstractPKIXTrustEngine::checkEntityNames( @@ -679,7 +341,7 @@ bool AbstractPKIXTrustEngine::validateWithCRLs( STACK_OF(X509)* certChain, const CredentialResolver& credResolver, CredentialCriteria* criteria, - const std::vector* inlineCRLs + const vector* inlineCRLs ) const { #ifdef _DEBUG @@ -715,18 +377,11 @@ bool AbstractPKIXTrustEngine::validateWithCRLs( auto_ptr pkix(getPKIXValidationInfoIterator(credResolver, criteria)); while (pkix->next()) { - if (::validate( - certEE, - certChain, - pkix.get(), - (m_checkRevocation=="entityOnly" || m_checkRevocation=="fullChain"), - (m_checkRevocation=="fullChain"), - (m_checkRevocation=="entityOnly" || m_checkRevocation=="fullChain") ? inlineCRLs : nullptr, - m_policyMappingInhibit, - m_anyPolicyInhibit, - &m_policyOIDs - )) { - return true; + PKIXParams params(*this, *pkix.get(), inlineCRLs); + for (vector::const_iterator v = m_pathValidators.begin(); v != m_pathValidators.end(); ++v) { + if ((*v)->validate(certEE, certChain, params)) { + return true; + } } } diff --git a/xmltooling/security/impl/PKIXPathValidator.cpp b/xmltooling/security/impl/PKIXPathValidator.cpp new file mode 100644 index 0000000..a142aa0 --- /dev/null +++ b/xmltooling/security/impl/PKIXPathValidator.cpp @@ -0,0 +1,546 @@ +/** + * Licensed to the University Corporation for Advanced Internet + * Development, Inc. (UCAID) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + * + * UCAID licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the + * License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + */ + +/** + * PKIXPathValidator.cpp + * + * A path validator based on PKIX support in OpenSSL. + */ + +#include "internal.h" +#include "logging.h" +#include "security/OpenSSLPathValidator.h" +#include "security/OpenSSLCryptoX509CRL.h" +#include "security/PKIXPathValidatorParams.h" +#include "security/SecurityHelper.h" +#include "util/NDC.h" +#include "util/PathResolver.h" +#include "util/Threads.h" +#include "util/XMLHelper.h" + +#include +#include +#include +#include +#include + +using namespace xmltooling::logging; +using namespace xmltooling; +using namespace std; + + +namespace { + static int XMLTOOL_DLLLOCAL error_callback(int ok, X509_STORE_CTX* ctx) + { + if (!ok) + Category::getInstance("OpenSSL").error("path validation failure: %s", X509_verify_cert_error_string(ctx->error)); + return ok; + } + + static string XMLTOOL_DLLLOCAL X509_NAME_to_string(X509_NAME* n) + { + string s; + BIO* b = BIO_new(BIO_s_mem()); + X509_NAME_print_ex(b,n,0,XN_FLAG_RFC2253); + BIO_flush(b); + BUF_MEM* bptr=nullptr; + BIO_get_mem_ptr(b, &bptr); + if (bptr && bptr->length > 0) { + s.append(bptr->data, bptr->length); + } + BIO_free(b); + return s; + } + + static time_t XMLTOOL_DLLLOCAL getCRLTime(const ASN1_TIME *a) + { + struct tm t; + memset(&t, 0, sizeof(t)); + // RFC 5280, sections 5.1.2.4 and 5.1.2.5 require thisUpdate and nextUpdate + // to be encoded as UTCTime until 2049, and RFC 5280 section 4.1.2.5.1 + // further restricts the format to "YYMMDDHHMMSSZ" ("even where the number + // of seconds is zero"). + // As long as OpenSSL doesn't provide any API to convert ASN1_TIME values + // time_t, we therefore have to parse it ourselves, unfortunately. + if (sscanf((const char*)a->data, "%2d%2d%2d%2d%2d%2dZ", + &t.tm_year, &t.tm_mon, &t.tm_mday, + &t.tm_hour, &t.tm_min, &t.tm_sec) == 6) { + if (t.tm_year <= 50) { + // RFC 5280, section 4.1.2.5.1 + t.tm_year += 100; + } + t.tm_mon--; +#if defined(HAVE_TIMEGM) + return timegm(&t); +#else + // Windows, and hopefully most others...? + return mktime(&t) - timezone; +#endif + } + return (time_t)-1; + } + + static const XMLCh minRefreshDelay[] = UNICODE_LITERAL_15(m,i,n,R,e,f,r,e,s,h,D,e,l,a,y); + static const XMLCh minSecondsRemaining[] = UNICODE_LITERAL_19(m,i,n,S,e,c,o,n,d,s,R,e,m,a,i,n,i,n,g); + static const XMLCh minPercentRemaining[] = UNICODE_LITERAL_19(m,i,n,P,e,r,c,e,n,t,R,e,m,a,i,n,i,n,g); +}; + +namespace xmltooling { + + class XMLTOOL_DLLLOCAL PKIXPathValidator : public OpenSSLPathValidator + { + public: + PKIXPathValidator(const xercesc::DOMElement* e) + : m_log(Category::getInstance(XMLTOOLING_LOGCAT".PathValidator.PKIX")), + m_lock(XMLToolingConfig::getConfig().getNamedMutex(XMLTOOLING_LOGCAT".PathValidator.PKIX")), + m_minRefreshDelay(XMLHelper::getAttrInt(e, 60, minRefreshDelay)), + m_minSecondsRemaining(XMLHelper::getAttrInt(e, 86400, minSecondsRemaining)), + m_minPercentRemaining(XMLHelper::getAttrInt(e, 10, minPercentRemaining)) { + } + + virtual ~PKIXPathValidator() {} + + bool validate( + XSECCryptoX509* certEE, const vector& certChain, const PathValidatorParams& params + ) const; + bool validate( + X509* certEE, STACK_OF(X509)* certChain, const PathValidatorParams& params + ) const; + + private: + XSECCryptoX509CRL* getRemoteCRLs(const char* cdpuri) const; + bool isFreshCRL(XSECCryptoX509CRL *c, Category* log=nullptr) const; + + Category& m_log; + Mutex& m_lock; + time_t m_minRefreshDelay,m_minSecondsRemaining; + unsigned short m_minPercentRemaining; + + static map m_crlUpdateMap; + }; + + PathValidator* XMLTOOL_DLLLOCAL PKIXPathValidatorFactory(const xercesc::DOMElement* const & e) + { + return new PKIXPathValidator(e); + } + +}; + +map PKIXPathValidator::m_crlUpdateMap; + +void XMLTOOL_API xmltooling::registerPathValidators() +{ + XMLToolingConfig& conf=XMLToolingConfig::getConfig(); + conf.PathValidatorManager.registerFactory(PKIX_PATHVALIDATOR, PKIXPathValidatorFactory); +} + +PathValidator::PathValidator() +{ +} + +PathValidator::~PathValidator() +{ +} + +PathValidator::PathValidatorParams::PathValidatorParams() +{ +} + +PathValidator::PathValidatorParams::~PathValidatorParams() +{ +} + +PKIXPathValidatorParams::PKIXPathValidatorParams() +{ +} + +PKIXPathValidatorParams::~PKIXPathValidatorParams() +{ +} + +OpenSSLPathValidator::OpenSSLPathValidator() +{ +} + +OpenSSLPathValidator::~OpenSSLPathValidator() +{ +} + +bool PKIXPathValidator::validate( + XSECCryptoX509* certEE, const vector& certChain, const PathValidatorParams& params + ) const +{ + if (certEE->getProviderName()!=DSIGConstants::s_unicodeStrPROVOpenSSL) { + m_log.error("only the OpenSSL XSEC provider is supported"); + return false; + } + + STACK_OF(X509)* untrusted=sk_X509_new_null(); + for (vector::const_iterator i=certChain.begin(); i!=certChain.end(); ++i) + sk_X509_push(untrusted,static_cast(*i)->getOpenSSLX509()); + + bool ret = validate(static_cast(certEE)->getOpenSSLX509(), untrusted, params); + sk_X509_free(untrusted); + return ret; +} + +bool PKIXPathValidator::validate(X509* EE, STACK_OF(X509)* untrusted, const PathValidatorParams& params) const +{ +#ifdef _DEBUG + NDC ndc("validate"); +#endif + + const PKIXPathValidatorParams* pkixParams = dynamic_cast(¶ms); + if (!pkixParams) { + m_log.error("input parameters were of incorrect type"); + return false; + } + + // First we build a stack of CA certs. These objects are all referenced in place. + m_log.debug("supplying PKIX Validation information"); + + // We need this for CRL support. + X509_STORE* store=X509_STORE_new(); + if (!store) { + log_openssl(); + return false; + } + + // PKIX policy checking (cf. RFCs 3280/5280 section 6) + if (pkixParams->isPolicyMappingInhibited() || pkixParams->isAnyPolicyInhibited() || (!pkixParams->getPolicies().empty())) { +#if (OPENSSL_VERSION_NUMBER < 0x00908000L) + m_log.error("PKIX policy checking option is configured, but OpenSSL version is less than 0.9.8"); + X509_STORE_free(store); + return false; +#else + unsigned long pflags = 0; + X509_VERIFY_PARAM *vpm = X509_VERIFY_PARAM_new(); + if (!vpm) { + log_openssl(); + X509_STORE_free(store); + return false; + } + + // populate the "user-initial-policy-set" input variable + const set& policies = pkixParams->getPolicies(); + if (!policies.empty()) { + for (set::const_iterator o=policies.begin(); o!=policies.end(); o++) { + ASN1_OBJECT *oid = OBJ_txt2obj(o->c_str(), 1); + if (oid && X509_VERIFY_PARAM_add0_policy(vpm, oid)) { + m_log.debug("OID (%s) added to set of acceptable policies", o->c_str()); + } + else { + log_openssl(); + m_log.error("unable to parse/configure policy OID value (%s)", o->c_str()); + if (oid) + ASN1_OBJECT_free(oid); + X509_VERIFY_PARAM_free(vpm); + X509_STORE_free(store); + return false; + } + } + // when the user has supplied at least one policy OID, he obviously wants to check + // for an explicit policy ("initial-explicit-policy") + pflags |= X509_V_FLAG_EXPLICIT_POLICY; + } + + // "initial-policy-mapping-inhibit" input variable + if (pkixParams->isPolicyMappingInhibited()) + pflags |= X509_V_FLAG_INHIBIT_MAP; + // "initial-any-policy-inhibit" input variable + if (pkixParams->isAnyPolicyInhibited()) + pflags |= X509_V_FLAG_INHIBIT_ANY; + + if (!X509_VERIFY_PARAM_set_flags(vpm, pflags) || !X509_STORE_set1_param(store, vpm)) { + log_openssl(); + m_log.error("unable to set PKIX policy checking parameters"); + X509_VERIFY_PARAM_free(vpm); + X509_STORE_free(store); + return false; + } + + X509_VERIFY_PARAM_free(vpm); +#endif + } + + // This contains the state of the validate operation. + int count=0; + X509_STORE_CTX ctx; + + // AFAICT, EE and untrusted are passed in but not owned by the ctx. +#if (OPENSSL_VERSION_NUMBER >= 0x00907000L) + if (X509_STORE_CTX_init(&ctx,store,EE,untrusted)!=1) { + log_openssl(); + m_log.error("unable to initialize X509_STORE_CTX"); + X509_STORE_free(store); + return false; + } +#else + X509_STORE_CTX_init(&ctx,store,EE,untrusted); +#endif + + STACK_OF(X509)* CAstack = sk_X509_new_null(); + const vector& CAcerts = pkixParams->getTrustAnchors(); + for (vector::const_iterator i=CAcerts.begin(); i!=CAcerts.end(); ++i) { + if ((*i)->getProviderName()==DSIGConstants::s_unicodeStrPROVOpenSSL) { + sk_X509_push(CAstack,static_cast(*i)->getOpenSSLX509()); + ++count; + } + } + m_log.debug("supplied (%d) CA certificate(s)", count); + + // Seems to be most efficient to just pass in the CA stack. + X509_STORE_CTX_trusted_stack(&ctx,CAstack); + X509_STORE_CTX_set_depth(&ctx,100); // we check the depth down below + X509_STORE_CTX_set_verify_cb(&ctx,error_callback); + + // Do a first pass verify. If CRLs aren't used, this is the only pass. + int ret=X509_verify_cert(&ctx); + if (ret==1) { + // Now see if the depth was acceptable by counting the number of intermediates. + int depth=sk_X509_num(ctx.chain)-2; + if (pkixParams->getVerificationDepth() < depth) { + m_log.error( + "certificate chain was too long (%d intermediates, only %d allowed)", + (depth==-1) ? 0 : depth, + pkixParams->getVerificationDepth() + ); + ret=0; + } + } + + if (pkixParams->getRevocationChecking() != PKIXPathValidatorParams::REVOCATION_OFF) { +#if (OPENSSL_VERSION_NUMBER >= 0x00907000L) + // When we add CRLs, we have to be sure the nextUpdate hasn't passed, because OpenSSL won't accept + // the CRL in that case. If we end up not adding a CRL for a particular link in the chain, the + // validation will fail (if the fullChain option was set). + set crlissuers; + time_t now = time(nullptr); + + const vector& crls = pkixParams->getCRLs(); + for (vector::const_iterator j=crls.begin(); j!=crls.end(); ++j) { + if ((*j)->getProviderName()==DSIGConstants::s_unicodeStrPROVOpenSSL && + (X509_cmp_time(X509_CRL_get_nextUpdate(static_cast(*j)->getOpenSSLX509CRL()), &now) > 0)) { + // owned by store + X509_STORE_add_crl(store, X509_CRL_dup(static_cast(*j)->getOpenSSLX509CRL())); + string crlissuer(X509_NAME_to_string(X509_CRL_get_issuer(static_cast(*j)->getOpenSSLX509CRL()))); + if (!crlissuer.empty()) { + m_log.debug("added CRL issued by (%s)", crlissuer.c_str()); + crlissuers.insert(crlissuer); + } + } + } + + for (int i = 0; i < sk_X509_num(untrusted); ++i) { + X509 *cert = sk_X509_value(untrusted, i); + string crlissuer(X509_NAME_to_string(X509_get_issuer_name(cert))); + if (crlissuers.count(crlissuer)) { + // We already have a CRL for this cert, so skip CRLDP processing for this one. + continue; + } + + bool foundUsableCDP = false; + STACK_OF(DIST_POINT)* dps = (STACK_OF(DIST_POINT)*)X509_get_ext_d2i(cert, NID_crl_distribution_points, nullptr, nullptr); + for (int ii = 0; !foundUsableCDP && ii < sk_DIST_POINT_num(dps); ++ii) { + DIST_POINT* dp = sk_DIST_POINT_value(dps, ii); + if (!dp->distpoint || dp->distpoint->type != 0) + continue; + for (int iii = 0; !foundUsableCDP && iii < sk_GENERAL_NAME_num(dp->distpoint->name.fullname); ++iii) { + GENERAL_NAME* gen = sk_GENERAL_NAME_value(dp->distpoint->name.fullname, iii); + // Only consider HTTP URIs, and stop after the first one we find. +#ifdef HAVE_STRCASECMP + if (gen->type == GEN_URI && (!strncasecmp((const char*)gen->d.ia5->data, "http:", 5))) { +#else + if (gen->type == GEN_URI && (!strnicmp((const char*)gen->d.ia5->data, "http:", 5))) { +#endif + const char* cdpuri = (const char*)gen->d.ia5->data; + auto_ptr crl(getRemoteCRLs(cdpuri)); + if (crl.get() && crl->getProviderName()==DSIGConstants::s_unicodeStrPROVOpenSSL && + (isFreshCRL(crl.get()) || (ii == sk_DIST_POINT_num(dps)-1 && iii == sk_GENERAL_NAME_num(dp->distpoint->name.fullname)-1))) { + // owned by store + X509_STORE_add_crl(store, X509_CRL_dup(static_cast(crl.get())->getOpenSSLX509CRL())); + m_log.debug("added CRL issued by (%s)", crlissuer.c_str()); + crlissuers.insert(crlissuer); + foundUsableCDP = true; + } + } + } + } + sk_DIST_POINT_free(dps); + } + + // Do a second pass verify with CRLs in place. + if (pkixParams->getRevocationChecking() == PKIXPathValidatorParams::REVOCATION_FULLCHAIN) + X509_STORE_CTX_set_flags(&ctx, X509_V_FLAG_CRL_CHECK|X509_V_FLAG_CRL_CHECK_ALL); + else + X509_STORE_CTX_set_flags(&ctx, X509_V_FLAG_CRL_CHECK); + ret=X509_verify_cert(&ctx); +#else + m_log.warn("CRL checking is enabled, but OpenSSL version is too old"); + ret = 0; +#endif + } + + // Clean up... + X509_STORE_CTX_cleanup(&ctx); + X509_STORE_free(store); + sk_X509_free(CAstack); + + if (ret==1) { + m_log.debug("successfully validated certificate chain"); + return true; + } + + return false; +} + +XSECCryptoX509CRL* PKIXPathValidator::getRemoteCRLs(const char* cdpuri) const +{ + // This is a filesystem-based CRL cache using a shared lock across all instances + // of this class. + + // The filenames for the CRL cache are based on a hash of the CRL location. + string cdpfile = SecurityHelper::doHash("SHA1", cdpuri, strlen(cdpuri)) + ".crl"; + XMLToolingConfig::getConfig().getPathResolver()->resolve(cdpfile, PathResolver::XMLTOOLING_RUN_FILE); + string cdpstaging = cdpfile + ".tmp"; + + time_t now = time(nullptr); + vector crls; + + try { + // While holding the lock, check for a cached copy of the CRL, and remove "expired" ones. + Lock glock(m_lock); +#ifdef WIN32 + struct _stat stat_buf; + if (_stat(cdpfile.c_str(), &stat_buf) == 0) { +#else + struct stat stat_buf; + if (stat(cdpfile.c_str(), &stat_buf) == 0) { +#endif + SecurityHelper::loadCRLsFromFile(crls, cdpfile.c_str()); + if (crls.empty() || crls.front()->getProviderName() != DSIGConstants::s_unicodeStrPROVOpenSSL || + X509_cmp_time(X509_CRL_get_nextUpdate(static_cast(crls.front())->getOpenSSLX509CRL()), &now) < 0) { + for_each(crls.begin(), crls.end(), xmltooling::cleanup()); + crls.clear(); + remove(cdpfile.c_str()); // may as well delete the local copy + m_crlUpdateMap.erase(cdpuri); + m_log.info("deleting cached CRL from %s with nextUpdate field in the past", cdpuri); + } + } + } + catch (exception& ex) { + m_log.error("exception loading cached copy of CRL from %s: %s", cdpuri, ex.what()); + } + + if (crls.empty() || !isFreshCRL(crls.front(), &m_log)) { + bool updateTimestamp = true; + try { + // If we get here, the cached copy didn't exist yet, or it's time to refresh. + // To limit the rate of unsuccessful attempts when a CRLDP is unreachable, + // we remember the timestamp of the last attempt (both successful/unsuccessful). + time_t ts = 0; + m_lock.lock(); + map::const_iterator tsit = m_crlUpdateMap.find(cdpuri); + if (tsit != m_crlUpdateMap.end()) + ts = tsit->second; + m_lock.unlock(); + + if (difftime(now, ts) > m_minRefreshDelay) { + SOAPTransport::Address addr("AbstractPKIXTrustEngine", cdpuri, cdpuri); + string scheme(addr.m_endpoint, strchr(addr.m_endpoint,':') - addr.m_endpoint); + auto_ptr soap(XMLToolingConfig::getConfig().SOAPTransportManager.newPlugin(scheme.c_str(), addr)); + soap->send(); + istream& msg = soap->receive(); + Lock glock(m_lock); + ofstream out(cdpstaging.c_str(), fstream::trunc|fstream::binary); + out << msg.rdbuf(); + out.close(); + SecurityHelper::loadCRLsFromFile(crls, cdpstaging.c_str()); + if (crls.empty() || crls.front()->getProviderName() != DSIGConstants::s_unicodeStrPROVOpenSSL || + X509_cmp_time(X509_CRL_get_nextUpdate(static_cast(crls.front())->getOpenSSLX509CRL()), &now) < 0) { + // The "new" CRL wasn't usable, so get rid of it. + for_each(crls.begin(), crls.end(), xmltooling::cleanup()); + crls.clear(); + remove(cdpstaging.c_str()); + m_log.error("ignoring CRL retrieved from %s with nextUpdate field in the past", cdpuri); + } + else { + // "Commit" the new CRL. Note that we might add a CRL which doesn't pass + // isFreshCRL, but that's preferrable over adding none at all. + m_log.info("CRL refreshed from %s", cdpuri); + remove(cdpfile.c_str()); + if (rename(cdpstaging.c_str(), cdpfile.c_str()) != 0) + m_log.error("unable to rename CRL staging file"); + } + } + else { + updateTimestamp = false; // don't update if we're within the backoff window + } + } + catch (exception& ex) { + m_log.error("exception downloading/caching CRL from %s: %s", cdpuri, ex.what()); + } + + if (updateTimestamp) { + Lock glock(m_lock); + m_crlUpdateMap[cdpuri] = now; + } + } + + if (crls.empty()) + return nullptr; + for_each(crls.begin() + 1, crls.end(), xmltooling::cleanup()); + return crls.front(); +} + +bool PKIXPathValidator::isFreshCRL(XSECCryptoX509CRL *c, Category* log) const +{ + if (c) { + const X509_CRL* crl = static_cast(c)->getOpenSSLX509CRL(); + time_t thisUpdate = getCRLTime(X509_CRL_get_lastUpdate(crl)); + time_t nextUpdate = getCRLTime(X509_CRL_get_nextUpdate(crl)); + time_t now = time(nullptr); + + if (thisUpdate < 0 || nextUpdate < 0) { + // we failed to parse at least one of the fields (they were not encoded + // as required by RFC 5280, actually) + time_t exp = now + m_minSecondsRemaining; + if (log) { + log->warn("isFreshCRL (issuer '%s'): improperly encoded thisUpdate or nextUpdate field - falling back to simple time comparison", + (X509_NAME_to_string(X509_CRL_get_issuer(crl))).c_str()); + } + return (X509_cmp_time(X509_CRL_get_nextUpdate(crl), &exp) > 0) ? true : false; + } + else { + if (log && log->isDebugEnabled()) { + log->debug("isFreshCRL (issuer '%s'): %.0f seconds until nextUpdate (%3.2f%% elapsed since thisUpdate)", + (X509_NAME_to_string(X509_CRL_get_issuer(crl))).c_str(), + difftime(nextUpdate, now), (difftime(now, thisUpdate) * 100) / difftime(nextUpdate, thisUpdate)); + } + + // consider it recent enough if there are at least MIN_SECS_REMAINING + // to the nextUpdate, and at least MIN_PERCENT_REMAINING of its + // overall "validity" are remaining to the nextUpdate + return (now + m_minSecondsRemaining < nextUpdate) && + ((difftime(nextUpdate, now) * 100) / difftime(nextUpdate, thisUpdate) > m_minPercentRemaining); + } + } + return false; +} diff --git a/xmltooling/soap/SOAPTransport.h b/xmltooling/soap/SOAPTransport.h index 48c67bb..8969808 100644 --- a/xmltooling/soap/SOAPTransport.h +++ b/xmltooling/soap/SOAPTransport.h @@ -235,7 +235,6 @@ namespace xmltooling { virtual long getStatusCode() const; }; -#ifndef XMLTOOLING_NO_XMLSEC /** * Registers SOAPTransport classes into the runtime. */ @@ -250,7 +249,6 @@ namespace xmltooling { * Notifies transport infrastructure to shutdown. */ void XMLTOOL_API termSOAPTransports(); -#endif }; diff --git a/xmltooling/soap/impl/CURLSOAPTransport.cpp b/xmltooling/soap/impl/CURLSOAPTransport.cpp index 60d6238..e6a2c66 100644 --- a/xmltooling/soap/impl/CURLSOAPTransport.cpp +++ b/xmltooling/soap/impl/CURLSOAPTransport.cpp @@ -247,13 +247,6 @@ namespace xmltooling { } }; -void xmltooling::registerSOAPTransports() -{ - XMLToolingConfig& conf=XMLToolingConfig::getConfig(); - conf.SOAPTransportManager.registerFactory("http", CURLSOAPTransportFactory); - conf.SOAPTransportManager.registerFactory("https", CURLSOAPTransportFactory); -} - void xmltooling::initSOAPTransports() { g_CURLPool=new CURLPool(); @@ -265,14 +258,6 @@ void xmltooling::termSOAPTransports() g_CURLPool = nullptr; } -OpenSSLSOAPTransport::OpenSSLSOAPTransport() -{ -} - -OpenSSLSOAPTransport::~OpenSSLSOAPTransport() -{ -} - CURLPool::~CURLPool() { for (poolmap_t::iterator i=m_bindingMap.begin(); i!=m_bindingMap.end(); i++) { diff --git a/xmltooling/soap/impl/SOAPClient.cpp b/xmltooling/soap/impl/SOAPClient.cpp index b3f7c6d..01a1789 100644 --- a/xmltooling/soap/impl/SOAPClient.cpp +++ b/xmltooling/soap/impl/SOAPClient.cpp @@ -28,6 +28,7 @@ #include "exceptions.h" #include "logging.h" #include "soap/HTTPSOAPTransport.h" +#include "soap/OpenSSLSOAPTransport.h" #include "soap/SOAP.h" #include "soap/SOAPClient.h" #include "util/XMLHelper.h" @@ -41,6 +42,32 @@ using namespace xmltooling; using namespace xercesc; using namespace std; +#if !defined(XMLTOOLING_NO_XMLSEC) && !defined(XMLTOOLING_LITE) +namespace xmltooling { + PluginManager::Factory CURLSOAPTransportFactory; +}; +#endif + +void xmltooling::registerSOAPTransports() +{ +#if !defined(XMLTOOLING_NO_XMLSEC) && !defined(XMLTOOLING_LITE) + XMLToolingConfig& conf=XMLToolingConfig::getConfig(); + conf.SOAPTransportManager.registerFactory("http", CURLSOAPTransportFactory); + conf.SOAPTransportManager.registerFactory("https", CURLSOAPTransportFactory); +#endif +} + + +#ifdef XMLTOOLING_NO_XMLSEC +void xmltooling::initSOAPTransports() +{ +} + +void xmltooling::termSOAPTransports() +{ +} +#endif + SOAPTransport::SOAPTransport() { } @@ -84,6 +111,16 @@ bool HTTPSOAPTransport::followRedirects(bool follow, unsigned int maxRedirs) return false; } +#ifndef XMLTOOLING_NO_XMLSEC +OpenSSLSOAPTransport::OpenSSLSOAPTransport() +{ +} + +OpenSSLSOAPTransport::~OpenSSLSOAPTransport() +{ +} +#endif + SOAPClient::SOAPClient(bool validate) : m_validate(validate), m_transport(nullptr) { } diff --git a/xmltooling/util/Threads.h b/xmltooling/util/Threads.h index abb8c10..959031d 100644 --- a/xmltooling/util/Threads.h +++ b/xmltooling/util/Threads.h @@ -296,6 +296,15 @@ namespace xmltooling } /** + * Locks and wraps the designated mutex. + * + * @param mtx mutex to lock + */ + Lock(Mutex& mtx) : mutex(&mtx) { + mtx.lock(); + } + + /** * Unlocks the wrapped mutex. */ ~Lock() { diff --git a/xmltooling/xmltooling.vcxproj b/xmltooling/xmltooling.vcxproj index 2e204be..b508703 100644 --- a/xmltooling/xmltooling.vcxproj +++ b/xmltooling/xmltooling.vcxproj @@ -190,6 +190,7 @@ + @@ -264,6 +265,9 @@ + + + diff --git a/xmltooling/xmltooling.vcxproj.filters b/xmltooling/xmltooling.vcxproj.filters index c8c0c38..99d1289 100644 --- a/xmltooling/xmltooling.vcxproj.filters +++ b/xmltooling/xmltooling.vcxproj.filters @@ -264,6 +264,9 @@ Source Files + + Source Files\security\impl + @@ -503,6 +506,15 @@ Header Files\soap + + Header Files\security + + + Header Files\security + + + Header Files\security + -- 2.1.4