Skip to content

🐛 Route Goose TLS and certificate generation through system OpenSSL - #19

Closed
djzager wants to merge 1 commit into
mainfrom
fix/goose-system-crypto
Closed

djzager wants to merge 1 commit into
mainfrom
fix/goose-system-crypto

Conversation

@djzager

@djzager djzager commented Sep 28, 2026 •

Copy link
Copy Markdown

Why

The MTA 8.3 release workflow runs an openshift/check-payload FIPS scan that doesn't run against stage builds. It rejects a Rust binary that compiles crypto into itself, detecting that from defined ELF symbols:

Prefix Backend
ring_core_ / GFp_ ring
aws_lc_ / AWSLC_ aws-lc-rs
BORINGSSL_ boring
OPENSSL_ statically vendored OpenSSL

A binary with none of those that lists libcrypto in DT_NEEDED passes. (Only defined symbols count — a dynamically linked system libcrypto leaves OPENSSL_* undefined, which the scan skips.)

Stock Goose fails on both counts: the default rustls-tls feature pulls in aws-lc-rs, and rcgen links ring to generate the self-signed ACP transport certificate.

What changed

Three things, and they are all load-bearing:

  1. Build goose-cli with --no-default-features --features native-tls,aws-providers,disable-update — the TLS stack becomes OpenSSL instead of rustls.
  2. Drop rcgen from the native-tls feature and build the self-signed certificate with OpenSSL instead. Without this, enabling native-tls still drags ring in through rcgen, whose only crypto backends are ring and aws-lc-rs.
  3. Set OPENSSL_NO_VENDOR=1 so openssl-sys can't fall back to openssl-src, which would compile OpenSSL in and define the OPENSSL_* symbols the scan rejects.

Two assertions run inside the build, because together they are the scan's pass condition:

  • verify-dependencies.sh fails if the resolved graph contains rustls, ring, aws-lc-rs, aws-lc-sys, aws-lc-fips-sys or openssl-src, or if it stops containing openssl/openssl-sys.
  • ldd target/release/goose | grep -q 'libcrypto.so.3' fails if the finished binary doesn't actually link the system library.

Also:

  • openssl-fips-provider declared in the runtime dnf install. UBI9 and UBI10 both already ship it along with /usr/lib64/ossl-modules/fips.so — verified directly against both images — so this changes nothing today. It's named so a future base-image swap can't silently drop the FIPS module, matching what 🌱 Build Go binaries FIPS-compliant #20 did for ubi-minimal.
  • CI cache key fixed. The action derived its key from ARG GOOSE_VERSION, which went away when goose moved to a submodule, so that step fails on main today. cache-key.sh keys on the submodule revision, the Containerfile goose build region, and the patch files instead — and deliberately excludes README.md and itself, so a typo fix doesn't discard a 35-minute compile.
  • e2e submodule checkout. The job that compiles goose checked out without submodules, so images/agent-base/goose-src was empty and git apply failed with crates/goose/Cargo.toml: No such file or directory. Set submodules: true there, and the goose-image action now checks the submodule is populated before starting a build, so the next workflow to miss this gets told what's wrong instead of a confusing error from inside the container.
  • Makefile guard. $(shell) swallows a non-zero exit, which tagged the image goose-dist: and silently reused the wrong binary. The $(error) sits in the recursively expanded GOOSE_DIST_IMG, so it fires only when a goose build needs the tag — make help still works.

Patch size

The Goose patch went from 1167 lines across 13 files to 158 lines across 2 files (crates/goose/Cargo.toml [features] and crates/goose/src/acp/transport/tls.rs).

The entire ~900-line Cargo.lock diff is gone: the only manifest change is to a [features] table, which doesn't change dependency resolution, so cargo build --locked still holds against the unmodified lockfile. Dropped along with it: the oauth2 and aws-sigv4 crate patches, crypto.rs, the jsonwebtoken-openssl swap, and the PKCE/nanoid/rand conversions across nine files. None of those were reachable by the scan — check-payload only consults its crate-name denylist (sha2, hmac, rsa, …) through a cargo auditable SBOM, which this binary doesn't embed.

apply.sh is gone too. With no crates to stage there's no curl, no checksum pinning and no network in the patch step — the Containerfile just runs git apply.

