---
status: accepted
date: 2026-05-13
decision-makers: Constructor Fabric Steering Committee
---

# Target-conditional `fips` feature activation via empty shim crate

**ID**: `cpt-toolkit-adr-fips-feature-target-conditional-shim`

## Table of Contents

<!-- generated by `cypilot toc` -->

## Context and Problem Statement

The user-facing `fips` Cargo feature needs to activate **different** transitive features per target:

* On non-macOS (Linux today, Windows later): activate `rustls/fips` and `hyper-rustls/fips` so the AWS-LC FIPS provider (`aws-lc-fips-sys`) is pulled in.
* On macOS: do **NOT** activate `rustls/fips` — Apple corecrypto is the FIPS provider there (see [`0001-macos-fips-via-corecrypto-provider`](0001-macos-fips-via-corecrypto-provider.md)), and `aws-lc-fips-sys` would be dead-but-linked, bloating the binary by ~3 MB and creating a misleading audit picture.

Cargo features defined in `[features]` are **global and target-agnostic**: there is no syntax for "activate `rustls/fips` only when `target_os != "macos"`". Cargo also **forbids two aliases for the same package** inside one crate, ruling out a direct `[target.'cfg(...)'.dependencies] rustls-fips-toggle = { package = "rustls", features = ["fips"] }` next to the existing `rustls = { workspace = true }`.

## Decision Drivers

* Single user-facing flag: `cargo build --features fips` must do the right thing on every OS — no `--features fips-linux` vs `--features fips-macos` matrix.
* No `aws-lc-fips-sys` dylib in macOS+fips binaries (linkage must be Apple-frameworks-only).
* No workspace-wide refactor of every consumer of `rustls` / `hyper-rustls`.
* Maintain compatibility with current Cargo (no nightly / unstable features).

## Considered Options

* Option A — Separate features per target (`fips-linux`, `fips-macos`).
* Option B — Empty shim crate with target-gated transitive deps (this ADR).
* Option C — Accept "dead-but-linked" `aws-lc-fips-sys` on macOS+fips (status quo before cleanup).
* Option D — `xtask` / `cargo-make` driver that picks features per target outside Cargo.
* Option E — Drop `rustls` / `hyper-rustls` from `[workspace.dependencies]` and have every consumer declare them per-target with the right features.

## Decision Outcome

Chosen option: **Option B — empty shim crate `cf-gears-rustls-fips-shim`**.

The shim's only purpose is to encode "pull `rustls` with the `fips` feature, but only on non-macOS targets" in a way Cargo accepts. Its `Cargo.toml` has a single target-conditional `[dependencies]` block; its `lib.rs` is empty. `toolkit` and `toolkit-http` activate `dep:rustls-fips-shim` from their `fips` feature; Cargo's feature unification then OR's `rustls/fips` into the resolved feature set on non-macOS only.

Justification:

* Preserves a clean single-flag user-facing API (`--features fips`).
* Minimal blast radius: ~30 lines of manifest + an empty `lib.rs`, runtime cost is zero.
* No workspace refactor — `rustls = { workspace = true }` stays in every consumer crate unchanged.
* Intent is local: anyone reading `cf-gears-rustls-fips-shim/Cargo.toml` sees the trick and its rationale in one place.
* The shim is trivially deletable if Cargo ever adds native target-conditional feature activation.

### Consequences

* One additional workspace member (`libs/rustls-fips-shim`) with zero runtime impact.
* `toolkit::fips` and `toolkit-http::fips` features reference `dep:rustls-fips-shim` rather than `rustls/fips` directly. New consumers of the FIPS chain follow the same pattern.
* `cargo tree -p cf-gears-example-server --features fips,mini-chat` on macOS must show **no** `aws-lc-fips-sys` in the graph — this is now a regression check.
* When Cargo eventually supports target-conditional feature activation, the shim can be removed and replaced with a single feature expression like `"cfg(not(target_os = \"macos\")): rustls/fips"`.

### Confirmation

