Add XML-based client SSL config.
[shibboleth/cpp-sp.git] / shib / XMLCredentials.cpp
1 /* 
2  * The Shibboleth License, Version 1. 
3  * Copyright (c) 2002 
4  * University Corporation for Advanced Internet Development, Inc. 
5  * All rights reserved
6  * 
7  * 
8  * Redistribution and use in source and binary forms, with or without 
9  * modification, are permitted provided that the following conditions are met:
10  * 
11  * Redistributions of source code must retain the above copyright notice, this 
12  * list of conditions and the following disclaimer.
13  * 
14  * Redistributions in binary form must reproduce the above copyright notice, 
15  * this list of conditions and the following disclaimer in the documentation 
16  * and/or other materials provided with the distribution, if any, must include 
17  * the following acknowledgment: "This product includes software developed by 
18  * the University Corporation for Advanced Internet Development 
19  * <http://www.ucaid.edu>Internet2 Project. Alternately, this acknowledegement 
20  * may appear in the software itself, if and wherever such third-party 
21  * acknowledgments normally appear.
22  * 
23  * Neither the name of Shibboleth nor the names of its contributors, nor 
24  * Internet2, nor the University Corporation for Advanced Internet Development, 
25  * Inc., nor UCAID may be used to endorse or promote products derived from this 
26  * software without specific prior written permission. For written permission, 
27  * please contact shibboleth@shibboleth.org
28  * 
29  * Products derived from this software may not be called Shibboleth, Internet2, 
30  * UCAID, or the University Corporation for Advanced Internet Development, nor 
31  * may Shibboleth appear in their name, without prior written permission of the 
32  * University Corporation for Advanced Internet Development.
33  * 
34  * 
35  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
36  * AND WITH ALL FAULTS. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
37  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 
38  * PARTICULAR PURPOSE, AND NON-INFRINGEMENT ARE DISCLAIMED AND THE ENTIRE RISK 
39  * OF SATISFACTORY QUALITY, PERFORMANCE, ACCURACY, AND EFFORT IS WITH LICENSEE. 
40  * IN NO EVENT SHALL THE COPYRIGHT OWNER, CONTRIBUTORS OR THE UNIVERSITY 
41  * CORPORATION FOR ADVANCED INTERNET DEVELOPMENT, INC. BE LIABLE FOR ANY DIRECT, 
42  * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 
43  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
44  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 
45  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
46  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
47  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
48  */
49
50 /* XMLTrust.h - a trust implementation that uses an XML file
51
52    Scott Cantor
53    9/27/02
54
55    $History:$
56 */
57
58 #include "internal.h"
59
60 #include <sys/types.h>
61 #include <sys/stat.h>
62
63 #include <log4cpp/Category.hh>
64 #include <xercesc/framework/URLInputSource.hpp>
65 #include <xercesc/util/regx/RegularExpression.hpp>
66 #include <xsec/enc/OpenSSL/OpenSSLCryptoX509.hpp>
67
68 using namespace shibboleth;
69 using namespace saml;
70 using namespace log4cpp;
71 using namespace std;
72
73 class shibboleth::XMLCredentialsImpl
74 {
75 public:
76     XMLCredentialsImpl(const char* pathname);
77     ~XMLCredentialsImpl();
78     
79     struct KeyUse
80     {
81         KeyUse() {}
82         ~KeyUse();
83
84         bool attach(SSL_CTX* ctx);
85         
86         enum format_t { X509DATA, DER, PEM };
87         
88         format_t m_certtype, m_keytype;
89         vector<X509*> m_certs;
90         string m_certfile, m_keyfile;
91         vector<pair<const XMLCh*,bool> > m_relying;
92     };
93     
94     vector<KeyUse*> m_keyuses;
95     typedef multimap<pair<const XMLCh*,bool>,KeyUse*> BindingMap;
96     BindingMap m_bindings;
97     
98     DOMDocument* m_doc;
99 };
100
101 XMLCredentialsImpl::KeyUse::~KeyUse()
102 {
103     for (vector<X509*>::iterator i=m_certs.begin(); i!=m_certs.end(); i++)
104         X509_free(*i);
105 }
106
107 bool XMLCredentialsImpl::KeyUse::attach(SSL_CTX* ctx)
108 {
109     switch (m_certtype)
110     {
111         case PEM:
112             if (SSL_CTX_use_certificate_chain_file(ctx, m_certfile.c_str()) != 1)
113             {
114                 log_openssl();
115                 throw TrustException("XMLCredentials::KeyUse::attach() unable to set PEM certificate chain");
116             }
117             break;
118             
119         case DER:
120             if (SSL_CTX_use_certificate_file(ctx, m_certfile.c_str(), SSL_FILETYPE_ASN1) != 1)
121             {
122                 log_openssl();
123                 throw TrustException("XMLCredentials::KeyUse::attach() unable to set DER certificate");
124             }
125             break;
126             
127         case X509DATA:
128             for (vector<X509*>::reverse_iterator i=m_certs.rbegin(); i!=m_certs.rend(); i++)
129             {
130                 if (i==m_certs.rbegin())
131                 {
132                     if (SSL_CTX_use_certificate(ctx, *i) != 1)
133                     {
134                         log_openssl();
135                         throw TrustException("XMLCredentials::KeyUse::attach() unable to set certificate from X509Data");
136                     }
137                 }
138                 else
139                 {
140                     // When we add extra certs, they don't get ref counted, so we need to duplicate them.
141                     X509* dup = X509_dup(*i);
142                     if (SSL_CTX_add_extra_chain_cert(ctx, dup) != 0)
143                     {
144                         X509_free(dup);
145                         log_openssl();
146                         throw TrustException("XMLCredentials::KeyUse::attach() unable to add CA certificate from X509Data");
147                     }
148                 }
149             }
150     }
151     
152     switch (m_keytype)
153     {
154         case PEM:
155             if (SSL_CTX_use_PrivateKey_file(ctx, m_keyfile.c_str(), SSL_FILETYPE_PEM) != 1)
156             {
157                 log_openssl();
158                 throw TrustException("XMLCredentials::KeyUse::attach() unable to set PEM private key");
159             }
160             break;
161             
162         case DER:
163             if (SSL_CTX_use_PrivateKey_file(ctx, m_keyfile.c_str(), SSL_FILETYPE_ASN1) != 1)
164             {
165                 log_openssl();
166                 throw TrustException("XMLCredentials::KeyUse::attach() unable to set PEM private key");
167             }
168     }
169     
170     if (!SSL_CTX_check_private_key(ctx))
171     {
172         log_openssl();
173         throw TrustException("XMLCredentials::KeyUse::attach found mismatch between the private key and certificate");
174     }
175     
176     return true;
177 }
178
179 XMLCredentialsImpl::XMLCredentialsImpl(const char* pathname) : m_doc(NULL)
180 {
181     NDC ndc("XMLCredentialsImpl");
182     Category& log=Category::getInstance(SHIB_LOGCAT".XMLCredentialsImpl");
183
184     saml::XML::Parser p;
185     try
186     {
187         static XMLCh base[]={chLatin_f, chLatin_i, chLatin_l, chLatin_e, chColon, chForwardSlash, chForwardSlash, chForwardSlash, chNull};
188         URLInputSource src(base,pathname);
189         Wrapper4InputSource dsrc(&src,false);
190         m_doc=p.parse(dsrc);
191
192         log.infoStream() << "Loaded and parsed creds file (" << pathname << ")" << CategoryStream::ENDLINE;
193
194         DOMElement* e = m_doc->getDocumentElement();
195         if (XMLString::compareString(XML::SHIB_NS,e->getNamespaceURI()) ||
196             XMLString::compareString(SHIB_L(Credentials),e->getLocalName()))
197         {
198             log.error("Construction requires a valid creds file: (shib:Credentials as root element)");
199             throw MetadataException("Construction requires a valid creds file: (shib:Credentials as root element)");
200         }
201
202         // Loop over the KeyUse elements.
203         DOMNodeList* nlist=e->getElementsByTagNameNS(XML::SHIB_NS,SHIB_L(KeyUse));
204         for (int i=0; nlist && i<nlist->getLength(); i++)
205         {
206             auto_ptr<KeyUse> ku(new KeyUse());
207
208             bool key=false,cert=false;
209             
210             // Grab all the RetrievalMethods for external material.
211             DOMNodeList* extlist=static_cast<DOMElement*>(nlist->item(i))->getElementsByTagNameNS(
212                 saml::XML::XMLSIG_NS,SHIB_L(RetrievalMethod)
213                 );
214             for (int j=0; (!key || !cert) && extlist && j<extlist->getLength(); j++)
215             {
216                 DOMElement* method=static_cast<DOMElement*>(extlist->item(j));
217                 const XMLCh* rmtype=method->getAttributeNS(NULL,SHIB_L(Type));
218                 auto_ptr<char> uri(XMLString::transcode(method->getAttributeNS(NULL,SHIB_L(URI))));
219                 
220                 // Is the URI locally accessible as a relative URL?
221 #ifdef WIN32
222                 struct _stat stat_buf;
223                 if (_stat(uri.get(), &stat_buf) != 0)
224 #else
225                 struct stat stat_buf;
226                 if (stat(uri.get(), &stat_buf) != 0)
227 #endif
228                 {
229                     log.warn("Credential referenced by ds:RetrievalMethod can't be opened");
230                     continue;
231                 }
232                 
233                 if (!XMLString::compareString(rmtype,shibboleth::Constants::XMLSIG_RETMETHOD_RAWX509))
234                 {
235                     if (cert)
236                         log.warn("Found another certificate credential (DER), replacing the original with it");
237                     ku->m_certfile=uri.get();
238                     ku->m_certtype=KeyUse::DER;
239                     cert=true;
240                 }
241                 else if (!XMLString::compareString(rmtype,shibboleth::Constants::SHIB_RETMETHOD_PEMX509))
242                 {
243                     if (cert)
244                         log.warn("Found another certificate credential (PEM), replacing the original with it");
245                     ku->m_certfile=uri.get();
246                     ku->m_certtype=KeyUse::PEM;
247                     cert=true;
248                 }
249                 else if (!XMLString::compareString(rmtype,shibboleth::Constants::SHIB_RETMETHOD_PEMRSA))
250                 {
251                     if (key)
252                         log.warn("Found another private key credential (PEM/RSA), replacing the original with it");
253                     ku->m_keyfile=uri.get();
254                     ku->m_keytype=KeyUse::PEM;
255                     key=true;
256                 }
257                 else if (!XMLString::compareString(rmtype,shibboleth::Constants::SHIB_RETMETHOD_DERRSA))
258                 {
259                     if (key)
260                         log.warn("Found another private key credential (DER/RSA), replacing the original with it");
261                     ku->m_keyfile=uri.get();
262                     ku->m_keytype=KeyUse::DER;
263                     key=true;
264                 }
265             }
266             
267             if (!cert)
268             {
269                 // Is there an X509Data?
270                 DOMNodeList* x509data=static_cast<DOMElement*>(nlist->item(i))->getElementsByTagNameNS(
271                     saml::XML::XMLSIG_NS,L(X509Data)
272                     );
273                 if (x509data && x509data->getLength())
274                 {
275                     if (x509data->getLength()>1)
276                         log.warn("Found multiple certificate chains, using the first");
277             
278                     // Grab up any X509Certificate elements, and flatten into one list.
279                     DOMNodeList* certlist=static_cast<DOMElement*>(x509data->item(0))->getElementsByTagNameNS(
280                         saml::XML::XMLSIG_NS,L(X509Certificate)
281                         );
282                     for (int k=0; certlist && k<certlist->getLength(); k++)
283                     {
284                         auto_ptr<char> blob(XMLString::transcode(certlist->item(k)->getFirstChild()->getNodeValue()));
285                         X509* x=B64_to_X509(blob.get());
286                         if (x)
287                             ku->m_certs.push_back(x);
288                         else
289                             log.warn("Unable to parse ds:X509Certificate element, can't include in chain");
290                     }
291                     
292                     if (ku->m_certs.size()>0)
293                     {
294                         ku->m_certtype=KeyUse::X509DATA;
295                         cert=true;
296                     }
297                     else
298                         log.warn("Found no inline certificates in the ds:X509Data element, ignoring it");
299                 }
300             }
301             
302             if (!cert)
303             {
304                 log.error("Found no acceptable certificate in shib:KeyUse element, ignoring it");
305                 continue;
306             }
307             
308             if (!key)
309             {
310                 log.error("Found no acceptable private/secret key in shib:KeyUse element, ignoring it");
311                 continue;
312             }
313             
314             // Pull in the relying parties.
315             DOMNodeList* parties=static_cast<DOMElement*>(nlist->item(i))->getElementsByTagNameNS(XML::SHIB_NS,SHIB_L(RelyingParty));
316             int m=0;
317             while (parties && m<parties->getLength())
318             {
319                 const XMLCh* name=parties->item(m)->getFirstChild()->getNodeValue();
320                 if (name && *name)
321                 {
322                     static const XMLCh one[]={ chDigit_1, chNull };
323                     static const XMLCh tru[]={ chLatin_t, chLatin_r, chLatin_u, chLatin_e, chNull };
324                     const XMLCh* regexp=
325                         static_cast<DOMElement*>(parties->item(m))->getAttributeNS(NULL,SHIB_L(regexp));
326                     bool flag=(!XMLString::compareString(regexp,one) || !XMLString::compareString(regexp,tru));
327                     ku->m_relying.push_back(pair<const XMLCh*,bool>(name,flag));
328                 }
329                 m++;
330             }
331             // If no RelyingParties, this is a catch-all binding.
332             if (m==0)
333                 ku->m_relying.push_back(pair<const XMLCh*,bool>(NULL,false));
334             
335             // Now map the subjects to the credentials.
336             DOMNodeList* subs=static_cast<DOMElement*>(nlist->item(i))->getElementsByTagNameNS(XML::SHIB_NS,L(Subject));
337             int l=0;
338             while (subs && l<subs->getLength())
339             {
340                 const XMLCh* name=subs->item(l)->getFirstChild()->getNodeValue();
341                 if (name && *name)
342                 {
343                     static const XMLCh one[]={ chDigit_1, chNull };
344                     static const XMLCh tru[]={ chLatin_t, chLatin_r, chLatin_u, chLatin_e, chNull };
345                     const XMLCh* regexp=
346                         static_cast<DOMElement*>(subs->item(l))->getAttributeNS(NULL,SHIB_L(regexp));
347                     bool flag=(!XMLString::compareString(regexp,one) || !XMLString::compareString(regexp,tru));
348                     m_bindings.insert(BindingMap::value_type(pair<const XMLCh*,bool>(name,flag),ku.get()));
349                 }
350                 l++;
351             }
352             // If no Subjects, this is a catch-all binding.
353             if (l==0)
354                 m_bindings.insert(BindingMap::value_type(pair<const XMLCh*,bool>(NULL,false),ku.get()));
355
356             m_keyuses.push_back(ku.release());
357         }
358     }
359     catch (SAMLException& e)
360     {
361         log.errorStream() << "XML error while parsing creds configuration: " << e.what() << CategoryStream::ENDLINE;
362         for (vector<KeyUse*>::iterator i=m_keyuses.begin(); i!=m_keyuses.end(); i++)
363             delete (*i);
364         if (m_doc)
365             m_doc->release();
366         throw;
367     }
368     catch (...)
369     {
370         log.error("Unexpected error while parsing creds configuration");
371         for (vector<KeyUse*>::iterator i=m_keyuses.begin(); i!=m_keyuses.end(); i++)
372             delete (*i);
373         if (m_doc)
374             m_doc->release();
375         throw;
376     }
377 }
378
379 XMLCredentialsImpl::~XMLCredentialsImpl()
380 {
381     for (vector<KeyUse*>::iterator i=m_keyuses.begin(); i!=m_keyuses.end(); i++)
382         delete (*i);
383     if (m_doc)
384         m_doc->release();
385 }
386
387 XMLCredentials::XMLCredentials(const char* pathname) : m_filestamp(0), m_source(pathname), m_impl(NULL)
388 {
389 #ifdef WIN32
390     struct _stat stat_buf;
391     if (_stat(pathname, &stat_buf) == 0)
392 #else
393     struct stat stat_buf;
394     if (stat(pathname, &stat_buf) == 0)
395 #endif
396         m_filestamp=stat_buf.st_mtime;
397     m_impl=new XMLCredentialsImpl(pathname);
398     m_lock=RWLock::create();
399 }
400
401 XMLCredentials::~XMLCredentials()
402 {
403     delete m_lock;
404     delete m_impl;
405 }
406
407 void XMLCredentials::lock()
408 {
409     m_lock->rdlock();
410
411     // Check if we need to refresh.
412 #ifdef WIN32
413     struct _stat stat_buf;
414     if (_stat(m_source.c_str(), &stat_buf) == 0)
415 #else
416     struct stat stat_buf;
417     if (stat(m_source.c_str(), &stat_buf) == 0)
418 #endif
419     {
420         if (m_filestamp>0 && m_filestamp<stat_buf.st_mtime)
421         {
422             // Elevate lock and recheck.
423             m_lock->unlock();
424             m_lock->wrlock();
425             if (m_filestamp>0 && m_filestamp<stat_buf.st_mtime)
426             {
427                 try
428                 {
429                     XMLCredentialsImpl* new_mapper=new XMLCredentialsImpl(m_source.c_str());
430                     delete m_impl;
431                     m_impl=new_mapper;
432                     m_filestamp=stat_buf.st_mtime;
433                     m_lock->unlock();
434                 }
435                 catch(SAMLException& e)
436                 {
437                     m_lock->unlock();
438                     saml::NDC ndc("lock");
439                     Category::getInstance(SHIB_LOGCAT".XMLCredentials").error("failed to reload credentials metadata, sticking with what we have: %s", e.what());
440                 }
441                 catch(...)
442                 {
443                     m_lock->unlock();
444                     saml::NDC ndc("lock");
445                     Category::getInstance(SHIB_LOGCAT".XMLCredentials").error("caught an unknown exception, sticking with what we have");
446                 }
447             }
448             else
449             {
450                 m_lock->unlock();
451             }
452             m_lock->rdlock();
453         }
454     }
455 }
456
457 void XMLCredentials::unlock()
458 {
459     m_lock->unlock();
460 }
461
462
463 bool XMLCredentials::attach(const XMLCh* subject, const ISite* relyingParty, SSL_CTX* ctx) const
464 {
465     NDC ndc("attach");
466
467     // Use the matching bindings.
468     for (XMLCredentialsImpl::BindingMap::const_iterator i=m_impl->m_bindings.begin(); i!=m_impl->m_bindings.end(); i++)
469     {
470         bool match=false;
471         
472         if (i->first.first==NULL)   // catch-all entry
473         {
474             match=true;
475         }
476         else if (i->first.second)   // regexp
477         {
478             try
479             {
480                 RegularExpression re(i->first.first);
481                 if (re.matches(subject))
482                     match=true;
483             }
484             catch (XMLException& ex)
485             {
486                 auto_ptr<char> tmp(XMLString::transcode(ex.getMessage()));
487                 Category& log=Category::getInstance(SHIB_LOGCAT".XMLCredentials");
488                 log.errorStream() << "caught exception while parsing regular expression: " << tmp.get()
489                     << CategoryStream::ENDLINE;
490             }
491         }
492         else if (!XMLString::compareString(subject,i->first.first))
493         {
494             match=true;
495         }
496         
497         if (match)
498         {
499             // See if the relying party applies...
500             match=false;
501             for (vector<pair<const XMLCh*,bool> >::const_iterator j=i->second->m_relying.begin(); j!=i->second->m_relying.end(); j++)
502             {
503                 if (j->first==NULL)     // catch-all entry
504                 {
505                     match=true;
506                 }
507                 else if (j->second)     // regexp
508                 {
509                     try
510                     {
511                         RegularExpression re(j->first);
512                         if (re.matches(relyingParty->getName()))
513                             match=true;
514                         else
515                         {
516                             Iterator<const XMLCh*> groups=relyingParty->getGroups();
517                             while (!match && groups.hasNext())
518                                 if (re.matches(groups.next()))
519                                     match=true;
520                         }
521                     }
522                     catch (XMLException& ex)
523                     {
524                         auto_ptr<char> tmp(XMLString::transcode(ex.getMessage()));
525                         Category& log=Category::getInstance(SHIB_LOGCAT".XMLCredentials");
526                         log.errorStream() << "caught exception while parsing regular expression: " << tmp.get()
527                             << CategoryStream::ENDLINE;
528                     }
529                 }
530                 else if (!XMLString::compareString(relyingParty->getName(),j->first))
531                 {
532                     match=true;
533                 }
534                 else
535                 {
536                     Iterator<const XMLCh*> groups=relyingParty->getGroups();
537                     while (!match && groups.hasNext())
538                         if (!XMLString::compareString(groups.next(),j->first))
539                             match=true;
540                 }
541             }
542         }
543         
544         if (match)
545         {
546             // We have the credentials to use...
547             return i->second->attach(ctx);
548         }
549     }
550
551     return false;
552 }