Running WolfSSL and cURL on Windows 2000

Created on 2024-03-03 at 14:04

As part of my current series of projects, I've been working on a screenshot tool intended for older Windows versions. The goal is to have something similar to ShareX, a tool that lets you take a screenshot and post it online.

These days, file sharing is done via the Hypertext Transfer Protocol (HTTP)[1], often using Transport Layer Security (TLS).[2] Built into every version of Windows since Windows 95 is WinINet, which provides HTTP, FTP, and (in XP/2003 and earlier) Gopher. Obviously for older Windows versions, this library won't include support for recent technologies like TLS 1.3 or HTTP/2. cURL still supports Visual C++ 6, but that only provides the HTTP/FTP part. We'll need a TLS library to get connected to most modern systems.

Note: All patches and documentation here are experimental, and should not be considered secure. My focus was on running something compliant with modern Web protocols, not improving the security posture of the system. Even if my patches haven't compromised the code's security, the versions of Visual C++ runtime and Windows involved have documented vulnerabilities that can affect libcurl and WolfSSL.

Don't do your banking on Windows 2000, whether via IE6 or cURL 8.6.0.

Prerequisites

While I'm currently getting things running on Windows 2000 Professional SP4, I'd like to get this working on Windows 95 and 98 too. To that end, I'll be using Visual Studio.NET 2003, which was the last version to support Windows 95. I'm also building everything in Windows 2000, but that part's optional.

You'll also need a copy of the Platform SDK from the era. I'm using the February 2003 MSDN release, for no other reason than it sounded like a good fit for VS2003.

And some general advice if you want to develop for old Windows versions: the MSDN Library that comes with VS2003 is so much more useful than the version on the Internet. Today's MSDN docs are a minefield of incorrect version information, and while I'm sure modern-day search engines are far better than WinHelp, the built-in search function is surprisingly useful.

Just here for the code?

The source code to both WolfSSL and cURL with my changes are available on my Git server:

To make things easy: any of my changes to WolfSSL are licensed under GPLv2, and cURL under the cURL license.

WolfSSL and Visual C++

Before we think about compiling cURL, we need a TLS library. In the past I've made hacks to OpenSSL to get it to build on pre-Vista Win32, but I'd really rather avoid that for something I share with others.

Inspired by the post on Dialup.net about building WolfSSL for Windows 3.11 for Workgroups, I decided that would be a better place to start.

At first glance, I noticed a file named wolfssl.vcproj, indicating a pre-Visual C++ 2010 project. Good start, except the project was made in Visual Studio 2008.[3]

<?xml version="1.0" encoding="Windows-1252"?>
<VisualStudioProject
        ProjectType="Visual C++"
        Version="9.00"

To make matters worse, the project was woefully outdated. It was missing several files required to link, meaning I had to cherry-pick files as I got errors.

Unfortunately, there's not a great way to automate this. So began the process of painstakingly adding files to the project until it compiled, using the CMakeLists.txt and VC2010+ .vcxproj files as references.

Configuring WolfSSL

As is common with software written in C, configuration is handled via #define statements. WolfSSL helpfully allows you to override the usual configuration process by defining WOLFSSL_USER_SETTINGS. This allows us to use the configuration already used by the .vcproj: IDE/WIN/user_settings.h.

Figuring out the right defines took more troubleshooting than I care to describe, so here's a lightning round:

  • First, force C89 by defining WOLF_C89 and NO_WOLF_C99. More on this in a bit.
  • Disable WOLFSSL_SP_X86_64,since we won't be running this on an x86_64 CPU.
  • We don't have strntok or strtok_s, so define USE_WOLF_STRTOK.

We also have to define a few functions, so add them:

#define XSTRTOK wc_strtok
#define XVSNPRINTF _vsnprintf

We also need to define strcpy_s. My workaround was to use StringCchCopy, which is close enough? Probably? You need to #include <Winerror.h> first though, so later #include <Windows.h> lines don't try to re-define the SUCCEEDED and FAILED macros. Win32 headers are annoying like that.

Aside: What is a C standard, really?

A quick and inaccurate history: some guy at a phone company made a language named C.[4] His coworkers were working on a bunch of different programs with it and thought it was useful, so a couple of them wrote a book about it.

Without a standard, companies decided to make fifty different versions with their own features. To combat that, the American National Standards Institute (ANSI) decided they should make a standard about it, and took half a decade hashing out C89, or C90 if you're more of an ISO person.[5] Those guys from the phone company rewrote their book for that, too. The companies decided to follow at least those rules when making their fifty different versions.

