M0 / Lab setup and verification

Native install deep dive

PractitionerAdvisor

After this lesson you can

  • Install or build OpenSSL 3.5+ on Debian/Ubuntu (apt) and RHEL/RockyLinux (dnf), knowing that no special PQC flag is needed
  • Show with a real example that a system package's version number does not guarantee which algorithms are actually installed
  • Explain how LD_LIBRARY_PATH decides whether a library built from source is actually used
  • Recognize the /opt/oqs static-lib/no-DSO error class from its real error text and know when it matters

Before thisDocker quickstart

Mental model

Building OpenSSL from source takes three steps: ./config (which features, installed where), make (compile), make install (copy). PQC does not need a fourth step, because from OpenSSL 3.5 onward ML-KEM, ML-DSA and SLH-DSA are part of the default provider; no separate flag or module is needed. But installing with a package manager (as you will see in the RHEL/RockyLinux example) is often simpler and less error-prone than building from source. This lesson shows both paths as they were actually tested.

Correction: there is no special PQC flag

The early research notes reviewed while preparing this lesson suggested three different ./config flags on three different days: one used no flag at all, one said enable-pqc, one said enable-pq. All three were trying to do the same thing (build OpenSSL 3.5 with PQC support), but all three differed, and two of them (enable-pqc, enable-pq) are not real ./config options. No such flag is defined in OpenSSL’s official INSTALL.md.

The command sequence in this lesson was actually run on 2026-09-04 with no special flags (only shared, a standard option that builds the libraries as shared objects), and the result was verified. That proves none of the three contradictory instructions was needed. But producing that proof also walked into an interesting trap, described below, because it is exactly the kind of error this module is meant to teach.

A version mismatch, caught live

Right after the build above, running /opt/openssl-3.5/bin/openssl version printed: OpenSSL 3.5.1 1 Jul 2025 (Library: OpenSSL 3.5.7 9 Jun 2026). Two different versions on one line. The CLI itself (3.5.1) is the version you just built; the “Library” in parentheses (3.5.7) is the shared library actually loaded at run time. They should match, and they did not.

Why: when libssl-dev was installed in the build environment, the system package manager had already installed a libssl3 runtime library as a dependency (here Debian trixie’s own 3.5.7). When the newly built openssl CLI ran without LD_LIBRARY_PATH set, it found the system’s libcrypto.so.3 through the operating system’s standard library search path and used that, not its own library just installed under /opt/openssl-3.5/lib.

The fix is one line:

LD_LIBRARY_PATH=/opt/openssl-3.5/lib /opt/openssl-3.5/bin/openssl version
# OpenSSL 3.5.1 1 Jul 2025 (Library: OpenSSL 3.5.1 1 Jul 2025)   <- now they match

After this fix, ML-DSA-65 key generation worked without problems, really using the newly built library. This is not an accidental find but a deliberately recorded teaching example: looking at a CLI’s version output does not guarantee which library actually runs. In production, this kind of mismatch can mean believing “we moved to PQC” while silently still using an old library.

RHEL / RockyLinux 9: with the package manager, but carefully

On Debian/Ubuntu with apt things are relatively simple. The RHEL family (tested on RockyLinux 9, binary compatible with RHEL 9) is more instructive: on a fresh image, openssl version -a shows 3.0.7MEASURED, with no PQC, built with a Red Hat FIPS patch. Just running dnf install openssl (which upgrades the existing install to the current repository version) moves it to 3.5.5MEASURED, and on that version ML-DSA-65 key generation really works.

What this means: on a RHEL 9-based system, the answer to “which OpenSSL version is installed” changes depending on when the image was built and when packages were last updated. For an inventory (see M12), this is another example of why “the image name is X” is not enough on its own.

WSL2 and macOS/Homebrew

WSL2 (Windows Subsystem for Linux 2) is a virtual machine running a real Linux kernel. An Ubuntu or Debian distribution inside it behaves exactly like the apt-based steps above, because at the level of these commands there is no difference between “native Linux” and “Linux inside WSL2”. This lesson gives no separate WSL2 sequence because none is needed: open an Ubuntu shell in WSL2 and run the steps from the apt section as they are.

macOS (Homebrew). The Homebrew install on the machine used for this course was checked: the openssl@3 formula (alias openssl@3.6) provides version 3.6.2, which is above the 3.5+ threshold and supports ML-KEM, ML-DSA and SLH-DSA natively. If you already use Homebrew, brew install openssl@3 (or brew upgrade openssl@3 to update) is enough; no separate source build is needed.

The /opt/oqs static-lib/no-DSO error: the real text

When loading a provider (for example oqsprovider) into OpenSSL, OpenSSL looks for a shared library (.so file, a “dynamic shared object” or DSO). If an installation has only a static library (.a file) and no .so, loading fails. This scenario was actually reproduced in this lesson (with an empty .a file and a missing .so), and OpenSSL’s real error is given in full in the lab section above.

When you see this error, check two things: (1) does the OPENSSL_MODULES environment variable really point at the directory with the .so file, and (2) is there really a .so file in that directory, or only an .a. But first ask: do you actually need this provider? For ML-KEM, ML-DSA and SLH-DSA, no: the default provider is enough. Only for an algorithm OpenSSL does not support yet (for example FN-DSA/Falcon, see M4).

Numbers to know

  • In OpenSSL 3.5+, ML-KEM, ML-DSA and SLH-DSA are native in the default provider; no config flag is needed
  • A fresh RHEL 9 / RockyLinux 9 image ships OpenSSL 3.0.7 (no PQC); dnf install openssl upgrades it to 3.5.5 (PQC available)

