Python x FIPS

Python x FIPS

January 13, 2024

Python x FIPS #

Building a FIPS-compliant version of Python is somewhat-involved. There are essentially three decent articles (all from the same site) on the internet that outline how to do this:

These posts are good but a bit outdated. I was recently looking into how to do this and ran into a few issues with the outlined steps. This post will reference these posts with slightly different documentation to make the process more straightforward and repeatable.

That said, massive shout-out to GyanBlog for documenting this in the first place (as well as writing the patch code that makes this work).

Requirements #

As of early 2024, the following OpenSSL and Python versions can be used:

  • OpenSSL-1.0.2u
  • OpenSSL-fips-2.0.16
  • Python 3.9.x
  • Technically, OpenSSL 3.0.8 is FIPS-validated; however, I ran into issues building Python using this version
  • Since OpenSSL 1.1.1 is only required for Python >=3.10, we can still get away with 1.0.2

Additionally, a FIPS-specific patch is required for Python. The patch is graciously provided here.

  • The blog post states that the patch is for 3.9.2; however, it also works with newer patch versions of Python 3.9.

It is possible that this process can be performed on an aarch64 system but I would recommend sticking with x86_64.

It makes logistical sense to centralize all of the code for this in a single directory (something like fips or python-fips). Create this directory and move into it:

mkdir fips
cd fips

Building and Installing OpenSSL #

The OpenSSL steps are fairly straightforward. First, download both of the archives:

export OPENSSL_FIPS_VERSION=openssl-fips-2.0.16
export OPENSSL_BASE_VERSION=1.0.2
export OPENSSL_VERSION=openssl-${OPENSSL_BASE_VERSION}u
wget https://www.openssl.org/source/old/fips/$OPENSSL_FIPS_VERSION.tar.gz
wget https://www.openssl.org/source/old/$OPENSSL_BASE_VERSION/$OPENSSL_VERSION.tar.gz

Then, extract both archives:

tar xvf $OPENSSL_FIPS_VERSION.tar.gz
tar xvf $OPENSSL_VERSION.tar.gz

Build and install the OpenSSL FIPS module:

cd $OPENSSL_FIPS_VERSION
./config
make -j $(nproc)
sudo make install -j $(nproc)
sudo ldconfig -v

Build and install OpenSSL:

cd ../$OPENSSL_VERSION
sudo ./config shared fips no-ssl2 no-ssl3 --prefix=/opt/openssl
make depend
make -j $(nproc)
sudo make install -j $(nproc)
sudo su -
sudo ln -s -f /opt/openssl/bin/openssl /usr/local/bin/openssl
sudo ln -s -f /opt/openssl/bin/openssl /usr/bin/openssl
sudo ldconfig -v
  • Ensure the config command is run as the root user so that the prefix can be used appropriately
  • A separate prefix is used to ensure that we know the separate OpenSSL build is the correct version
  • Reusing the system’s SSL paths can be confusing and may lead to the wrong OpenSSL version/headers being used when building Python
  • For the purposes of this post, I only symlink the FIPS build to the PATH locations
  • Running ldconfig after each build probably isn’t necessary but it can’t hurt, surely

Ensure the OpenSSL version is correct:

openssl version
OpenSSL 1.0.2u-fips  20 Dec 2019

Building and Installing Python #

Save the contents of the patch linked above into a file named something like fips.patch:

diff -aur Lib/ssl.py Lib/ssl.py
--- ./Lib/ssl.py	2020-10-05 15:07:58.000000000 +0000
+++ ./Lib/ssl.py	2021-03-02 04:23:32.026226000 +0000
@@ -111,6 +111,11 @@
     # LibreSSL does not provide RAND_egd
     pass
 
