From 82a861f8d88bbe5d978dfa2ebfc184d903981ead Mon Sep 17 00:00:00 2001 From: Nils Maier Date: Mon, 8 Apr 2013 04:12:31 +0200 Subject: [PATCH] AppleTLS: Support credentials via KeyChain fingerprints --- src/AppleTLSContext.cc | 187 +++++++++++++++++++++++++++++++++++- src/AppleTLSContext.h | 10 +- src/AppleTLSSession.cc | 58 ++++++++++- src/MessageDigest.cc | 33 ++++--- src/MessageDigest.h | 6 +- src/MultiUrlRequestInfo.cc | 7 +- src/OptionHandlerFactory.cc | 13 ++- 7 files changed, 292 insertions(+), 22 deletions(-) diff --git a/src/AppleTLSContext.cc b/src/AppleTLSContext.cc index e301a0ed..3f011ebd 100644 --- a/src/AppleTLSContext.cc +++ b/src/AppleTLSContext.cc @@ -34,21 +34,111 @@ /* copyright --> */ #include "AppleTLSContext.h" +#include +#include + #include "LogFactory.h" #include "Logger.h" +#include "MessageDigest.h" #include "fmt.h" #include "message.h" +#include "util.h" + +namespace { + using namespace aria2; + +#if defined(__MAC_10_6) + static const void *query_keys[] = { + kSecClass, + kSecReturnRef, + kSecMatchPolicy, + kSecMatchLimit + }; + + class cfrelease { + const void *ptr_; + public: + inline cfrelease(const void *ptr) : ptr_(ptr) {} + inline ~cfrelease() { if (ptr_) CFRelease(ptr_); } + }; + + static inline bool isWhitespace(char c) + { + // Fingerprints are often separated by colons + return isspace(c) || c == ':'; + } + static inline std::string stripWhitespace(std::string str) + { + str.erase(std::remove_if(str.begin(), str.end(), isWhitespace), str.end()); + return str; + } + + struct hash_validator { + const std::string& hash_; + hash_validator(const std::string& hash) : hash_(hash) {} + inline bool operator()(std::string type) const { + return MessageDigest::isValidHash(type, hash_); + } + }; + + struct hash_finder { + CFDataRef data_; + const std::string& hash_; + hash_finder(CFDataRef data, const std::string& hash) + : data_(data), hash_(hash) + {} + inline bool operator()(std::string type) const { + std::string hash = MessageDigest::create(type)->update( + CFDataGetBytePtr(data_), CFDataGetLength(data_)).digest(); + hash = util::toHex(hash); + return hash == hash_; + } + }; + + + std::string errToString(OSStatus err) + { + std::string rv = "Unkown error"; + CFStringRef cerr = SecCopyErrorMessageString(err, 0); + if (cerr) { + size_t len = CFStringGetLength(cerr) * 4; + char *buf = new char[len]; + if (CFStringGetCString(cerr, buf, len, kCFStringEncodingUTF8)) { + rv = buf; + } + delete [] buf; + CFRelease(cerr); + } + return rv; + } + +#endif // defined(__MAC_10_6) + +} namespace aria2 { -TLSContext* TLSContext::make(TLSSessionSide side) { +TLSContext* TLSContext::make(TLSSessionSide side) +{ return new AppleTLSContext(side); } +AppleTLSContext::~AppleTLSContext() +{ + if (credentials_) { + CFRelease(credentials_); + credentials_ = 0; + } +} + bool AppleTLSContext::addCredentialFile(const std::string& certfile, const std::string& keyfile) { - A2_LOG_WARN("TLS credential files are not supported. Use the KeyChain to manage your certificates."); + if (tryAsFingerprint(certfile)) { + return true; + } + + A2_LOG_WARN("TLS credential files are not supported. Use the KeyChain to manage your certificates and provide a fingerprint. See the manual."); return false; } @@ -58,5 +148,98 @@ bool AppleTLSContext::addTrustedCACertFile(const std::string& certfile) return false; } +SecIdentityRef AppleTLSContext::getCredentials() +{ + return credentials_; +} + +bool AppleTLSContext::tryAsFingerprint(const std::string& fingerprint) +{ + std::string fp = stripWhitespace(fingerprint); + // Verify this is a valid hex representation and normalize. + fp = util::toHex(util::fromHex(fp.begin(), fp.end())); + + // Verify this can represent a hash + std::vector ht = MessageDigest::getSupportedHashTypes(); + if (std::find_if(ht.begin(), ht.end(), hash_validator(fp)) == ht.end()) { + A2_LOG_INFO(fmt("%s is not a fingerprint, invalid hash representation", fingerprint.c_str())); + return false; + } + +#if defined(__MAC_10_6) + A2_LOG_DEBUG(fmt("Looking for cert with fingerprint %s", fp.c_str())); + + // Build and run the KeyChain the query. + SecPolicyRef policy = SecPolicyCreateSSL(true, 0); + if (!policy) { + A2_LOG_ERROR("Failed to create SecPolicy"); + return false; + } + cfrelease del_policy(policy); + const void *query_values[] = { + kSecClassIdentity, + kCFBooleanTrue, + policy, + kSecMatchLimitAll + }; + CFDictionaryRef query = CFDictionaryCreate(0, query_keys, query_values, + 4, 0, 0); + if (!query) { + A2_LOG_ERROR("Failed to create identity query"); + return false; + } + cfrelease del_query(query); + CFArrayRef identities; + OSStatus err = SecItemCopyMatching(query, (CFTypeRef*)&identities); + if (err != errSecSuccess) { + A2_LOG_ERROR("Query failed: " + errToString(err)); + return false; + } + + // Alrighty, search the fingerprint. + const size_t nvals = CFArrayGetCount(identities); + for (size_t i = 0; i < nvals; ++i) { + SecIdentityRef id = (SecIdentityRef)CFArrayGetValueAtIndex(identities, i); + if (!id) { + A2_LOG_ERROR("Failed to get a value!"); + continue; + } + SecCertificateRef ref = 0; + if (SecIdentityCopyCertificate(id, &ref) != errSecSuccess) { + A2_LOG_ERROR("Failed to get a certref!"); + continue; + } + cfrelease del_ref(ref); + CFDataRef data = SecCertificateCopyData(ref); + if (!data) { + A2_LOG_ERROR("Failed to get a data!"); + continue; + } + cfrelease del_data(data); + + // Do try all supported hash algorithms. + // Usually the fingerprint would be sha1 or md5, however this is more + // future-proof. Also "usually" doesn't cut it; there is already software + // using SHA-2 class algos, and SHA-3 is standardized and potential users + // cannot be far. + if (std::find_if(ht.begin(), ht.end(), hash_finder(data, fp)) == ht.end()) { + continue; + } + A2_LOG_INFO("Found cert with matching fingerprint"); + credentials_ = id; + CFRetain(id); + return true; + } + + A2_LOG_ERROR(fmt("Failed to lookup %s in your KeyChain", fingerprint.c_str())); + return false; + +#else // defined(__MAC_10_6) + + A2_LOG_ERROR("Your system does not support creditials via fingerprints; Upgrade to OSX 10.6 or later"); + return false; + +#endif // defined(__MAC_10_6) +} } // namespace aria2 diff --git a/src/AppleTLSContext.h b/src/AppleTLSContext.h index 549d432e..d7ac6c25 100644 --- a/src/AppleTLSContext.h +++ b/src/AppleTLSContext.h @@ -50,10 +50,11 @@ class AppleTLSContext : public TLSContext { public: AppleTLSContext(TLSSessionSide side) : side_(side), - verifyPeer_(true) + verifyPeer_(true), + credentials_(0) {} - virtual ~AppleTLSContext() {} + virtual ~AppleTLSContext(); // private key `keyfile' must be decrypted. virtual bool addCredentialFile(const std::string& certfile, @@ -80,9 +81,14 @@ public: verifyPeer_ = verify; } + SecIdentityRef getCredentials(); + private: TLSSessionSide side_; bool verifyPeer_; + SecIdentityRef credentials_; + + bool tryAsFingerprint(const std::string& fingerprint); }; } // namespace aria2 diff --git a/src/AppleTLSSession.cc b/src/AppleTLSSession.cc index 6ac36094..385c7df1 100644 --- a/src/AppleTLSSession.cc +++ b/src/AppleTLSSession.cc @@ -39,8 +39,9 @@ #include -#include "fmt.h" #include "LogFactory.h" +#include "a2functional.h" +#include "fmt.h" #define ioErr -36 #define paramErr -50 @@ -52,6 +53,28 @@ namespace { static const SSLProtocol kTLSProtocol12 = (SSLProtocol)(kSSLProtocolAll + 2); #endif +#ifndef CIPHER_NO_DHPARAM + // Diffie-Hellman params, to seed the engine instead of having it spend up + // to 30 seconds on generating them. It should be save to share these. :p + // This was generated using: openssl dhparam -outform DER 2048 + static const uint8_t dhparam[] = + "\x30\x82\x01\x08\x02\x82\x01\x01\x00\x97\xea\xd0\x46\xf7\xae\xa7\x76\x80" + "\x9c\x74\x56\x98\xd8\x56\x97\x2b\x20\x6c\x77\xe2\x82\xbb\xc8\x84\xbe\xe7" + "\x63\xaf\xcc\x30\xd0\x67\x97\x7d\x1b\xab\x59\x30\xa9\x13\x67\x21\xd7\xd4" + "\x0e\x46\xcf\xe5\x80\xdf\xc9\xb9\xba\x54\x9b\x46\x2f\x3b\x45\xfc\x2f\xaf" + "\xad\xc0\x17\x56\xdd\x52\x42\x57\x45\x70\x14\xe5\xbe\x67\xaa\xde\x69\x75" + "\x30\x0d\xf9\xa2\xc4\x63\x4d\x7a\x39\xef\x14\x62\x18\x33\x44\xa1\xf9\xc1" + "\x52\xd1\xb6\x72\x21\x98\xf8\xab\x16\x1b\x7b\x37\x65\xe3\xc5\x11\x00\xf6" + "\x36\x1f\xd8\x5f\xd8\x9f\x43\xa8\xce\x9d\xbf\x5e\xd6\x2d\xfa\x0a\xc2\x01" + "\x54\xc2\xd9\x81\x54\x55\xb5\x26\xf8\x88\x37\xf5\xfe\xe0\xef\x4a\x34\x81" + "\xdc\x5a\xb3\x71\x46\x27\xe3\xcd\x24\xf6\x1b\xf1\xe2\x0f\xc2\xa1\x39\x53" + "\x5b\xc5\x38\x46\x8e\x67\x4c\xd9\xdd\xe4\x37\x06\x03\x16\xf1\x1d\x7a\xba" + "\x2d\xc1\xe4\x03\x1a\x58\xe5\x29\x5a\x29\x06\x69\x61\x7a\xd8\xa9\x05\x9f" + "\xc1\xa2\x45\x9c\x17\xad\x52\x69\x33\xdc\x18\x8d\x15\xa6\x5e\xcd\x94\xf4" + "\x45\xbb\x9f\xc2\x7b\x85\x00\x61\xb0\x1a\xdc\x3c\x86\xaa\x9f\x5c\x04\xb3" + "\x90\x0b\x35\x64\xff\xd9\xe3\xac\xf2\xf2\xeb\x3a\x63\x02\x01\x02"; +#endif // CIPHER_NO_DHPARAM + static inline const char *protoToString(SSLProtocol proto) { switch (proto) { case kSSLProtocol2: @@ -309,8 +332,41 @@ AppleTLSSession::AppleTLSSession(AppleTLSContext* ctx) if (SSLSetEnabledCiphers(sslCtx_, &enabled[0], enabled.size()) != noErr) { A2_LOG_ERROR("AppleTLS: Failed to set enabled ciphers list"); state_ = st_error; + return; } #endif + + if (ctx->getSide() == TLS_SERVER) { + SecIdentityRef creds = ctx->getCredentials(); + if (!creds) { + A2_LOG_ERROR("AppleTLS: No credentials"); + state_ = st_error; + return; + } + CFArrayRef certs = CFArrayCreate(0, (const void**)&creds, 1, 0); + if (!certs) { + A2_LOG_ERROR("AppleTLS: Failed to setup credentials"); + state_ = st_error; + return; + } + auto_delete del_certs(certs, CFRelease); + lastError_ = SSLSetCertificate(sslCtx_, certs); + if (lastError_ != noErr) { + A2_LOG_ERROR(fmt("AppleTLS: Failed to set credentials: %s", getLastErrorString().c_str())); + state_ = st_error; + return; + } + +#ifndef CIPHER_NO_DHPARAM + lastError_ = SSLSetDiffieHellmanParams(sslCtx_, dhparam, sizeof(dhparam)); + if (lastError_ != noErr) { + A2_LOG_WARN(fmt("AppleTLS: Failed to set DHParams: %s", getLastErrorString().c_str())); + // Engine will still generate some for us, so this is no problem, except + // it will take longer. + } +#endif // CIPHER_NO_DHPARAM + + } } AppleTLSSession::~AppleTLSSession() diff --git a/src/MessageDigest.cc b/src/MessageDigest.cc index 87010f78..c07c46cb 100644 --- a/src/MessageDigest.cc +++ b/src/MessageDigest.cc @@ -33,6 +33,9 @@ */ /* copyright --> */ #include "MessageDigest.h" + +#include + #include "MessageDigestImpl.h" #include "util.h" #include "array_fun.h" @@ -84,19 +87,24 @@ bool MessageDigest::supports(const std::string& hashType) return MessageDigestImpl::supports(hashType); } +std::vector MessageDigest::getSupportedHashTypes() +{ + std::vector rv; + for (HashTypeEntry *i = vbegin(hashTypes), *eoi = vend(hashTypes); + i != eoi; ++i) { + if (MessageDigestImpl::supports(i->hashType)) { + rv.push_back(i->hashType); + } + } + return rv; +} + std::string MessageDigest::getSupportedHashTypeString() { - std::string s; - for(HashTypeEntry* i = vbegin(hashTypes), *eoi = vend(hashTypes); i != eoi; - ++i) { - if(MessageDigestImpl::supports(i->hashType)) { - if(!s.empty()) { - s += ", "; - } - s += i->hashType; - } - } - return s; + std::vector ht = getSupportedHashTypes(); + std::stringstream ss; + std::copy(ht.begin(), ht.end(), std::ostream_iterator(ss, ", ")); + return ss.str().substr(ss.str().length() - 2); } size_t MessageDigest::getDigestLength(const std::string& hashType) @@ -162,9 +170,10 @@ void MessageDigest::reset() pImpl_->reset(); } -void MessageDigest::update(const void* data, size_t length) +MessageDigest& MessageDigest::update(const void* data, size_t length) { pImpl_->update(data, length); + return *this; } void MessageDigest::digest(unsigned char* md) diff --git a/src/MessageDigest.h b/src/MessageDigest.h index 6441e748..f5e6ca55 100644 --- a/src/MessageDigest.h +++ b/src/MessageDigest.h @@ -38,6 +38,7 @@ #include "common.h" #include +#include #include "SharedHandle.h" @@ -68,6 +69,9 @@ public: // Returns true if hashType is supported. Otherwise returns false. static bool supports(const std::string& hashType); + // Returns a vector containing supported hash function textual names. + static std::vector getSupportedHashTypes(); + // Returns string containing supported hash function textual names // joined with ','. static std::string getSupportedHashTypeString(); @@ -93,7 +97,7 @@ public: // Resets this object so that it can be reused. void reset(); - void update(const void* data, size_t length); + MessageDigest& update(const void* data, size_t length); // Stores digest in the region pointed by md. It is caller's // responsibility to allocate memory at least getDigestLength(). diff --git a/src/MultiUrlRequestInfo.cc b/src/MultiUrlRequestInfo.cc index a8bb2342..b0156aca 100644 --- a/src/MultiUrlRequestInfo.cc +++ b/src/MultiUrlRequestInfo.cc @@ -141,8 +141,11 @@ error_code::Value MultiUrlRequestInfo::execute() #ifdef ENABLE_SSL if(option_->getAsBool(PREF_ENABLE_RPC) && option_->getAsBool(PREF_RPC_SECURE)) { - if(!option_->blank(PREF_RPC_CERTIFICATE) && - !option_->blank(PREF_RPC_PRIVATE_KEY)) { + if(!option_->blank(PREF_RPC_CERTIFICATE) +#ifndef HAVE_APPLETLS + && !option_->blank(PREF_RPC_PRIVATE_KEY) +#endif // HAVE_APPLETLS + ) { // We set server TLS context to the SocketCore before creating // DownloadEngine instance. SharedHandle svTlsContext(TLSContext::make(TLS_SERVER)); diff --git a/src/OptionHandlerFactory.cc b/src/OptionHandlerFactory.cc index f5cb61fe..835d2388 100644 --- a/src/OptionHandlerFactory.cc +++ b/src/OptionHandlerFactory.cc @@ -788,11 +788,20 @@ std::vector OptionHandlerFactory::createOptionHandlers() handlers.push_back(op); } { - OptionHandler* op(new LocalFilePathOptionHandler + OptionHandler* op( +#ifdef HAVE_APPLETLS + new DefaultOptionHandler + (PREF_RPC_CERTIFICATE, + TEXT_RPC_CERTIFICATE, + NO_DEFAULT_VALUE) +#else // HAVE_APPLETLS + new LocalFilePathOptionHandler (PREF_RPC_CERTIFICATE, TEXT_RPC_CERTIFICATE, NO_DEFAULT_VALUE, - false)); + false) +#endif + ); op->addTag(TAG_RPC); handlers.push_back(op); }