diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b1098dcdc..a53035a7d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -100,6 +100,89 @@ jobs: npx doctoc README.md [[ -z $(git status --porcelain README.md) ]] || (git diff README.md; echo "README changed"; exit 1) + # Verify the optional FIPS build: the Rust core must link aws-lc-fips-sys + # (aws-lc-rs FIPS mode) and must NOT link `ring` (the cargo-tree guard, ported + # from sdk-ruby PR #466's `fips_tree` guard); then run the test suite against the + # FIPS binary and build the release wheel. + fips-build: + timeout-minutes: 45 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + submodules: recursive + - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: "3.10" + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + with: + workspaces: temporalio/bridge -> target + key: fips-${{ env.pythonLocation }} + # aws-lc-fips-sys builds the validated AWS-LC module from source, which + # needs Go, CMake, Perl and a C compiler (see the FIPS Compliance section + # in the README). + - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5 + with: + go-version: "1.24" + - name: Verify FIPS linkage (aws-lc-fips-sys present, ring absent) + working-directory: temporalio/bridge + run: | + set -uo pipefail + # Resolve the FIPS build's dependency graph once and query it for a + # single crate. `cargo tree -i ` ("invert") prints the chain of + # packages that pull in , or nothing if is not in the + # graph at all. We capture that output and decide PRESENT/ABSENT by + # whether the string is empty -- NOT by the exit code, because + # `cargo tree -i` exits 0 either way (an absent crate just prints + # "nothing to print" to stderr). Ported from sdk-ruby PR #466. + links_in_fips_build() { + cargo tree -p temporal-sdk-bridge --no-default-features --features fips -i "$1" 2>/dev/null + } + echo "== aws-lc-fips-sys must be PRESENT ==" + aws_lc_fips="$(links_in_fips_build aws-lc-fips-sys)" + if [ -z "$aws_lc_fips" ]; then + echo "ERROR: aws-lc-fips-sys is absent from the FIPS dependency tree" >&2 + exit 1 + fi + echo "$aws_lc_fips" + echo "== ring must be ABSENT ==" + ring="$(links_in_fips_build ring)" + if [ -n "$ring" ]; then + echo "ERROR: 'ring' is still linked in the FIPS build" >&2 + echo "$ring" >&2 + exit 1 + fi + echo "FIPS linkage verified: aws-lc-fips-sys linked, ring absent." + - uses: arduino/setup-protoc@c65c819552d16ad3c9b72d9dfd5ba5237b9c906b # v3 + with: + version: "23.x" + repo-token: ${{ secrets.GITHUB_TOKEN }} + - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8 + - run: uv tool install poethepoet + - run: uv sync --all-extras + # Develop build so the FIPS extension is importable, then run the suite + # against it to confirm the aws-lc-rs stack works end to end (not just links). + - run: poe build-develop-fips + - name: Confirm the FIPS build is loaded + run: uv run python -c "from temporalio.bridge import temporal_sdk_bridge; assert temporal_sdk_bridge.FIPS, 'not a FIPS build'" + - run: mkdir junit-xml + - name: Run tests against the FIPS build + run: | + poe test --reruns 3 --only-rerun "RuntimeError: Failed validating workflow" -s --junit-xml=junit-xml/fips.xml + timeout-minutes: 15 + - name: "Upload junit-xml artifacts" + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + if: always() + with: + name: junit-xml--${{github.run_id}}--${{github.run_attempt}}--fips + path: junit-xml + retention-days: 14 + - name: Build FIPS wheel (proves the release feature set compiles) + run: uv run maturin build --release --no-default-features --features fips + env: + TEMPORALIO_FIPS: "1" + alpine-package-test: timeout-minutes: 60 strategy: diff --git a/README.md b/README.md index bd3ac195c..7a1caedd9 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,7 @@ informal introduction to the features and their implementation. - [Prepare](#prepare) - [Build](#build) - [Use](#use) + - [FIPS Compliance (Experimental)](#fips-compliance-experimental) - [Local SDK development environment](#local-sdk-development-environment) - [Testing](#testing-2) - [Proto Generation and Testing](#proto-generation-and-testing) @@ -2112,6 +2113,50 @@ It should output: Result: Hello, Temporal! +#### FIPS Compliance (Experimental) + +> **NOTE**: FIPS support is **experimental**. It is opt-in, source-build only, and currently exercised +> on Linux only. This build wires the TLS/gRPC cryptography through a FIPS-validated module; it is +> **not** a claim that the SDK has passed a FIPS compliance audit or certification. + +FIPS 140-3 compliant cryptography is available as an **opt-in source build**. The published wheels are +**not** FIPS compliant — they use the [`ring`](https://github.com/briansmith/ring) backend, which is not +FIPS-validated. Because the crypto backend is chosen at compile time, FIPS cannot be enabled on a +precompiled wheel: you must build the native extension yourself with `TEMPORALIO_FIPS=1`. When set, the +build selects [`aws-lc-rs`](https://github.com/aws/aws-lc-rs) in FIPS mode (AWS-LC's FIPS 140-3 module) +for the gRPC client (and the OTLP metric exporter, when enabled), in place of `ring`. + +Building `aws-lc-rs` in FIPS mode compiles AWS-LC from source, so in addition to the +[Prepare](#prepare) prerequisites it requires **Go**, **CMake**, **Perl**, and a **C compiler**. + +To produce an installable FIPS wheel: + +```bash +TEMPORALIO_FIPS=1 uv run maturin build --release --no-default-features --features fips +``` + +or, equivalently, the provided task: + +```bash +poe build-wheel-fips +``` + +For a local develop build, use `poe build-develop-fips`. You can confirm at runtime that a FIPS build is +loaded: + +```python +from temporalio.bridge import temporal_sdk_bridge +assert temporal_sdk_bridge.FIPS +``` + +> **NOTE**: When a `Worker` or `Replayer` is created without a `build_id` (or `deployment_config`), the +> SDK derives a default build ID by hashing loaded module bytecode with MD5 (via +> `hashlib.md5(usedforsecurity=False)`). Although md5 is among Python's +> [guaranteed hash algorithms](https://docs.python.org/3/library/hashlib.html#hashlib.algorithms_guaranteed), +> some vendors ship "FIPS" Python builds that remove it entirely — on such an interpreter this call +> raises. If you run on one, pass an explicit `build_id` (directly or inside `deployment_config`) so the +> default MD5-based path is not used. + ### Local SDK development environment For local development, it is quicker to use a debug build. diff --git a/pyproject.toml b/pyproject.toml index ab9f2638e..d5de4c077 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,6 +105,9 @@ dev = [ [tool.poe.tasks] build-develop = "uv run maturin develop --uv" build-develop-with-release = { cmd = "uv run maturin develop --release --uv" } +# FIPS builds: swap the rustls stack onto aws-lc-rs FIPS mode (aws-lc-fips-sys), eliminating `ring`. +build-develop-fips = { cmd = "uv run maturin develop --uv --no-default-features --features fips", env = { TEMPORALIO_FIPS = "1" } } +build-wheel-fips = { cmd = "uv run maturin build --release --no-default-features --features fips", env = { TEMPORALIO_FIPS = "1" } } format = [ { cmd = "uv run ruff check --select I --fix" }, { cmd = "uv run ruff format" }, diff --git a/temporalio/bridge/Cargo.lock b/temporalio/bridge/Cargo.lock index 97a247eb4..0c00e8ec5 100644 --- a/temporalio/bridge/Cargo.lock +++ b/temporalio/bridge/Cargo.lock @@ -58,6 +58,46 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "aws-lc-fips-sys" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "118303cd75f63d1933a90c2ceb7e697281ac6acbdbcc490b46419f25a527ab90" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", + "regex", +] + +[[package]] +name = "aws-lc-rs" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" +dependencies = [ + "aws-lc-fips-sys", + "aws-lc-sys", + "untrusted 0.7.1", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + [[package]] name = "axum" version = "0.8.9" @@ -116,6 +156,26 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex 1.3.0", + "syn 2.0.119", +] + [[package]] name = "bitflags" version = "2.13.1" @@ -177,7 +237,16 @@ dependencies = [ "find-msvc-tools", "jobserver", "libc", - "shlex", + "shlex 2.0.1", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", ] [[package]] @@ -186,6 +255,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + [[package]] name = "chacha20" version = "0.10.1" @@ -207,6 +282,26 @@ dependencies = [ "serde", ] +[[package]] +name = "clang-sys" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + [[package]] name = "combine" version = "4.6.7" @@ -370,6 +465,12 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "dyn-clone" version = "1.0.20" @@ -516,6 +617,12 @@ dependencies = [ "futures-core", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures" version = "0.3.33" @@ -632,8 +739,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -655,11 +764,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", "rand_core 0.10.1", + "wasm-bindgen", ] +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + [[package]] name = "h2" version = "0.4.15" @@ -957,6 +1074,15 @@ version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.14.0" @@ -1060,6 +1186,16 @@ version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + [[package]] name = "libredox" version = "0.1.18" @@ -1105,6 +1241,12 @@ dependencies = [ "hashbrown 0.17.1", ] +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "matchers" version = "0.2.0" @@ -1132,6 +1274,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1185,6 +1333,16 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "ntapi" version = "0.4.3" @@ -1365,7 +1523,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ed4d5c6ae95e08ac768883c8401cf0e8deb4e6e1d6a4e1fd3d2ec4f0ec63200" dependencies = [ "heck", - "itertools", + "itertools 0.14.0", "prost", "prost-types", ] @@ -1537,7 +1695,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042" dependencies = [ "heck", - "itertools", + "itertools 0.14.0", "log", "multimap", "petgraph", @@ -1558,7 +1716,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", - "itertools", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.119", @@ -1722,6 +1880,63 @@ dependencies = [ "serde", ] +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.2", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + [[package]] name = "quote" version = "1.0.47" @@ -1789,6 +2004,15 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1859,6 +2083,7 @@ dependencies = [ "log", "percent-encoding", "pin-project-lite", + "quinn", "rustls", "rustls-pki-types", "rustls-platform-verifier", @@ -1889,7 +2114,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.17", "libc", - "untrusted", + "untrusted 0.9.0", "windows-sys 0.52.0", ] @@ -1904,6 +2129,12 @@ dependencies = [ "portable-atomic-util", ] +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + [[package]] name = "rustc_version" version = "0.4.1" @@ -1932,6 +2163,7 @@ version = "0.23.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" dependencies = [ + "aws-lc-rs", "log", "once_cell", "ring", @@ -1959,6 +2191,7 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ + "web-time", "zeroize", ] @@ -1995,9 +2228,10 @@ version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", - "untrusted", + "untrusted 0.9.0", ] [[package]] @@ -2138,6 +2372,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "shlex" version = "2.0.1" @@ -2323,6 +2563,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "aws-lc-rs", "futures", "prost", "pyo3", @@ -2484,7 +2725,7 @@ dependencies = [ "futures", "futures-util", "gethostname", - "itertools", + "itertools 0.14.0", "lru", "mockall", "opentelemetry-otlp", @@ -2560,6 +2801,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.53.1" @@ -2908,6 +3164,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "untrusted" version = "0.9.0" @@ -3065,6 +3327,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webpki-root-certs" version = "1.0.9" diff --git a/temporalio/bridge/Cargo.toml b/temporalio/bridge/Cargo.toml index 4cf6a02fe..b9391a0bb 100644 --- a/temporalio/bridge/Cargo.toml +++ b/temporalio/bridge/Cargo.toml @@ -15,9 +15,36 @@ module-name = "temporalio.bridge.temporal_sdk_bridge" name = "temporal_sdk_bridge" crate-type = ["cdylib"] +[features] +# Default builds keep the historical `ring` rustls provider (unchanged behavior). +# A FIPS build — `maturin build --no-default-features --features fips` +# (TEMPORALIO_FIPS=1) — swaps the entire rustls stack onto aws-lc-rs in FIPS +# mode (aws-lc-fips-sys) and eliminates `ring`: the transitive core/client/common +# TLS *and* this crate's own tokio-rustls use in client.rs. Ported from +# temporalio/sdk-ruby PR #466. +# The extra `tokio-rustls/*` entry is sdk-python-specific: the bridge has its own +# direct tokio-rustls dep (client.rs custom verifier) that Ruby lacked, so its +# provider must track the build too — else `cargo tree -i ring` would still find ring. +default = ["tls-ring"] +tls-ring = [ + "temporalio-sdk-core/tls-ring", + "temporalio-client/tls-ring", + "tokio-rustls/ring", +] +tls-aws-lc = [ + "temporalio-sdk-core/tls-aws-lc", + "temporalio-client/tls-aws-lc", + "tokio-rustls/aws_lc_rs", +] +fips = ["tls-aws-lc", "dep:aws-lc-rs"] + [dependencies] anyhow = "1.0" async-trait = "0.1" +# Only compiled under `fips`. Not called directly — its presence flips the +# shared aws-lc-rs crate (rustls' aws_lc_rs provider) into FIPS mode, linking +# aws-lc-fips-sys instead of aws-lc-sys. Ported from sdk-ruby PR #466. +aws-lc-rs = { version = "1", features = ["fips"], optional = true } futures = "0.3" prost = "0.14" pyo3 = { version = "0.29", features = [ @@ -28,17 +55,29 @@ pyo3 = { version = "0.29", features = [ ] } pyo3-async-runtimes = { version = "0.29", features = ["tokio-runtime"] } pythonize = "0.29" -temporalio-client = { version = "0.6", path = "./sdk-core/crates/client" } +# default-features disabled so the rustls provider isn't pinned to `tls-ring`; +# the bridge's tls-ring/tls-aws-lc features (above) select it. Re-add the +# crate's non-TLS default (`envconfig`). +temporalio-client = { version = "0.6", path = "./sdk-core/crates/client", default-features = false, features = [ + "envconfig", +] } temporalio-common = { version = "0.6", path = "./sdk-core/crates/common", features = [ "envconfig", "otel" ]} -temporalio-sdk-core = { version = "0.6", path = "./sdk-core/crates/sdk-core", features = [ +# default-features disabled (drops the pinned `tls-ring`); re-add the non-TLS +# defaults the bridge relied on (`envconfig`, `prometheus`) plus `ephemeral-server`. +temporalio-sdk-core = { version = "0.6", path = "./sdk-core/crates/sdk-core", default-features = false, features = [ "ephemeral-server", + "envconfig", + "prometheus", ] } tokio = "1.26" # Matches the sdk-core client crate's rustls stack; used to build the custom # server certificate verifier for ClientTlsConfig.verification_server_name. -tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] } +# Provider is NOT pinned here — the bridge's tls-ring/tls-aws-lc features select +# `ring` (default) or `aws_lc_rs` (fips), so this crate's direct rustls use in +# client.rs tracks the rest of the stack and the `cargo tree -i ring` FIPS gate passes. +tokio-rustls = { version = "0.26", default-features = false } tokio-stream = "0.1" tonic = "0.14" tracing = "0.1" diff --git a/temporalio/bridge/src/client.rs b/temporalio/bridge/src/client.rs index aec5c0ef8..c154d6a58 100644 --- a/temporalio/bridge/src/client.rs +++ b/temporalio/bridge/src/client.rs @@ -366,12 +366,23 @@ fn fixed_server_name_verifier( // Root loading and provider selection mirror tonic's default (no custom // verifier) client path: unparsable certificates in the bundle are // skipped, and the provider is the process default if one is installed, - // else ring, as with the connection's `tls-ring` feature. + // else the build's compiled-in provider. Under a `fips` build lib.rs + // installs aws-lc-rs (FIPS) as the process default, so `get_default()` + // returns it and this fallback is not taken; the fallback is still made + // `cfg`-conditional because the `ring` module is absent in a FIPS build + // (`tokio-rustls/ring` off) and would otherwise fail to compile. let mut roots = RootCertStore::empty(); roots.add_parsable_certificates(certs); - let provider = CryptoProvider::get_default() - .cloned() - .unwrap_or_else(|| Arc::new(rustls::crypto::ring::default_provider())); + let provider = CryptoProvider::get_default().cloned().unwrap_or_else(|| { + #[cfg(feature = "fips")] + { + Arc::new(rustls::crypto::aws_lc_rs::default_provider()) + } + #[cfg(not(feature = "fips"))] + { + Arc::new(rustls::crypto::ring::default_provider()) + } + }); let inner = WebPkiServerVerifier::builder_with_provider(roots.into(), provider) .build() .map_err(|err| { diff --git a/temporalio/bridge/src/lib.rs b/temporalio/bridge/src/lib.rs index ee157fb18..a4fda26db 100644 --- a/temporalio/bridge/src/lib.rs +++ b/temporalio/bridge/src/lib.rs @@ -11,6 +11,19 @@ mod worker; #[pymodule] fn temporal_sdk_bridge(py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { + // Lets ops assert at runtime that the FIPS wheel is actually loaded, e.g. + // `temporalio.bridge.temporal_sdk_bridge.FIPS is True`. Mirrors sdk-ruby PR #466. + m.add("FIPS", cfg!(feature = "fips"))?; + + // Under a FIPS build, install aws-lc-rs (FIPS mode) as the process-wide + // rustls provider before any client is constructed. This makes + // CryptoProvider::get_default() resolve to aws-lc-rs everywhere (including + // the custom verifier in client.rs), so no `ring` provider is ever used. + #[cfg(feature = "fips")] + { + let _ = tokio_rustls::rustls::crypto::aws_lc_rs::default_provider().install_default(); + } + // Client stuff m.add("RPCError", py.get_type::())?; m.add_class::()?;