+try:
+    from _ssl import FIPS_mode, FIPS_mode_set
+except ImportError as e:
+    sys.stderr.write('error in importing\n')
+    sys.stderr.write(str(e))
 
 from _ssl import (
     HAS_SNI, HAS_ECDH, HAS_NPN, HAS_ALPN, HAS_SSLv2, HAS_SSLv3, HAS_TLSv1,
diff -aur Modules/Setup Modules/Setup
--- ./Modules/Setup	2020-10-05 15:07:58.000000000 +0000
+++ ./Modules/Setup	2021-03-02 04:24:28.071717000 +0000
@@ -207,14 +207,14 @@
 #_csv _csv.c
 
 # Socket module helper for socket(2)
-#_socket socketmodule.c
+_socket socketmodule.c
 
 # Socket module helper for SSL support; you must comment out the other
 # socket line above, and possibly edit the SSL variable:
-#SSL=/usr/local/ssl
-#_ssl _ssl.c \
-#	-DUSE_SSL -I$(SSL)/include -I$(SSL)/include/openssl \
-#	-L$(SSL)/lib -lssl -lcrypto
+SSL=/usr/local/ssl
+_ssl _ssl.c \
+	-DUSE_SSL -I$(SSL)/include -I$(SSL)/include/openssl \
+	-L$(SSL)/lib -lssl -lcrypto
 
 # The crypt module is now disabled by default because it breaks builds
 # on many systems (where -lcrypt is needed), e.g. Linux (I believe).
diff -aur Modules/_ssl.c Modules/_ssl.c
--- ./Modules/_ssl.c	2020-10-05 15:07:58.000000000 +0000
+++ ./Modules/_ssl.c	2021-03-02 04:25:30.930669000 +0000
@@ -5394,6 +5394,20 @@
     return PyLong_FromLong(RAND_status());
 }
 
+static PyObject *
+_ssl_FIPS_mode_impl(PyObject *module) {
+    return PyLong_FromLong(FIPS_mode());
+}
+
+static PyObject *
+_ssl_FIPS_mode_set_impl(PyObject *module, int n) {
+    if (FIPS_mode_set(n) == 0) {
+        _setSSLError(ERR_error_string(ERR_get_error(), NULL) , 0, __FILE__, __LINE__);
+        return NULL;
+    }
+    Py_RETURN_NONE;
+}
+
 #ifndef OPENSSL_NO_EGD
 /* LCOV_EXCL_START */
 /*[clinic input]
@@ -5875,6 +5889,8 @@
     _SSL_ENUM_CRLS_METHODDEF
     _SSL_TXT2OBJ_METHODDEF
     _SSL_NID2OBJ_METHODDEF
+    _SSL_FIPS_MODE_METHODDEF
+    _SSL_FIPS_MODE_SET_METHODDEF
     {NULL,                  NULL}            /* Sentinel */
 };
 
diff -aur Modules/clinic/_ssl.c.h Modules/clinic/_ssl.c.h
--- ./Modules/clinic/_ssl.c.h	2020-10-05 15:07:58.000000000 +0000
+++ ./Modules/clinic/_ssl.c.h	2021-03-02 04:27:06.120295000 +0000
@@ -1204,6 +1204,45 @@
     return _ssl_RAND_status_impl(module);
 }
 
