calog/tests/testHttps.c

323 lines
11 KiB
C

// testHttps.c -- exercises calogHttp's TLS certificate verification. An in-test HTTPS server
// runs on loopback with a freshly generated SELF-SIGNED certificate (not chained to any trusted
// CA). A default httpGet must therefore REJECT it (verification on), while an httpRequest with
// insecure=true must ACCEPT it (verification off). Proves both the default-secure behavior and
// the documented opt-out. A third request then proves the trust-store loader end to end: with
// CALOG_CA_BUNDLE pointing at the server's own certificate, a DEFAULT (verifying) request must
// SUCCEED, since the pinned bundle makes that certificate a trusted anchor. The certificate carries
// a subjectAltName of DNS:localhost so hostname verification passes for an https://localhost URL.
#define _GNU_SOURCE
#include "calog.h"
#include "calogHttp.h"
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <time.h>
#include <unistd.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/ssl.h>
#include <openssl/x509.h>
#include <openssl/x509v3.h>
#define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__)
#define PUMP_LIMIT 4000
#define RESULT_SLOTS 8
static CalogT *calog = NULL;
static _Atomic int64_t results[RESULT_SLOTS];
static _Atomic int32_t doneCount = 0;
static _Atomic int32_t errorCount = 0;
static int32_t testsRun = 0;
static int32_t testsFailed = 0;
static int gListenFd = -1;
static X509 *gCert = NULL;
static EVP_PKEY *gPkey = NULL;
static char gCaFile[64] = { 0 };
static void checkImpl(bool condition, const char *message, const char *file, int32_t line);
static EVP_PKEY *makeSelfSigned(X509 **certOut);
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static void onError(uint64_t contextId, const char *message, void *userData);
static void pumpUntilDone(int32_t target);
static void *serverThread(void *arg);
static bool writeCaBundle(char *path, size_t cap, X509 *cert);
static void checkImpl(bool condition, const char *message, const char *file, int32_t line) {
testsRun++;
if (!condition) {
testsFailed++;
printf("FAIL %s:%d %s\n", file, line, message);
}
}
static EVP_PKEY *makeSelfSigned(X509 **certOut) {
EVP_PKEY *pkey;
X509 *cert;
X509_EXTENSION *ext;
X509V3_CTX v3ctx;
pkey = EVP_RSA_gen(2048);
if (pkey == NULL) {
return NULL;
}
cert = X509_new();
if (cert == NULL) {
EVP_PKEY_free(pkey);
return NULL;
}
X509_set_version(cert, 2);
ASN1_INTEGER_set(X509_get_serialNumber(cert), 1);
X509_gmtime_adj(X509_getm_notBefore(cert), 0);
X509_gmtime_adj(X509_getm_notAfter(cert), 3600);
X509_set_pubkey(cert, pkey);
X509_NAME_add_entry_by_txt(X509_get_subject_name(cert), "CN", MBSTRING_ASC, (const unsigned char *)"127.0.0.1", -1, -1, 0);
X509_set_issuer_name(cert, X509_get_subject_name(cert));
// subjectAltName = DNS:localhost, so SSL_set1_host("localhost") hostname verification passes for
// the https://localhost request that trusts this certificate via CALOG_CA_BUNDLE.
X509V3_set_ctx_nodb(&v3ctx);
X509V3_set_ctx(&v3ctx, cert, cert, NULL, NULL, 0);
ext = X509V3_EXT_conf_nid(NULL, &v3ctx, NID_subject_alt_name, "DNS:localhost");
if (ext == NULL) {
X509_free(cert);
EVP_PKEY_free(pkey);
return NULL;
}
X509_add_ext(cert, ext, -1);
X509_EXTENSION_free(ext);
if (X509_sign(cert, pkey, EVP_sha256()) == 0) {
X509_free(cert);
EVP_PKEY_free(pkey);
return NULL;
}
*certOut = cert;
return pkey;
}
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)args;
(void)argCount;
(void)userData;
atomic_fetch_add(&doneCount, 1);
calogValueNil(result);
return calogOkE;
}
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
calogValueNil(result);
if (argCount != 2 || args[0].type != calogIntE) {
return calogFail(result, calogErrArgE, "report expects (tag, value)");
}
if (args[0].as.i < 0 || args[0].as.i >= RESULT_SLOTS) {
return calogFail(result, calogErrArgE, "report: tag out of range");
}
atomic_store(&results[args[0].as.i], (args[1].type == calogIntE) ? args[1].as.i : (int64_t)args[1].as.r);
return calogOkE;
}
static void onError(uint64_t contextId, const char *message, void *userData) {
(void)contextId;
(void)userData;
fprintf(stderr, " [script error] %s\n", (message != NULL) ? message : "(null)");
atomic_fetch_add(&errorCount, 1);
}
static void pumpUntilDone(int32_t target) {
struct timespec ts = { 0, 500000 };
int i;
for (i = 0; i < PUMP_LIMIT; i++) {
calogPump(calog);
if (atomic_load(&doneCount) >= target) {
calogPump(calog);
return;
}
nanosleep(&ts, NULL);
}
}
static void *serverThread(void *arg) {
static const char response[] = "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello";
SSL_CTX *ctx;
int n;
(void)arg;
ctx = SSL_CTX_new(TLS_server_method());
if (ctx == NULL) {
return NULL;
}
// gCert / gPkey are created (and freed) by main so the certificate can also be written to the
// CALOG_CA_BUNDLE pin file the third request trusts.
SSL_CTX_use_certificate(ctx, gCert);
SSL_CTX_use_PrivateKey(ctx, gPkey);
// Three connections: the first (default verify) has its handshake aborted by the client when it
// rejects the untrusted cert; the second (insecure=true) completes and gets the reply; the third
// (default verify, cert pinned via CALOG_CA_BUNDLE) verifies successfully and gets the reply.
for (n = 0; n < 3; n++) {
SSL *ssl;
int cfd;
cfd = accept(gListenFd, NULL, NULL);
if (cfd < 0) {
break;
}
ssl = SSL_new(ctx);
SSL_set_fd(ssl, cfd);
if (SSL_accept(ssl) == 1) {
char buf[1024];
(void)SSL_read(ssl, buf, sizeof(buf));
(void)SSL_write(ssl, response, (int)(sizeof(response) - 1));
SSL_shutdown(ssl);
}
SSL_free(ssl);
close(cfd);
}
SSL_CTX_free(ctx);
return NULL;
}
// Write cert as a PEM bundle to a fresh temp file, storing its path in path (cap bytes). Used as the
// CALOG_CA_BUNDLE pin so a verifying request can trust the server's own self-signed certificate.
static bool writeCaBundle(char *path, size_t cap, X509 *cert) {
FILE *fp;
int fd;
if ((size_t)snprintf(path, cap, "/tmp/calogHttpsCa.XXXXXX") >= cap) {
return false;
}
fd = mkstemp(path);
if (fd < 0) {
return false;
}
fp = fdopen(fd, "wb");
if (fp == NULL) {
close(fd);
return false;
}
if (PEM_write_X509(fp, cert) != 1) {
fclose(fp);
return false;
}
fclose(fp);
return true;
}
int main(void) {
CalogContextT *ctx;
pthread_t server;
struct sockaddr_in addr;
socklen_t addrLen;
char script[512];
int port;
int yes;
int32_t i;
calog = calogCreate();
if (calog == NULL) {
printf("calog create failed\n");
return 1;
}
calogSetErrorHandler(calog, onError, NULL);
calogRegister(calog, "report", nativeReport, NULL);
calogRegister(calog, "done", nativeDone, NULL);
calogHttpRegister(calog);
for (i = 0; i < RESULT_SLOTS; i++) {
atomic_store(&results[i], -1);
}
gListenFd = socket(AF_INET, SOCK_STREAM, 0);
if (gListenFd < 0) {
printf("socket failed\n");
return 1;
}
yes = 1;
setsockopt(gListenFd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
addr.sin_port = 0;
if (bind(gListenFd, (struct sockaddr *)&addr, sizeof(addr)) != 0 || listen(gListenFd, 4) != 0) {
printf("bind/listen failed\n");
return 1;
}
addrLen = sizeof(addr);
if (getsockname(gListenFd, (struct sockaddr *)&addr, &addrLen) != 0) {
printf("getsockname failed\n");
return 1;
}
port = (int)ntohs(addr.sin_port);
gPkey = makeSelfSigned(&gCert);
if (gPkey == NULL || !writeCaBundle(gCaFile, sizeof(gCaFile), gCert)) {
printf("certificate/bundle setup failed\n");
return 1;
}
pthread_create(&server, NULL, serverThread, NULL);
snprintf(script, sizeof(script),
"local ok = pcall(function() return httpGet('https://127.0.0.1:%d/') end)\n"
"report(1, ok and 0 or 1)\n" // 1, a self-signed cert is rejected by default
"local r = httpRequest({url = 'https://127.0.0.1:%d/', insecure = true})\n"
"report(2, r.status)\n" // 200, insecure skips verification
"report(3, r.body == 'hello' and 1 or 0)\n" // 1, body over TLS
"done()", port, port);
ctx = calogContextOpen(calog, &calogLuaEngine);
calogContextEval(ctx, script);
pumpUntilDone(1);
// Now trust the server's own certificate via CALOG_CA_BUNDLE and repeat with DEFAULT (verifying)
// requests: the handshake must succeed, exercising httpLoadTrustStore's pinned-bundle path.
setenv("CALOG_CA_BUNDLE", gCaFile, 1);
snprintf(script, sizeof(script),
"local r = httpRequest({url = 'https://localhost:%d/'})\n"
"report(4, r.status)\n" // 200, the cert is trusted via the pinned bundle
"report(5, r.body == 'hello' and 1 or 0)\n" // 1, body over verified TLS
"done()", port);
calogContextEval(ctx, script);
pumpUntilDone(2);
pthread_join(server, NULL);
close(gListenFd);
CHECK(atomic_load(&results[1]) == 1, "httpGet rejects an untrusted self-signed certificate by default");
CHECK(atomic_load(&results[2]) == 200, "httpRequest with insecure=true completes the TLS request");
CHECK(atomic_load(&results[3]) == 1, "the body is delivered over TLS when verification is skipped");
CHECK(atomic_load(&results[4]) == 200, "a verifying request succeeds when the cert is trusted via CALOG_CA_BUNDLE");
CHECK(atomic_load(&results[5]) == 1, "the body is delivered over verified TLS through the pinned bundle");
CHECK(atomic_load(&errorCount) == 0, "no uncaught errors");
calogDestroy(calog);
if (gCaFile[0] != '\0') {
unlink(gCaFile);
}
X509_free(gCert);
EVP_PKEY_free(gPkey);
printf("\n%d checks, %d failed\n", testsRun, testsFailed);
fflush(stdout);
if (testsFailed != 0) {
return 1;
}
return 0;
}