Then, C99 arrived a decade later, with a ton of great features that everyone loved.[citation needed] Boolean types, variadic macros, the ability to declare variables wherever you want; all the classics.

VC2003 doesn't support any of those though, so. Oops.

Out with the new

As mentioned previously, WolfSSL helps us avoid most of the C99 stuff with the WOLF_C89 and NO_WOLF_C99 defines. Unfortunately, this doesn't seem to account for variadic macros. They were first supported with VC2005, so we'll have to work around it. They're all used as a way to NOP functions, so we can either patch simpler functions like so:

// from ...
#define X509_check_purpose(...)     0
// ... to
#define X509_check_purpose(x)       0

Or for more complex functions where I don't feel like faking ten parameters, we can define an __inline variadic function:

// from...
#define SSL_CTX_add_server_custom_ext(...) 0
// ... to
__inline int SSL_CTX_add_server_custom_ext(SSL_CTX *ctx, ...) {
	(void)(ctx); // avoid compiler complaints about unused parameters
	return 0; // return 0
}

Lastly, although not a C99 feature, we need to stop wc_port.h from including <intrin.h>, since VC2003 doesn't include that. My understanding is VC2005 does, so we just change:

#elif defined(_MSC_VER)
     /* Use MSVC compiler intrinsics for atomic ops */
     #include <intrin.h>

to:

#elif _MSC_VER >= 1400
     /* Use MSVC compiler intrinsics for atomic ops */
     #include <intrin.h>

And now we're good. All done, ready to build.

Just kidding, I forgot about WinSock

There's one last function I couldn't just stub out: inet_pton. The prototype is pretty simple:

INT WSAAPI inet_pton(
  [in]  INT   Family,
  [in]  PCSTR pszAddrString,
  [out] PVOID pAddrBuf
);

In this case, Family can be either AF_INET or AF_INET6. You feed in a string representing the address (e.g., "198.18.0.1") and it writes either an IN_ADDR or IN6_ADDR struct, both more machine-readable than a C string. It's pretty convenient!

It's also not a thing on old Windows versions. Windows 2000 only supports IPv6 as part of a technology preview. And on older versions of Windows, you definitely aren't gonna see that turtle dance. From what I can find, inet_pton was first supported in Windows Vista. So we'll have to write our own.

That's pretty easy with inet_addr, which takes a C string and gives you an unsigned long. So, let's just build our own:

#ifndef InetPton

int __stdcall
InetPton(int Family,
         const char *pszAddrString,
         void *pAddrBuf)
{
        IN_ADDR *addr = pAddrBuf;
        unsigned long ulTmp;
        
        if (Family != AF_INET) {
		        // Oops! We don't support that addr family
                WSASetLastError(WSAEAFNOSUPPORT);
                return -1;
        }

        if (!pAddrBuf || !pszAddrString ||
	        pszAddrString[0] == '\0') {
                WSASetLastError(WSAEFAULT);
                return -1;
        }

		// Technically we could just put the return val directly
		// into addr->S_un.S_addr, but I don't like writing bad
		// data unless absolutely necessary. So store it in a temp
		// variable, first.
        ulTmp = inet_addr(pszAddrString);
        if (ulTmp == INADDR_NONE) {
		        // Not an IP address
                WSASetLastError(WSAEFAULT);
                return -1;
        }

		// S_un.S_addr is an unsigned long. Easy!
        addr->S_un.S_addr = ulTmp;
        return 0;
}

#endif /* !InetPton */

Now, this does throw a warning during compilation ("incompatible types - from 'PCWSTR' to 'const char *'"), but WolfSSL is already casting an ASCII string to PCWSTR. So I think it's fine.

Probably.

Deployment

Visual Studio doesn't provide a great way of deploying to a random prefix along the lines of your standard make DESTDIR=whatever install. So I tried to replicate that with a script called install.js. It performs the following steps:

  1. Copies the include files in wolfssl to %DESTDIR%\include\wolfssl, excluding the files referenced by CMakeLists.txt;
  2. Writes a hardcoded set of options to %DESTDIR%\include\wolfssl\options.h, similar to how it would be handled by CMake; and
  3. Copies the wolfssl.lib file to %DESTDIR%\include\wolfssl\lib.

From the repository root, it should be as simple as running:

cscript install.js X:\whatever Release

to "install" the Release build.

Step 2: cURL

