You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Teams publishing a wheel index for FIPS-enabled hosts need a per-wheel answer to one
question: does this wheel use the host's OpenSSL, or does it carry its own? A wheel that
resolves libcrypto.so.3 from the host inherits the host's FIPS provider and crypto
policy. A wheel that ships or statically links its own copy does not, and no amount of
host configuration changes that.
That question is answerable only from the wheel's bytes, and fromager is the one place
those bytes are already open. add_extra_metadata_to_wheels() unpacks every wheel it
builds and already writes analysis results into .dist-info: fromager-elf-requires.txt, fromager-elf-provides.txt, and the SPDX SBOM. Answering the question later, index-side,
means downloading and unpacking everything a second time and losing the link back to the
build settings that produced the posture in the first place.
I have been working on a scanner for this and would like to wire it up to fromager. Below
is what I measured: what works today with no fromager change, and the one hook I think is
missing.
The tool
wheel-crypto-scan, Apache-2.0. Static
analysis over a wheel. No network, no LLM, no dataflow analysis. Deterministic: same wheel
in, byte-identical JSONL out, and records carry no timestamps, host paths or hostnames, so
embedding one does not break a reproducible build.
It reports evidence, not compliance. There is no passing verdict class and there will not
be one. openssl_linkage resolves to system, bundled, static, mixed, none or unknown, findings say what matched and where, and a human or a policy step decides what
that means.
Dependencies are pyelftools and packaging. Fromager already has both (elfdeps pulls
in pyelftools, packaging is direct), so a plugin adds no new transitive dependency.
What it finds on real wheels
Scanned today, PyPI manylinux x86_64, 7 wheels in 1.1s at --jobs 4:
Wheel
What the wheel carries
Why a builder cares
grpcio 1.84.0
BoringSSL, statically linked (third_party/boringssl-with-bazel/... paths plus the BoringSSL banner in read-only data)
Host FIPS provider is not in the picture at all
pyzmq 27.2.0
pyzmq.libs/libsodium-1c6bac97.so.26.4.0, hash-renamed by auditwheel
Bundled crypto that a vendor-directory check finds, but a dependency check alone does not
hf-xet 1.6.0
rustls, aws-lc-rs, ring, blake3
A complete alternative TLS stack in Rust. Never uses the host OpenSSL
pycryptodome 3.23.0
Its own AES, MD5 and curve25519 implementations
Primitives implemented in-tree, outside any validated module
cryptography 50.0.1
OpenSSL 4.0.2 compiled into _rust.abi3.so, no DT_NEEDED on libcrypto
The same version rebuilt against the system OpenSSL has the opposite posture
requests 2.34.2
hashlib.md5(..., usedforsecurity=False)
Marked, so it does not raise under FIPS. Worth telling apart from the call that does
The interesting part for fromager is the contrast rather than any single verdict. A
package rebuilt from source against the system OpenSSL has a different posture from the
same version on PyPI, and a scan at build time is what proves the rebuild did what the
build settings intended. That makes it a regression check (did our override quietly stop
working?) rather than a compliance stamp. Prebuilt wheels (pre_built: true) are the
other end of it: fromager never builds them, so a scan is the only thing that says what
they carry.
What works today, with no change to fromager
A plugin of about 40 lines, registered on the existing global hooks:
Against fromager 0.96.0, fromager bootstrap six==1.17.0:
INFO setuptools-84.0.0: crypto scan setuptools-84.0.0-0-py3-none-any.whl: CONDITIONAL openssl_linkage=none reasons=PY_INSECURE_RNG: random; PY_TLS_POLICY_OVERRIDE: ...
INFO six-1.17.0: crypto scan six-1.17.0-0-py2.py3-none-any.whl: NO_CRYPTO_DETECTED openssl_linkage=none reasons=none
One JSON record per wheel lands in work-dir/crypto-scan/. Cost is 0.26s to 0.57s per
wheel including interpreter startup (grpcio, 6.8 MiB, is the 0.57s), and wheels are read
from the zip in memory rather than extracted.
So the sidecar case needs nothing from you. Four things I ran into:
No global hook fires while the wheel is unpacked.add_extra_metadata_to_wheels()
is the only place with the tree open, and its extension point is a per-package
override: at most one module per package, replacing the default implementation. A
cross-cutting analyzer cannot write into .dist-info there. The alternative is
unpacking and repacking the wheel a second time after the build, which is exactly what
you pushed back on in Generate SBOM for wheels #908 for large wheels, and rightly.
post_build does not fire for a reused wheel (_is_wheel_built() short-circuits
it), and bootstrap runs only post_bootstrap. A plugin has to register all three hooks
to cover the paths, and a wheel served from a previous build is still never seen.
Gating from a hook means raising, which aborts the whole run rather than marking one
package. That is fine as stop-the-line policy and wrong for "record everything, decide
at release". Not asking for anything here, hooks are fire-and-forget by design.
Package settings are extra="forbid", so per-package plugin config (say, the
posture expected for one package) cannot live in the settings YAML. An env var covers
it for now.
Proposal
1. Keep the scanner out of fromager. What counts as risky crypto is policy, it moves
faster than fromager releases, and you already said in #908 that you do not want fromager
working out what is inside a wheel. Agreed. Fromager only needs to offer the hook.
2. Add one global hook that runs while the wheel is unpacked, after the elfdeps and
SBOM steps and before wheel pack:
Plugins get the unpacked tree, may write files into .dist-info, and the wheel is still
packed exactly once. Nothing about this is crypto specific: a license scanner, the
metadata consistency checks in #1147, or the Maturin SBOM merge in #1327 would use the
same point. Happy to send the PR with docs and tests.
3. Later, a standard format. Once CycloneDX support lands (#987), the evidence can be
expressed as a CBOM in .dist-info/sboms/ and ride inside the wheel, instead of a
tool-specific JSON file.
Two things to settle if evidence gets embedded
Both measured rather than guessed.
An embedded record describes the pre-pack wheel. I simulated the flow on pyzmq: scan,
write the record into .dist-info, wheel pack --build-number 0, rescan. binaries, findings and verdict come back identical. What changes is identity: filename (build
tag), sha256, size_bytes, record_entries, total_uncompressed_bytes. So an embedded
document cannot carry the final wheel's digest. It either names its input explicitly or
leaves identity out, and a sidecar written after packing is what carries the digest. That
is the same split #1238 proposes for attestations, and I would keep it: evidence inside
the wheel, digest-bearing record beside it.
An SBOM directory is a statement, not a scratchpad. My scanner treats a component
declared in .dist-info/sboms/ as the strongest evidence available, on the grounds that a
build tool wrote it rather than anyone inferring it. Write inferred evidence there in
CycloneDX form and the next scan reads its own guess back as a declaration. Anything
embedded needs a tool marker that readers skip. Related, and my bug to fix rather than
yours: fromager's SPDX document uses packages, which CycloneDX-only readers (mine
included, today) skip in silence. Worth knowing for anyone who assumes an SBOM is an SBOM.
Next steps
If wheel_contents sounds right, I will open a PR with the hook, docs and tests. Tell me
what you want it called and whether the signature above is the shape you would take.
I will publish the plugin (the three existing hooks, sidecar JSON per wheel) as a
separate package. Happy to put it under python-wheel-build instead if you would rather
it lived near fromager.
Open question: sidecar only, embedded only, or both? My preference is both, for the
reasons above, but the sidecar alone is genuinely useful and costs you nothing.
Problem
Teams publishing a wheel index for FIPS-enabled hosts need a per-wheel answer to one
question: does this wheel use the host's OpenSSL, or does it carry its own? A wheel that
resolves
libcrypto.so.3from the host inherits the host's FIPS provider and cryptopolicy. A wheel that ships or statically links its own copy does not, and no amount of
host configuration changes that.
That question is answerable only from the wheel's bytes, and fromager is the one place
those bytes are already open.
add_extra_metadata_to_wheels()unpacks every wheel itbuilds and already writes analysis results into
.dist-info:fromager-elf-requires.txt,fromager-elf-provides.txt, and the SPDX SBOM. Answering the question later, index-side,means downloading and unpacking everything a second time and losing the link back to the
build settings that produced the posture in the first place.
I have been working on a scanner for this and would like to wire it up to fromager. Below
is what I measured: what works today with no fromager change, and the one hook I think is
missing.
The tool
wheel-crypto-scan, Apache-2.0. Static
analysis over a wheel. No network, no LLM, no dataflow analysis. Deterministic: same wheel
in, byte-identical JSONL out, and records carry no timestamps, host paths or hostnames, so
embedding one does not break a reproducible build.
It reports evidence, not compliance. There is no passing verdict class and there will not
be one.
openssl_linkageresolves tosystem,bundled,static,mixed,noneorunknown, findings say what matched and where, and a human or a policy step decides whatthat means.
Dependencies are
pyelftoolsandpackaging. Fromager already has both (elfdepspullsin pyelftools, packaging is direct), so a plugin adds no new transitive dependency.
What it finds on real wheels
Scanned today, PyPI manylinux x86_64, 7 wheels in 1.1s at
--jobs 4:grpcio1.84.0third_party/boringssl-with-bazel/...paths plus theBoringSSLbanner in read-only data)pyzmq27.2.0pyzmq.libs/libsodium-1c6bac97.so.26.4.0, hash-renamed by auditwheelhf-xet1.6.0pycryptodome3.23.0cryptography50.0.1_rust.abi3.so, noDT_NEEDEDon libcryptorequests2.34.2hashlib.md5(..., usedforsecurity=False)A trimmed record:
{ "wheel": {"filename": "pyzmq-27.2.0-cp312-abi3-manylinux_2_28_x86_64.whl", "...": "..."}, "verdict": { "class": "NON_APPROVED_CRYPTO", "conditions": {"libsodium_linkage": "bundled", "openssl_linkage": "none"}, "reasons": ["BIN_BUNDLED_CRYPTO_LIB: libsodium", "BIN_LIBSODIUM: libsodium"] }, "artifacts": {"bundled_libs": ["pyzmq.libs/libsodium-1c6bac97.so.26.4.0", "..."]} }The interesting part for fromager is the contrast rather than any single verdict. A
package rebuilt from source against the system OpenSSL has a different posture from the
same version on PyPI, and a scan at build time is what proves the rebuild did what the
build settings intended. That makes it a regression check (did our override quietly stop
working?) rather than a compliance stamp. Prebuilt wheels (
pre_built: true) are theother end of it: fromager never builds them, so a scan is the only thing that says what
they carry.
What works today, with no change to fromager
A plugin of about 40 lines, registered on the existing global hooks:
Against fromager 0.96.0,
fromager bootstrap six==1.17.0:One JSON record per wheel lands in
work-dir/crypto-scan/. Cost is 0.26s to 0.57s perwheel including interpreter startup (grpcio, 6.8 MiB, is the 0.57s), and wheels are read
from the zip in memory rather than extracted.
So the sidecar case needs nothing from you. Four things I ran into:
add_extra_metadata_to_wheels()is the only place with the tree open, and its extension point is a per-package
override: at most one module per package, replacing the default implementation. A
cross-cutting analyzer cannot write into
.dist-infothere. The alternative isunpacking and repacking the wheel a second time after the build, which is exactly what
you pushed back on in Generate SBOM for wheels #908 for large wheels, and rightly.
post_builddoes not fire for a reused wheel (_is_wheel_built()short-circuitsit), and bootstrap runs only
post_bootstrap. A plugin has to register all three hooksto cover the paths, and a wheel served from a previous build is still never seen.
package. That is fine as stop-the-line policy and wrong for "record everything, decide
at release". Not asking for anything here, hooks are fire-and-forget by design.
extra="forbid", so per-package plugin config (say, theposture expected for one package) cannot live in the settings YAML. An env var covers
it for now.
Proposal
1. Keep the scanner out of fromager. What counts as risky crypto is policy, it moves
faster than fromager releases, and you already said in #908 that you do not want fromager
working out what is inside a wheel. Agreed. Fromager only needs to offer the hook.
2. Add one global hook that runs while the wheel is unpacked, after the elfdeps and
SBOM steps and before
wheel pack:Plugins get the unpacked tree, may write files into
.dist-info, and the wheel is stillpacked exactly once. Nothing about this is crypto specific: a license scanner, the
metadata consistency checks in #1147, or the Maturin SBOM merge in #1327 would use the
same point. Happy to send the PR with docs and tests.
3. Later, a standard format. Once CycloneDX support lands (#987), the evidence can be
expressed as a CBOM in
.dist-info/sboms/and ride inside the wheel, instead of atool-specific JSON file.
Two things to settle if evidence gets embedded
Both measured rather than guessed.
An embedded record describes the pre-pack wheel. I simulated the flow on pyzmq: scan,
write the record into
.dist-info,wheel pack --build-number 0, rescan.binaries,findingsandverdictcome back identical. What changes is identity:filename(buildtag),
sha256,size_bytes,record_entries,total_uncompressed_bytes. So an embeddeddocument cannot carry the final wheel's digest. It either names its input explicitly or
leaves identity out, and a sidecar written after packing is what carries the digest. That
is the same split #1238 proposes for attestations, and I would keep it: evidence inside
the wheel, digest-bearing record beside it.
An SBOM directory is a statement, not a scratchpad. My scanner treats a component
declared in
.dist-info/sboms/as the strongest evidence available, on the grounds that abuild tool wrote it rather than anyone inferring it. Write inferred evidence there in
CycloneDX form and the next scan reads its own guess back as a declaration. Anything
embedded needs a tool marker that readers skip. Related, and my bug to fix rather than
yours: fromager's SPDX document uses
packages, which CycloneDX-only readers (mineincluded, today) skip in silence. Worth knowing for anyone who assumes an SBOM is an SBOM.
Next steps
wheel_contentssounds right, I will open a PR with the hook, docs and tests. Tell mewhat you want it called and whether the signature above is the shape you would take.
separate package. Happy to put it under
python-wheel-buildinstead if you would ratherit lived near fromager.
reasons above, but the sidecar alone is genuinely useful and costs you nothing.