/** * Copyright (C) 2023 Graham Leggett * * 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. * */ /* * redwax_keychain - import / export routines for MacOS keychains. * */ #include #include "config.h" #include "redwax-tool.h" #include "redwax_util.h" #if HAVE_SECURITY_SECURITY_H #include #include //#include module keychain_module; typedef struct { unsigned int in:1; SecKeychainRef keychain; } keychain_config_t; typedef struct { CFDictionaryRef kref; const UInt8 *fingerprint; CFIndex fingerprint_len; } keychain_key_config_t; #if 0 static apr_status_t cleanup_key(void *dummy) { if (dummy) { redwax_key_t *key = dummy; keychain_key_config_t *key_config; key_config = redwax_get_module_config(key->per_module, &keychain_module); if (key_config) { if (key_config->kref) { CFRelease(key_config->kref); } if (key->keys_index && key_config->fingerprint) { apr_hash_set(key->keys_index, key_config->fingerprint, key_config->fingerprint_len, NULL); } } } return APR_SUCCESS; } #endif static apr_status_t redwax_keychain_initialise(redwax_tool_t *r) { return OK; } static apr_status_t redwax_keychain_complete_keychain_in(redwax_tool_t *r, const char *url, apr_hash_t *paths) { SecPreferencesDomain domains[] = { kSecPreferencesDomainUser, kSecPreferencesDomainSystem, kSecPreferencesDomainCommon, kSecPreferencesDomainDynamic }; OSStatus err; CFArrayRef searchList = NULL; CFIndex count; CFIndex i, j; apr_hash_set(paths, "*", APR_HASH_KEY_STRING, "*"); for (i = 0; i < (sizeof(domains) / sizeof(SecPreferencesDomain)); i++) { #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" err = SecKeychainCopyDomainSearchList(domains[i], &searchList); #pragma GCC diagnostic pop count = CFArrayGetCount(searchList); for (j = 0; j < count; j++) { const char *name; char buffer[1024]; UInt32 len = sizeof(buffer); SecKeychainRef kref = (SecKeychainRef) CFArrayGetValueAtIndex(searchList, j); #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" err = SecKeychainGetPath(kref, &len, buffer); #pragma GCC diagnostic pop if (err == errSecSuccess) { name = apr_pstrndup(r->pool, buffer, len); apr_hash_set(paths, name, APR_HASH_KEY_STRING, name); } } } return OK; } static apr_status_t redwax_keychain_process_certificates(redwax_tool_t *r) { CFDictionaryRef query; CFTypeRef certs = NULL; CFIndex count; CFIndex i; OSStatus err; keychain_config_t *config; config = redwax_get_module_config(r->per_module, &keychain_module); if (!config->keychain) { CFStringRef keys[] = { kSecClass, kSecMatchLimit, kSecReturnRef }; CFTypeRef values[] = { kSecClassCertificate, kSecMatchLimitAll, kCFBooleanTrue }; query = CFDictionaryCreate( NULL, (const void **) keys, values, sizeof(keys) / sizeof(keys[0]), &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks ); } else { CFStringRef keys[] = { kSecClass, kSecMatchLimit, kSecUseKeychain, kSecReturnRef }; CFTypeRef values[] = { kSecClassCertificate, kSecMatchLimitAll, config->keychain, kCFBooleanTrue }; query = CFDictionaryCreate( NULL, (const void **) keys, values, sizeof(keys) / sizeof(keys[0]), &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks ); } // SecKeychainRef keychain = NULL; // kSecUseKeychain - search by keychain err = SecItemCopyMatching(query, &certs); if (err != errSecSuccess) { CFStringRef error = SecCopyErrorMessageString(err, NULL); CFIndex len = CFStringGetLength(error); CFIndex max = CFStringGetMaximumSizeForEncoding(len, kCFStringEncodingUTF8) + 1; char *buffer = apr_palloc(r->pool, max); if (CFStringGetCString(error, buffer, max, kCFStringEncodingUTF8)) { redwax_print_error(r, "keychain-in: certificates could not be returned: %s\n", buffer); } return APR_EGENERAL; } count = CFArrayGetCount(certs); for (i = 0; i < count; i++) { SecCertificateRef cref = (SecCertificateRef) CFArrayGetValueAtIndex(certs, i); apr_pool_t *p; apr_pool_create(&p, r->pool); redwax_certificate_t *cert = apr_pcalloc(p, sizeof(redwax_certificate_t)); cert->pool = p; cert->per_module = redwax_create_module_config(cert->pool); cert->common.type = REDWAX_CERTIFICATE_X509; CFDataRef der = SecCertificateCopyData(cref); cert->len = CFDataGetLength(der); cert->der = apr_pmemdup(p, CFDataGetBytePtr(der), cert->len); rt_run_normalise_certificate(r, cert, 1); switch (cert->common.category) { case REDWAX_CERTIFICATE_END_ENTITY: { redwax_certificate_t *c = apr_array_push(r->certs_in); memcpy(c, cert, sizeof(*cert)); redwax_print_error(r, "keychain-in: certificate: %s\n", cert->common.subject); break; } case REDWAX_CERTIFICATE_INTERMEDIATE: { redwax_certificate_t *c = apr_array_push(r->intermediates_in); memcpy(c, cert, sizeof(*cert)); redwax_print_error(r, "keychain-in: intermediate: %s\n", cert->common.subject); break; } case REDWAX_CERTIFICATE_ROOT: { redwax_certificate_t *c = apr_array_push(r->intermediates_in); memcpy(c, cert, sizeof(*cert)); redwax_print_error(r, "keychain-in: root: %s\n", cert->common.subject); break; } case REDWAX_CERTIFICATE_TRUSTED: { redwax_certificate_t *c = apr_array_push(r->trusted_in); memcpy(c, cert, sizeof(*cert)); redwax_print_error(r, "keychain-in: trusted: %s\n", cert->common.subject); break; } default: { redwax_print_debug(r, "keychain-in: unrecognised " "certificate, skipped: %s\n", cert->common.subject); break; } } } CFRelease(certs); return APR_SUCCESS; } static apr_status_t redwax_keychain_process_trusted(redwax_tool_t *r) { CFArrayRef trusted = NULL; CFIndex count; CFIndex i; OSStatus err = SecTrustCopyAnchorCertificates(&trusted); if (err != errSecSuccess) { CFStringRef error = SecCopyErrorMessageString(err, NULL); CFIndex len = CFStringGetLength(error); CFIndex max = CFStringGetMaximumSizeForEncoding(len, kCFStringEncodingUTF8) + 1; char *buffer = apr_palloc(r->pool, max); if (CFStringGetCString(error, buffer, max, kCFStringEncodingUTF8)) { redwax_print_error(r, "keychain-in: trusted certificates could not be returned: %s\n", buffer); } return APR_EGENERAL; } count = CFArrayGetCount(trusted); for (i = 0; i < count; i++) { SecCertificateRef cref = (SecCertificateRef) CFArrayGetValueAtIndex(trusted, i); apr_pool_t *p; apr_pool_create(&p, r->pool); redwax_certificate_t *cert = apr_pcalloc(p, sizeof(redwax_certificate_t)); cert->pool = p; cert->per_module = redwax_create_module_config(cert->pool); cert->common.type = REDWAX_CERTIFICATE_X509; CFDataRef der = SecCertificateCopyData(cref); cert->len = CFDataGetLength(der); cert->der = apr_pmemdup(p, CFDataGetBytePtr(der), cert->len); rt_run_normalise_certificate(r, cert, 1); switch (cert->common.category) { case REDWAX_CERTIFICATE_INTERMEDIATE: case REDWAX_CERTIFICATE_ROOT: case REDWAX_CERTIFICATE_TRUSTED: { redwax_certificate_t *c = apr_array_push(r->trusted_in); memcpy(c, cert, sizeof(*cert)); redwax_print_error(r, "keychain-in: trusted: %s\n", cert->common.subject); break; } default: { redwax_print_debug(r, "keychain-in: unrecognised " "certificate, skipped: %s\n", cert->common.subject); break; } } } CFRelease(trusted); return APR_SUCCESS; } #if 0 /* * This code demonstrates how the keychain might be queried for * keys the same way we query certificates. * * The current mechanism pulls keys in the search_key hook. */ static apr_status_t redwax_keychain_process_keys(redwax_tool_t *r) { CFTypeRef keys = NULL; CFIndex count; CFIndex i; CFStringRef dictkeys[] = { kSecClass, kSecMatchLimit, kSecAttrKeyClass, kSecReturnRef, #if 0 kSecReturnAttributes #endif }; CFTypeRef dictvalues[] = { kSecClassKey, kSecMatchLimitAll, kSecAttrKeyClassPublic, kCFBooleanTrue, #if 0 kCFBooleanTrue #endif }; CFDictionaryRef query = CFDictionaryCreate( NULL, (const void **) dictkeys, dictvalues, sizeof(dictkeys) / sizeof(dictkeys[0]), &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks ); OSStatus err = SecItemCopyMatching(query, &keys); if (err != errSecSuccess) { CFStringRef error = SecCopyErrorMessageString(err, NULL); CFIndex len = CFStringGetLength(error); CFIndex max = CFStringGetMaximumSizeForEncoding(len, kCFStringEncodingUTF8) + 1; char *buffer = apr_palloc(r->pool, max); if (CFStringGetCString(error, buffer, max, kCFStringEncodingUTF8)) { redwax_print_error(r, "keychain-in: keys could not be returned: %s\n", buffer); } return APR_EGENERAL; } #if 0 CFShow(keys); #endif count = CFArrayGetCount(keys); for (i = 0; i < count; i++) { SecKeyRef keyref = (SecKeyRef) CFArrayGetValueAtIndex(keys, i); redwax_key_t *key; keychain_key_config_t *key_config; CFShow(keyref); CFDataRef der; /* kSecFormatOpenSSL is undefined - experimentation shows * it creates SubjectPublicKeyInfo for most public keys. */ err = SecItemExport(keyref, kSecFormatOpenSSL, 0, NULL, &der); if (err != errSecSuccess) { CFStringRef error = SecCopyErrorMessageString(err, NULL); CFIndex len = CFStringGetLength(error); CFIndex max = CFStringGetMaximumSizeForEncoding(len, kCFStringEncodingUTF8) + 1; char *buffer = apr_palloc(r->pool, max); if (CFStringGetCString(error, buffer, max, kCFStringEncodingUTF8)) { redwax_print_error(r, "keychain-in: public key could not be converted: %s\n", buffer); } continue; } CFDictionaryRef kdict = CFRetain(SecKeyCopyAttributes(keyref)); key = apr_array_push(r->keys_in); apr_pool_create(&key->pool, r->keys_in->pool); key->common.subjectpublickeyinfo_len = CFDataGetLength(der); key->common.subjectpublickeyinfo_der = apr_pmemdup(key->pool, CFDataGetBytePtr(der), key->common.subjectpublickeyinfo_len); CFRelease(der); key->per_module = redwax_create_module_config(key->pool); key_config = apr_pcalloc(key->pool, sizeof(keychain_key_config_t)); redwax_set_module_config(key->per_module, &keychain_module, key_config); key_config->kref = kdict; apr_pool_cleanup_register(key->pool, key, cleanup_key, apr_pool_cleanup_null); CFStringRef label = CFDictionaryGetValue(kdict, kSecAttrLabel); if (label) { CFIndex len = CFStringGetLength(label); CFIndex max = CFStringGetMaximumSizeForEncoding(len, kCFStringEncodingUTF8) + 1; char *buffer = apr_palloc(r->pool, max); if (CFStringGetCString(label, buffer, max, kCFStringEncodingUTF8)) { redwax_print_error(r, "keychain-in: key: %s\n", buffer); } } else { redwax_print_error(r, "keychain-in: key\n"); } /* index the key */ CFDataRef fingerprint = CFDictionaryGetValue(kdict, kSecAttrApplicationLabel); if (fingerprint) { key_config->fingerprint_len = CFDataGetLength(fingerprint); key_config->fingerprint = apr_pmemdup(key->pool, CFDataGetBytePtr(fingerprint), key_config->fingerprint_len); } if (!apr_hash_get(r->keys_index, key->common.subjectpublickeyinfo_der, key->common.subjectpublickeyinfo_len)) { key->keys_index = r->keys_index; apr_hash_set(key->keys_index, key->common.subjectpublickeyinfo_der, key->common.subjectpublickeyinfo_len, key); } } CFRelease(keys); return APR_SUCCESS; } #endif static apr_status_t redwax_keychain_process_keychain_in(redwax_tool_t *r, const char *name) { apr_status_t status; keychain_config_t *config; config = apr_pcalloc(r->pool, sizeof(keychain_config_t)); redwax_set_module_config(r->per_module, &keychain_module, config); config->in = 1; if (strcmp(name, "*")) { SecPreferencesDomain domains[] = { kSecPreferencesDomainUser, kSecPreferencesDomainSystem, kSecPreferencesDomainCommon, kSecPreferencesDomainDynamic }; OSStatus err; CFArrayRef searchList = NULL; CFIndex count; CFIndex i, j; for (i = 0; i < (sizeof(domains) / sizeof(SecPreferencesDomain)); i++) { #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" err = SecKeychainCopyDomainSearchList(domains[i], &searchList); #pragma GCC diagnostic pop count = CFArrayGetCount(searchList); for (j = 0; j < count; j++) { char buffer[1024]; UInt32 len = sizeof(buffer); SecKeychainRef kref = (SecKeychainRef) CFArrayGetValueAtIndex(searchList, j); #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" err = SecKeychainGetPath(kref, &len, buffer); #pragma GCC diagnostic pop if (err == errSecSuccess && !strncmp(name, buffer, sizeof(buffer))) { config->keychain = kref; goto found; } } } redwax_print_error(r, "keychain-in: name '%s' not recognised, use '*' for all.\n", name); return APR_EINVAL; } found: if (APR_SUCCESS != (status = redwax_keychain_process_certificates(r))) { return status; } // fixme - trusted needs own config option if (APR_SUCCESS != (status = redwax_keychain_process_trusted(r))) { return status; } #if 0 if (APR_SUCCESS != (status = redwax_keychain_process_keys(r))) { return status; } #endif return OK; } static apr_status_t redwax_keychain_search_key(redwax_tool_t *r, const redwax_certificate_t *cert) { keychain_config_t *config; config = redwax_get_module_config(r->per_module, &keychain_module); if (cert->der && config && config->in) { OSStatus err; redwax_key_t *key; SecIdentityRef idref = NULL; CFDataRef data = CFDataCreateWithBytesNoCopy(kCFAllocatorDefault, cert->der, cert->len, kCFAllocatorNull); SecCertificateRef certref = SecCertificateCreateWithData(kCFAllocatorDefault, data); CFRelease(data); err = SecIdentityCreateWithCertificate(NULL, certref, &idref); CFRelease(certref); if (err != errSecSuccess) { CFStringRef error = SecCopyErrorMessageString(err, NULL); CFIndex len = CFStringGetLength(error); CFIndex max = CFStringGetMaximumSizeForEncoding(len, kCFStringEncodingUTF8) + 1; char *buffer = apr_palloc(r->pool, max); if (CFStringGetCString(error, buffer, max, kCFStringEncodingUTF8)) { if (err == errSecItemNotFound) { redwax_print_debug(r, "keychain-in: key could not be searched: %s\n", buffer); } else { redwax_print_error(r, "keychain-in: key could not be searched: %s\n", buffer); } } return DECLINED; } SecKeyRef keyref; err = SecIdentityCopyPrivateKey(idref, &keyref); CFRelease(idref); if (err != errSecSuccess) { CFStringRef error = SecCopyErrorMessageString(err, NULL); CFIndex len = CFStringGetLength(error); CFIndex max = CFStringGetMaximumSizeForEncoding(len, kCFStringEncodingUTF8) + 1; char *buffer = apr_palloc(r->pool, max); if (CFStringGetCString(error, buffer, max, kCFStringEncodingUTF8)) { redwax_print_error(r, "keychain-in: key found but could not be retrieved: %s\n", buffer); } return DECLINED; } CFDataRef der; err = SecItemExport(keyref, kSecFormatBSAFE, 0, NULL, &der); CFRelease(keyref); if (err != errSecSuccess) { CFStringRef error = SecCopyErrorMessageString(err, NULL); CFIndex len = CFStringGetLength(error); CFIndex max = CFStringGetMaximumSizeForEncoding(len, kCFStringEncodingUTF8) + 1; char *buffer = apr_palloc(r->pool, max); if (CFStringGetCString(error, buffer, max, kCFStringEncodingUTF8)) { redwax_print_error(r, "keychain-in: key retrieved but could not be exported: %s\n", buffer); } return DECLINED; } key = apr_array_push(r->keys_out); apr_pool_create(&key->pool, r->keys_out->pool); key->len = CFDataGetLength(der); key->der = apr_pmemdup(key->pool, CFDataGetBytePtr(der), key->len); CFRelease(der); return APR_SUCCESS; } return DECLINED; } void redwax_add_default_keychain_hooks() { rt_hook_complete_keychain_in(redwax_keychain_complete_keychain_in, NULL, NULL, APR_HOOK_MIDDLE); rt_hook_process_keychain_in(redwax_keychain_process_keychain_in, NULL, NULL, APR_HOOK_MIDDLE); rt_hook_search_key(redwax_keychain_search_key, NULL, NULL, APR_HOOK_MIDDLE); rt_hook_initialise(redwax_keychain_initialise, NULL, NULL, APR_HOOK_MIDDLE); } #else void redwax_add_default_keychain_hooks() { } #endif REDWAX_DECLARE_MODULE(keychain) = { STANDARD_MODULE_STUFF, redwax_add_default_keychain_hooks /* register hooks */ };