Now that we're done with the gauntlet of WolfSSL, installing cURL should be pretty straightforward! I mean, they provide a Makefile that they call out as compatible with Visual C++ 6.0. The README has no reference to WolfSSL—just MbedTLS or OpenSSL—but we'll come back to that later. For now, I want to see if this builds without TLS support.

If you're following along, definitely read winbuild\README.md first. I'm only going to talk about the switches I used, and you need to set up the dependencies in a certain way for the build system to find them.

According to the README, we'll need the following switches set in the command line:

  • I'm setting mode=static to make a static library, just to arbitrarily pick something.
  • VC=7 is more for our reference, since it sets the "full name" of the library. It doesn't affect anything else that we care about.[6]
  • ENABLE_IDN=no and ENABLE_IPV6=no, since our Windows versions don't support those.
  • USE_SSPI=no, because some of the features used require higher than Windows 2000.

So with that, we run the build:

> nmake /f Makefile.vc mode=static VC=7 ENABLE_IDN=no ENABLE_IPV6=no USE_SSPI=no

Microsoft (R) Program Maintenance Utility Version 7.10.3077
Copyright (C) Microsoft Corporation.  All rights reserved.

configuration name: libcurl-vc7-x86-release-static
The input line is too long.
NMAKE : fatal error U1077: 'CALL' : return code '0xff'
Stop.

Ah.

A Quick Aside About Batch Files

no wait before you skip this, i promise it'll be quick

The Windows NT Command Prompt (cmd.exe) has a maximum amount of characters per command you can write. UNIX-y shells have this limitation too, and you can see it by running getconf ARG_MAX. Per some old Microsoft docs, the maximum in Windows 2000 (and NT 4.0) is 2047 characters. When you surpass this limit, you get The input line is too long. In modern technical terms, this is called an "annoyingly small limitation."

So, why are we hitting this limit? Makefile.vc is calling out to gen_resp_file.bat, which takes a bunch of files as input and dumps them out as an NMake .inc file to be imported later on. For example:

!INCLUDE "../lib/Makefile.inc"
LIBCURL_OBJS=$(CSOURCES:.c=.obj)

[...]
	@SET DIROBJ=$(LIBCURL_DIROBJ)
	@SET MACRO_NAME=LIBCURL_OBJS
	@SET OUTFILE=LIBCURL_OBJS.inc
	@CALL gen_resp_file.bat $(LIBCURL_OBJS)

I won't copy the contents of "../lib/Makefile.inc", but there's a lot of files in there. Enough that we run up against this limit, but not when the batch file is run. The culprit is this loop in the batch file:

for %%i in (%*) do echo         %DIROBJ%/%%i \>> %OUTFILE%

%* is a variable representing all the arguments passed to the batch file. Unlike shells like bash, for loops don't get special treatment—the variables get expanded before the command line is run, and we trip over the 2047 character limit before we can even get to looping.

So I'll make my own for loop, with SHIFT and GOTO:

:loop
IF [%1] == [] GOTO done
echo            %DIROBJ%/%1 \>> %OUTFILE%
SHIFT
GOTO loop
:done

In this case, we only need to expand one parameter at a time, avoiding the command line limitation altogether. It might be slower, but on my Pentium II @ 400MHz, I couldn't tell.

(Re)Configuring cURL

Of course, "compatible with Visual C++ 6.0" doesn't necessarily mean "compatible with wildly outdated operating systems." So there are a few extra changes to make. All of these changes can be made in lib\config-win32.h in the source tree.

Starting off: I cannot for the life of me figure out why __fseeki64 is defined in some versions of Visual C++ 6.0 and above, but not in others. It seems like it's probably not supported in Windows 9x anyway? So, sorry if you wanted to upload files larger than 2GiB, but I'm enforcing USE_WIN32_SMALL_FILES, which reverts to the old fseek function.

cURL wants a type named ADDRESS_FAMILY, but it never gets defined. I think this is also defined in later WinSock versions, but it also doesn't really matter since we can just #define ADDRESS_FAMILY USHORT.

Bonus Round: SMB

cURL has SMB support using your TLS library of choice, which I'd like to keep. It does throw a couple errors that can be fixed, but the easiest workaround is defining CURL_DISABLE_SMB somewhere.

If you, like me, care to stare into this abyss, you'll first have to return to WolfSSL and add the WOLFSSL_DES_ECB define to user_settings and the DEFINES array in install.js. Rebuild and reinstall, and you shouldn't see compile errors.

For some reason, lib/smb.c defines getpid to GetCurrentProcessId, despite there already being a function in MSVC. This leads to a confusing error where GetCurrentProcessId gets "re-declared" in <process.h>. You can just remove this #define to resolve the issue.