Lab: Build from source on two platforms and reproduce a version trap

Requires: build-essential/gcc, libssl-dev, perl, wget (also works inside the Docker lab image). Check your setup

shell
# Debian/Ubuntu (apt)
apt-get install -y build-essential libssl-dev perl wget
Recorded output
(packages are installed)
shell
wget https://www.openssl.org/source/openssl-3.5.1.tar.gz && tar xzf openssl-3.5.1.tar.gz && cd openssl-3.5.1
Recorded output
(you move into the directory)
shell
./config --prefix=/opt/openssl-3.5 --libdir=lib shared && make -j$(nproc) && make install_sw
Recorded output
(the build takes a few minutes, depending on the environment and core count)
shell
/opt/openssl-3.5/bin/openssl version   # without setting LD_LIBRARY_PATH
Recorded output
OpenSSL 3.5.1 1 Jul 2025 (Library: OpenSSL 3.5.7 9 Jun 2026)  <- CLI and Library versions DO NOT MATCH; the section below explains why
shell
LD_LIBRARY_PATH=/opt/openssl-3.5/lib /opt/openssl-3.5/bin/openssl version
Recorded output
OpenSSL 3.5.1 1 Jul 2025 (Library: OpenSSL 3.5.1 1 Jul 2025)  <- now they match
shell
LD_LIBRARY_PATH=/opt/openssl-3.5/lib /opt/openssl-3.5/bin/openssl genpkey -algorithm ML-DSA-65 -out /tmp/t.key && echo OK
Recorded output
OK (really using the newly built library)
shell
# RHEL / RockyLinux 9 (dnf) -- on a separate platform
openssl version -a
Recorded output
OpenSSL 3.0.7 1 Nov 2022 ... -- NO PQC on the fresh image
shell
dnf install -y openssl && openssl version && openssl genpkey -algorithm ML-DSA-65 -out /tmp/t2.key && echo OK
Recorded output
OpenSSL 3.5.5 27 Jan 2026 ... followed by OK
shell
mkdir -p /opt/oqs/lib/ossl-modules && touch /opt/oqs/lib/liboqs.a && OPENSSL_MODULES=/opt/oqs/lib/ossl-modules openssl list -providers -provider oqsprovider -provider default
Recorded output
list: unable to load provider oqsprovider
Hint: use -provider-path option or OPENSSL_MODULES environment variable.
...:error:12800067:DSO support routines:dlfcn_load:could not load the shared library:...:filename(/opt/oqs/lib/ossl-modules/oqsprovider.so): /opt/oqs/lib/ossl-modules/oqsprovider.so: cannot open shared object file: No such file or directory
...:error:12800067:DSO support routines:DSO_load:could not load the shared library:...
...:error:07880025:common libcrypto routines:provider_init:reason(37):...:name=oqsprovider

At the table

How to say this in a bank meeting.

To an executive
Knowing which OpenSSL version runs where in our own infrastructure is a precondition for answering an audit question with 'we check this'. As this lesson shows, 'version X is installed' does not always mean 'algorithm Y works'.
To an architect
PQC support comes with OpenSSL 3.5 itself, with no extra module. oqs-provider is only needed for experimental algorithms OpenSSL does not yet support (for example FN-DSA/Falcon until it is standardized); do not add an unnecessary dependency for ML-KEM, ML-DSA or SLH-DSA. Also: on RHEL-based systems a fresh image does not always bring a current OpenSSL, so do not use that as an inventory assumption.
Objection
“"Our research notes said oqs-provider must be installed. Why does this say it isn't needed?"”
Answer
Because ML-KEM, ML-DSA and SLH-DSA are already in OpenSSL 3.5's default provider. oqs-provider is only needed for non-standard algorithms or ones not yet in OpenSSL (today the clearest example is FN-DSA/Falcon). If you do not keep this distinction clear, you take on an unnecessary dependency and the DSO and version compatibility burden that comes with it.

Sources

  • OpenSSL Project (GitHub), 2025. The primary, current documentation of config/Configure options; used to check which flags actually exist

    "Configuration Options" section / 10 min

  • Linux man-pages project, 2024. The primary reference for how DSOs (dynamic shared objects) are loaded, LD_LIBRARY_PATH and search order; the root cause of both the provider-loading and the CLI/Library version mismatch errors seen in this lesson

    "DESCRIPTION" and search path section / 8 min

  • Rocky Linux / Red Hat errata, 2026. A traceable record of how and when OpenSSL moved from 3.0.7 to 3.5.5 in the RHEL 9 family; the RHEL-side evidence for this lesson's claim that checking the version number is not enough

    change list / 3 min

Checkpoint

Answer first, then compare with the model answer and score yourself against the rubric. Saved in this browser only.

  1. 01Recall

    Do you need to install oqs-provider to use ML-DSA in OpenSSL 3.5+? Why?

  2. 02Recall

    What does the "cannot open shared object file" error mean, and when do you see it?

  3. 03Scenario

    A colleague tells you 'I built openssl with a special --enable-pqc flag for ML-DSA.' Where do you check that flag, and is it right?

  4. 04Scenario

    On a freshly installed RHEL 9 server, openssl version shows 3.0.7. Does that mean PQC can never work on this system? What do you do?

  5. 05Hostile

    An auditor asks: 'How do you prove your PQC support really comes from OpenSSL itself and not from an experimental add-on?' Which command do you show?

  6. 06Hostile

    openssl version says 'OpenSSL 3.5.1', but in parentheses the same output says 'Library: OpenSSL 3.5.7'. Why are there two numbers, which is the 'real' version, and when does this cause problems?