Skip to content
Open
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
47 changes: 17 additions & 30 deletions src/lib/src/signature/keyless/fulcio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,11 @@ impl FulcioClient {
///
/// # Certificate Pinning
///
/// Certificate pinning is ENFORCED by default using embedded pins for Sigstore
/// production infrastructure. Custom pins can be set via `WSC_FULCIO_PINS`.
/// Set `WSC_REQUIRE_CERT_PINNING=1` to fail if pinning cannot be configured.
pub fn new() -> Self {
/// Certificate pinning is unconditionally ENFORCED using embedded pins for
/// Sigstore production infrastructure. Custom pins can be set via
/// `WSC_FULCIO_PINS`. If the pinned agent cannot be constructed, this
/// returns an error rather than silently downgrading to unpinned TLS.
pub fn new() -> Result<Self, WSError> {
Self::with_url("https://fulcio.sigstore.dev".to_string())
}

Expand All @@ -103,36 +104,28 @@ impl FulcioClient {
///
/// # Certificate Pinning
///
/// Certificate pinning is now ENFORCED when configured via `WSC_FULCIO_PINS`.
/// Set `WSC_REQUIRE_CERT_PINNING=1` to fail if pinning cannot be configured.
pub fn with_url(base_url: String) -> Self {
/// Certificate pinning is unconditionally ENFORCED. Returns an error if the
/// pinned agent cannot be constructed — there is no unpinned fallback.
pub fn with_url(base_url: String) -> Result<Self, WSError> {
#[cfg(not(target_os = "wasi"))]
{
use super::transport::create_agent_with_optional_pinning;
use super::cert_pinning::PinningConfig;

// Create pinning configuration for Fulcio
let pinning = Some(PinningConfig::fulcio());
// Certificate pinning is mandatory for Sigstore endpoints: a
// failure to build the pinned agent is a hard error, never a
// silent downgrade to unpinned TLS (audit finding C-4).
let agent = create_agent_with_optional_pinning(Some(PinningConfig::fulcio()))?;

// Create agent with certificate pinning (or fall back to standard TLS)
let agent = match create_agent_with_optional_pinning(pinning) {
Ok(agent) => agent,
Err(e) => {
// Log error but don't panic - fall back to standard agent
log::error!("Failed to create pinned agent for Fulcio: {}. Using standard TLS.", e);
super::transport::create_standard_agent()
}
};

Self {
Ok(Self {
base_url,
client: agent,
}
})
}

#[cfg(target_os = "wasi")]
{
Self { base_url }
Ok(Self { base_url })
}
}

Expand Down Expand Up @@ -458,12 +451,6 @@ impl FulcioClient {
}
}

impl Default for FulcioClient {
fn default() -> Self {
Self::new()
}
}

// Add pem crate for parsing PEM certificates
mod pem {
use crate::error::WSError;
Expand Down Expand Up @@ -518,13 +505,13 @@ mod tests {

#[test]
fn test_fulcio_client_creation() {
let client = FulcioClient::new();
let client = FulcioClient::new().unwrap();
assert_eq!(client.base_url, "https://fulcio.sigstore.dev");
}

#[test]
fn test_fulcio_client_with_custom_url() {
let client = FulcioClient::with_url("https://custom.fulcio.dev".to_string());
let client = FulcioClient::with_url("https://custom.fulcio.dev".to_string()).unwrap();
assert_eq!(client.base_url, "https://custom.fulcio.dev");
}

Expand Down
57 changes: 22 additions & 35 deletions src/lib/src/signature/keyless/rekor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
/// ```no_run
/// use wsc::keyless::{RekorClient, FulcioCertificate};
///
/// // Create a Rekor client
/// let client = RekorClient::new();
/// // Create a Rekor client (certificate pinning is enforced)
/// let client = RekorClient::new().expect("pinned Rekor client");
///
/// // Create a mock certificate (in real use, get from Fulcio)
/// let certificate = FulcioCertificate {
Expand Down Expand Up @@ -160,47 +160,40 @@ impl RekorClient {
///
/// # Certificate Pinning
///
/// Certificate pinning is ENFORCED by default using embedded pins for Sigstore
/// production infrastructure. Custom pins can be set via `WSC_REKOR_PINS`.
/// Set `WSC_REQUIRE_CERT_PINNING=1` to fail if pinning cannot be configured.
pub fn new() -> Self {
/// Certificate pinning is unconditionally ENFORCED using embedded pins for
/// Sigstore production infrastructure. Custom pins can be set via
/// `WSC_REKOR_PINS`. If the pinned agent cannot be constructed, this
/// returns an error rather than silently downgrading to unpinned TLS.
pub fn new() -> Result<Self, WSError> {
Self::with_url("https://rekor.sigstore.dev".to_string())
}

/// Create client with custom Rekor server
///
/// # Certificate Pinning
///
/// Certificate pinning is ENFORCED when configured via `WSC_REKOR_PINS`.
/// Set `WSC_REQUIRE_CERT_PINNING=1` to fail if pinning cannot be configured.
pub fn with_url(base_url: String) -> Self {
/// Certificate pinning is unconditionally ENFORCED. Returns an error if the
/// pinned agent cannot be constructed — there is no unpinned fallback.
pub fn with_url(base_url: String) -> Result<Self, WSError> {
#[cfg(not(target_os = "wasi"))]
{
use super::transport::create_agent_with_optional_pinning;
use super::cert_pinning::PinningConfig;

// Create pinning configuration for Rekor
let pinning = Some(PinningConfig::rekor());
// Certificate pinning is mandatory for Sigstore endpoints: a
// failure to build the pinned agent is a hard error, never a
// silent downgrade to unpinned TLS (audit finding C-4).
let agent = create_agent_with_optional_pinning(Some(PinningConfig::rekor()))?;

// Create agent with certificate pinning (or fall back to standard TLS)
let agent = match create_agent_with_optional_pinning(pinning) {
Ok(agent) => agent,
Err(e) => {
// Log error but don't panic - fall back to standard agent
log::error!("Failed to create pinned agent for Rekor: {}. Using standard TLS.", e);
super::transport::create_standard_agent()
}
};

Self {
Ok(Self {
base_url,
client: agent,
}
})
}

#[cfg(target_os = "wasi")]
{
Self { base_url }
Ok(Self { base_url })
}
}

Expand Down Expand Up @@ -446,12 +439,6 @@ impl RekorClient {
}
}

impl Default for RekorClient {
fn default() -> Self {
Self::new()
}
}

/// Build a `RekorEntry` from a parsed Rekor upload response (audit H-5).
///
/// Pure helper so the empty-SET / empty-inclusion-proof rejection rules can be
Expand Down Expand Up @@ -540,14 +527,14 @@ mod tests {

#[test]
fn test_rekor_client_new() {
let client = RekorClient::new();
let client = RekorClient::new().unwrap();
assert_eq!(client.base_url, "https://rekor.sigstore.dev");
}

#[test]
fn test_rekor_client_with_url() {
let custom_url = "https://custom.rekor.server".to_string();
let client = RekorClient::with_url(custom_url.clone());
let client = RekorClient::with_url(custom_url.clone()).unwrap();
assert_eq!(client.base_url, custom_url);
}

Expand Down Expand Up @@ -582,7 +569,7 @@ mod tests {

#[test]
fn test_upload_entry_invalid_hash_length() {
let client = RekorClient::new();
let client = RekorClient::new().unwrap();

// Create stub certificate
let cert = FulcioCertificate {
Expand All @@ -609,7 +596,7 @@ mod tests {

#[test]
fn test_verify_inclusion_rejects_invalid() {
let client = RekorClient::new();
let client = RekorClient::new().unwrap();
let entry = RekorEntry {
uuid: "test-uuid".to_string(),
log_index: 1,
Expand Down Expand Up @@ -687,7 +674,7 @@ mod tests {
#[test]
fn test_mock_rekor_entry_flow() {
// This test demonstrates the expected flow with mock data
let client = RekorClient::new();
let client = RekorClient::new().unwrap();

// Create mock certificate
let cert = FulcioCertificate {
Expand Down
12 changes: 6 additions & 6 deletions src/lib/src/signature/keyless/signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,20 +205,20 @@ impl KeylessSigner {

// Create Fulcio client with appropriate URL
let fulcio = if let Some(url) = &config.fulcio_url {
FulcioClient::with_url(url.clone())
FulcioClient::with_url(url.clone())?
} else if use_staging {
FulcioClient::with_url("https://fulcio.staging.sigstore.dev".to_string())
FulcioClient::with_url("https://fulcio.staging.sigstore.dev".to_string())?
} else {
FulcioClient::new()
FulcioClient::new()?
};

// Create Rekor client with appropriate URL
let rekor = if let Some(url) = &config.rekor_url {
RekorClient::with_url(url.clone())
RekorClient::with_url(url.clone())?
} else if use_staging {
RekorClient::with_url("https://rekor.staging.sigstore.dev".to_string())
RekorClient::with_url("https://rekor.staging.sigstore.dev".to_string())?
} else {
RekorClient::new()
RekorClient::new()?
};

Ok(Self {
Expand Down
56 changes: 27 additions & 29 deletions src/lib/src/signature/keyless/transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,10 +257,12 @@ pub fn create_standard_agent() -> ureq::Agent {

/// Create an HTTP agent with optional certificate pinning.
///
/// This is the recommended function for creating HTTP clients. It automatically:
/// - Enables certificate pinning if a valid `PinningConfig` is provided
/// - Falls back to standard WebPKI if pinning config is empty or fails
/// - Respects `WSC_REQUIRE_CERT_PINNING` environment variable
/// This is the recommended function for creating HTTP clients:
/// - When a non-empty `PinningConfig` is supplied, pinning is **mandatory** —
/// the pinned agent is built or a hard error is returned. There is no
/// silent downgrade to unpinned TLS. (This closes audit finding C-4.)
/// - When no pins are supplied, the caller has explicitly opted out and a
/// standard WebPKI agent is returned.
///
/// # Arguments
/// * `pinning` - Optional certificate pinning configuration
Expand All @@ -269,38 +271,34 @@ pub fn create_standard_agent() -> ureq::Agent {
/// A configured ureq::Agent
///
/// # Errors
/// Returns error only if `WSC_REQUIRE_CERT_PINNING=1` and pinning cannot be configured
/// Returns `WSError::CertificatePinningError` if a non-empty pin set is
/// supplied but the pinned agent cannot be constructed.
///
/// # Deprecated environment variable
/// `WSC_REQUIRE_CERT_PINNING` is retained for backwards compatibility but is
/// redundant: pinning is now unconditionally enforced whenever a non-empty
/// pin set is supplied. The Sigstore production path (`PinningConfig::fulcio`,
/// `PinningConfig::rekor`) always supplies one. The variable now only affects
/// the explicit no-pins opt-out branch below.
#[cfg(not(target_os = "wasi"))]
pub fn create_agent_with_optional_pinning(
pinning: Option<PinningConfig>,
) -> Result<ureq::Agent, WSError> {
let require_pinning = std::env::var("WSC_REQUIRE_CERT_PINNING")
.unwrap_or_default()
== "1";

match pinning {
Some(config) if config.is_enabled() => {
match create_pinned_agent(config) {
Ok(agent) => Ok(agent),
Err(e) => {
if require_pinning {
Err(e)
} else {
log::warn!("Failed to enable certificate pinning: {}. Falling back to standard TLS.", e);
Ok(create_standard_agent())
}
}
}
}
// Pins are configured: pinning is MANDATORY. A configured pin set
// must never silently downgrade to unpinned TLS — that silent
// posture downgrade was audit finding C-4. Any failure to build the
// pinned agent is propagated as a hard error.
Some(config) if config.is_enabled() => create_pinned_agent(config),
// No pins configured: the caller explicitly opted out of pinning.
_ => {
if require_pinning {
Err(WSError::CertificatePinningError(
"Certificate pinning required (WSC_REQUIRE_CERT_PINNING=1) but no pins configured".to_string()
))
} else {
log::debug!("Certificate pinning disabled, using standard TLS");
Ok(create_standard_agent())
if std::env::var("WSC_REQUIRE_CERT_PINNING").unwrap_or_default() == "1" {
return Err(WSError::CertificatePinningError(
"Certificate pinning required (WSC_REQUIRE_CERT_PINNING=1) but no pins configured".to_string(),
));
}
log::debug!("Certificate pinning disabled, using standard TLS");
Ok(create_standard_agent())
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/lib/tests/keyless_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ fn test_github_actions_keyless_signing() {
println!("\n🔐 Testing Rekor verification with REAL production data...");

use wsc::keyless::RekorClient;
let rekor_client = RekorClient::new();
let rekor_client = RekorClient::new().expect("pinned Rekor client");
let verification_result = rekor_client.verify_inclusion(&signature.rekor_entry);

match &verification_result {
Expand Down
Loading