Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ panic = "unwind"
[workspace.package]
version = "1.29.1"
edition = "2024"
rust-version = "1.93"
# Kept at the floor mise can build with (its distro packaging pins the
# toolchain) so mise can embed the library crates. CI enforces this via
# `cargo msrv verify` — bump deliberately, not as a side effect.
rust-version = "1.91"
license = "MIT"
repository = "https://github.com/jdx/aube"
homepage = "https://github.com/jdx/aube"
Expand Down Expand Up @@ -72,7 +75,13 @@ clap = { version = "4", features = ["derive"] }
strum = { version = "0.28", features = ["derive"] }

# HTTP
reqwest = { version = "0.13", default-features = false, features = ["json", "stream", "rustls", "http2", "charset", "system-proxy", "gzip", "brotli", "zstd", "hickory-dns"] }
# TLS backend and DNS resolver are deliberately NOT chosen here. Library
# crates must stay backend-agnostic so embedders (mise) don't get rustls or
# hickory-dns forced into their build via feature unification — reqwest's
# hickory-dns cargo feature flips the default resolver for every client in
# the final binary. The aube binary gets rustls + hickory-dns through
# aube-registry's default features.
reqwest = { version = "0.13", default-features = false, features = ["json", "stream", "http2", "charset", "system-proxy", "gzip", "brotli", "zstd"] }
webpki-root-certs = "1"

# Hashing
Expand Down Expand Up @@ -153,7 +162,11 @@ log = "0.4"
regex = "1"

# Archive extraction
flate2 = { version = "1", default-features = false, features = ["zlib-ng"] }
# Pure-Rust backend here so library crates don't force libz-ng-sys (cmake +
# C toolchain) onto embedders' builds. The aube binary re-enables zlib-ng —
# feature unification makes it win over rust_backend for the whole workspace
# build, so standalone aube keeps the fast C backend.
flate2 = { version = "1", default-features = false, features = ["rust_backend"] }
tar = "0.4"
zstd = { version = "0.13", default-features = false }
# Zip reader for the OSV advisory mirror — the bulk dump at
Expand Down
12 changes: 12 additions & 0 deletions crates/aube-registry/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ tokio = { workspace = true }
tracing = { workspace = true }
zip = { workspace = true }

[features]
# Defaults preserve standalone aube behavior: rustls + in-process DNS
# caching. Embedders (mise) use `default-features = false` and re-enable
# `rustls` so hickory-dns doesn't leak into their build via feature
# unification — reqwest's hickory-dns cargo feature flips the default
# resolver for every client in the final binary. rustls is the only
# supported TLS backend (no OpenSSL / native-tls — see CLAUDE.md) and is
# required; without it the crate fails to compile.
default = ["rustls", "hickory-dns"]
rustls = ["reqwest/rustls", "aube-util/rustls"]
hickory-dns = ["reqwest/hickory-dns"]

