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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion crates/sigstore-bundle/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,13 @@ impl TlogEntryBuilder {
kind: self.kind,
version: self.kind_version,
},
integrated_time: self.integrated_time.to_string(),
// For V2 entries, integrated_time is 0 and should be omitted from JSON
// (skip_serializing_if = "String::is_empty" handles this)
integrated_time: if self.integrated_time == 0 {
String::new()
} else {
self.integrated_time.to_string()
},
inclusion_promise: self.inclusion_promise,
inclusion_proof: self.inclusion_proof,
canonicalized_body: CanonicalizedBody::new(self.canonicalized_body),
Expand Down
75 changes: 21 additions & 54 deletions crates/sigstore-conformance/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use sigstore_crypto::KeyPair;
use sigstore_fulcio::FulcioClient;
use sigstore_oidc::IdentityToken;
use sigstore_rekor::RekorClient;
use sigstore_trust_root::TrustedRoot;
use sigstore_trust_root::{SigningConfig, TrustedRoot};
use sigstore_tsa::TimestampClient;
use sigstore_types::{Bundle, Sha256Hash, SignatureContent};
use sigstore_verify::{verify, VerificationPolicy};
Expand Down Expand Up @@ -114,62 +114,29 @@ async fn sign_bundle(args: &[String]) -> Result<(), Box<dyn std::error::Error>>
let bundle_path = bundle_path.ok_or("Missing required --bundle")?;
let artifact_path = artifact_path.ok_or("Missing artifact path")?;

// Parse signing config if present to get URLs
let (fulcio_url, rekor_url, use_rekor_v2, tsa_url) = if let Some(config_path) = &_signing_config
{
let config_content = fs::read_to_string(config_path)?;
let config_json: serde_json::Value = serde_json::from_str(&config_content)?;

let fulcio_url = config_json
.get("caUrls")
.and_then(|v| v.as_array())
.and_then(|a| a.first())
.and_then(|s| s.get("url"))
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.ok_or("No caUrls in signing config")?;

let tlogs = config_json
.get("tlogs")
.or_else(|| config_json.get("rekorTlogUrls"))
.and_then(|v| v.as_array())
.ok_or("No tlogs in signing config")?;

let log = tlogs.first().ok_or("Empty tlogs list")?;
let url = log
.get("baseUrl")
.or_else(|| log.get("url"))
.and_then(|v| v.as_str())
.ok_or("No baseUrl or url in tlog")?;
let version = log
.get("majorApiVersion")
.and_then(|v| v.as_u64())
.unwrap_or(1);

let tsa_url = config_json
.get("tsaUrls")
.and_then(|v| v.as_array())
.and_then(|a| a.first())
.and_then(|t| t.get("url"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());

(fulcio_url, url.to_string(), version == 2, tsa_url)
// Parse signing config to get URLs
let signing_config = if let Some(config_path) = &_signing_config {
SigningConfig::from_file(config_path)?
} else if staging {
SigningConfig::staging()?
} else {
let fulcio_url = if staging {
"https://fulcio.sigstage.dev".to_string()
} else {
"https://fulcio.sigstore.dev".to_string()
};

let url = if staging {
"https://rekor.sigstage.dev".to_string()
} else {
"https://rekor.sigstore.dev".to_string()
};
(fulcio_url, url, false, None)
SigningConfig::production()?
};

// Get the best endpoints from signing config
let fulcio_endpoint = signing_config
.get_fulcio_url()
.ok_or("No valid Fulcio CA found in signing config")?;
let fulcio_url = fulcio_endpoint.url.clone();

let rekor_endpoint = signing_config
.get_rekor_url(None) // Get highest available version
.ok_or("No valid Rekor tlog found in signing config")?;
let rekor_url = rekor_endpoint.url.clone();
let use_rekor_v2 = rekor_endpoint.major_api_version == 2;

let tsa_url = signing_config.get_tsa_url().map(|e| e.url.clone());

// Read artifact
let artifact_data = fs::read(&artifact_path)?;

Expand Down
61 changes: 59 additions & 2 deletions crates/sigstore-rekor/src/client.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
//! Rekor client for transparency log operations

use crate::entry::{
DsseEntry, HashedRekord, HashedRekordV2, LogEntry, LogEntryResponse, LogInfo, SearchIndex,
DsseEntry, DsseEntryV2, HashedRekord, HashedRekordV2, LogEntry, LogEntryResponse, LogInfo,
SearchIndex,
};
use crate::error::{Error, Result};

Expand Down Expand Up @@ -257,7 +258,7 @@ impl RekorClient {
})
}

