From: Scott Cantor Date: Tue, 28 Oct 2008 17:50:00 +0000 (+0000) Subject: Multi-line svn commit, see body. X-Git-Tag: 1.2.0~60 X-Git-Url: http://www.project-moonshot.org/gitweb/?p=shibboleth%2Fcpp-xmltooling.git;a=commitdiff_plain;h=fe799793a4aced0cf8a21bb1c4c401215e04c8c6 Multi-line svn commit, see body. Initial set of helper functions to handle key/cert loading. Additional unit tests and data. --- diff --git a/.cproject b/.cproject index 2473079..b78749e 100644 --- a/.cproject +++ b/.cproject @@ -61,6 +61,7 @@ + @@ -73,6 +74,8 @@ + + diff --git a/xmltooling/Makefile.am b/xmltooling/Makefile.am index a580e53..87a5bae 100644 --- a/xmltooling/Makefile.am +++ b/xmltooling/Makefile.am @@ -77,6 +77,7 @@ secinclude_HEADERS = \ security/KeyInfoCredentialContext.h \ security/KeyInfoResolver.h \ security/OpenSSLCredential.h \ + security/SecurityHelper.h \ security/SignatureTrustEngine.h \ security/TrustEngine.h \ security/X509Credential.h \ @@ -141,6 +142,7 @@ xmlsec_sources = \ security/impl/InlineKeyResolver.cpp \ security/impl/KeyInfoResolver.cpp \ security/impl/OpenSSLCryptoX509CRL.cpp \ + security/impl/SecurityHelper.cpp \ security/impl/StaticPKIXTrustEngine.cpp \ security/impl/TrustEngine.cpp \ security/impl/XSECCryptoX509CRL.cpp \ diff --git a/xmltooling/security/SecurityHelper.h b/xmltooling/security/SecurityHelper.h new file mode 100644 index 0000000..4c0bbf9 --- /dev/null +++ b/xmltooling/security/SecurityHelper.h @@ -0,0 +1,122 @@ +/* + * Copyright 2001-2008 Internet2 + * + * Licensed 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/SecurityHelper.h + * + * A helper class for working with keys, certificates, etc. + */ + +#if !defined(__xmltooling_sechelper_h__) && !defined(XMLTOOLING_NO_XMLSEC) +#define __xmltooling_sechelper_h__ + +#include +#include + +#include +#include +#include + +namespace xmltooling { + /** + * A helper class for working with keys, certificates, etc. + */ + class XMLTOOL_API SecurityHelper + { + public: + /** + * Loads a private key from a local file. + * + * @param pathname path to file containing key + * @param format optional constant identifying key encoding format + * @param password optional password to decrypt key + * @return a populated key object + */ + static XSECCryptoKey* loadKeyFromFile(const char* pathname, const char* format=NULL, const char* password=NULL); + + /** + * Loads certificate(s) from a local file. + * + * @param certs array to populate with certificate(s) + * @param pathname path to file containing certificate(s) + * @param format optional constant identifying certificate encoding format + * @return size of the resulting array + */ + static std::vector::size_type loadCertificatesFromFile( + std::vector& certs, const char* pathname, const char* format=NULL, const char* password=NULL + ); + + /** + * Loads CRL(s) from a local file. + * + * @param crls array to populate with CRL(s) + * @param pathname path to file containing CRL(s) + * @param format optional constant identifying CRL encoding format + * @return size of the resulting array + */ + static std::vector::size_type loadCRLsFromFile( + std::vector& crls, const char* pathname, const char* format=NULL + ); + + /** + * Loads a private key from a URL. + * + * @param transport object to use to acquire key + * @param backing backing file for key (written to or read from if download fails) + * @param format optional constant identifying key encoding format + * @param password optional password to decrypt key + * @return a populated key object + */ + static XSECCryptoKey* loadKeyFromURL(SOAPTransport& transport, const char* backing, const char* format=NULL, const char* password=NULL); + + /** + * Loads certificate(s) from a URL. + * + * @param certs array to populate with certificate(s) + * @param transport object to use to acquire certificate(s) + * @param backing backing file for certificate(s) (written to or read from if download fails) + * @param format optional constant identifying certificate encoding format + * @return size of the resulting array + */ + static std::vector::size_type loadCertificatesFromURL( + std::vector& certs, SOAPTransport& transport, const char* backing, const char* format=NULL, const char* password=NULL + ); + + /** + * Loads CRL(s) from a URL. + * + * @param crls array to populate with CRL(s) + * @param transport object to use to acquire CRL(s) + * @param backing backing file for CRL(s) (written to or read from if download fails) + * @param format optional constant identifying CRL encoding format + * @return size of the resulting array + */ + static std::vector::size_type loadCRLsFromURL( + std::vector& crls, SOAPTransport& transport, const char* backing, const char* format=NULL + ); + + /** + * Compares two keys for equality. + * + * @param key1 first key to compare + * @param key2 second key to compare + * @return true iff the keys match + */ + static bool matches(const XSECCryptoKey* key1, const XSECCryptoKey* key2); + }; +}; + +#endif /* __xmltooling_sechelper_h__ */ diff --git a/xmltooling/security/impl/SecurityHelper.cpp b/xmltooling/security/impl/SecurityHelper.cpp new file mode 100644 index 0000000..5c39bf8 --- /dev/null +++ b/xmltooling/security/impl/SecurityHelper.cpp @@ -0,0 +1,436 @@ +/* + * Copyright 2001-2008 Internet2 + * + * Licensed 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. + */ + +/** + * SecurityHelper.cpp + * + * A helper class for working with keys, certificates, etc. + */ + +#include "internal.h" +#include "logging.h" +#include "security/OpenSSLCryptoX509CRL.h" +#include "security/SecurityHelper.h" +#include "util/NDC.h" + +#include +#include +#include +#include +#include +#include + +using namespace xmltooling::logging; +using namespace xmltooling; +using namespace std; + +// OpenSSL password callback... +static int passwd_callback(char* buf, int len, int verify, void* passwd) +{ + if(!verify) + { + if(passwd && len > strlen(reinterpret_cast(passwd))) + { + strcpy(buf,reinterpret_cast(passwd)); + return strlen(buf); + } + } + return 0; +} + +XSECCryptoKey* SecurityHelper::loadKeyFromFile(const char* pathname, const char* format, const char* password) +{ +#ifdef _DEBUG + NDC ndc("loadKeyFromFile"); +#endif + Category& log = Category::getInstance(XMLTOOLING_LOGCAT".SecurityHelper"); + log.info("loading private key from file (%s)", pathname); + + // Native objects. + PKCS12* p12=NULL; + EVP_PKEY* pkey=NULL; + + BIO* in=BIO_new(BIO_s_file_internal()); + if (in && BIO_read_filename(in, pathname)>0) { + // If the format isn't set, try and guess it. + if (!format) { + const int READSIZE = 1; + char buf[READSIZE]; + int mark; + + // Examine the first byte. + try { + if ((mark = BIO_tell(in)) < 0) + throw XMLSecurityException("Error loading key: BIO_tell() can't get the file position."); + if (BIO_read(in, buf, READSIZE) <= 0) + throw XMLSecurityException("Error loading key: BIO_read() can't read from the stream."); + if (BIO_seek(in, mark) < 0) + throw XMLSecurityException("Error loading key: BIO_seek() can't reset the file position."); + } + catch (exception&) { + log_openssl(); + BIO_free(in); + throw; + } + + // Check the first byte of the file. If it's some kind of DER-encoded structure + // (including PKCS12), it will begin with ASCII 048. Otherwise, assume it's PEM. + if (buf[0] != 48) { + format = "PEM"; + } + else { + // Here we know it's DER-encoded, now try to parse it as a PKCS12 ASN.1 structure. + // If it fails, must be another kind of DER-encoded structure. + if ((p12=d2i_PKCS12_bio(in, NULL)) == NULL) { + format = "DER"; + if (BIO_seek(in, mark) < 0) { + log_openssl(); + BIO_free(in); + throw XMLSecurityException("Error loading key: BIO_seek() can't reset the file position."); + } + } + else { + format = "PKCS12"; + } + } + log.debug("key encoding format for (%s) dynamically resolved as (%s)", pathname, format); + } + + // The format should be known, so parse accordingly. + if (!strcmp(format, "PEM")) { + pkey = PEM_read_bio_PrivateKey(in, NULL, passwd_callback, const_cast(password)); + } + else if (!strcmp(format, "DER")) { + pkey=d2i_PrivateKey_bio(in, NULL); + } + else if (!strcmp(format, "PKCS12")) { + if (!p12) + p12 = d2i_PKCS12_bio(in, NULL); + if (p12) { + X509* x=NULL; + PKCS12_parse(p12, const_cast(password), &pkey, &x, NULL); + PKCS12_free(p12); + X509_free(x); + } + } + else { + log.error("unknown key encoding format (%s)", format); + } + } + if (in) + BIO_free(in); + + // Now map it to an XSEC wrapper. + if (pkey) { + XSECCryptoKey* ret=NULL; + switch (pkey->type) { + case EVP_PKEY_RSA: + ret=new OpenSSLCryptoKeyRSA(pkey); + break; + + case EVP_PKEY_DSA: + ret=new OpenSSLCryptoKeyDSA(pkey); + break; + + default: + log.error("unsupported private key type"); + } + EVP_PKEY_free(pkey); + if (ret) + return ret; + } + + log_openssl(); + throw XMLSecurityException("Unable to load private key from file ($1).", params(1, pathname)); +} + +vector::size_type SecurityHelper::loadCertificatesFromFile( + vector& certs, const char* pathname, const char* format, const char* password + ) +{ +#ifdef _DEBUG + NDC ndc("loadCertificatesFromFile"); +#endif + Category& log = Category::getInstance(XMLTOOLING_LOGCAT".SecurityHelper"); + log.info("loading certificate(s) from file (%s)", pathname); + + vector::size_type count = certs.size(); + + // Native objects. + X509* x=NULL; + PKCS12* p12=NULL; + + BIO* in=BIO_new(BIO_s_file_internal()); + if (in && BIO_read_filename(in, pathname)>0) { + // If the format isn't set, try and guess it. + if (!format) { + const int READSIZE = 1; + char buf[READSIZE]; + int mark; + + // Examine the first byte. + try { + if ((mark = BIO_tell(in)) < 0) + throw XMLSecurityException("Error loading certificate: BIO_tell() can't get the file position."); + if (BIO_read(in, buf, READSIZE) <= 0) + throw XMLSecurityException("Error loading certificate: BIO_read() can't read from the stream."); + if (BIO_seek(in, mark) < 0) + throw XMLSecurityException("Error loading certificate: BIO_seek() can't reset the file position."); + } + catch (exception&) { + log_openssl(); + BIO_free(in); + throw; + } + + // Check the first byte of the file. If it's some kind of DER-encoded structure + // (including PKCS12), it will begin with ASCII 048. Otherwise, assume it's PEM. + if (buf[0] != 48) { + format = "PEM"; + } + else { + // Here we know it's DER-encoded, now try to parse it as a PKCS12 ASN.1 structure. + // If it fails, must be another kind of DER-encoded structure. + if ((p12=d2i_PKCS12_bio(in, NULL)) == NULL) { + format = "DER"; + if (BIO_seek(in, mark) < 0) { + log_openssl(); + BIO_free(in); + throw XMLSecurityException("Error loading certificate: BIO_seek() can't reset the file position."); + } + } + else { + format = "PKCS12"; + } + } + } + + // The format should be known, so parse accordingly. + if (!strcmp(format, "PEM")) { + while (x=PEM_read_bio_X509(in, NULL, NULL, NULL)) { + certs.push_back(new OpenSSLCryptoX509(x)); + X509_free(x); + } + } + else if (!strcmp(format, "DER")) { + x=d2i_X509_bio(in, NULL); + if (x) { + certs.push_back(new OpenSSLCryptoX509(x)); + X509_free(x); + } + } + else if (!strcmp(format, "PKCS12")) { + if (!p12) + p12 = d2i_PKCS12_bio(in, NULL); + if (p12) { + EVP_PKEY* pkey=NULL; + STACK_OF(X509)* CAstack = sk_X509_new_null(); + PKCS12_parse(p12, const_cast(password), &pkey, &x, &CAstack); + PKCS12_free(p12); + EVP_PKEY_free(pkey); + if (x) { + certs.push_back(new OpenSSLCryptoX509(x)); + X509_free(x); + } + x = sk_X509_pop(CAstack); + while (x) { + certs.push_back(new OpenSSLCryptoX509(x)); + X509_free(x); + x = sk_X509_pop(CAstack); + } + sk_X509_free(CAstack); + } + } + } + if (in) + BIO_free(in); + + if (certs.size() == count) { + log_openssl(); + throw XMLSecurityException("Unable to load certificate(s) from file ($1).", params(1, pathname)); + } + + return certs.size(); +} + +vector::size_type SecurityHelper::loadCRLsFromFile( + vector& crls, const char* pathname, const char* format + ) +{ +#ifdef _DEBUG + NDC ndc("loadCRLsFromFile"); +#endif + Category& log = Category::getInstance(XMLTOOLING_LOGCAT".SecurityHelper"); + log.info("loading CRL(s) from file (%s)", pathname); + + vector::size_type count = crls.size(); + + BIO* in=BIO_new(BIO_s_file_internal()); + if (in && BIO_read_filename(in, pathname)>0) { + // If the format isn't set, try and guess it. + if (!format) { + const int READSIZE = 1; + char buf[READSIZE]; + int mark; + + // Examine the first byte. + try { + if ((mark = BIO_tell(in)) < 0) + throw XMLSecurityException("Error loading CRL: BIO_tell() can't get the file position."); + if (BIO_read(in, buf, READSIZE) <= 0) + throw XMLSecurityException("Error loading CRL: BIO_read() can't read from the stream."); + if (BIO_seek(in, mark) < 0) + throw XMLSecurityException("Error loading CRL: BIO_seek() can't reset the file position."); + } + catch (exception&) { + log_openssl(); + BIO_free(in); + throw; + } + + // Check the first byte of the file. If it's some kind of DER-encoded structure + // it will begin with ASCII 048. Otherwise, assume it's PEM. + if (buf[0] != 48) { + format = "PEM"; + } + else { + format = "DER"; + } + log.debug("CRL encoding format for (%s) dynamically resolved as (%s)", pathname, format); + } + + X509_CRL* crl=NULL; + if (!strcmp(format, "PEM")) { + while (crl=PEM_read_bio_X509_CRL(in, NULL, NULL, NULL)) { + crls.push_back(new OpenSSLCryptoX509CRL(crl)); + X509_CRL_free(crl); + } + } + else if (!strcmp(format, "DER")) { + crl=d2i_X509_CRL_bio(in, NULL); + if (crl) { + crls.push_back(new OpenSSLCryptoX509CRL(crl)); + X509_CRL_free(crl); + } + } + else { + log.error("unknown CRL encoding format (%s)", format); + } + } + if (in) + BIO_free(in); + + if (crls.size() == count) { + log_openssl(); + throw XMLSecurityException("Unable to load CRL(s) from file ($1).", params(1, pathname)); + } + + return crls.size(); +} + +XSECCryptoKey* SecurityHelper::loadKeyFromURL(SOAPTransport& transport, const char* backing, const char* format, const char* password) +{ + // Fetch the data. + istringstream dummy; + transport.send(dummy); + istream& msg = transport.receive(); + + // Dump to output file. + ofstream out(backing, fstream::trunc|fstream::binary); + out << msg.rdbuf(); + + return loadKeyFromFile(backing, format, password); +} + +vector::size_type SecurityHelper::loadCertificatesFromURL( + vector& certs, SOAPTransport& transport, const char* backing, const char* format, const char* password + ) +{ + // Fetch the data. + istringstream dummy; + transport.send(dummy); + istream& msg = transport.receive(); + + // Dump to output file. + ofstream out(backing, fstream::trunc|fstream::binary); + out << msg.rdbuf(); + + return loadCertificatesFromFile(certs, backing, format, password); +} + +vector::size_type SecurityHelper::loadCRLsFromURL( + vector& crls, SOAPTransport& transport, const char* backing, const char* format + ) +{ + // Fetch the data. + istringstream dummy; + transport.send(dummy); + istream& msg = transport.receive(); + + // Dump to output file. + ofstream out(backing, fstream::trunc|fstream::binary); + out << msg.rdbuf(); + + return loadCRLsFromFile(crls, backing, format); +} + +bool SecurityHelper::matches(const XSECCryptoKey* key1, const XSECCryptoKey* key2) +{ + if (key1->getProviderName()!=DSIGConstants::s_unicodeStrPROVOpenSSL || + key2->getProviderName()!=DSIGConstants::s_unicodeStrPROVOpenSSL) { + Category::getInstance(XMLTOOLING_LOGCAT".SecurityHelper").warn("comparison of non-OpenSSL keys not supported"); + return false; + } + + // If one key is public or both, just compare the public key half. + if (key1->getKeyType()==XSECCryptoKey::KEY_RSA_PUBLIC || key1->getKeyType()==XSECCryptoKey::KEY_RSA_PAIR) { + if (key2->getKeyType()!=XSECCryptoKey::KEY_RSA_PUBLIC && key2->getKeyType()!=XSECCryptoKey::KEY_RSA_PAIR) + return false; + const RSA* rsa1 = static_cast(key1)->getOpenSSLRSA(); + const RSA* rsa2 = static_cast(key2)->getOpenSSLRSA(); + return (BN_cmp(rsa1->n,rsa2->n) == 0 && BN_cmp(rsa1->e,rsa2->e) == 0); + } + + // For a private key, compare the private half. + if (key1->getKeyType()==XSECCryptoKey::KEY_RSA_PRIVATE) { + if (key2->getKeyType()!=XSECCryptoKey::KEY_RSA_PRIVATE && key2->getKeyType()!=XSECCryptoKey::KEY_RSA_PAIR) + return false; + const RSA* rsa1 = static_cast(key1)->getOpenSSLRSA(); + const RSA* rsa2 = static_cast(key2)->getOpenSSLRSA(); + return (BN_cmp(rsa1->n,rsa2->n) == 0 && BN_cmp(rsa1->d,rsa2->d) == 0); + } + + // If one key is public or both, just compare the public key half. + if (key1->getKeyType()==XSECCryptoKey::KEY_DSA_PUBLIC || key1->getKeyType()==XSECCryptoKey::KEY_DSA_PAIR) { + if (key2->getKeyType()!=XSECCryptoKey::KEY_DSA_PUBLIC && key2->getKeyType()!=XSECCryptoKey::KEY_DSA_PAIR) + return false; + const DSA* dsa1 = static_cast(key1)->getOpenSSLDSA(); + const DSA* dsa2 = static_cast(key2)->getOpenSSLDSA(); + return (BN_cmp(dsa1->pub_key,dsa2->pub_key) == 0); + } + + // For a private key, compare the private half. + if (key1->getKeyType()==XSECCryptoKey::KEY_DSA_PRIVATE) { + if (key2->getKeyType()!=XSECCryptoKey::KEY_DSA_PRIVATE && key2->getKeyType()!=XSECCryptoKey::KEY_DSA_PAIR) + return false; + const DSA* dsa1 = static_cast(key1)->getOpenSSLDSA(); + const DSA* dsa2 = static_cast(key2)->getOpenSSLDSA(); + return (BN_cmp(dsa1->priv_key,dsa2->priv_key) == 0); + } + + Category::getInstance(XMLTOOLING_LOGCAT".SecurityHelper").warn("unsupported key type for comparison"); + return false; +} diff --git a/xmltoolingtest/Makefile.am b/xmltoolingtest/Makefile.am index 053f6ab..33e4392 100644 --- a/xmltoolingtest/Makefile.am +++ b/xmltoolingtest/Makefile.am @@ -23,6 +23,7 @@ xmlsec_sources = \ FilesystemCredentialResolverTest.h \ InlineKeyResolverTest.h \ MemoryStorageServiceTest.h \ + SecurityHelperTest.h \ SignatureTest.h else xmlsec_sources = diff --git a/xmltoolingtest/SecurityHelperTest.h b/xmltoolingtest/SecurityHelperTest.h new file mode 100644 index 0000000..aa80fbc --- /dev/null +++ b/xmltoolingtest/SecurityHelperTest.h @@ -0,0 +1,66 @@ +/* + * Copyright 2001-2007 Internet2 + * + * Licensed 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. + */ + +#include "XMLObjectBaseTestCase.h" + +#include + +class SecurityHelperTest : public CxxTest::TestSuite { + vector certs; +public: + void setUp() { + } + + void tearDown() { + for_each(certs.begin(), certs.end(), xmltooling::cleanup()); + } + + void testKeysFromFiles() { + string pathname = data_path + "key.pem"; + auto_ptr key1(SecurityHelper::loadKeyFromFile(pathname.c_str())); + pathname = data_path + "key.der"; + auto_ptr key2(SecurityHelper::loadKeyFromFile(pathname.c_str())); + pathname = data_path + "test.pfx"; + auto_ptr key3(SecurityHelper::loadKeyFromFile(pathname.c_str(), NULL, "password")); + + TSM_ASSERT("PEM/DER keys did not match", SecurityHelper::matches(key1.get(), key2.get())); + TSM_ASSERT("DER/PKCS12 keys did not match", SecurityHelper::matches(key2.get(), key3.get())); + + pathname = data_path + "key2.pem"; + auto_ptr key4(SecurityHelper::loadKeyFromFile(pathname.c_str())); + TSM_ASSERT("Different keys matched", !SecurityHelper::matches(key3.get(), key4.get())); + } + + void testCertificatesFromFiles() { + string pathname = data_path + "cert.pem"; + SecurityHelper::loadCertificatesFromFile(certs, pathname.c_str()); + pathname = data_path + "cert.der"; + SecurityHelper::loadCertificatesFromFile(certs, pathname.c_str()); + pathname = data_path + "test.pfx"; + SecurityHelper::loadCertificatesFromFile(certs, pathname.c_str(), NULL, "password"); + + TSM_ASSERT_EQUALS("Wrong certificate count", certs.size(), 3); + + auto_ptr key1(certs[0]->clonePublicKey()); + auto_ptr key2(certs[0]->clonePublicKey()); + auto_ptr key3(certs[0]->clonePublicKey()); + + TSM_ASSERT("PEM/DER keys did not match", SecurityHelper::matches(key1.get(), key2.get())); + TSM_ASSERT("DER/PKCS12 keys did not match", SecurityHelper::matches(key2.get(), key3.get())); + + for_each(certs.begin(), certs.end(), xmltooling::cleanup()); + } +}; diff --git a/xmltoolingtest/data/cert.der b/xmltoolingtest/data/cert.der new file mode 100644 index 0000000..2776767 Binary files /dev/null and b/xmltoolingtest/data/cert.der differ diff --git a/xmltoolingtest/data/key.der b/xmltoolingtest/data/key.der new file mode 100644 index 0000000..454fdc7 Binary files /dev/null and b/xmltoolingtest/data/key.der differ diff --git a/xmltoolingtest/data/test.pfx b/xmltoolingtest/data/test.pfx new file mode 100644 index 0000000..4bbd936 Binary files /dev/null and b/xmltoolingtest/data/test.pfx differ