Supporting WolfSSL

While we're already messing with build files, this seems like a good time to add WolfSSL support into the Makefile. You can already include MbedTLS with WITH_MBEDTLS=<dll/static>, so that seems like a good starting point. In MakefileBuild.vc:

!IFDEF MBEDTLS_PATH
MBEDTLS_INC_DIR  = $(MBEDTLS_PATH)\include
MBEDTLS_LIB_DIR  = $(MBEDTLS_PATH)\lib
MBEDTLS_LFLAGS   = $(MBEDTLS_LFLAGS) "/LIBPATH:$(MBEDTLS_LIB_DIR)"
!ELSE
MBEDTLS_INC_DIR  = $(DEVEL_INCLUDE)
MBEDTLS_LIB_DIR  = $(DEVEL_LIB)
!ENDIF

[...]

!IF "$(WITH_MBEDTLS)"=="dll" || "$(WITH_MBEDTLS)"=="static"
USE_MBEDTLS    = true
MBEDTLS        = $(WITH_MBEDTLS)
MBEDTLS_CFLAGS = /DUSE_MBEDTLS /I"$(MBEDTLS_INC_DIR)"
MBEDTLS_LIBS   = mbedtls.lib mbedcrypto.lib mbedx509.lib
!ENDIF

!IF "$(USE_MBEDTLS)"=="true"
CFLAGS = $(CFLAGS) $(MBEDTLS_CFLAGS)
LFLAGS = $(LFLAGS) $(MBEDTLS_LFLAGS) $(MBEDTLS_LIBS)
!ENDIF

For the most part, we can just copy and paste these lines, changing MBEDTLS to WOLFSSL. The only other line to worry about is updating the library files to be... well, wolfssl.lib:

WOLFSSL_LIBS    = wolfssl.lib

Also, now that we set USE_WOLFSSL, cURL will try to #include <stdint.h>. VC2003 doesn't provide that, so copy this public domain version that I patched into your deps/include folder. It's the same one that's been floating around the Internet for years, but I removed the WCHAR_MIN and WCHAR_MAX defines because they conflict with the ones in <Windows.h>.

Compiling cURL

> nmake /f mode=static VC=7 ENABLE_IDN=no ENABLE_IPV6=no WITH_WOLFSSL=static USE_SSPI=no

Assuming no errors occur, the completed curl.exe should be in builds\libcurl-vc7-x86-release-static\bin. But does it work?

> curl -kv https://bell.2ki.xyz
* Host bell.2ki.xyz:443 was resolved.
* IPv4: 209.251.245.54
*   Trying 209.251.245.54:443...
* Connected to bell.2ki.xyz (209.251.245.54) port 443
* SSL connection using TLSv1.3 / TLS13-AES128-GCM-SHA256
* using HTTP/1.x
> GET / HTTP/1.1
> Host: bell.2ki.xyz
> User-Agent: curl/8.6.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: nginx/1.24.0
< Date: Sat, 02 Mar 2024 00:52:43 GMT
< Content-Type: application/octet-stream
< Content-Length: 16
< Connection: keep-alive
<
It works! (TLS)
* Connection #0 to host bell.2ki.xyz left intact

At least on my system, it seems so!

What now?

There are a few projects other than the aforementioned screenshot project that I think could be useful. Providing a WinHTTP.dll shim library is my first thought, although I imagine most services using that library are already dead.

As you'll see in the build files for both projects, my next plan is to try for Windows 9x support. I don't currently have a 95/98 workstation readily available, but I'll update this post when I do. If you give it a try before then, please let me know!


  1. For more information, see RFC 2616, Hypertext Transfer Protocol -- HTTP/1.1 (https://www.rfc-editor.org/rfc/rfc2616.html).

  2. For more information, scream into your nearest soft, sound-absorbing material. A pillow, blanket, or limb should do.

  3. For context, since I'll vaguely reference it throughout this post: Visual Studio.NET 2003 is Version 7.1, 2005 is 8.0, and 2008 is 9.0. The "Visual Studio.NET" moniker was started with 2002 and dropped with 2003, with the next version being "Visual Studio 2005."

  4. See "The Development of the C Language*" by Dennis M. Ritchie (https://www.bell-labs.com/usr/dmr/www/chist.html).

  5. See "The Origin of ANSI C and ISO C" from the American National Standards Institute (https://blog.ansi.org/2017/09/origin-ansi-c-iso-c/).

  6. Technically it matters if you're using VC6, since it sets different compiler flags. But we're not, so I don't care.