+PyDoc_STRVAR(_ssl_FIPS_mode__doc__,
+"FIPS Mode");
+
+#define _SSL_FIPS_MODE_METHODDEF    \
+    {"FIPS_mode", (PyCFunction)_ssl_FIPS_mode, METH_NOARGS, _ssl_FIPS_mode__doc__},
+
+static PyObject *
+_ssl_FIPS_mode_impl(PyObject *module);
+
+static PyObject *
+_ssl_FIPS_mode(PyObject *module, PyObject *Py_UNUSED(ignored))
+{
+    return _ssl_FIPS_mode_impl(module);
+}
+
+PyDoc_STRVAR(_ssl_FIPS_mode_set_doc__,
+"FIPS Mode Set");
+
+#define _SSL_FIPS_MODE_SET_METHODDEF    \
+    {"FIPS_mode_set", (PyCFunction)_ssl_FIPS_mode_set, METH_O, _ssl_FIPS_mode_set_doc__},
+
+static PyObject *
+_ssl_FIPS_mode_set_impl(PyObject *module, int n);
+
+static PyObject *
+_ssl_FIPS_mode_set(PyObject *module, PyObject *arg)
+{
+    PyObject *return_value = NULL;
+    int n;
+
+    if (!PyArg_Parse(arg, "i:FIPS_mode_set", &n)) {
+        goto exit;
+    }
+    return_value = _ssl_FIPS_mode_set_impl(module, n);
+
+exit:
+    return return_value;
+}
+
 #if !defined(OPENSSL_NO_EGD)
 
 PyDoc_STRVAR(_ssl_RAND_egd__doc__,

Credits for this patch code go directly to GyanBlog This version has been updated to fix the file paths (i.e. the addition of ./)

First, download the Python archive:

export PYTHON_VERSION=3.9.16
wget https://www.python.org/ftp/python/$PYTHON_VERSION/Python-$PYTHON_VERSION.tgz

Extract the archive:

tar xvf Python-$PYTHON_VERSION.tgz

Move into the Python directory and apply the patch created above:

cd Python-$PYTHON_VERSION
patch -p1 < ../fips.patch
  • If patch is not found, ensure that patchutils is installed

The patch process applies changes to four separate files:

  • Lib/ssl.py
  • Modules/Setup
  • Modules/_ssl.c
  • Modules/clinic/_ssl.c.h

If prompted, enter each of these file names (the patch should apply automatically with the version above)

Once the patch is applied, Python can be built:

CFLAGS=-Wl,--enable-new-dtags,-rpath,/opt/openssl/lib LDFLAGS=-L/opt/openssl/lib CPPFLAGS=-I/opt/openssl/include LIBS=-lcrypto ./configure --enable-shared --enable-optimizations --with-openssl=/opt/openssl --with-builtin-hashlib-hashes=sha1,sha256,sha512,sha3,blake2
sudo ldconfig -v
make -j $(nproc)
sudo make altinstall -j $(nproc)
  • Remove hashlib ciphers as appropriate from --with-builtin-hashlib-hashes
  • Note that MD5 was intentionally left out here
  • Feel free to create a symlink for Python; I kept it separate here for clarity
  • Given the build configuration, using something like Pyenv may cause complications or prevent the build from working as expected

Verify the Python configuration #

If Python builds successfully and does not throw errors about implicit function declarations for FIPS_mode() or otherwise, open the Python 3 REPL:

/usr/local/bin/python3.9
>>> import ssl
>>> ssl.OPENSSL_VERSION
'OpenSSL 1.0.2u-fips  20 Dec 2019'
>>> ssl.FIPS_mode()
0
>>> ssl.FIPS_mode_set(1)
>>> ssl.FIPS_mode()
1

Installing Cryptography #

As stated in the aforementioned blog posts, a specific version of Cryptography is required given the relatively-outdated version of OpenSSL used for Python.

First, ensure the Wheel package is installed:

/usr/local/bin/python3.9 -m pip install wheel

Then, build the cryptography==3.0 wheel and install it:

CRYPTOGRAPHY_DONT_BUILD_RUST=1 CFLAGS="-I/opt/openssl/include" LDFLAGS="-L/opt/openssl/lib" /usr/local/bin/python3.9 -m pip wheel --no-binary :all: cryptography==3.0
CRYPTOGRAPHY_DONT_BUILD_RUST=1 CFLAGS="-I/opt/openssl/include" LDFLAGS="-L/opt/openssl/lib" /usr/local/bin/python3.9 -m pip install cryptography-3.0-cp39-cp39-linux_x86_64.whl
  • Ensure that the FIPS-compliant OpenSSL path is used here!

Dockerfile #

I spent some time (read: quite a bit of time) porting this over to a Dockerfile and the results are located here.

The Dockerfile is based on Rocky Linux; however, the steps should be relatively portable for other distros (e.g. AL2023).

The steps above were performed on Debian in WSL2 so that is also a possibility. The methodology and configuration differs slightly in the Dockerfile but overall the approach is the same. The primary difference is how Python is configured (LDFLAGS, LD_LIBRARY_PATH, and CPPFLAGS) and where it is ultimately installed to (--prefix).

For clarity, here are the current contents of the Dockerfile:

# Patch Python to support OpenSSL FIPS module
# Patchutils installs Python which we want to avoid in the final image
FROM --platform=$TARGETPLATFORM rockylinux:9-minimal as patch

ARG PYTHON_VERSION="3.9.18"

RUN microdnf -y update \
    && microdnf install -y \
        patchutils \
        tar \
        wget

WORKDIR /fips

COPY fips.patch .
RUN wget https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VERSION}.tgz \
    && tar -xzf Python-${PYTHON_VERSION}.tgz \
    && cd Python-${PYTHON_VERSION} \
    && patch -p1 < ../fips.patch \
    && cd -