Verification

  • cargo tree --locked --target x86_64-unknown-linux-gnu -p goose-cli --no-default-features --features native-tls,aws-providers,disable-update: 619 crates, zero denied crypto crates, openssl + openssl-sys present, and --locked holds.
  • Negative test: the unpatched tree fails verify-dependencies.sh with Unexpected crypto dependency: ring. The guardrail is real, not decorative.
  • cargo check --locked -p goose --no-default-features --features native-tls --all-targets passes.
  • New test native_crypto_certificate_matches_key_and_localhost passes: the OpenSSL path produces a P-256 / ECDSA-SHA256 self-signed cert that verifies against its own key, with localhost and 127.0.0.1 SANs — the same certificate rcgen was producing.
  • Patch applies clean to a pristine submodule; the submodule itself is left unmodified.
  • Cache key verified in both directions: editing README.md leaves it unchanged, editing the patch changes it.

Scope

This makes the binary carry no crypto of its own and routes TLS and certificate generation through system OpenSSL. It is not an audit of every cryptographic call site. Goose still compiles pure-Rust sha2/hmac for OAuth PKCE, pairing codes and similar. Those define no symbol the scan looks for, and the crate-name denylist that would catch them is only reachable via an embedded SBOM this binary doesn't have. If Goose gains one, or the scan grows a source-level check, those call sites are the next thing to move.

The reasoning above is written up in images/agent-base/patches/README.md so the next person to bump the submodule knows which parts are load-bearing.

@coderabbitai

coderabbitai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 1942e2d0-f028-4c8a-96e2-3a5e28e6fc41


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@djzager
djzager force-pushed the fix/goose-system-crypto branch from bceb3a1 to 03130b1 Compare September 28, 2026 23:12
@djzager
djzager marked this pull request as ready for review September 28, 2026 23:13
@djzager djzager changed the title 🐛 Route Goose authentication and TLS through system OpenSSL 🐛 Route Goose TLS and certificate generation through system OpenSSL Sep 28, 2026
The release FIPS scan (openshift/check-payload) rejects a Rust binary that
compiles crypto into itself. It detects that from defined ELF symbols:
ring_core_/GFp_ (ring), aws_lc_/AWSLC_ (aws-lc-rs), BORINGSSL_ (boring) and
OPENSSL_ (statically vendored OpenSSL). A binary with none of those that
links libcrypto dynamically passes.

Stock Goose fails on both counts: the default rustls-tls feature pulls in
aws-lc-rs, and rcgen links ring to generate the self-signed ACP transport
certificate.

Build goose-cli with --no-default-features --features
native-tls,aws-providers,disable-update so the TLS stack is OpenSSL rather
than rustls, drop rcgen from the native-tls feature and build the self-signed
certificate with OpenSSL instead, and set OPENSSL_NO_VENDOR=1 so openssl-sys
cannot fall back to compiling its own OpenSSL. verify-dependencies.sh asserts
the resolved graph keeps those crates out, and the ldd check asserts the
finished binary really does link the system library. Both run inside the build
because together they are the scan's pass condition.

The patch touches only the [features] table and acp/transport/tls.rs, so
dependency resolution is unchanged and Cargo.lock needs no edit: the build
stays --locked and needs no network beyond the normal crate fetch.

This moves TLS and certificate generation onto system OpenSSL and leaves the
binary carrying no crypto of its own. It is not an audit of every call site --
Goose still compiles sha2 and hmac for OAuth PKCE and pairing codes. Those
define no scanned symbol, and check-payload only reaches its crate-name
denylist through a cargo-auditable SBOM that this binary does not embed.

Also declare openssl-fips-provider in the runtime install. UBI9 and UBI10 both
already ship it, but naming it keeps a future base-image change from silently
dropping the FIPS module.

Finally, replace the CI cache key's ARG GOOSE_VERSION lookup with
cache-key.sh. That ARG went away when goose moved to a submodule, so the step
reading it fails today; the script keys on the submodule revision, the
Containerfile goose build region and the patch files instead.

The e2e job that compiles goose checked out without submodules, so
images/agent-base/goose-src was empty and the new git apply step failed with
"crates/goose/Cargo.toml: No such file or directory". Set submodules: true on
that checkout, and have the goose-image action check the submodule is
populated before it starts a build, so the next workflow to miss this gets
told what is wrong instead of a confusing error from inside the container.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: David Zager <david.j.zager@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants