From 2c5302b47e4ef2d8505e0596aeec05d2d60c5a00 Mon Sep 17 00:00:00 2001 From: Guillaume Egles Date: Tue, 30 Jul 2024 00:33:15 -0700 Subject: [PATCH] add FIPS module verification mechanism --- recipes/openssl/3.x.x/conanfile.py | 118 ++++++++++++++++++++--------- 1 file changed, 83 insertions(+), 35 deletions(-) diff --git a/recipes/openssl/3.x.x/conanfile.py b/recipes/openssl/3.x.x/conanfile.py index f13e86a1532d31..425d1314efa30d 100644 --- a/recipes/openssl/3.x.x/conanfile.py +++ b/recipes/openssl/3.x.x/conanfile.py @@ -1,7 +1,10 @@ +import re +from io import StringIO + from conan import ConanFile -from conan.errors import ConanInvalidConfiguration +from conan.errors import ConanException, ConanInvalidConfiguration from conan.tools.apple import fix_apple_shared_install_name, is_apple_os, XCRun -from conan.tools.build import build_jobs +from conan.tools.build import build_jobs, can_run from conan.tools.files import chdir, copy, get, replace_in_file, rm, rmdir, save from conan.tools.gnu import AutotoolsToolchain from conan.tools.layout import basic_layout @@ -115,27 +118,30 @@ def _settings_build(self): return getattr(self, "settings_build", self.settings) @property - def _fips_validated_version(self): - # As of version 3.3.1, the FIPS module is validated for the following versions - # see https://openssl-library.org/source/ (excluding ancient 3.0.0) - versions = ['3.0.8', '3.0.9'] - versions = sorted([Version(v) for v in versions], reverse=True) - - # Find the closest version that is less than or equal to the current version - fips_validated_version = next((v for v in versions if v <= Version(self.version)), None) - return fips_validated_version + def _fips_version(self): + if self.options.use_validated_fips: + # As of version 3.3.1, the FIPS module is validated for the following versions + # see https://openssl-library.org/source/ (excluding ancient 3.0.0) + versions = ['3.0.8', '3.0.9'] + versions = sorted([Version(v) for v in versions], reverse=True) + + # Find the closest version that is less than or equal to the current version + fips_validated_version = next((v for v in versions if v <= Version(self.version)), None) + return fips_validated_version + else: + return self.version @property def _is_fips_enabled(self): return not self.options.no_fips or self.options.use_validated_fips @property - def _is_fips_validated(self): - return self.version == self._fips_validated_version + def _provides_validated_fips(self): + return self.options.use_validated_fips and self.version == self._fips_version @property def _fips_provider_dir(self): - if self.options.use_validated_fips and not self._is_fips_validated: + if self.options.use_validated_fips and not self._provides_validated_fips: return self.dependencies["openssl"].runenv_info.vars(self)["OPENSSL_MODULES"] elif not self.options.no_fips: return os.path.join(self.source_folder, "providers") @@ -167,14 +173,12 @@ def requirements(self): if not self.options.no_zlib: self.requires("zlib/[>=1.2.11 <2]") - if self.options.use_validated_fips and self._fips_validated_version: - fips_version = self._fips_validated_version - self_validated = self._is_fips_validated - if not self_validated: - self.output.info(f"Using validated FIPS module from openssl/{fips_version}") - self.requires(f"openssl/{fips_version}", visible=False, libs=False, headers=False, run=False, options={'no_fips': False}) + if self.options.use_validated_fips: + if not self._provides_validated_fips: + self.output.info(f"Using validated FIPS module from openssl/{self._fips_version}") + self.requires(f"openssl/{self._fips_version}", visible=False, libs=False, headers=False, run=False, options={'no_fips': False}) else: - self.output.info(f"Using validated FIPS module from self (i.e. {fips_version})") + self.output.info(f"Using validated FIPS module from self (i.e. {self._fips_version})") def validate(self): if self.settings.os == "Emscripten": @@ -185,13 +189,12 @@ def validate(self): raise ConanInvalidConfiguration("OpenSSL 3 does not support building shared libraries for iOS") if self.options.use_validated_fips: - fips_version = self._fips_validated_version - if fips_version is None: + if self._fips_version is None: raise ConanInvalidConfiguration(f"OpenSSL {self.version} - no compatible FIPS validated version found") if self.options.no_fips: - raise ConanInvalidConfiguration(f"FIPS support is requested, but no_fips is set to True") - elif not self._is_fips_validated and self.dependencies["openssl"].options.no_fips: - raise ConanInvalidConfiguration(f"In order to use FIPS module from openssl/{fips_version}, it needs to be built with `no_fips` option set to False") + raise ConanInvalidConfiguration("FIPS support is requested, but no_fips is set to True") + elif not self._provides_validated_fips and self.dependencies["openssl"].options.no_fips: + raise ConanInvalidConfiguration(f"In order to use FIPS module from openssl/{self._fips_version}, it needs to be built with `no_fips` option set to False") def build_requirements(self): if self._settings_build.os == "Windows": @@ -429,7 +432,7 @@ def _configure_args(self): # pass no-fips to the current build if: # - use_validated_fips is enabled and using the fips module from a different version # - user requested no-fips - no_fips = self.options.use_validated_fips and not self._is_fips_validated or self.options.no_fips + no_fips = self.options.use_validated_fips and not self._provides_validated_fips or self.options.no_fips args.append("no-fips" if no_fips else "enable-fips") args.append("no-md2" if self.options.get_safe("no_md2", True) else "enable-md2") @@ -597,6 +600,58 @@ def _replace_runtime_in_file(self, filename): replace_in_file(self, filename, f"/{e} ", f"/{runtime} ", strict=False) replace_in_file(self, filename, f"/{e}\"", f"/{runtime}\"", strict=False) + def _package_and_test_fips(self): + provdir = self._fips_provider_dir + modules_dir = os.path.join(self.package_folder, "lib", "ossl-modules") + module_filename = f"fips.{({'Macos': 'dylib', 'Windows': 'dll'}.get(str(self.settings.os), 'so'))}" + copy(self, module_filename, src=provdir, dst=modules_dir) + + self.output.info("Testing FIPS module (via fipsinstall & fipsinstall -verify)") + if can_run(self): + openssl_bin = os.path.join(self.package_folder, "bin", "openssl") + fips_module = os.path.join(modules_dir, module_filename) + fips_cnf = os.path.join(self.build_folder, "fips.cnf") + + # fipsinstall + fipsinstall_command = [openssl_bin, "fipsinstall", f"-module {fips_module}", f"-out {fips_cnf}"] + stderr = StringIO() + try: + self.run(" ".join(fipsinstall_command), stderr=stderr, env="conanrun") + except ConanException as e: + stderr_text = stderr.getvalue() + raise ConanException(f"{str(e)}\n{stderr_text}") from e + + stderr_text = stderr.getvalue() + self.output.info(f"{stderr_text}") + if not re.search(r"INSTALL PASSED", stderr_text): + raise ConanInvalidConfiguration(f"The FIPS Module could not be installed properly:\n{stderr_text}") + + # Check version match + # Only the version of OpenSSL >= 3.1.x prints the version of the fips module at install time + # TODO: Find a better to obtain the version of the FIPS module installed + if Version(self.version) >= "3.1.0": + self.output.info("Checking FIPS version match") + fipsinstall_version_match = re.search(r"version:\s+(\S+)", stderr_text) + fipsinstall_version = fipsinstall_version_match.group(1) if fipsinstall_version_match else None + + if fipsinstall_version != self._fips_version: + raise ConanInvalidConfiguration(f"The FIPS Module version installed ({fipsinstall_version}) " + f"does not match the desired version ({self._fips_version})\n{stderr_text}") + + # fipsinstall -verify + verify_command = [openssl_bin, "fipsinstall", f"-module {fips_module}", f"-in {fips_cnf}", "-verify"] + stderr = StringIO() + try: + self.run(" ".join(verify_command), stderr=stderr, env="conanrun") + except ConanException as e: + stderr_text = stderr.getvalue() + raise ConanException(f"{str(e)}\n{stderr_text}") from e + + stderr_text = stderr.getvalue() + self.output.info(f"{stderr_text}") + if not re.search(r"VERIFY PASSED", stderr_text): + raise ConanInvalidConfiguration(f"The FIPS Module installation did not verify properly:\n{stderr_text}") + def package(self): copy(self, "*LICENSE*", src=self.source_folder, dst=os.path.join(self.package_folder, "licenses")) self._make_install() @@ -613,14 +668,7 @@ def package(self): os.unlink(os.path.join(libdir, file)) if self._is_fips_enabled: - provdir = self._fips_provider_dir - modules_dir = os.path.join(self.package_folder, "lib", "ossl-modules") - if self.settings.os == "Macos": - copy(self, "fips.dylib", src=provdir, dst=modules_dir) - elif self.settings.os == "Windows": - copy(self, "fips.dll", src=provdir, dst=modules_dir) - else: - copy(self, "fips.so", src=provdir, dst=modules_dir) + self._package_and_test_fips() rmdir(self, os.path.join(self.package_folder, "lib", "pkgconfig")) rmdir(self, os.path.join(self.package_folder, "lib", "cmake"))