Policy data: what the ruleset enumerates¶
Entries about the shape of the data in data/ruleset.toml rather than about any one
reader: which lists close mechanically, which never close, and what happens when one
stops matching the world without anything failing.
openssl_banner names every major digit, not the majors that shipped¶
Accepted.
A group listing OpenSSL 3., OpenSSL 1.1. and OpenSSL 1.0. misses OpenSSL 4.0, and
the current PyPI wheel of the package this tool was written for compiles it in:
cryptography 50.0.1 carries OpenSSL 4.0.2 25 Aug 2026 in read-only data, declares no
DT_NEEDED on libcrypto, and exports no OpenSSL symbol. Read through .dynsym and the
banner alone, the banner is the entire evidence, and with that major unlisted the wheel
comes out openssl_linkage: none — a wheel carrying its own OpenSSL reading exactly like
a wheel with none in it. Nothing fails while it does: the object parses, every structural
check passes, the record is clean.
Why a digit is still required. OpenSSL alone also matches prose. That same object
carries OpenSSL 3's legacy provider failed to load, and so does a build that links the
system library. Requiring a digit and a dot rules that undotted string out, but not a
sentence naming a dotted version — "enable OpenSSL 3.0 legacy provider" still matches.
What keeps that from reading static is the copy marker, not the shape of the string:
see the entry on an uncorroborated banner.
What all ten digits cost, and what they buy. Listing 0 through 9 is one line and no
code. Measured over 18 native wheels off PyPI against the three-major list, exactly one
record differs — cryptography from none to static — and the other seventeen are
byte-identical.
What was rejected. A bounded pattern capability for [[string_group]], which is the
same answer with a code change behind it: the loader refuses non-printable substrings
precisely so the strings matcher can assume no pattern spans its run separator, and a
raw pattern voids that assumption. Also rejected: a note telling the next maintainer to
add the following major when it ships — a list that depends on somebody remembering is
the failure, not the remedy.
Two kinds of list: a field's spellings and an entity list¶
The ruleset's lists fall into two kinds that read alike but answer different questions.
A field's spellings, enumerated. These close mechanically and are silent when stale.
nss (NSS 3.) names every digit. The Windows version suffix matches the architecture
decoration as a token, not as x64|x86|arm64|arm64ec: four spellings of a field whose
fifth — a vendor writing aarch64 — would leave libcrypto-3-aarch64.dll resolving to
no library at all. cargo_path_regex accepts both path separators: cryptography's
win_amd64 .pyd carries 153 cargo\registry paths and no cargo/registry path, so with
one separator every Rust wheel built on Windows reads as carrying no crates at all. That
one costs more evidence than the rest together — with both separators hf-xet has 74
crates on Windows rather than 0, including rustls and aws-lc-rs, while all 18 Linux
records are identical either way.
An entity list. The crate table names openssl-src, boring, boring-sys, sha-1,
md5, sha1_smol and sha3 alongside the rest. No pattern closes the set of crates that
exist in the world, so this list is incomplete by construction rather than stale by
neglect, and adding to it buys reach rather than closing a hole. openssl-src carries its
own limit in its why: its code runs in a build script and is not linked into the
artifact, so nobody has observed the path in a shipped wheel, and on an object with no
other OpenSSL evidence it gives openssl_linkage: unknown, never static.
A guarantee that has to be checked to hold. [conventions] says naming the Go string
groups in the ruleset means renaming a group cannot silently flip a verdict-relevant
field. The loader checks those names where it checks every other group reference: a
loader that did not would load a name no group has and leave GoBuildInfo.boring_crypto
false for every Go binary in the run.
Revisit if a [[string_group]] needs a match ten literals cannot spell — not on a
count of cases. A capability earns itself when the enumeration cannot express the match,
not when the list is long.
Crates are read from every cargo source layout, and a vendored crate has no version¶
Accepted, and it changes records.
Crates are read from four cargo source layouts: the crates.io registry layout cargo
build uses straight from a checkout, cargo/registry/src/<index>/<name>-<version>/;
distro packaging, where Fedora's RPM Rust macros lay a crate out at
/usr/share/cargo/registry/<name>-<version>/ with no src/<index>/ segment; cargo
vendor — what fromager configures for an offline build — which writes
vendor/<name>/... with no cargo/registry segment and, without --versioned-dirs, no
version anywhere in the path; and a git dependency checkout,
$CARGO_HOME/git/checkouts/<repo>-<16 hex hash>/<short rev>/..., cargo's layout for a
crate pinned by a git revision. Measured on a Fedora python3-cryptography build: the
object yields 14 crates, including openssl and openssl-sys. Measured for the git
layout (cargo 1.98.1): a cdylib built from git dependencies on rust-base64 and
hashes (the sha2 crate's repository) embeds paths like
.../git/checkouts/rust-base64-9af66aca7bf9fca2/5b98ee1/src/engine/mod.rs; CARGO_HOME
is arbitrary (a custom directory here, /usr/local/cargo in the Rust Docker images), so
the pattern anchors on git/checkouts/, not cargo/git/checkouts/.
cargo_path_regex treats the src/<index>/ segment as optional, which covers the distro
layout. The vendor and git-checkout layouts each have their own convention,
cargo_vendor_path_regex and cargo_git_path_regex, rather than a branch of the
registry pattern — Python's re refuses two groups sharing a name in one alternation —
and both must contain .rs past the crate directory (no terminator required, since a
Rust panic location is not NUL-terminated), which keeps a vendored or checked-out C or
Go tree from being misread as a Rust crate in the common case, without a word-boundary
guarantee. Every repetition in both is bounded (crate name at 64 characters, path
segments at 255, nesting at 16 levels): an unbounded version of the vendor pattern
measures about 40 seconds at 20,000 repetitions of a near-miss input, against well
under a second bounded. A repository/hash near-miss and a member-directory near-miss
stay under a second for the git-checkout pattern either way, bounded or not, at
comparable sizes; a run of literal src/ segments ahead of a non-matching tail does
reach the bound that matters, since src/ is itself a candidate for the pattern's
required anchor at every repetition — bounded, that shape runs in milliseconds;
unbounded, it takes on the order of ten seconds at 50 repetitions of 40 src/
segments. The vendor pattern's name class also excludes ., which
no crates.io crate name can contain: that is what makes vendor/gimli-0.32.3/ split
uniquely into name gimli and version 0.32.3 rather than one long name, regardless of
whether the name group is lazy or greedy. Allowing . lets the name group and the
version group's leading digits split a run of digits and dots several ways, which costs
3.7 seconds per MiB against 0.4 without it. A git checkout names no version at all — a
revision, not a version — so cargo_git_path_regex's version group is declared but can
never participate, the same way it is for a crate named literally without one. The
checkout directory is named after the repository, not any one crate inside it: a
workspace member's own directory, immediately above its src/, is read as the crate
name (rust-openssl holds openssl-sys); a root crate with no member directory of its
own (ring) is read under its repository's name instead.
A vendor/ tree inside a registry crate's own directory —
.../bar-1.0.0/vendor/ring/src/x.rs — is that crate's own vendored source, not a
crate of its own: it reads as bar 1.0.0, and the nested ring match is dropped. The
registry crate's directory is taken to end at its first .rs file rather than the end
of the printable run, since rustc packs panic locations for unrelated crates back to
back in read-only data; a vendor/ match after that point is a separate path and is
kept. Precedence runs one way — a registry match nested inside an outer vendor/
directory is unaffected. A vendor/ tree inside a git-checkout workspace member is not
given the same precedence: cargo_git_path_regex's own name group already reads the
directory immediately above src/, so it agrees with cargo_vendor_path_regex on the
same crate without needing one pattern to defer to the other.
RustCrate.version is str | None: a layout that names no version records null,
never an invented one. That makes rust_crates[].version nullable in the record, which
is why schema_version is 2.
What it costs. A crate that contributes no panic location or assert! message
anywhere in the object is invisible whichever layout built it. No fromager-built wheel
has been measured, only cdylibs built the way fromager configures cargo for the
registry and vendor layouts, and by hand for the git-checkout layout. A root crate read
from a git checkout carries its repository's name rather than its own whenever the two
differ (rust-base64, not base64), and a workspace member's directory name is
conventionally, but not necessarily, the crate it holds. The vendor pattern always
anchors on the first vendor/ path component: a build tree that itself sits inside a
directory named vendor collapses every crate nested inside it into one crate named
after that outer component, losing the real, possibly
claimed, names underneath. A vendor/ tree inside a git-checkout workspace member is
unaffected, since cargo_vendor_path_regex matches on its own vendor/ component
regardless of what layout surrounds it.
A Go FIPS build is told from a stock one by its build settings¶
Accepted.
Since Go 1.24 the standard library implements its crypto on top of
crypto/internal/fips140, so a binary built against the validated module carries the
stock package paths exactly as a stock build does, and with nothing to suppress it would
read as NON_APPROVED_CRYPTO. Built one program two ways on go1.27.1: crypto/sha256.
appears 9 times in both, crypto/aes. 6 times in both, and crypto/internal/fips140 more
often in the stock binary (381) than in the FIPS one (334). What separates them is
GOFIPS140= and fips140=on, which the toolchain writes into .go.buildinfo — an
allocated section the strings pass reads, so the verdict needs no reader change.
The verdict is CONDITIONAL: the module being compiled in does not mean it is in force,
since GODEBUG=fips140 can be turned back off at run time.
The record needs [conventions] to name every Go group. binaries[].go.markers is built
from the Go group names [conventions] lists, so a rule on a group missing from that list
would make one record say two things — markers: ["go_stock_crypto"] beside a verdict of
BIN_GO_FIPS140, from the same strings. A test holds the two lists equal.
Full argument, with the measurements and what is deliberately left open:
DESIGN.md.
A static OpenSSL's legacy primitives lead the headline¶
Accepted, knowing what it costs.
Reading .symtab local definitions beside a present .dynsym surfaces a statically
linked OpenSSL's own low-level API — BF_*, MD4_*, SHA1_*, RIPEMD160_*, and its
Curve25519 entry points: 1.1.1's X25519/ED25519_*, 3.x's
ossl_x25519/ossl_ed25519_* provider names, and both versions' internal crypto/ec
x25519_fe51_*/x25519_fe64_* field-arithmetic helpers (present only where the build
includes the assembly path, not provider entry points) — so such a wheel also matches
BIN_BCRYPT_BLOWFISH, BIN_OWN_WEAK_HASH_IMPL and BIN_CURVE25519 alongside
BIN_STATIC_OPENSSL. A bundled libcrypto matches BIN_BCRYPT_BLOWFISH and
BIN_OWN_WEAK_HASH_IMPL the same way, through its own .dynsym exports alongside
BIN_BUNDLED_OPENSSL, with no .symtab read involved; none of BIN_CURVE25519's
OpenSSL names is part of libcrypto's public API, so a bundled copy matches it only if
the bundle still carries a .symtab. Either way NON_APPROVED_CRYPTO outranks the
CONDITIONAL of BIN_STATIC_OPENSSL or
BIN_BUNDLED_OPENSSL. The taxonomy calls this correctly: a static or bundled OpenSSL
bundles those primitives and the host FIPS provider cannot refuse them.
An import of these same names -- BF_encrypt called through a linked library rather
than defined by the wheel -- neither implements nor bundles Blowfish, so it does not
match BIN_BCRYPT_BLOWFISH and does not read NON_APPROVED_CRYPTO. It still carries a
class, through BIN_BCRYPT_BLOWFISH_IMPORTED at CONDITIONAL, so a Blowfish import
against a library other than OpenSSL never reads NO_CRYPTO_DETECTED. OpenSSL's
low-level BF_ API bypasses its provider mechanism, which is why the import goes to a
human rather than being called acceptable.
What was rejected. A narrower binding on the three rules, because the record has
no way to tell an exported symbol from one a version script kept local, and the names
that would need separating are OpenSSL's own. Co-occurrence-aware precedence, because it
changes what verdict.class means for every wheel and wants a wider corpus than the one
measured for .symtab local definitions. Dropping the field helpers' reach, because
they match through the same generic x25519_ prefix as every other lowercase
Curve25519 implementation, so narrowing that prefix to exclude OpenSSL's own would
also drop the group's hits on those other implementations, leaving every static
OpenSSL, with or without the assembly path, consistently silent about a primitive it
still bundles.
What it costs. The headline for a static or bundled-OpenSSL wheel carrying any of
the three legacy groups drifts to NON_APPROVED_CRYPTO. confluent-kafka is the
measured static instance; a bundled libcrypto is the more common PyPI shape and hits
the same drift through ordinary .dynsym exports. verdict.classes still carries
CONDITIONAL, and verdict.conditions.openssl_linkage answers whether the wheel
carries its own OpenSSL at all -- though not, on its own, which case produced the
headline when a wheel carries both; the object's own matched_symbols is what tells
them apart.
Suppression is keyed on rule, subject and object¶
Accepted.
suppressed_by keys on the object a hit fired on, not the whole wheel: a suppressor on
one binary never drops a finding on a different one. What a rule locates on follows its
matcher kind, not its layer, so a same-layer relation between two kinds that never
share a path is refused at load time exactly like a cross-layer one -- a linkage match
with object_values set is the one exception, since it locates per object rather than by
its kind alone. MATCHER_LOCATIONS in ruleset.py is the one declaration of which class
of path each matcher kind locates on, and the loader refuses any relation between two
rules that can never share one.
A [[rust_crate]] entry can also carry its own suppressed_by, naming another crate
entry, so two crates that stay on one rule can relate; no other table reads entry-level
suppressed_by, and the loader refuses it there. No shipped entry carries one: the
AWS-LC relation is rule-level instead, aws-lc-rs and aws-lc-fips-sys each routed to
a rule of their own, and the former naming the latter in its rule-level suppressed_by.
sbom_component honours both the entry- and rule-level relations too, keyed on the SBOM
document as the object, so an SBOM naming both crates in one document reports only
the FIPS build's finding. SBOM and binary evidence never suppress each other, because
they never share a path, which is the accepted over-flag that remains: an SBOM naming
aws-lc-rs beside a FIPS binary object still reports both. Suppression is
non-cascading, and the loader refuses a suppressed_by cycle across rules and crates,
including one closed by an ownerless crate, rather than silently dropping every member
of one.
Full entry,
including why aws-lc-sys and rustls are not suppressed, and why the cross-source
over-flag is accepted.
An AWS-LC FIPS build is told from a stock one by its symbol prefix¶
Accepted.
aws_lc matches AWS-LC and aws-lc in read-only data, and lowercase aws-lc also
matches the cargo path of either AWS-LC crate, so BIN_AWS_LC alone cannot tell the
validated build from the stock one. Built one program two ways on linux/x86_64 with
aws-lc-rs 1.18.1: the FIPS build (aws-lc-fips-sys 0.14.2) carries 2118 local
aws_lc_fips_0_14_2_* names in .symtab and the stock build (aws-lc-sys 0.45.0)
carries none. AWS-LC's FIPS build moves its constants, including the version string,
into .text so its integrity hash covers them: AWS-LC FIPS 4.2.0 sits at file offset
0x8ae5c. ELF searches executable sections for the one string group that asks to be
found there, so a stripped object -- the shape a release wheel usually ships as, and the
one that loses the symbol prefix above -- is still identified by the version string
alone; every other string group stays read-only-data-only.
The verdict is CONDITIONAL, the same reasoning as the Go entry above: the module being
compiled in is not the same as it being in force, and which certificate covers the
compiled version is not something the wheel states.
aws-lc-rs's own crate finding needs a rule of its own to be suppressible: on the
default crate rule, alongside every crate suppression cannot reach, a FIPS build's
aws-lc-rs cargo path (present on every measured build, FIPS or stock) keeps
NON_APPROVED_CRYPTO in the record even once the FIPS condition is identified.
aws-lc-sys stays on the default rule, since it names the stock build specifically.
CONDITIONAL is reached whenever the FIPS build is identified and the object defines no
non-approved primitive of its own; the cargo path being the object's sole evidence is one
way to reach it, not the only one, and an SBOM naming both aws-lc-rs and
aws-lc-fips-sys in the same document reads the same way. A stripped object reaches it in
full: .symtab is gone either way, so curve25519_x25519 and md5_final -- the FIPS
module's own primitive implementations, defined only there -- were never reachable on a
stripped object to begin with, and the version string in .text is what identifies the
build once the symbol prefix is gone with it. An unstripped one still fires
BIN_AWS_LC_FIPS but keeps NON_APPROVED_CRYPTO, because those two symbols are exactly
what that class is defined to catch, condition or not.
A BoringSSL FIPS module is told from a stock build by its integrity test¶
Accepted.
boringssl matches BoringSSL in read-only data, but a stripped stock BoringSSL object
(measured on grpcio's cygrpc extension) carries that string, BoringCrypto and
FIPS self test too, so none of them tell a validated BoringCrypto build apart.
BORINGSSL_integrity_test, the FIPS module's power-on self-test entry point, does:
upstream compiles it only into a FIPS build without ASAN, and it is a local .symtab
definition, read the same way as the AWS-LC symbol prefix above.
The rule is fork-neutral: AWS-LC's FIPS module shares the same source and the same
symbol, so the rule names a FIPS build of the BoringSSL-lineage module rather than which
fork compiled it, and suppresses only BIN_BORINGSSL. Every Go binary built with the
BoringCrypto backend also suppresses BIN_BORINGSSL, since it vendors BoringSSL's own
strings regardless of whether the integrity-test symbol survives stripping.
The verdict is CONDITIONAL, the same reasoning as the AWS-LC entry above. A stripped
static object that carries neither marker still reads as stock NON_APPROVED_CRYPTO,
and an unstripped BoringCrypto binary keeps NON_APPROVED_CRYPTO in classes through
its own primitives, unsuppressed for the same reason AWS-LC's are.
Every symbol group is read by a rule, or says it is evidence only¶
Accepted, and it changes verdicts.
A [[symbol_group]] the binary readers populate but no rule reads is evidence gathered
and thrown away: a defined argon2id_hash_raw, alone in an object with no other
evidence, would read NO_CRYPTO_DETECTED, because no rule looks at the argon2 group.
blake's string-group match alone has the same gap: a reference implementation with no
banner string carries nothing that arm matches.
Argon2 gets its own rule, BIN_ARGON2, NON_APPROVED_CRYPTO on either binding: OpenSSL
exports no argon2*/blake2*/blake3* name (measured with nm -D --defined-only on
Fedora, its own Argon2 reaches only through EVP_KDF), so an imported name from
either group can only be a dependency on a library like libargon2, never a call the
host FIPS provider could answer. blake is read by BIN_NON_CRYPTO_HASH as a second
match arm beside its existing string-group one, keeping CONTEXT_DEPENDENT; a hit from
either arm on the same group folds into one finding.
The loader refuses a ruleset where some [[symbol_group]] is neither read by a
dynamic_symbol rule or a [[crypto_library]]'s symbol_group, nor marked
evidence_only = true on the group itself -- and refuses the opposite too, a group
marked evidence_only that something does read. boringssl and aws_lc take the
evidence_only mark: BIN_BORINGSSL and BIN_AWS_LC already read the same-named
string groups instead, and AWS-LC keeps BoringSSL-named symbols, so a rule over the
boringssl symbol group would report BoringSSL on an AWS-LC object.
What it costs. A BLAKE-only object reads CONTEXT_DEPENDENT instead of
NO_CRYPTO_DETECTED, which outranks both that and OPAQUE in [verdict] precedence,
so a partially read object carrying a BLAKE symbol headlines CONTEXT_DEPENDENT
instead of OPAQUE. An Argon2-only object moves further, to NON_APPROVED_CRYPTO.
Full entry,
including the measurement behind the any binding and why a [[crypto_library]]
symbol_group on argon2 was rejected.
The ruleset cites a standard and the record carries only the pointer¶
Accepted, and it changes records.
A finding's basis is a list of [[standard]] ids and nothing else: title,
edition, status, successor, sunset and why never reach a scan record.
wheel-crypto-scan rules --format json and the HTML report read them back out of the
loaded Ruleset for display, on demand, rather than the scan record duplicating them
into every wheel that cites the standard; the Markdown report has no Ruleset in hand
and prints only the raw ids. A record describes one wheel at one point in time, and a
standard's edition and status describe NIST/FIPS publication state on its own
schedule, unrelated to when any wheel was scanned.
ruleset_coherence.check_basis_targets_a_live_standard refuses, at load time, a
basis naming a standard whose status is draft, planned or withdrawn, rather
than letting the ruleset load with a citation that no longer -- or not yet -- governs
anything: the id in the record would look exactly as authoritative as a live one, with
nothing in the record itself to say otherwise.
What was rejected. Carrying edition/status on the finding alongside the id, so
a reader would not need the ruleset to interpret basis -- rejected because it
reintroduces the staleness the pointer avoids: two records for the same wheel, scanned
months apart, would describe the same standard two different ways for reasons that
have nothing to do with either wheel.
relation and the verdict class are checked against each other at load time¶
Accepted.
RELATION_CLASSES in ruleset.py names which verdict classes each relation is
consistent with, and ruleset_coherence.check_relation_matches_verdict refuses, at
load time, any rule or override-bearing entry whose (verdict, relation) pair is not
one of those rows -- restricted is the one relation with two rows,
NON_APPROVED_CRYPTO when the wheel implements the primitive itself and
CONTEXT_DEPENDENT when it defers to the host module, and no relation names OPAQUE.
Why a load error, not a test over the shipped ruleset. A pair outside the table is
a rule stating two facts about the same finding -- what would fix it, how bad leaving
it is -- that contradict each other, and that is a property of one rule's own two
fields, checkable the moment it is parsed. A test over data/ruleset.toml alone would
leave --ruleset PATH unguarded, so the check runs in ruleset_coherence.py against
every Ruleset load_ruleset builds. An override-bearing entry with no owning rule --
every shipped [[crypto_library]] entry today -- gets its own two rules instead of
being skipped: a relation with no verdict anywhere to check it against is refused
outright, and a verdict with no relation of its own is checked against every rule
that could resolve as the entry's owner at scan time.
What was rejected. Enforcing the pair only in tests/test_ruleset_data.py's
totality checks over the shipped ruleset's own data -- cheaper, but totality (every
verdict-bearing pair carries a relation, except a rule a table routes through, whose
entries carry it instead) and compatibility (a stated pair does not contradict itself)
are different questions, and only compatibility needs enforcing for every ruleset
--ruleset can load, not just the one this repository ships.
family is the FIPS-agnostic axis; category stays the FIPS one¶
Accepted, and it changes records.
category already existed on every rule and reads as a FIPS-lens classification --
non-approved-impl, fips-breaking, trust-policy, and twenty more, several naming
a detection mechanism (dynamic-load, python-import, rust-crate) rather than a
primitive kind at all. family is a second, closed vocabulary (FAMILIES in
ruleset.py) describing what primitive a finding is evidence of, with no FIPS
content: hash, block_cipher, signature, key_agreement, library and the rest,
carried on a rule, an entry in the four override-bearing tables, and every
[[symbol_group]]/[[string_group]], so crypto.families can be built without
reading a FIPS class at all -- a NO_CRYPTO_DETECTED wheel has an empty families
list because nothing fired, not because a FIPS question was asked and answered no.
Why category was not reused. Widening category's values to also answer "what
kind of primitive is this" was rejected: a rule that delegates rather than embodying
one primitive, like DIST_SYSTEM_CRYPTO_WRAPPER's crypto-wrapper category, carries
no family at all, and reading the inventory off category would mean reading the
FIPS lens to answer a question that has nothing to do with it -- the inventory would
inherit category's open, per-rule-author vocabulary rather than a small closed one a
report's grouping can rely on.
SHA-1 is restricted, not refused¶
Accepted, and it changes verdicts.
[conventions] splits Python's weak hash constructors into refused_hash_algorithms
(md4, md5, md5-sha1, ripemd160, sm3, whirlpool) and
restricted_hash_algorithms (sha1 alone). PY_WEAK_HASH_CALL matches the refused
list and reads runtime_refusal/FIPS_BREAKING: those constructors raise on a
FIPS-enforcing host unless the caller passes usedforsecurity=False.
PY_RESTRICTED_HASH_CALL matches SHA-1 alone and reads
restricted/CONTEXT_DEPENDENT -- it does not raise. BIN_OWN_WEAK_HASH_IMPL/
BIN_OWN_SHA1_IMPL carry the same split on the binary side, both
NON_APPROVED_CRYPTO.
Why the split. The OpenSSL FIPS provider's own fips_digests[] table
(providers/fips/fipsprov.c) registers SHA1 with its default, approved properties
beside SHA-2/SHA-3; the other five are absent from it entirely, so a provider fetch
for one of them fails -- which is what makes hashlib.md5() raise -- while a SHA1
fetch succeeds. SP 800-131A Rev. 2 Table 8 gives the other half: SHA-1 is acceptable
only for a non-digital-signature use that does not require collision resistance, a
restriction no static call site can answer on its own, so CONTEXT_DEPENDENT sends it
to a human rather than clearing or refusing it outright.
The open residual. Both rules' why text cites the provider's source
registration, not a live measurement: hashlib.sha1()'s and hashlib.md5()'s actual
behaviour under FIPS enforcement is left unmeasured, read out of the provider's source
rather than run and observed on a host with fips=1 enforcing and the FIPS provider
active. A documented gap; closing it needs nothing more than such a host.