What a host running under FIPS 140 needs from this package, and what this package needs
from the host. The package enables nothing: FIPS mode belongs to the runtime the host builds
and starts. What this page says about the runtime is Erlang/OTP's documentation for
crypto, restated and quoted where the words matter, so a host verifies it there rather
than here; the two sentences below that are this page's own inference from OTP's source are
marked as such.
Only that :crypto.hash/2 answer for :sha256, :sha384 and :sha512. Those are the SHA-2
digests of FIPS 180-4, approved under FIPS 140-3, and they are the only three names the
canonical envelope may carry (docs/crypto-posture.md). Nothing this
package calls is disallowed in FIPS mode: it uses no MD5, no SHA-1, no cipher, no MAC, no
key derivation and no random source: :crypto.hash/2 at one site is its whole use of the
library (test/beam_mcp/boundary/no_key_holding_test.exs, both tests). A host that has
enabled FIPS mode therefore runs this package unchanged, and a graph hashed under FIPS mode
has the same bytes and the same hash as one hashed outside it: the digest is the digest. Both
are measured, not only reasoned: the whole suite passes in FIPS mode, the canonical-hash
goldens included (below, "Measured").
FIPS mode is a property of the crypto application and the OpenSSL it is linked against,
and it is the host's to establish. What follows is OTP 28's crypto documentation (the
-doc strings of crypto.erl in crypto-5.7, and its application configuration
parameters), restated; where this page and OTP's differ, OTP's is right.
- An Erlang/OTP
cryptobuilt with FIPS support (the configure option--enable-fips), linked against an OpenSSL 3 whose FIPS provider is installed and validated on that machine.:crypto.info/0reportsfips_provider_available: truewhen the provider is there.:crypto.info_fips/0answers, in OTP's words,:enabledwhen running in FIPS mode or:not_enabledifcryptowas built with FIPS support, and "for other builds this value is always:not_supported", so:not_supportednames the build, not the provider: a FIPS-builtcryptoover a library with no provider answers:not_enabled, when FIPS mode was not requested; when it was, see the next step, since OTP does not load at all. Measured on the development runtime that wrote this page (OTP 28.1.1, OpenSSL 3.0.13, no FIPS provider)::not_supported,fips_provider_available: false, and:crypto.enable_fips_mode(true)answersfalse. - The
cryptoconfiguration parameterfips_mode: true, in the application environment before thecryptomodule is first loaded, insys.configor the release's configuration, not set at runtime after the fact. In OTP's words, "this setting will take effect when the nif module is loaded", and: "if FIPS mode is requested but not available at run time the nif module and thus the crypto module will fail to load. This mechanism prevents the accidental use of non-validated algorithms." So a host that sets the parameter on a runtime without a validated provider does not get:not_enabled; it gets nocryptomodule at all. Measured (2026-09-23, OTP 28.1.1 built with FIPS support, the provider configuration withheld): the module'son_loadfails ("Library load-call unsuccessful") and an error report says so, butapplication:start(crypto)andapplication:ensure_all_started(crypto)still answerok, since thecryptoapplication has no callback module to fail. So a release whose.apprequirescrypto(this package's does) boots, and the first:cryptocall raisesUndefinedFunctionError: this package's:crypto.hash/2raises then, and nothing is hashed without the provider. An earlier version of this page said the application fails to start and the release does not boot; that was reasoned from OTP's words and the measurement corrected it. The refusal is real but arrives at the first call, not at boot, which is why the check in step 3 matters. (This page's own inference from OTP'son_load, not OTP's words: the parameter is read once, when the module loads, so a value set after that changes nothing until the module is loaded again.) This is why OTP sayscrypto:start/0"does not work if FIPS mode is to be enabled" and to useapplication:start(crypto)instead: the application must be loaded, with its environment, before the module is. A release does this for every application its.appfiles require, and this package's.applistscryptoas a required application, so a release that includesbeam_mcploads and starts it (it did not until 0.6.0, the release this page arrives in: an HTTP host hadcryptoonly throughplugandbandit, both optional, and a stdio-only release would have had no:crypto.hash/2at all; writing this page found it, andtest/beam_mcp/connectome/canonical_test.exs"the .app the build writes depends on crypto, so a release without plug and bandit still hashes" holds it). - A check at start.
:crypto.info_fips/0answers:enabledonce the parameter took; a host that requires FIPS mode checks it at start and refuses to serve otherwise, because OTP's own refusal (the module not loading) surfaces only at the first:cryptocall, not at boot (measured, step 2). Loadcryptobefore the check and apart from it: in Elixir, expanding a remote call to:cryptoloads the module, so a check written in the same expression as the load can read acryptoloaded beforefips_modeapplied. The older way,:crypto.enable_fips_mode(true)at runtime (truewhen it took,falsewhen it did not), is deprecated in OTP 28 ("use config parameter fips_mode", in the deprecation's own words) and is named here only so a host reading older guidance knows what replaced it.
This package calls none of :crypto.enable_fips_mode/1, :crypto.info_fips/0 or
:crypto.info/0 (the three are barred from lib/ by the same census that bars every
:crypto. function but hash/2), and it sets no application environment: the
package-reach census (test/beam_mcp/boundary/package_reach_test.exs) pins the functions
called on Application to load/1 and spec/2, reads only, so the package cannot enable
FIPS mode by accident or on purpose, and cannot report on it; the host's configuration and
start-up are where that lives. Nothing here is a claim that this package, or a host running
it, is FIPS-validated: validation is a property of a cryptographic module (the OpenSSL FIPS
provider) and of the process that certified it, and this package holds no such module.
.github/workflows/fips.yml runs the suite on every push to main, on each pull request that
touches the code, and weekly, on this toolchain: Erlang/OTP 28.1.1 (the pinned line) built
with --enable-fips, over OpenSSL 3.5.8's libcrypto with the FIPS provider built from
OpenSSL 3.1.2, the source that CMVP certificate #4985 (FIPS 140-3) validates, following its
security policy (enable-fips, make install_fips, openssl fipsinstall on the machine
that runs it). Before any test runs, the same VM asserts :crypto.info_fips() is :enabled
and the provider reports build 3.1.2; a negative control shows that without the provider
configuration fips_mode true gives no working crypto.
Measured 2026-09-23 on that toolchain: :crypto.info_fips() answers :enabled,
fips_provider_available: true, fips_provider_buildinfo: "3.1.2", the linked library
OpenSSL 3.5.8; :crypto.hash(:md5, _) raises notsup ("Bad digest type in FIPS");
SHA-256, SHA-384 and SHA-512 answer, SHA-256 of abc being the FIPS 180-4 test vector; and
the suite passes, 11 properties and 729 tests, 0 failures. A FIPS-built OTP started without
fips_mode answers :not_enabled, as step 1 says.
This is not a validation claim. Validation belongs to a cryptographic module and the operational environments its certificate names; the CI runner (ubuntu-24.04) is not one of #4985's, and a host's FIPS posture is its own build of OTP and OpenSSL on its own platform. What the job proves is that this package's behaviour does not change in FIPS mode.
Nothing about the bytes or the hash. A verifier in FIPS mode and one outside it compute the
same digest over the same bytes; the algorithm the envelope names is one the FIPS provider
serves. A consumer whose policy accepts only SHA-384 or SHA-512 asks the host for that
algorithm: and refuses envelopes naming another: the consumer's policy, stated in the
consumer (docs/connectome-canonical.md, rule 9).