[dev-dependencies]
tempfile = "3"
flate2 = { workspace = true }
Expand Down
44 changes: 32 additions & 12 deletions crates/aube-registry/src/client/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,13 @@ fn build_http_client_inner(
std::env::consts::ARCH
)
});
// Pin rustls explicitly instead of relying on reqwest's default backend
// selection: if an embedder's dependency graph also turns on reqwest's
// native-tls feature, reqwest's default silently becomes native-tls,
// which would mismatch the rustls `Identity` built below and the
// session-ticket wiring in the aube binary.
let mut builder = aube_util::http::with_webpki_root_fallback(reqwest::Client::builder())
.use_rustls_tls()
.user_agent(user_agent)
// Wire-level decompression for packument JSON. Tarball
// requests explicitly send `Accept-Encoding: identity`
Expand Down Expand Up @@ -188,13 +194,19 @@ fn build_http_client_inner(
} else {
builder = builder.http1_only();
}
// In-process DNS caching via hickory-dns. The system resolver
// does not cache and uses a thread pool for `getaddrinfo`,
// which serializes the first cold lookup per origin. hickory
// resolves async + caches for the process lifetime. Feature-gated
// so embedders that keep their host binary on getaddrinfo (reqwest's
// hickory-dns cargo feature flips the default for every client in
// the binary) can opt out; default features keep it on for aube.
#[cfg(feature = "hickory-dns")]
{
builder = builder.hickory_dns(true);
}
builder = builder
.tcp_keepalive(std::time::Duration::from_secs(60))
// In-process DNS caching via hickory-dns. The system resolver
// does not cache and uses a thread pool for `getaddrinfo`,
// which serializes the first cold lookup per origin. hickory
// resolves async + caches for the process lifetime.
.hickory_dns(true)
// `strict-ssl=false` disables cert validation entirely. This
// is a security hole on purpose: corporate registries should
// prefer per-registry `ca` / `cafile` so validation stays on.
Expand Down Expand Up @@ -291,13 +303,7 @@ fn build_http_client_inner(
"per-registry",
);
if let (Some(cert), Some(key)) = (&registry_config.tls.cert, &registry_config.tls.key) {
let mut pem = Vec::with_capacity(cert.len() + key.len() + 1);
pem.extend_from_slice(cert.as_bytes());
if !cert.ends_with('\n') {
pem.push(b'\n');
}
pem.extend_from_slice(key.as_bytes());
match reqwest::Identity::from_pem(&pem) {
match client_identity(cert, key) {
Ok(identity) => builder = builder.identity(identity),
Err(e) => tracing::warn!(
code = aube_codes::warnings::WARN_AUBE_INVALID_CLIENT_CERT,
Expand All @@ -310,6 +316,20 @@ fn build_http_client_inner(
builder.build().expect("failed to build HTTP client")
}

/// Build a client-cert identity from `.npmrc` `cert` / `key` values.
/// rustls' `Identity::from_pem` takes cert and key concatenated in a single
/// PEM buffer and accepts both PKCS#8 (`BEGIN PRIVATE KEY`) and PKCS#1
/// (`BEGIN RSA PRIVATE KEY`) keys.
fn client_identity(cert: &str, key: &str) -> reqwest::Result<reqwest::Identity> {
let mut pem = Vec::with_capacity(cert.len() + key.len() + 1);
pem.extend_from_slice(cert.as_bytes());
if !cert.ends_with('\n') {
pem.push(b'\n');
}
pem.extend_from_slice(key.as_bytes());
reqwest::Identity::from_pem(&pem)
}

/// BATS-fixture escape hatch: ask the registry for the unabbreviated
/// packument instead of the corgi (`application/vnd.npm.install-v1+json`)
/// shape. Our Verdaccio-backed fixture strips `bundledDependencies`
Expand Down
7 changes: 7 additions & 0 deletions crates/aube-registry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ use serde::{Deserialize, Deserializer, Serialize};
use std::collections::BTreeMap;
use std::fmt;

// The registry client is https-only; without a TLS backend every request
// would fail at runtime with an opaque scheme error. Fail at compile time
// instead so embedders disabling default features re-enable `rustls`
// (the only supported backend — see the TLS policy in CLAUDE.md).
#[cfg(not(feature = "rustls"))]
compile_error!("aube-registry requires the `rustls` feature");

// Visitor helper macros — each tolerant deserializer below picks the
// subset that matches its custom handlers. Splitting these granularly
// is what lets `funding_url` keep its own `visit_seq` (array case)
Expand Down
8 changes: 8 additions & 0 deletions crates/aube-util/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ reqwest = { workspace = true }
webpki-root-certs = { workspace = true }
thiserror = { workspace = true }

[features]
# No TLS backend by default so embedders control when reqwest/rustls is
# enabled; aube-registry's default features turn it on for the aube binary.
# rustls is the only supported backend (no OpenSSL / native-tls — see the
# TLS policy in CLAUDE.md).
default = []
rustls = ["reqwest/rustls"]

[target.'cfg(unix)'.dependencies]
libc = { workspace = true }

Expand Down
15 changes: 13 additions & 2 deletions crates/aube-util/src/http/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,16 @@ pub mod ticket_cache;
/// reqwest 0.13 can merge extra roots with the platform verifier on Unix
/// (except Android) and Windows. On other targets, leave the builder alone
/// so client construction does not fail at runtime.
///
/// Compiled to a no-op when the `rustls` feature is not enabled — reqwest's
/// `Certificate`/`tls_certs_merge` APIs only exist with a TLS backend, and
/// this crate leaves that choice to the final binary (the aube binary selects
/// rustls via aube-registry's defaults).
pub fn with_webpki_root_fallback(builder: reqwest::ClientBuilder) -> reqwest::ClientBuilder {
#[cfg(any(all(unix, not(target_os = "android")), target_os = "windows"))]
#[cfg(all(
feature = "rustls",
any(all(unix, not(target_os = "android")), target_os = "windows")
))]
{
let certs = webpki_root_certs::TLS_SERVER_ROOT_CERTS
.iter()
Expand All @@ -36,7 +44,10 @@ pub fn with_webpki_root_fallback(builder: reqwest::ClientBuilder) -> reqwest::Cl
builder.tls_certs_merge(certs)
}

#[cfg(not(any(all(unix, not(target_os = "android")), target_os = "windows")))]
#[cfg(not(all(
feature = "rustls",
any(all(unix, not(target_os = "android")), target_os = "windows")
)))]
{
builder
}
Expand Down
6 changes: 5 additions & 1 deletion crates/aube/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,11 @@ sigstore-types = { workspace = true }
ambient-id = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
flate2 = { workspace = true }
# zlib-ng re-enabled at the binary level only: feature unification makes the
# C backend win for the whole aube build (same perf as before), while library
# crates stay on the pure-Rust backend so embedders don't inherit a cmake
# build dependency. See the workspace flate2 entry.
flate2 = { workspace = true, features = ["zlib-ng"] }
glob = { workspace = true }
ignore = { workspace = true }
node-semver = { workspace = true }
Expand Down
Loading