# Main stage
FROM --platform=$TARGETPLATFORM rockylinux:9-minimal

ARG OPENSSL_BASE_VERSION="1.0.2"
ARG OPENSSL_FIPS_VERSION="openssl-fips-2.0.16"
ARG OPENSSL_VERSION="openssl-${OPENSSL_BASE_VERSION}u"
ARG PYTHON_VERSION="3.9.18"
ARG TARGETPLATFORM

ENV OPENSSL_FIPS=1

WORKDIR /fips

COPY --from=patch /fips/Python-${PYTHON_VERSION} ./Python-${PYTHON_VERSION}

# Install dependencies
RUN microdnf -y update \
    && microdnf -y install \
        autoconf \
        automake \
        bzip2-devel \
        diffutils \
        gcc \
        libffi \
        libffi-devel \
        libjpeg-devel \
        libssh-devel \
        libtool \
        libxml2-devel \
        libxslt-devel \
        make \
        wget \
        zlib-devel \
    && microdnf clean all

# Download archives
RUN wget https://www.openssl.org/source/old/fips/${OPENSSL_FIPS_VERSION}.tar.gz \
    && wget https://www.openssl.org/source/old/${OPENSSL_BASE_VERSION}/${OPENSSL_VERSION}.tar.gz

# Extract archives
RUN tar -xzf ${OPENSSL_FIPS_VERSION}.tar.gz \
    && tar -xzf ${OPENSSL_VERSION}.tar.gz 

# Build and Install OpenSSL FIPS Module
RUN cd ${OPENSSL_FIPS_VERSION} \
    && ./config \
    && make \
    && make install \
    && ldconfig -v \
    && cd -

# Build and Install OpenSSL
RUN cd ${OPENSSL_VERSION} \
    && ./config shared fips no-ssl2 no-ssl3 \
    && make depend \
    && make \
    && make install \
    && echo "/usr/local/ssl/lib" > /etc/ld.so.conf.d/${OPENSSL_VERSION}.conf \
    && ln -s -f /usr/local/ssl/bin/openssl /usr/bin/openssl \
    && ln -s -f /usr/local/ssl/bin/openssl /usr/local/bin/openssl \
    && ldconfig -v \
    && cd -

# Build and Install Python
RUN cd Python-${PYTHON_VERSION} \
    && LDFLAGS="-L/usr/local/lib/ -L/usr/local/lib64/ -Wl,-rpath=/opt/python-fips/lib" LD_LIBRARY_PATH="/usr/local/lib/:/usr/local/lib64/" CPPFLAGS="-I/usr/local/include -I/usr/local/ssl/include/openssl" ./configure --enable-shared --enable-optimizations --with-builtin-hashlib-hashes=sha1,sha256,sha512,sha3,blake2 --prefix=/opt/python-fips \
    && make \
    && make install \
    && echo "/opt/python-fips/lib" > /etc/ld.so.conf.d/python.conf \
    && ldconfig -v \
    && ln -s -f /opt/python-fips/bin/python3.9 /usr/bin/python3 \
    && ln -s -f /opt/python-fips/bin/python3.9 /usr/local/bin/python3 \
    && cd -

# Install Python dependencies
RUN python3 -m pip install wheel \
    && CRYPTOGRAPHY_DONT_BUILD_RUST=1 CFLAGS="-I/usr/local/ssl/include" LDFLAGS="-L/usr/local/ssl/lib" python3 -m pip wheel --no-binary :all: cryptography==3.0 \
    && CRYPTOGRAPHY_DONT_BUILD_RUST=1 CFLAGS="-I/usr/local/ssl/include" LDFLAGS="-L/usr/local/ssl/lib" python3 -m pip install cryptography-3.0-cp39-cp39-linux_x86_64.whl

The repository contains all of the necessary documentation for getting up and running.

The COPY fips.patch . step uses the same patch mentioned above in this post.

Acknowledgements #

Again, I would like to extend my appreciation to GyanBlog for documenting this process. It provided a good start for implementing this myself.

Overall, I would categorize this process as arcane which makes sense given the relatively small number of people who will ever need to know how to do this to begin with. That said, when you need something, you need it and it’s nice to have the information readily-accessible.