/// Create a new DSSE log entry
/// Create a new DSSE log entry (V1)
pub async fn create_dsse_entry(&self, entry: DsseEntry) -> Result<LogEntry> {
let url = format!("{}/api/v1/log/entries", self.url);
let response = self
Expand Down Expand Up @@ -291,6 +292,62 @@ impl RekorClient {
Ok(entry)
}

/// Create a new DSSE log entry (V2)
pub async fn create_dsse_entry_v2(&self, entry: DsseEntryV2) -> Result<LogEntry> {
let url = format!("{}/api/v2/log/entries", self.url);
let response = self
.client
.post(&url)
.json(&entry)
.send()
.await
.map_err(|e| Error::Http(e.to_string()))?;

if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
return Err(Error::Api(format!(
"failed to create DSSE entry: {} - {}",
status, body
)));
}

let response_text = response
.text()
.await
.map_err(|e| Error::Http(e.to_string()))?;

let entry_v2: crate::entry::LogEntryV2 = serde_json::from_str(&response_text)
.map_err(|e| Error::Http(format!("failed to parse JSON: {}", e)))?;

// Convert V2 entry to LogEntry
let log_index = entry_v2.log_index.parse::<i64>().unwrap_or_default();
let integrated_time = entry_v2.integrated_time.parse::<i64>().unwrap_or_default();

let verification = Some(crate::entry::Verification {
inclusion_proof: entry_v2
.inclusion_proof
.map(|p| crate::entry::RekorInclusionProof {
checkpoint: p.checkpoint.envelope,
// Convert Sha256Hash to hex strings (V1 format)
hashes: p.hashes.iter().map(|h| h.to_hex()).collect(),
log_index: p.log_index.parse::<i64>().unwrap_or_default(),
root_hash: p.root_hash.to_hex(),
tree_size: p.tree_size.parse::<i64>().unwrap_or_default(),
}),
signed_entry_timestamp: entry_v2.inclusion_promise.map(|p| p.signed_entry_timestamp),
});

Ok(LogEntry {
uuid: Default::default(), // V2 response doesn't include UUID in body
body: entry_v2.canonicalized_body,
integrated_time,
log_id: entry_v2.log_id.key_id.into_string().into(),
log_index,
verification,
})
}

/// Search the index for entries
pub async fn search_index(&self, query: SearchIndex) -> Result<Vec<String>> {
let url = format!("{}/api/v1/index/retrieve", self.url);
Expand Down
84 changes: 84 additions & 0 deletions crates/sigstore-rekor/src/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,38 @@ use sigstore_types::{
};
use std::collections::HashMap;

/// Rekor API version
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RekorApiVersion {
/// V1 API - uses hashedrekord 0.0.1 and dsse 0.0.1
/// Available at: <https://rekor.sigstore.dev>
#[default]
V1,
/// V2 API - uses hashedrekord 0.0.2 and dsse 0.0.2
/// Returns inclusion proofs with checkpoints
/// Available at: <https://log2025-1.rekor.sigstore.dev> (as of Oct 2025)
/// Note: V2 uses a different URL than V1!
V2,
}

impl RekorApiVersion {
/// Get the default Rekor URL for this API version
pub fn default_url(&self) -> &'static str {
match self {
RekorApiVersion::V1 => "https://rekor.sigstore.dev",
RekorApiVersion::V2 => "https://log2025-1.rekor.sigstore.dev",
}
}

/// Get the default staging Rekor URL for this API version
pub fn default_staging_url(&self) -> &'static str {
match self {
RekorApiVersion::V1 => "https://rekor.sigstage.dev",
RekorApiVersion::V2 => "https://log2025-alpha2.rekor.sigstage.dev",
}
}
}

/// A log entry from Rekor
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
Expand Down Expand Up @@ -357,6 +389,58 @@ impl HashedRekordV2 {
}
}

/// DSSE entry for creating new log entries (V2)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DsseEntryV2 {
#[serde(rename = "dsseRequestV002")]
pub request: DsseRequestV002,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DsseRequestV002 {
/// The DSSE envelope
pub envelope: sigstore_types::DsseEnvelope,
/// Verifiers (certificates) for the signatures
pub verifiers: Vec<DsseVerifierV2>,
}

/// Verifier in DSSE V2 (same structure as HashedRekord V2)
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DsseVerifierV2 {
/// Key details (enum value as string)
pub key_details: String,
/// X.509 certificate
#[serde(skip_serializing_if = "Option::is_none")]
pub x509_certificate: Option<HashedRekordPublicKeyV2>,
/// Public key (alternative to certificate)
#[serde(skip_serializing_if = "Option::is_none")]
pub public_key: Option<HashedRekordPublicKeyV2>,
}

impl DsseEntryV2 {
/// Create a new DsseEntryV2 from an envelope and certificate
///
/// # Arguments
/// * `envelope` - The DSSE envelope containing signatures
/// * `certificate` - DER-encoded X.509 certificate from Fulcio
pub fn new(envelope: &sigstore_types::DsseEnvelope, certificate: &DerCertificate) -> Self {
Self {
request: DsseRequestV002 {
envelope: envelope.clone(),
verifiers: vec![DsseVerifierV2 {
// Assuming ECDSA P-256 SHA-256 for now as per conformance tests
key_details: "PKIX_ECDSA_P256_SHA_256".to_string(),
x509_certificate: Some(HashedRekordPublicKeyV2 {
content: certificate.clone(),
}),
public_key: None,
}],
},
}
}
}

/// V2 Log Entry response
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
Expand Down
5 changes: 4 additions & 1 deletion crates/sigstore-rekor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,8 @@ pub mod error;

pub use body::RekorEntryBody;
pub use client::{get_public_log_info, RekorClient, RekorClientBuilder};
pub use entry::{DsseEntry, HashedRekord, HashedRekordV2, LogEntry, LogInfo, SearchIndex};
pub use entry::{
DsseEntry, DsseEntryV2, HashedRekord, HashedRekordV2, LogEntry, LogInfo, RekorApiVersion,
SearchIndex,
};
pub use error::{Error, Result};
1 change: 1 addition & 0 deletions crates/sigstore-sign/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ sigstore-rekor = { workspace = true }
sigstore-fulcio = { workspace = true }
sigstore-oidc = { workspace = true }
sigstore-tsa = { workspace = true }
sigstore-trust-root = { workspace = true }
thiserror = { workspace = true }
base64 = { workspace = true }
x509-cert = { workspace = true }
Expand Down
Loading