calog/libs/calogTrust.c
2026-09-12 01:20:52 -05:00

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