* `cargo tree -p cf-gears-example-server --features fips,mini-chat -e features 2>&1 | grep aws-lc-fips` returns empty on macOS, non-empty on Linux.
* `otool -L target/debug/cf-gears-example-server` on macOS+fips shows no `libaws_lc_fips_*.dylib`.
* `cargo build` matrix passes on all four combinations: default, `--features mini-chat`, `--features fips`, `--features fips,mini-chat`.

## Pros and Cons of the Options

### Option A — Separate features per target

* Good, because explicit; no Cargo trickery.
* Good, because well-precedented in the Rust ecosystem (many crates do this).
* Bad, because the user has to know which feature to pick per OS — error-prone, especially in CI matrices.
* Bad, because `--all-features` becomes meaningless / broken.
* Bad, because cross-OS code paths that depend on "the FIPS feature" need cfg-cascades to map back to a unified meaning.

### Option B — Empty shim crate (chosen)

* Good, because a single `--features fips` does the right thing on every OS.
* Good, because the trick is fully localized to one crate with explicit comments.
* Good, because zero runtime cost.
* Good, because reversible — when Cargo supports native target-conditional features, delete the shim and one feature line.
* Neutral, because adds one workspace member.
* Bad, because requires understanding of Cargo feature unification to read; "magic" if not commented.

### Option C — Accept dead-but-linked `aws-lc-fips-sys`

* Good, because zero implementation cost.
* Bad, because adds ~3 MB to macOS binaries for code never executed.
* Bad, because forces an rpath workaround (build.rs in every binary crate) so dyld can find the dylib.
* Bad, because creates a confusing audit picture: an unrelated FIPS-validated module sits next to the actually-used Apple `corecrypto`.

### Option D — `xtask` / `cargo-make` driver

* Good, because gives unlimited flexibility (arbitrary per-target build logic).
* Bad, because `cargo build` / `cargo run` no longer "just work"; everyone must learn the wrapper.
* Bad, because tooling integration (IDEs, `cargo metadata` consumers, CI templates) breaks or needs separate configuration.
* Bad, because pushes complexity outside Cargo's own resolver — harder to reason about reproducible builds.

### Option E — Drop `rustls` from `[workspace.dependencies]` and per-crate per-target declarations

* Good, because gives each consumer full control over its `rustls` / `hyper-rustls` feature set per target.
* Bad, because every crate that touches `rustls` must be updated; large, repetitive workspace churn.
* Bad, because dropping a workspace dep removes the central pin — future version bumps now require touching N crates instead of one.
* Bad, because feature unification still applies — a single mis-declared consumer can re-pull `rustls/fips` on macOS, silently regressing the cleanup.

## More Information

* Crate: [`libs/rustls-fips-shim`](../../../../libs/rustls-fips-shim) — see its `Cargo.toml` for the target-gated `[dependencies]` block.
* Cargo issues tracking native target-conditional feature activation: `rust-lang/cargo#1197`, `rust-lang/cargo#5777` (longstanding, no fix landed).
* Related: [`0001-macos-fips-via-corecrypto-provider`](0001-macos-fips-via-corecrypto-provider.md) — the broader FIPS-on-macOS architecture this shim plugs into.

## Traceability

- **DESIGN**: [`libs/toolkit/Cargo.toml`](../../../../libs/toolkit/Cargo.toml), [`libs/toolkit-http/Cargo.toml`](../../../../libs/toolkit-http/Cargo.toml), [`libs/rustls-fips-shim/Cargo.toml`](../../../../libs/rustls-fips-shim/Cargo.toml).
- **Related ADR**: [`0001-macos-fips-via-corecrypto-provider`](0001-macos-fips-via-corecrypto-provider.md).

This decision directly addresses:

* `cpt-toolkit-nfr-fips-cross-os` — Single FIPS feature flag works correctly on every supported OS.
* `cpt-toolkit-design-binary-hygiene` — macOS+fips binaries link only Apple frameworks (no dead FIPS dylib).
