diff --git a/rustfs/src/connect/diagnostics/job.rs b/rustfs/src/connect/diagnostics/job.rs index a59040d96..8e427d4f2 100644 --- a/rustfs/src/connect/diagnostics/job.rs +++ b/rustfs/src/connect/diagnostics/job.rs @@ -37,7 +37,7 @@ use super::{ ProfileProvenance, TOP_API_CAPABILITY, TOP_CLASSIFICATION, TOP_LOCKS_CAPABILITY, TOP_SCHEMA_VERSION, TopApiOperation, TopCaptureLimits, TopCaptureRequest, TopCaptureScope, TopOutcome, capture_cpu_profile, capture_top_api, capture_top_locks, encode_signed_profile_export, measure_drive, measure_network, runtime_network_peer_aliases, sign_drive_export, - sign_network_export, sign_top_export, + sign_network_export, sign_top_export, sign_top_export_with_nonce, }; use crate::connect::DeviceIdentity; @@ -505,7 +505,7 @@ pub async fn execute_diagnostic_job( DiagnosticJobKind::PerformanceNetwork => { execute_performance_network_job(envelope, nonce, identity, provenance, cancel).await } - DiagnosticJobKind::TopApi => execute_top_api_job(envelope, identity, provenance, cancel).await, + DiagnosticJobKind::TopApi => execute_top_api_job(envelope, nonce, identity, provenance, cancel).await, DiagnosticJobKind::TopLocks => execute_top_locks_job(envelope, identity, provenance, cancel).await, } } @@ -765,6 +765,7 @@ async fn execute_profile_cpu_job( async fn execute_top_api_job( envelope: DiagnosticJobEnvelope, + nonce: [u8; 32], identity: &DeviceIdentity, provenance: ProfileProvenance, cancel: &CancellationToken, @@ -814,7 +815,7 @@ async fn execute_top_api_job( artifact_bytes: None, }); } - let export = sign_top_export(&request, &result, identity, cancel).map_err(top_export_failure)?; + let export = sign_top_export_with_nonce(&request, &result, identity, cancel, nonce).map_err(top_export_failure)?; if export.archive_bytes.len() > usize::try_from(envelope.limits.max_output_bytes).unwrap_or(usize::MAX) { return Err(DiagnosticJobError::LimitExceeded); } diff --git a/rustfs/src/connect/diagnostics/mod.rs b/rustfs/src/connect/diagnostics/mod.rs index af579fdd6..b7172ae72 100644 --- a/rustfs/src/connect/diagnostics/mod.rs +++ b/rustfs/src/connect/diagnostics/mod.rs @@ -135,6 +135,7 @@ pub use schedule::{ DiagnosticCollectionPolicy, DiagnosticReceipt, DiagnosticScheduleError, DiagnosticScheduleRuntime, DiagnosticScheduleStatus, ReceiptOutcome, run_local_environment_once, spawn_environment_schedule, }; +pub(crate) use top_api::sign_top_export_with_nonce; pub use top_api::{ LocalTopConsent, MAX_TOP_DURATION, MAX_TOP_EXPORT_VALIDITY, SavedTopExport, SignedTopExport, TOP_API_CAPABILITY, TOP_CLASSIFICATION, TOP_SCHEMA_VERSION, TopApiData, TopApiOperation, TopCaptureError, TopCaptureLimits, TopCaptureRequest, diff --git a/rustfs/src/connect/diagnostics/top_api.rs b/rustfs/src/connect/diagnostics/top_api.rs index 7f2700886..c89017e39 100644 --- a/rustfs/src/connect/diagnostics/top_api.rs +++ b/rustfs/src/connect/diagnostics/top_api.rs @@ -454,6 +454,26 @@ pub fn sign_top_export( result: &TopResult, identity: &DeviceIdentity, cancel: &CancellationToken, +) -> Result { + sign_top_export_inner(request, result, identity, cancel, None) +} + +pub(crate) fn sign_top_export_with_nonce( + request: &TopCaptureRequest, + result: &TopResult, + identity: &DeviceIdentity, + cancel: &CancellationToken, + nonce: [u8; 32], +) -> Result { + sign_top_export_inner(request, result, identity, cancel, Some(nonce)) +} + +fn sign_top_export_inner( + request: &TopCaptureRequest, + result: &TopResult, + identity: &DeviceIdentity, + cancel: &CancellationToken, + supplied_nonce: Option<[u8; 32]>, ) -> Result { if !matches!(result.tool_id, "top.api" | "top.disk" | "top.locks" | "top.net" | "top.rpc") { return Err(TopCaptureError::Result); @@ -495,8 +515,13 @@ pub fn sign_top_export( return Err(TopCaptureError::Expired); } - let mut nonce = [0u8; 32]; - SysRng.try_fill_bytes(&mut nonce).map_err(|_| TopCaptureError::Random)?; + let nonce = if let Some(nonce) = supplied_nonce { + nonce + } else { + let mut nonce = [0u8; 32]; + SysRng.try_fill_bytes(&mut nonce).map_err(|_| TopCaptureError::Random)?; + nonce + }; let device_key_id = hex_lower(&Sha256::digest(identity.public_key_der())); let envelope = DiagnosticEnvelope { format_version: ENVELOPE_FORMAT, @@ -1046,7 +1071,7 @@ mod tests { let result = capture.await.expect("capture task").expect("top.api result"); assert_eq!(result.outcome, TopOutcome::Succeeded); - let data = result.data.expect("successful capture data"); + let data = result.data.as_ref().expect("successful capture data"); assert_eq!(data.operation, TopApiOperation::GetObject); assert_eq!(data.request_count, 2); assert_eq!(data.error_count, 1); @@ -1062,6 +1087,11 @@ mod tests { "windowMillis" ] ); + let nonce = [7_u8; 32]; + let export = sign_top_export_with_nonce(&request, &result, &DeviceIdentity::generate(), &CancellationToken::new(), nonce) + .expect("signed top.api export"); + let envelope: serde_json::Value = serde_json::from_slice(&export.envelope_json).expect("diagnostic envelope"); + assert_eq!(envelope["nonce"], URL_SAFE_NO_PAD.encode_to_string(nonce)); } #[tokio::test]