// Minimal WinHTTP GET through a *secure* (HTTPS) forward proxy. // Proxy: https://mysecureproxy.duckdns.org:3129 (TLS to the proxy itself) // // Purpose: prove/disprove the "CRL theory" -- when WinHTTP does TLS to a // secure proxy, does it choke on the proxy cert's revocation check (CRL/OCSP)? // A secure-failure callback prints the EXACT reason the cert was rejected. // // Build: cmake -B build && cmake --build build --config Release // Run : wproxy [--url https://host/path] [--revocation] [--insecure] #include #include #include #include #include // Named secure proxy (scheme prefix "https://" => TLS to the proxy). // Requires Windows 8.1+ for WinHTTP proxy-over-TLS support. static const wchar_t* kProxy = L"https://mysecureproxy.duckdns.org:3129"; // Filled in by the status callback when the TLS handshake (server OR proxy) // fails cert validation. This is what settles the CRL argument. static volatile DWORD g_secureFlags = 0; static void CALLBACK statusCb(HINTERNET, DWORD_PTR, DWORD status, LPVOID info, DWORD len) { if (status == WINHTTP_CALLBACK_STATUS_SECURE_FAILURE && info && len >= sizeof(DWORD)) g_secureFlags = *reinterpret_cast(info); } static void printSecureFlags(DWORD f) { if (!f) return; fprintf(stderr, "secure-failure flags = 0x%08lx\n", f); if (f & WINHTTP_CALLBACK_STATUS_FLAG_CERT_REV_FAILED) fprintf(stderr, " * CERT_REV_FAILED -> revocation check could NOT be performed (CRL/OCSP unreachable)\n"); if (f & WINHTTP_CALLBACK_STATUS_FLAG_CERT_REVOKED) fprintf(stderr, " * CERT_REVOKED -> cert is actually revoked\n"); if (f & WINHTTP_CALLBACK_STATUS_FLAG_INVALID_CA) fprintf(stderr, " * INVALID_CA -> issuing CA not trusted\n"); if (f & WINHTTP_CALLBACK_STATUS_FLAG_CERT_CN_INVALID) fprintf(stderr, " * CERT_CN_INVALID -> name mismatch\n"); if (f & WINHTTP_CALLBACK_STATUS_FLAG_CERT_DATE_INVALID) fprintf(stderr, " * CERT_DATE_INVALID -> expired / not yet valid\n"); if (f & WINHTTP_CALLBACK_STATUS_FLAG_CERT_WRONG_USAGE) fprintf(stderr, " * CERT_WRONG_USAGE -> bad EKU\n"); if (f & WINHTTP_CALLBACK_STATUS_FLAG_INVALID_CERT) fprintf(stderr, " * INVALID_CERT -> generic invalid cert\n"); if (f & WINHTTP_CALLBACK_STATUS_FLAG_SECURITY_CHANNEL_ERROR) fprintf(stderr, " * SECURITY_CHANNEL_ERR -> schannel-level error\n"); } static void fail(const char* what) { DWORD e = GetLastError(); fprintf(stderr, "%s failed, error=%lu%s\n", what, e, e == 12188 ? " (ERROR_WINHTTP_SECURE_FAILURE_PROXY)" : e == 12175 ? " (ERROR_WINHTTP_SECURE_FAILURE)" : ""); printSecureFlags(g_secureFlags); ExitProcess(1); } int wmain(int argc, wchar_t** argv) { std::wstring url = L"https://www.example.com/"; bool enableRevocation = false; // CRL/OCSP checking off by default bool insecure = false; // ignore all cert errors for (int i = 1; i < argc; ++i) { std::wstring a = argv[i]; if (a == L"--url" && i + 1 < argc) url = argv[++i]; else if (a == L"--revocation") enableRevocation = true; else if (a == L"--insecure") insecure = true; } // ---- Crack the target URL ---------------------------------------------- URL_COMPONENTS uc{}; uc.dwStructSize = sizeof(uc); wchar_t host[256] = {0}, path[2048] = {0}; uc.lpszHostName = host; uc.dwHostNameLength = _countof(host); uc.lpszUrlPath = path; uc.dwUrlPathLength = _countof(path); if (!WinHttpCrackUrl(url.c_str(), 0, 0, &uc)) fail("WinHttpCrackUrl"); bool https = (uc.nScheme == INTERNET_SCHEME_HTTPS); // ---- Session bound to the named secure proxy --------------------------- HINTERNET hSession = WinHttpOpen( L"winhttp-secure-proxy/1.0", WINHTTP_ACCESS_TYPE_NAMED_PROXY, kProxy, WINHTTP_NO_PROXY_BYPASS, 0); if (!hSession) fail("WinHttpOpen"); HINTERNET hConnect = WinHttpConnect(hSession, host, uc.nPort, 0); if (!hConnect) fail("WinHttpConnect"); HINTERNET hRequest = WinHttpOpenRequest( hConnect, L"GET", path, NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, https ? WINHTTP_FLAG_SECURE : 0); if (!hRequest) fail("WinHttpOpenRequest"); // Capture the precise cert-rejection reason (server or proxy). WinHttpSetStatusCallback(hRequest, statusCb, WINHTTP_CALLBACK_FLAG_SECURE_FAILURE, 0); // ---- CRL / OCSP revocation checking (the thing under test) ------------- if (enableRevocation) { DWORD feature = WINHTTP_ENABLE_SSL_REVOCATION; if (!WinHttpSetOption(hRequest, WINHTTP_OPTION_ENABLE_FEATURE, &feature, sizeof(feature))) fail("WinHttpSetOption(REVOCATION)"); } // ---- Optionally ignore cert errors (isolate revocation from trust) ----- // NB: these ignore flags cover CA/CN/date/usage. There is NO flag to // "ignore revocation-check-failed", so if --insecure still dies with // CERT_REV_FAILED, the CRL path is the cause -- QED. if (insecure) { DWORD flags = SECURITY_FLAG_IGNORE_UNKNOWN_CA | SECURITY_FLAG_IGNORE_CERT_DATE_INVALID | SECURITY_FLAG_IGNORE_CERT_CN_INVALID | SECURITY_FLAG_IGNORE_CERT_WRONG_USAGE; if (!WinHttpSetOption(hRequest, WINHTTP_OPTION_SECURITY_FLAGS, &flags, sizeof(flags))) fail("WinHttpSetOption(SECURITY_FLAGS)"); } wprintf(L"GET %s\n", url.c_str()); wprintf(L"proxy %s\n", kProxy); wprintf(L"revocation=%s insecure=%s\n\n", enableRevocation ? L"on" : L"off", insecure ? L"on" : L"off"); // ---- Send + receive, timed --------------------------------------------- auto t0 = std::chrono::steady_clock::now(); if (!WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0)) fail("WinHttpSendRequest"); if (!WinHttpReceiveResponse(hRequest, NULL)) fail("WinHttpReceiveResponse"); auto t1 = std::chrono::steady_clock::now(); double ms = std::chrono::duration(t1 - t0).count(); DWORD status = 0, len = sizeof(status); WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, &status, &len, WINHTTP_NO_HEADER_INDEX); wprintf(L"HTTP %lu (%.0f ms send->response)\n\n", status, ms); // ---- Drain body to stdout ---------------------------------------------- DWORD avail = 0; do { avail = 0; if (!WinHttpQueryDataAvailable(hRequest, &avail)) break; if (!avail) break; std::string buf(avail, '\0'); DWORD read = 0; if (!WinHttpReadData(hRequest, &buf[0], avail, &read)) break; fwrite(buf.data(), 1, read, stdout); } while (avail > 0); WinHttpCloseHandle(hRequest); WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); return 0; }