245 lines
9 KiB
C
245 lines
9 KiB
C
// calogTrust.c -- trust anchor loading for TLS clients (see calogTrust.h). Moved out of
|
|
// calogHttp.c on 2026-09-11 so the tcp transport's client-side TLS shares one implementation with
|
|
// the http client rather than carrying a second copy of it.
|
|
|
|
#include "calogTrust.h"
|
|
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include <openssl/err.h>
|
|
#include <openssl/x509.h>
|
|
|
|
// Native trust-store access is platform specific. Windows reads the system certificate stores via
|
|
// wincrypt.h, whose macros collide with OpenSSL's X.509 API, so they are undefined right after
|
|
// inclusion. macOS (only when built against the Apple SDK) reads the keychain via the Security
|
|
// framework. Every POSIX target also probes the well-known CA-bundle files, which needs unistd.h.
|
|
#if defined(_WIN32)
|
|
#include <windows.h>
|
|
#include <wincrypt.h>
|
|
#undef X509_NAME
|
|
#undef X509_EXTENSIONS
|
|
#undef PKCS7_ISSUER_AND_SERIAL
|
|
#undef PKCS7_SIGNER_INFO
|
|
#undef OCSP_REQUEST
|
|
#undef OCSP_RESPONSE
|
|
#else
|
|
#include <unistd.h>
|
|
#if defined(__APPLE__) && defined(CALOG_MAC_KEYCHAIN_TRUST)
|
|
#include <CoreFoundation/CoreFoundation.h>
|
|
#include <Security/Security.h>
|
|
#endif
|
|
#endif
|
|
|
|
#ifndef _WIN32
|
|
static bool trustLoadProbedPaths(SSL_CTX *ctx);
|
|
#endif
|
|
#if defined(__APPLE__) && defined(CALOG_MAC_KEYCHAIN_TRUST)
|
|
static bool trustLoadMacRoots(SSL_CTX *ctx);
|
|
#endif
|
|
#if defined(_WIN32)
|
|
static bool trustLoadWindowsRoots(SSL_CTX *ctx);
|
|
#endif
|
|
|
|
|
|
#if defined(__APPLE__) && defined(CALOG_MAC_KEYCHAIN_TRUST)
|
|
// Load trust anchors from the macOS system keychain via the Security framework. This needs the
|
|
// Apple SDK, so calog's SDK-less cross build leaves it disabled (falling back to
|
|
// trustLoadProbedPaths); it is compiled only for a native macOS build made with
|
|
// -DCALOG_MAC_KEYCHAIN_TRUST that links -framework Security -framework CoreFoundation. Each anchor
|
|
// is DER, so it round-trips through OpenSSL's d2i_X509 into ctx's store. Returns true once any
|
|
// anchor was added.
|
|
static bool trustLoadMacRoots(SSL_CTX *ctx) {
|
|
X509_STORE *store;
|
|
CFArrayRef anchors;
|
|
CFIndex count;
|
|
CFIndex i;
|
|
bool loaded;
|
|
|
|
store = SSL_CTX_get_cert_store(ctx);
|
|
anchors = NULL;
|
|
loaded = false;
|
|
if (SecTrustCopyAnchorCertificates(&anchors) != errSecSuccess || anchors == NULL) {
|
|
return false;
|
|
}
|
|
count = CFArrayGetCount(anchors);
|
|
for (i = 0; i < count; i++) {
|
|
SecCertificateRef cert;
|
|
CFDataRef der;
|
|
const unsigned char *bytes;
|
|
X509 *x;
|
|
|
|
cert = (SecCertificateRef)CFArrayGetValueAtIndex(anchors, i);
|
|
if (cert == NULL) {
|
|
continue;
|
|
}
|
|
der = SecCertificateCopyData(cert);
|
|
if (der == NULL) {
|
|
continue;
|
|
}
|
|
bytes = CFDataGetBytePtr(der);
|
|
x = d2i_X509(NULL, &bytes, (long)CFDataGetLength(der));
|
|
CFRelease(der);
|
|
if (x == NULL) {
|
|
continue;
|
|
}
|
|
if (X509_STORE_add_cert(store, x) == 1) {
|
|
loaded = true;
|
|
}
|
|
X509_free(x);
|
|
}
|
|
CFRelease(anchors);
|
|
ERR_clear_error();
|
|
return loaded;
|
|
}
|
|
#endif
|
|
|
|
|
|
#ifndef _WIN32
|
|
// Load trust anchors from the CA-bundle files (and hash directories) that Linux distributions and
|
|
// the BSDs/macOS ship. The well-known locations are tried in order -- one bundle is enough (a
|
|
// system generally has exactly one), but a hash directory is also tried for distros that ship only
|
|
// that. access() gates each try so a missing path does not push errors onto OpenSSL's error queue.
|
|
// Returns true once any anchors loaded. (CALOG_CA_BUNDLE / SSL_CERT_* overrides are handled, ahead
|
|
// of this probe, by calogTrustLoad.)
|
|
static bool trustLoadProbedPaths(SSL_CTX *ctx) {
|
|
static const char *const files[] = {
|
|
"/etc/ssl/certs/ca-certificates.crt", // Debian, Ubuntu, Arch, Gentoo, Alpine
|
|
"/etc/pki/tls/certs/ca-bundle.crt", // Fedora, RHEL, CentOS
|
|
"/etc/ssl/ca-bundle.pem", // openSUSE
|
|
"/etc/pki/tls/cacert.pem", // OpenELEC
|
|
"/etc/ssl/cert.pem", // Alpine, macOS, OpenBSD, FreeBSD
|
|
"/usr/local/share/certs/ca-root-nss.crt", // FreeBSD (ports)
|
|
"/etc/openssl/certs/ca-certificates.crt" // NetBSD
|
|
};
|
|
static const char *const dirs[] = {
|
|
"/etc/ssl/certs", // Debian, openSUSE (hashed)
|
|
"/etc/pki/tls/certs", // Fedora, RHEL
|
|
"/system/etc/security/cacerts" // Android
|
|
};
|
|
size_t i;
|
|
bool loaded;
|
|
|
|
loaded = false;
|
|
for (i = 0; i < sizeof(files) / sizeof(files[0]); i++) {
|
|
if (access(files[i], R_OK) != 0) {
|
|
continue;
|
|
}
|
|
if (SSL_CTX_load_verify_locations(ctx, files[i], NULL) == 1) {
|
|
loaded = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!loaded) {
|
|
for (i = 0; i < sizeof(dirs) / sizeof(dirs[0]); i++) {
|
|
if (access(dirs[i], R_OK) != 0) {
|
|
continue;
|
|
}
|
|
if (SSL_CTX_load_verify_locations(ctx, NULL, dirs[i]) == 1) {
|
|
loaded = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return loaded;
|
|
}
|
|
#endif
|
|
|
|
|
|
// Populate ctx's certificate trust store from the host's native trust configuration. OpenSSL's
|
|
// compiled-in default paths point at the vendored build prefix, which does not exist at runtime, so
|
|
// they resolve nothing on their own; instead we consult the operating system directly (the Windows
|
|
// system stores, the macOS keychain, or the distro CA-bundle files). Overrides are honored ahead of
|
|
// the native store, in priority order: CALOG_CA_BUNDLE (authoritative and EXCLUSIVE -- trust only
|
|
// that bundle, so it can pin to a private CA, and fail closed if it cannot be loaded), then the
|
|
// OpenSSL-standard SSL_CERT_FILE / SSL_CERT_DIR. Returns true only if a trust source actually
|
|
// loaded (not merely because an override variable is set), so a verified request can fail with a
|
|
// clear "no trust store" error instead of a misleading per-certificate verification failure.
|
|
bool calogTrustLoad(SSL_CTX *ctx) {
|
|
const char *pin;
|
|
const char *envFile;
|
|
const char *envDir;
|
|
bool loaded;
|
|
|
|
// A pinned bundle is authoritative and exclusive: trust EXACTLY it and nothing else, and fail
|
|
// closed if it will not load. Load return value (not mere presence of the variable) decides.
|
|
pin = getenv("CALOG_CA_BUNDLE");
|
|
if (pin != NULL) {
|
|
return SSL_CTX_load_verify_locations(ctx, pin, NULL) == 1;
|
|
}
|
|
loaded = false;
|
|
// The OpenSSL-standard operator override. Load each explicitly and confirm it, so a broken
|
|
// SSL_CERT_FILE still surfaces the clear no-trust-store error rather than counting as loaded.
|
|
// (An SSL_CERT_DIR is a lazy hash-dir lookup, so its load cannot be pre-confirmed; a set-but-
|
|
// empty directory is the one case that still counts as loaded, which is fail-closed-safe.)
|
|
envFile = getenv("SSL_CERT_FILE");
|
|
if (envFile != NULL && SSL_CTX_load_verify_locations(ctx, envFile, NULL) == 1) {
|
|
loaded = true;
|
|
}
|
|
envDir = getenv("SSL_CERT_DIR");
|
|
if (envDir != NULL && SSL_CTX_load_verify_locations(ctx, NULL, envDir) == 1) {
|
|
loaded = true;
|
|
}
|
|
#if defined(_WIN32)
|
|
if (trustLoadWindowsRoots(ctx)) {
|
|
loaded = true;
|
|
}
|
|
#else
|
|
#if defined(__APPLE__) && defined(CALOG_MAC_KEYCHAIN_TRUST)
|
|
if (trustLoadMacRoots(ctx)) {
|
|
loaded = true;
|
|
}
|
|
#endif
|
|
if (trustLoadProbedPaths(ctx)) {
|
|
loaded = true;
|
|
}
|
|
#endif
|
|
return loaded;
|
|
}
|
|
|
|
|
|
#if defined(_WIN32)
|
|
// Load trust anchors from the Windows system certificate stores (ROOT = trusted roots, CA =
|
|
// intermediates). Each store certificate is DER, so it round-trips through OpenSSL's d2i_X509 into
|
|
// ctx's store. Returns true once any certificate was added.
|
|
static bool trustLoadWindowsRoots(SSL_CTX *ctx) {
|
|
static const char *const stores[] = { "ROOT", "CA" };
|
|
X509_STORE *store;
|
|
size_t i;
|
|
bool loaded;
|
|
|
|
store = SSL_CTX_get_cert_store(ctx);
|
|
loaded = false;
|
|
for (i = 0; i < sizeof(stores) / sizeof(stores[0]); i++) {
|
|
HCERTSTORE sys;
|
|
PCCERT_CONTEXT cert;
|
|
|
|
sys = CertOpenSystemStoreA(0, stores[i]);
|
|
if (sys == NULL) {
|
|
continue;
|
|
}
|
|
cert = NULL;
|
|
while ((cert = CertEnumCertificatesInStore(sys, cert)) != NULL) {
|
|
const unsigned char *der;
|
|
X509 *x;
|
|
|
|
der = cert->pbCertEncoded;
|
|
x = d2i_X509(NULL, &der, (long)cert->cbCertEncoded);
|
|
if (x == NULL) {
|
|
continue;
|
|
}
|
|
// X509_STORE_add_cert up-refs on success, so our reference is always released here; a
|
|
// duplicate (already present) returns 0 and is simply skipped.
|
|
if (X509_STORE_add_cert(store, x) == 1) {
|
|
loaded = true;
|
|
}
|
|
X509_free(x);
|
|
}
|
|
CertCloseStore(sys, 0);
|
|
}
|
|
// Duplicate-add attempts push errors onto OpenSSL's thread-local queue; clear them so a later
|
|
// SSL_get_error is not misled by this bookkeeping.
|
|
ERR_clear_error();
|
|
return loaded;
|
|
}
|
|
#endif
|