fix(release):完善当前分发证明与质量门禁
bat-rust / Build and test Rust (push) Canceled after 0s
bat-rust / Build and test Go API (push) Canceled after 0s

This commit is contained in:
2026-09-14 06:39:47 +08:00
parent c17904ee1c
commit 13b0bd5b45
39 changed files with 1590 additions and 228 deletions
+129 -27
View File
@@ -7,17 +7,19 @@ use bat_core::{ApiError, ErrorCode};
use bat_infrastructure::DEFAULT_DOWNLOAD_CONCURRENCY;
use bat_infrastructure::{
apply_patch_file, apply_unityfs_field_patch_file, apply_unityfs_string_field_patch_file,
apply_unityfs_text_asset_patch_file, build_release_list, build_release_status,
changed_endpoint_urls, cleanup_releases, completed_worker_translation_workbench,
diff_extended_snapshot, export_translation_workbench, gc_orphan_staging_with_cas_root,
get_translation_entry, inspect_localized_release_artifact, lexical_absolute,
localized_patch_operations_with_glossary_path, open_append_file, read_download_manifest_at,
apply_unityfs_text_asset_patch_file, build_official_distribution_attestation,
build_release_list, build_release_status, changed_endpoint_urls, cleanup_releases,
completed_worker_translation_workbench, diff_extended_snapshot, export_translation_workbench,
gc_orphan_staging_with_cas_root, get_translation_entry, inspect_localized_release_artifact,
lexical_absolute, localized_patch_operations_with_glossary_path,
official_distribution_mapping_identity, open_append_file, read_download_manifest_at,
read_file_no_symlink, read_localized_patch_manifest_at, read_localized_version_state,
read_parse_cache_at, read_snapshot, read_textunit_index_at, read_translation_workbench,
read_version_state, redact_proxy_url, repack_bundle, resolve_curl_proxy,
select_release_distribution, set_translation, set_translation_checked_with_glossary_path,
unset_translation, validate_output_root, validate_runtime_state_dir,
validate_translation_workbench_with_glossary_path, write_file_atomic,
validate_translation_workbench_with_glossary_path,
verify_and_record_official_distribution_attestation, write_file_atomic,
write_official_textunit_queues, CurlProxyConfig, CurlProxyMode, LocalizedPatchConfig,
LocalizedPatchReport, LocalizedPatchService, LocalizedRollbackReport,
OfficialEndpointMarkerRole, OfficialFailedVersionRecord, OfficialParseCacheService,
@@ -1155,6 +1157,25 @@ struct LocalizedRollbackRpcParams {
localized_release_id: Option<String>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct ResourceManifestRpcParams {
#[serde(default)]
release_id: Option<String>,
#[serde(default)]
expected_publication_identity: Option<String>,
#[serde(default)]
expected_manifest_identity: Option<String>,
#[serde(default)]
offset: usize,
#[serde(default = "default_rpc_manifest_limit")]
limit: usize,
}
fn default_rpc_manifest_limit() -> usize {
100
}
// 规范方法名采用国际惯例的 `<namespace>.<action>`。`bat.*` 保留为向后兼容别名。
const RPC_METHOD_STATUS: &str = "daemon.status";
const RPC_METHOD_STOP: &str = "daemon.stop";
@@ -1199,6 +1220,7 @@ const RPC_METHOD_LOCALIZED_STATUS: &str = "localized.status";
const RPC_METHOD_LOCALIZED_PUBLISH: &str = "localized.publish";
const RPC_METHOD_LOCALIZED_ROLLBACK: &str = "localized.rollback";
const RPC_METHOD_RELEASE_STATUS: &str = "release.status";
const RPC_METHOD_RELEASE_ATTESTATION: &str = "release.attestation";
const RPC_METHOD_RELEASE_LIST: &str = "release.list";
const RPC_METHOD_RELEASE_DISTRIBUTION: &str = "release.distribution";
const RPC_METHOD_RELEASE_CLEANUP: &str = "release.cleanup";
@@ -1982,6 +2004,18 @@ fn dispatch_rpc_method(
"resource.state",
build_resource_state_report(state_dir),
),
RPC_METHOD_RELEASE_ATTESTATION => {
let _sync_guard = tasks
.sync_lock
.lock()
.unwrap_or_else(|poison| poison.into_inner());
rpc_envelope_from_result(
request_id,
RPC_METHOD_RELEASE_ATTESTATION,
build_official_distribution_attestation(&tasks.base_config.output_root)
.and_then(|report| serde_json::to_value(report).map_err(anyhow::Error::from)),
)
}
RPC_METHOD_SCHEDULE_LIST => {
let params = request
.params
@@ -2084,23 +2118,27 @@ fn dispatch_rpc_method(
enqueue_task_envelope(tasks, TaskKind::Repair, false, request_id)
}
RPC_METHOD_RESOURCE_MANIFEST => {
let (offset, limit) = match rpc_page_params(request.params.as_ref()) {
Ok(page) => page,
Err(error) => {
return rpc_envelope_error(
request_id,
ApiError::new(
ErrorCode::RPC_INVALID_PARAMS,
"resource.manifest",
error.to_string(),
),
)
}
let params = match rpc_optional_struct_params::<ResourceManifestRpcParams>(
request.params.as_ref(),
RPC_METHOD_RESOURCE_MANIFEST,
) {
Ok(params) => params,
Err(error) => return rpc_envelope_error(request_id, error),
};
if params.limit == 0 || params.limit > 1000 {
return rpc_envelope_error(
request_id,
ApiError::new(
ErrorCode::RPC_INVALID_PARAMS,
RPC_METHOD_RESOURCE_MANIFEST,
"limit 必须在 1..=1000 范围内",
),
);
};
rpc_envelope_from_result(
request_id,
"resource.manifest",
build_resource_manifest_report(state_dir, offset, limit),
RPC_METHOD_RESOURCE_MANIFEST,
build_resource_manifest_report(state_dir, params),
)
}
RPC_METHOD_RESOURCE_INDEX => {
@@ -2969,39 +3007,102 @@ fn build_catalog_diff_report(state_dir: &Path) -> anyhow::Result<serde_json::Val
/// `resource.manifest`:当前版本下载 manifest 的分页查询。
fn build_resource_manifest_report(
state_dir: &Path,
offset: usize,
limit: usize,
params: ResourceManifestRpcParams,
) -> anyhow::Result<serde_json::Value> {
let (_, version_state) = read_daemon_resource_state(state_dir)?;
let (status_file, version_state) = read_daemon_resource_state(state_dir)?;
let current = version_state
.as_ref()
.and_then(|state| state.current_completed_version.as_ref());
let Some(record) = current else {
return Ok(serde_json::json!({ "available": false }));
};
if params
.release_id
.as_deref()
.is_some_and(|release_id| release_id != record.id)
{
return Err(anyhow::anyhow!(
"resource.manifest release_id 与当前 release 不一致:expected={:?} current={}",
params.release_id,
record.id
));
}
let manifest_path = record.resource_root.join("official-download-manifest.json");
let Some(manifest_bytes) =
read_file_no_symlink(&manifest_path, "官方下载 manifest").map_err(anyhow::Error::msg)?
else {
return Ok(serde_json::json!({
"available": false,
"release_id": record.id,
"resource_root": record.resource_root,
}));
};
let manifest = read_download_manifest_at(&record.resource_root).map_err(anyhow::Error::msg)?;
let Some(manifest) = manifest else {
return Ok(serde_json::json!({
"available": false,
"release_id": record.id,
"resource_root": record.resource_root,
}));
};
let manifest_identity = blake3::hash(&manifest_bytes).to_hex().to_string();
let mapping_identity = official_distribution_mapping_identity(&manifest);
let attestation = status_file.as_ref().and_then(|status| {
build_official_distribution_attestation(&status.resource_output_root).ok()
});
let publication_identity = attestation
.as_ref()
.map(|report| report.publication_identity.clone())
.filter(|identity| !identity.is_empty())
.unwrap_or_else(|| format!("manifest-v1-{manifest_identity}"));
let verification_generation = attestation
.as_ref()
.map(|report| report.verification_generation)
.unwrap_or(0);
if params
.expected_manifest_identity
.as_deref()
.is_some_and(|expected| expected != manifest_identity)
{
return Err(anyhow::anyhow!(
"resource.manifest manifest_identity 不一致:expected={:?} actual={}",
params.expected_manifest_identity,
manifest_identity
));
}
if params
.expected_publication_identity
.as_deref()
.is_some_and(|expected| expected != publication_identity)
{
return Err(anyhow::anyhow!(
"resource.manifest publication_identity 不一致:expected={:?} actual={}",
params.expected_publication_identity,
publication_identity
));
}
let total_entries = manifest.entries.len();
// BTreeMap 按 URL 有序迭代,分页结果稳定。
let entries: Vec<_> = manifest
.entries
.values()
.skip(offset)
.take(limit)
.skip(params.offset)
.take(params.limit)
.cloned()
.collect();
Ok(serde_json::json!({
"available": true,
"channel": "official",
"release_id": record.id,
"resource_root": record.resource_root,
"manifest_version": manifest.version,
"publication_identity": publication_identity,
"mapping_identity": mapping_identity,
"manifest_identity": manifest_identity,
"generation": verification_generation,
"total_entries": total_entries,
"offset": offset,
"limit": limit,
"offset": params.offset,
"limit": params.limit,
"entries": entries,
}))
}
@@ -5457,6 +5558,7 @@ fn run_verify_command(options: &CliOptions) -> anyhow::Result<bool> {
zip_error: item.zip_error.clone(),
})
.collect::<Vec<_>>();
verify_and_record_official_distribution_attestation(&config)?;
let healthy = update_report.update_status == OfficialUpdateStatus::UpToDate
&& update_report.local_manifest_repair_needed_count == 0
&& verification.is_clean();
+21
View File
@@ -4453,6 +4453,9 @@ fn dispatch_resource_manifest_paginates() {
assert_eq!(value["data"]["available"], true);
assert_eq!(value["data"]["total_entries"], 3);
assert_eq!(value["data"]["offset"], 1);
assert_eq!(value["data"]["release_id"], "v-current");
assert!(value["data"]["manifest_identity"].as_str().is_some());
assert!(value["data"]["generation"].as_u64().is_some());
let entries = value["data"]["entries"].as_array().unwrap();
assert_eq!(entries.len(), 2);
assert_eq!(entries[0]["destination"], "b");
@@ -4486,6 +4489,24 @@ fn dispatch_resource_manifest_paginates() {
let entries = value["data"]["entries"].as_array().unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0]["destination"], "c");
let envelope = dispatch_rpc_method(
&rpc_request(
"resource.manifest",
Some(serde_json::json!({
"release_id": "v-current",
"expected_manifest_identity": "wrong-generation",
"offset": 0,
"limit": 1,
})),
),
&state_dir,
&new_daemon_control(),
&test_task_context(),
"req-man-4".to_string(),
);
let value = serde_json::to_value(&envelope).unwrap();
assert_eq!(value["ok"], false);
}
fn write_resource_index_fixture(repository_path: &Path) {
@@ -603,6 +603,14 @@ pub(super) fn run_task_worker(
)
};
run_result
.and_then(|report| {
if job.kind == TaskKind::Verify {
bat_infrastructure::verify_and_record_official_distribution_attestation(
&job.config,
)?;
}
Ok(report)
})
.map(|report| serde_json::to_value(&report).map_err(anyhow::Error::from))
.and_then(|result| result)
};
+18 -15
View File
@@ -82,13 +82,15 @@ pub use official_changes::{
pub use official_download::{
official_distribution_mapping_identity, read_cas_reuse_reference_manifest_at,
read_download_manifest_at, release_cas_reuse_references, DownloadError,
OfficialCasReuseReferenceManifest, OfficialDownloadManifest, OfficialDownloadManifestEntry,
OfficialLocalManifestAuditItem, OfficialLocalManifestAuditReport,
OfficialLocalManifestAuditStatus, OfficialLocalVerificationReport,
OfficialResourceHashAlgorithm, OfficialResourceHashVerification, OfficialResourcePullItem,
OfficialResourcePullProgress, OfficialResourcePullProgressKind, OfficialResourcePullReport,
OfficialResourcePullService, OfficialResourcePullStatus, OfficialResourceReuseWarning,
OfficialResourceVerification, OFFICIAL_CAS_REUSE_REFERENCES_FILE,
OfficialCasReuseReferenceManifest, OfficialDistributionAttestation, OfficialDownloadManifest,
OfficialDownloadManifestEntry, OfficialLocalManifestAuditItem,
OfficialLocalManifestAuditReport, OfficialLocalManifestAuditStatus,
OfficialLocalVerificationReport, OfficialResourceHashAlgorithm,
OfficialResourceHashVerification, OfficialResourcePullItem, OfficialResourcePullProgress,
OfficialResourcePullProgressKind, OfficialResourcePullReport, OfficialResourcePullService,
OfficialResourcePullStatus, OfficialResourceReuseWarning, OfficialResourceVerification,
OFFICIAL_CAS_REUSE_REFERENCES_FILE, OFFICIAL_DISTRIBUTION_ATTESTATION_FILE,
OFFICIAL_DISTRIBUTION_ATTESTATION_MAX_AGE_SECONDS, OFFICIAL_DISTRIBUTION_ATTESTATION_VERSION,
OFFICIAL_DISTRIBUTION_PUBLICATION_FILE,
};
pub use official_game_main_config::OfficialGameMainConfigBootstrapService;
@@ -130,13 +132,13 @@ pub use official_textunit_queue::{
pub use official_update::{
cached_game_main_config_for_metadata, diff_extended_snapshot, gc_orphan_staging,
gc_orphan_staging_with_cas_root, read_bootstrap_cache, read_snapshot, read_version_state,
write_bootstrap_cache, write_snapshot, write_version_state, ExtendedSnapshotDelta,
GameMainConfigSnapshot, LauncherMetadataSnapshot, LocalizedReleaseStatus,
OfficialBootstrapCache, OfficialEndpointMarkerRole, OfficialEndpointMarkerSnapshot,
OfficialFailedVersionRecord, OfficialServerInfoSource, OfficialUpdateConfig,
OfficialUpdateProgress, OfficialUpdateReport, OfficialUpdateService, OfficialUpdateSnapshot,
OfficialUpdateStatus, OfficialVerificationSummary, OfficialVersionRecord, OfficialVersionState,
ResolvedBootstrap,
verify_and_record_official_distribution_attestation, write_bootstrap_cache, write_snapshot,
write_version_state, ExtendedSnapshotDelta, GameMainConfigSnapshot, LauncherMetadataSnapshot,
LocalizedReleaseStatus, OfficialBootstrapCache, OfficialEndpointMarkerRole,
OfficialEndpointMarkerSnapshot, OfficialFailedVersionRecord, OfficialServerInfoSource,
OfficialUpdateConfig, OfficialUpdateProgress, OfficialUpdateReport, OfficialUpdateService,
OfficialUpdateSnapshot, OfficialUpdateStatus, OfficialVerificationSummary,
OfficialVersionRecord, OfficialVersionState, ResolvedBootstrap,
};
pub use patch_ops::{
apply_patch_file, apply_unityfs_field_patch_file, apply_unityfs_string_field_patch_file,
@@ -151,7 +153,8 @@ pub use path_security::{
};
pub use release_flow::ReleaseFlowStatusCode;
pub use release_ops::{
build_release_list, build_release_status, cleanup_releases, select_release_distribution,
build_official_distribution_attestation, build_release_list, build_release_status,
cleanup_releases, select_release_distribution, OfficialDistributionAttestationReport,
ReleaseCleanupParams, ReleaseCleanupReport, ReleaseDistributionEntry, ReleaseDistributionPage,
ReleaseDistributionParams, ReleaseListParams, ReleaseStatusReport, ReleaseSummary,
};
+154
View File
@@ -71,6 +71,12 @@ const DOWNLOAD_MANIFEST_FILE: &str = "official-download-manifest.json";
const DOWNLOAD_QUARANTINE_FILE: &str = "official-download-quarantine.json";
/// Independent publication fact for the official distribution manifest.
pub const OFFICIAL_DISTRIBUTION_PUBLICATION_FILE: &str = "official-distribution-publication.json";
/// Current official distribution verification result.
pub const OFFICIAL_DISTRIBUTION_ATTESTATION_FILE: &str = "official-distribution-attestation.json";
/// Persisted attestation schema version.
pub const OFFICIAL_DISTRIBUTION_ATTESTATION_VERSION: u32 = 1;
/// Attestations older than this are no longer allowed to authorize current CDN.
pub const OFFICIAL_DISTRIBUTION_ATTESTATION_MAX_AGE_SECONDS: u64 = 900;
/// 记录一个已发布官方 release 获取的 CAS 引用。
pub const OFFICIAL_CAS_REUSE_REFERENCES_FILE: &str = "official-cas-reuse-references.json";
const OFFICIAL_CAS_REUSE_REFERENCES_VERSION: u32 = 1;
@@ -566,6 +572,154 @@ pub(crate) struct OfficialDistributionPublicationAnchor {
const OFFICIAL_DISTRIBUTION_PUBLICATION_VERSION: u32 = 1;
/// Rust-owned lightweight proof that the current official publication is safe
/// for the read-only distribution path.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OfficialDistributionAttestation {
/// Attestation schema version.
pub version: u32,
/// Distribution channel; currently always `official`.
pub channel: String,
/// Stable official release ID.
pub official_release_id: String,
/// Published version root this result describes.
pub resource_root: PathBuf,
/// Identity of the publication anchor and its manifest generation.
pub publication_identity: String,
/// Identity of the complete destination mapping.
pub mapping_identity: String,
/// BLAKE3 identity of the manifest bytes.
pub manifest_identity: String,
/// Number of entries in the bound manifest.
pub entry_count: u64,
/// `verified`, `stale`, `invalid`, or `unavailable`.
pub integrity_status: String,
/// Human-readable stable state label.
pub status: String,
/// Namespaced status code consumed by RPC clients.
pub status_code: String,
/// Whether this attestation currently authorizes distribution.
pub ready: bool,
/// Monotonic verification generation for this published root.
pub verification_generation: u64,
/// Time of the last successful full local verification.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub verified_at: Option<u64>,
/// Freshness window used by the lightweight RPC reader.
pub max_age_seconds: u64,
/// Diagnostics retained with the result.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub diagnostics: Vec<String>,
}
pub(crate) fn official_distribution_publication_identity(
anchor: &OfficialDistributionPublicationAnchor,
) -> String {
format!(
"odp-v1-{}-{}",
anchor.mapping_identity, anchor.manifest_identity
)
}
fn official_distribution_attestation_status_code(integrity_status: &str) -> &'static str {
match integrity_status {
"verified" => "distribution.ready",
"stale" => "distribution.attestation_stale",
"invalid" => "distribution.attestation_invalid",
_ => "distribution.attestation_unavailable",
}
}
fn official_distribution_attestation_status(integrity_status: &str) -> &'static str {
match integrity_status {
"verified" => "ready",
"stale" => "stale",
"invalid" => "invalid",
_ => "unavailable",
}
}
pub(crate) fn read_official_distribution_attestation_at(
release_root: &Path,
) -> Result<Option<OfficialDistributionAttestation>, String> {
let path = release_root.join(OFFICIAL_DISTRIBUTION_ATTESTATION_FILE);
let Some(bytes) = read_file_no_symlink(&path, "官方 distribution attestation")? else {
return Ok(None);
};
let attestation: OfficialDistributionAttestation = serde_json::from_slice(&bytes)
.map_err(|error| format!("解析官方 distribution attestation 失败:{error}"))?;
if attestation.version != OFFICIAL_DISTRIBUTION_ATTESTATION_VERSION {
return Err(format!(
"不支持的官方 distribution attestation 版本:{}",
attestation.version
));
}
Ok(Some(attestation))
}
/// Records a verification result for one already published official root.
///
/// The publication anchor is reused as the immutable generation identity.
/// The caller chooses `verified` only after the existing full local audit has
/// passed; this function itself never turns a partial audit into a healthy
/// result.
pub(crate) fn write_official_distribution_attestation_at(
release_root: &Path,
official_release_id: &str,
integrity_status: &str,
diagnostics: Vec<String>,
) -> Result<OfficialDistributionAttestation, String> {
if !matches!(
integrity_status,
"verified" | "stale" | "invalid" | "unavailable"
) {
return Err(format!(
"不支持的官方 distribution attestation 状态:{integrity_status}"
));
}
ensure_safe_directory_path(release_root, "官方 distribution attestation 根目录")?;
let anchor = verify_official_distribution_publication_at(release_root, official_release_id)?
.ok_or_else(|| {
format!(
"官方 distribution attestation 缺少 publication anchor{}",
release_root.display()
)
})?;
let previous_generation = read_official_distribution_attestation_at(release_root)?
.map(|previous| previous.verification_generation)
.unwrap_or(0);
let verified_at = (integrity_status == "verified").then_some(unix_seconds_now());
let attestation = OfficialDistributionAttestation {
version: OFFICIAL_DISTRIBUTION_ATTESTATION_VERSION,
channel: "official".to_string(),
official_release_id: official_release_id.to_string(),
resource_root: release_root.to_path_buf(),
publication_identity: official_distribution_publication_identity(&anchor),
mapping_identity: anchor.mapping_identity,
manifest_identity: anchor.manifest_identity,
entry_count: anchor.entry_count,
integrity_status: integrity_status.to_string(),
status: official_distribution_attestation_status(integrity_status).to_string(),
status_code: official_distribution_attestation_status_code(integrity_status).to_string(),
ready: integrity_status == "verified",
verification_generation: previous_generation.saturating_add(1),
verified_at,
max_age_seconds: OFFICIAL_DISTRIBUTION_ATTESTATION_MAX_AGE_SECONDS,
diagnostics,
};
let path = release_root.join(OFFICIAL_DISTRIBUTION_ATTESTATION_FILE);
ensure_safe_file_target(release_root, &path, "官方 distribution attestation")?;
let bytes = serde_json::to_vec_pretty(&attestation)
.map_err(|error| format!("序列化官方 distribution attestation 失败:{error}"))?;
write_file_atomic(
&path,
&bytes,
STATE_FILE_MODE,
"官方 distribution attestation",
)?;
Ok(attestation)
}
/// Writes the independent publication anchor after the complete official
/// release verification has succeeded.
///
+83 -1
View File
@@ -14,7 +14,8 @@ use crate::official_changes::{
OfficialResourceChangeSummary,
};
use crate::official_download::{
write_official_distribution_publication_anchor_at, OFFICIAL_CAS_REUSE_REFERENCES_FILE,
write_official_distribution_attestation_at, write_official_distribution_publication_anchor_at,
OfficialDistributionAttestation, OFFICIAL_CAS_REUSE_REFERENCES_FILE,
OFFICIAL_DISTRIBUTION_PUBLICATION_FILE,
};
use crate::official_game_main_config::{
@@ -1785,6 +1786,28 @@ impl OfficialUpdateService {
&active_resource_root,
&snapshot_path,
)?;
if let Some(audit) = local_audit.as_ref() {
let integrity_status = if audit.is_clean() {
"verified"
} else {
"invalid"
};
let diagnostics = if audit.is_clean() {
Vec::new()
} else {
vec![format!(
"本地 manifest 审计失败:{} 项需要修复",
audit.repair_needed_count()
)]
};
write_official_distribution_attestation_at(
&active_resource_root,
&active_release_id,
integrity_status,
diagnostics,
)
.map_err(anyhow::Error::msg)?;
}
let active_launcher_bootstrap_path =
active_resource_root.join(OFFICIAL_LAUNCHER_BOOTSTRAP_FILE);
if bootstrap.is_some()
@@ -2015,6 +2038,13 @@ impl OfficialUpdateService {
&publish_plan.id,
)
.map_err(anyhow::Error::msg)?;
write_official_distribution_attestation_at(
&publish_plan.staging_path,
&publish_plan.id,
"verified",
Vec::new(),
)
.map_err(anyhow::Error::msg)?;
progress(OfficialUpdateProgress::new(
"snapshot",
format!("写入快照 {}", staging_snapshot_path.display()),
@@ -2180,6 +2210,58 @@ impl OfficialUpdateService {
}
}
/// Runs the explicit full local verification and records its result for the
/// lightweight current-distribution health RPC.
///
/// This is intentionally called only by the explicit verify task/command. The
/// high-frequency health path reads the resulting attestation and never hashes
/// resource artifacts.
pub fn verify_and_record_official_distribution_attestation(
config: &OfficialUpdateConfig,
) -> anyhow::Result<OfficialDistributionAttestation> {
let version_state = read_version_state(&config.version_state_path())?
.ok_or_else(|| anyhow::anyhow!("官方版本状态不存在,无法记录 distribution attestation"))?;
let record = version_state
.current_completed_version
.as_ref()
.ok_or_else(|| {
anyhow::anyhow!("没有当前已发布官方 release,无法记录 distribution attestation")
})?;
let resource_root = OfficialPublishLayout::new(&config.output_root)
.active_resource_root()
.map_err(anyhow::Error::msg)?;
if resource_root != record.resource_root {
return Err(anyhow::anyhow!(
"current resource root 与版本状态不一致:current={} state={}",
resource_root.display(),
record.resource_root.display()
));
}
let verification =
OfficialResourcePullService::with_curl_command(&resource_root, &config.curl_command)
.with_proxy_config(config.curl_proxy.clone())
.verify_local_download_manifest()
.map_err(anyhow::Error::msg)?;
let diagnostics = verification
.items
.iter()
.filter(|item| !item.status.is_verified())
.map(|item| format!("{}: {}", item.destination.display(), item.status.as_str()))
.collect::<Vec<_>>();
let integrity_status = if verification.is_clean() {
"verified"
} else {
"invalid"
};
write_official_distribution_attestation_at(
&resource_root,
&record.id,
integrity_status,
diagnostics,
)
.map_err(anyhow::Error::msg)
}
fn run_post_sync_resource_handoff(
previous_resource_root: Option<&Path>,
current_resource_root: &Path,
+308 -3
View File
@@ -11,9 +11,11 @@ use crate::localized_patch::{
LOCALIZED_VERSIONS_DIR,
};
use crate::official_download::{
read_cas_reuse_reference_manifest_at, read_download_manifest_at, release_cas_reuse_references,
verify_official_distribution_publication_at, OfficialDownloadManifest,
OfficialDownloadManifestEntry,
official_distribution_publication_identity, read_cas_reuse_reference_manifest_at,
read_download_manifest_at, read_official_distribution_attestation_at,
release_cas_reuse_references, verify_official_distribution_publication_at,
OfficialDownloadManifest, OfficialDownloadManifestEntry,
OFFICIAL_DISTRIBUTION_ATTESTATION_MAX_AGE_SECONDS,
};
use crate::official_update::{read_version_state, OfficialVersionRecord, OfficialVersionState};
use crate::path_security::{
@@ -24,6 +26,7 @@ use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::{Component, Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
const OFFICIAL_CURRENT_LINK: &str = "current";
const OFFICIAL_STAGING_DIR: &str = ".staging";
@@ -148,6 +151,47 @@ pub struct ReleaseStatusReport {
pub releases: Vec<ReleaseSummary>,
}
/// Lightweight current-official distribution health proof.
///
/// Unlike `ReleaseStatusReport`, this report never scans historical releases
/// or hashes resource artifacts. It only evaluates the current pointer,
/// publication anchor, persisted attestation, and attestation freshness.
#[derive(Debug, Clone, Serialize)]
pub struct OfficialDistributionAttestationReport {
/// Whether a current official version record exists.
pub available: bool,
/// Distribution channel.
pub channel: String,
/// Current official release ID.
pub release_id: String,
/// Current versioned resource root.
pub resource_root: String,
/// Publication/generation identity.
pub publication_identity: String,
/// Complete mapping identity.
pub mapping_identity: String,
/// Manifest byte identity.
pub manifest_identity: String,
/// Number of bound manifest entries.
pub entry_count: u64,
/// `verified`, `stale`, `invalid`, or `unavailable`.
pub integrity_status: String,
/// Stable state label.
pub status: String,
/// Stable namespaced status code.
pub status_code: String,
/// Whether current official distribution may be served.
pub ready: bool,
/// Verification generation from the persisted attestation.
pub verification_generation: u64,
/// Last successful full verification time.
pub verified_at: Option<u64>,
/// Freshness window used by the reader.
pub max_age_seconds: u64,
/// Diagnostics explaining a blocked result.
pub diagnostics: Vec<String>,
}
/// A page of resources from a Rust-verified release choice.
#[derive(Debug, Clone, Serialize)]
pub struct ReleaseDistributionPage {
@@ -309,6 +353,196 @@ pub fn build_release_status(
})
}
/// Builds the lightweight attestation for the current official release.
pub fn build_official_distribution_attestation(
official_root: &Path,
) -> anyhow::Result<OfficialDistributionAttestationReport> {
let state = read_version_state(&official_root.join(OFFICIAL_VERSION_STATE_FILE))?;
let Some(record) = state
.as_ref()
.and_then(|state| state.current_completed_version.as_ref())
else {
return Ok(OfficialDistributionAttestationReport {
available: false,
channel: "official".to_string(),
release_id: String::new(),
resource_root: String::new(),
publication_identity: String::new(),
mapping_identity: String::new(),
manifest_identity: String::new(),
entry_count: 0,
integrity_status: "unavailable".to_string(),
status: "unavailable".to_string(),
status_code: "distribution.attestation_unavailable".to_string(),
ready: false,
verification_generation: 0,
verified_at: None,
max_age_seconds: OFFICIAL_DISTRIBUTION_ATTESTATION_MAX_AGE_SECONDS,
diagnostics: vec!["没有当前已发布官方 release".to_string()],
});
};
let mut report = OfficialDistributionAttestationReport {
available: true,
channel: "official".to_string(),
release_id: record.id.clone(),
resource_root: record.resource_root.display().to_string(),
publication_identity: String::new(),
mapping_identity: String::new(),
manifest_identity: String::new(),
entry_count: 0,
integrity_status: "unavailable".to_string(),
status: "unavailable".to_string(),
status_code: "distribution.attestation_unavailable".to_string(),
ready: false,
verification_generation: 0,
verified_at: None,
max_age_seconds: OFFICIAL_DISTRIBUTION_ATTESTATION_MAX_AGE_SECONDS,
diagnostics: Vec::new(),
};
let current_id = read_managed_current_id(
&official_root.join(OFFICIAL_CURRENT_LINK),
OFFICIAL_VERSIONS_DIR,
);
if current_id.as_deref() != Some(record.id.as_str()) {
report.integrity_status = "invalid".to_string();
report.status = "invalid".to_string();
report.status_code = "distribution.attestation_invalid".to_string();
report.diagnostics.push(format!(
"current 指针与版本状态不一致:pointer={:?} state={}",
current_id, record.id
));
}
if let Err(error) = ensure_path_within_root(official_root, &record.resource_root) {
report.integrity_status = "invalid".to_string();
report.status = "invalid".to_string();
report.status_code = "distribution.attestation_invalid".to_string();
report.diagnostics.push(error);
}
if let Err(error) =
ensure_safe_directory_path(&record.resource_root, "当前官方 distribution 根目录")
{
report.integrity_status = "invalid".to_string();
report.status = "invalid".to_string();
report.status_code = "distribution.attestation_invalid".to_string();
report.diagnostics.push(error);
}
let anchor =
match verify_official_distribution_publication_at(&record.resource_root, &record.id) {
Ok(Some(anchor)) => {
report.publication_identity = official_distribution_publication_identity(&anchor);
report.mapping_identity = anchor.mapping_identity.clone();
report.manifest_identity = anchor.manifest_identity.clone();
report.entry_count = anchor.entry_count;
Some(anchor)
}
Ok(None) => {
report
.diagnostics
.push("缺少 publication anchor".to_string());
None
}
Err(error) => {
report.diagnostics.push(error);
None
}
};
let attestation = match read_official_distribution_attestation_at(&record.resource_root) {
Ok(attestation) => attestation,
Err(error) => {
report.diagnostics.push(error);
None
}
};
let Some(attestation) = attestation else {
if report.integrity_status != "invalid" {
report.integrity_status = "unavailable".to_string();
report.status = "unavailable".to_string();
report.status_code = "distribution.attestation_unavailable".to_string();
}
report
.diagnostics
.push("缺少当前 release attestation".to_string());
return Ok(report);
};
report.integrity_status = attestation.integrity_status.clone();
report.status = attestation.status.clone();
report.status_code = attestation.status_code.clone();
report.publication_identity = attestation.publication_identity.clone();
report.mapping_identity = attestation.mapping_identity.clone();
report.manifest_identity = attestation.manifest_identity.clone();
report.entry_count = attestation.entry_count;
report.verification_generation = attestation.verification_generation;
report.verified_at = attestation.verified_at;
report.max_age_seconds = if attestation.max_age_seconds == 0 {
OFFICIAL_DISTRIBUTION_ATTESTATION_MAX_AGE_SECONDS
} else {
attestation.max_age_seconds
};
report.diagnostics.extend(attestation.diagnostics);
let identity_matches = anchor.as_ref().is_some_and(|anchor| {
attestation.channel == "official"
&& attestation.official_release_id == record.id
&& attestation.resource_root == record.resource_root
&& attestation.publication_identity
== official_distribution_publication_identity(anchor)
&& attestation.mapping_identity == anchor.mapping_identity
&& attestation.manifest_identity == anchor.manifest_identity
&& attestation.entry_count == anchor.entry_count
});
if !identity_matches {
report.integrity_status = "invalid".to_string();
report.status = "invalid".to_string();
report.status_code = "distribution.attestation_invalid".to_string();
report.ready = false;
report
.diagnostics
.push("attestation 与当前 publication generation 不一致".to_string());
return Ok(report);
}
if report.integrity_status == "verified" {
let fresh = report.verified_at.is_some_and(|verified_at| {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_secs())
.unwrap_or_default();
now.saturating_sub(verified_at) <= report.max_age_seconds
});
if !fresh {
report.integrity_status = "stale".to_string();
report.status = "stale".to_string();
report.status_code = "distribution.attestation_stale".to_string();
report.ready = false;
report
.diagnostics
.push("当前官方 attestation 已超过 freshness window".to_string());
return Ok(report);
}
if !attestation.ready {
report.integrity_status = "invalid".to_string();
report.status = "invalid".to_string();
report.status_code = "distribution.attestation_invalid".to_string();
report.ready = false;
report
.diagnostics
.push("attestation integrity=verified 但 ready=false".to_string());
return Ok(report);
}
report.status = "ready".to_string();
report.status_code = "distribution.ready".to_string();
report.ready = true;
} else {
report.ready = false;
}
Ok(report)
}
/// Lists one or both release namespaces.
pub fn build_release_list(
official_root: &Path,
@@ -1793,6 +2027,77 @@ mod tests {
assert!(!official_blocked.available);
}
#[test]
fn lightweight_attestation_is_current_fresh_and_does_not_hash_artifacts() {
let temp = tempfile::tempdir().unwrap();
let official_root = temp.path().join("official");
let version = prepare_official(&official_root, "official-v1");
fs::create_dir_all(&official_root).unwrap();
symlink(
Path::new(OFFICIAL_VERSIONS_DIR).join("official-v1"),
official_root.join(OFFICIAL_CURRENT_LINK),
)
.unwrap();
fs::write(
official_root.join(OFFICIAL_VERSION_STATE_FILE),
serde_json::to_vec(&OfficialVersionState {
current_completed_version: Some(official_record(&official_root, "official-v1")),
..OfficialVersionState::default()
})
.unwrap(),
)
.unwrap();
crate::official_download::write_official_distribution_attestation_at(
&version,
"official-v1",
"verified",
Vec::new(),
)
.unwrap();
let healthy = build_official_distribution_attestation(&official_root).unwrap();
assert!(healthy.ready);
assert_eq!(healthy.integrity_status, "verified");
assert_eq!(healthy.release_id, "official-v1");
assert!(healthy.verification_generation > 0);
// The lightweight read path only rechecks the publication anchor and
// manifest bytes; artifact damage is recorded by explicit verify.
fs::write(version.join("other.bin"), b"tampered").unwrap();
let still_authorized = build_official_distribution_attestation(&official_root).unwrap();
assert!(still_authorized.ready);
crate::official_download::write_official_distribution_attestation_at(
&version,
"official-v1",
"invalid",
vec!["other.bin: size_or_hash_mismatch".to_string()],
)
.unwrap();
let invalid = build_official_distribution_attestation(&official_root).unwrap();
assert!(!invalid.ready);
assert_eq!(invalid.integrity_status, "invalid");
fs::write(version.join("other.bin"), b"official-other").unwrap();
let mut fresh =
crate::official_download::read_official_distribution_attestation_at(&version)
.unwrap()
.unwrap();
fresh.integrity_status = "verified".to_string();
fresh.status = "ready".to_string();
fresh.status_code = "distribution.ready".to_string();
fresh.ready = true;
fresh.verified_at = Some(0);
fs::write(
version.join(crate::OFFICIAL_DISTRIBUTION_ATTESTATION_FILE),
serde_json::to_vec(&fresh).unwrap(),
)
.unwrap();
let stale = build_official_distribution_attestation(&official_root).unwrap();
assert!(!stale.ready);
assert_eq!(stale.integrity_status, "stale");
}
#[test]
fn distribution_selection_does_not_fallback_from_damaged_localized_release() {
let temp = tempfile::tempdir().unwrap();