mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 10:04:55 +08:00
fix(release):完善当前分发证明与质量门禁
This commit is contained in:
+161
-61
@@ -12,16 +12,18 @@ import (
|
||||
|
||||
// Backend is the subset of daemon RPC used by bat-api.
|
||||
//
|
||||
// Call order for discovery (per plan review):
|
||||
// Call order for discovery:
|
||||
// 1. daemon.status
|
||||
// 2. daemon.doctor
|
||||
// 3. catalog.status / resource.manifest (and resource.state as needed)
|
||||
// 3. release.attestation
|
||||
// 4. catalog.status / bound resource.manifest (and resource.state as needed)
|
||||
type Backend interface {
|
||||
DaemonStatus(ctx context.Context) (*backendrpc.DaemonStatusReport, error)
|
||||
DaemonDoctor(ctx context.Context) (*backendrpc.DoctorReport, error)
|
||||
ResourceState(ctx context.Context) (*backendrpc.ResourceState, error)
|
||||
CatalogStatus(ctx context.Context) (json.RawMessage, error)
|
||||
ResourceManifest(ctx context.Context, offset int, limit int) (*backendrpc.ResourceManifestPage, error)
|
||||
ResourceManifest(ctx context.Context, params backendrpc.ResourceManifestParams) (*backendrpc.ResourceManifestPage, error)
|
||||
ReleaseAttestation(ctx context.Context) (*backendrpc.DistributionAttestation, error)
|
||||
}
|
||||
|
||||
// ControlBackend is the explicitly allowlisted mutation subset exposed through
|
||||
@@ -146,8 +148,11 @@ func (r RPCClient) ResourceState(ctx context.Context) (*backendrpc.ResourceState
|
||||
func (r RPCClient) CatalogStatus(ctx context.Context) (json.RawMessage, error) {
|
||||
return r.Client.CatalogStatus(ctx)
|
||||
}
|
||||
func (r RPCClient) ResourceManifest(ctx context.Context, offset int, limit int) (*backendrpc.ResourceManifestPage, error) {
|
||||
return r.Client.ResourceManifest(ctx, offset, limit)
|
||||
func (r RPCClient) ResourceManifest(ctx context.Context, params backendrpc.ResourceManifestParams) (*backendrpc.ResourceManifestPage, error) {
|
||||
return r.Client.ResourceManifest(ctx, params)
|
||||
}
|
||||
func (r RPCClient) ReleaseAttestation(ctx context.Context) (*backendrpc.DistributionAttestation, error) {
|
||||
return r.Client.ReleaseAttestation(ctx)
|
||||
}
|
||||
func (r RPCClient) DaemonRestart(ctx context.Context) (*backendrpc.Ack, error) {
|
||||
return r.Client.DaemonRestart(ctx)
|
||||
@@ -310,6 +315,7 @@ type DiscoverResult struct {
|
||||
DoctorHealthy *bool
|
||||
Status *backendrpc.DaemonStatusReport
|
||||
Doctor *backendrpc.DoctorReport
|
||||
Attestation *backendrpc.DistributionAttestation
|
||||
ReleaseStatus *backendrpc.ReleaseStatusReport
|
||||
Distribution DistributionHealth
|
||||
Snapshot *SnapshotSummary
|
||||
@@ -318,10 +324,11 @@ type DiscoverResult struct {
|
||||
Warnings []string
|
||||
}
|
||||
|
||||
// DiscoverAndIndex contacts the daemon (status first, then doctor, then
|
||||
// release.status) and builds a release index from paginated resource.manifest
|
||||
// plus on-disk checks. Rust's release.status is the only release-level
|
||||
// integrity authorization used for the production RPC path.
|
||||
// DiscoverAndIndex contacts the daemon (status first, then doctor, then the
|
||||
// lightweight current-release attestation) and builds a release index from
|
||||
// pages bound to that attestation plus on-disk checks. Rust's attestation is
|
||||
// the only release-level integrity authorization used for the production RPC
|
||||
// path; release.status remains an administrative diagnostic.
|
||||
//
|
||||
// If resourceRootOverride is non-empty, it wins over RPC-reported roots after
|
||||
// RPC health probes (still preferred for production to call status/doctor).
|
||||
@@ -390,30 +397,33 @@ func DiscoverAndIndex(ctx context.Context, backend Backend, resourceRootOverride
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// 3) release.status is the Rust-owned whole-release distribution gate.
|
||||
releaseStatusBackend, ok := backend.(ReleaseStatusBackend)
|
||||
// 3) release.attestation is the Rust-owned current-release distribution gate.
|
||||
attestationBackend, ok := backend.(AttestationBackend)
|
||||
if !ok {
|
||||
out.Warnings = append(out.Warnings, "release.status: backend does not expose Rust release health")
|
||||
out.Warnings = append(out.Warnings, "release.attestation: backend does not expose Rust health proof")
|
||||
return emptyRPCResult(out, nil, "Rust release health is unavailable"),
|
||||
fmt.Errorf("rust release health is unavailable")
|
||||
}
|
||||
releaseStatus, err := releaseStatusBackend.ReleaseStatus(ctx)
|
||||
attestation, err := attestationBackend.ReleaseAttestation(ctx)
|
||||
if err != nil {
|
||||
out.Warnings = append(out.Warnings, fmt.Sprintf("release.status: %v", err))
|
||||
out.Warnings = append(out.Warnings, fmt.Sprintf("release.attestation: %v", err))
|
||||
return emptyRPCResult(out, nil, "Rust release health query failed"),
|
||||
fmt.Errorf("release.status failed: %w", err)
|
||||
fmt.Errorf("release.attestation failed: %w", err)
|
||||
}
|
||||
if releaseStatus == nil {
|
||||
out.Warnings = append(out.Warnings, "release.status: empty response")
|
||||
if attestation == nil {
|
||||
out.Warnings = append(out.Warnings, "release.attestation: empty response")
|
||||
return emptyRPCResult(out, nil, "Rust release health query returned no response"),
|
||||
fmt.Errorf("release.status returned an empty response")
|
||||
fmt.Errorf("release.attestation returned an empty response")
|
||||
}
|
||||
out.Attestation = attestation
|
||||
out.Distribution = rustAttestationHealth(attestation)
|
||||
if !attestation.Available {
|
||||
return emptyRPCResult(out, nil, "Rust current official distribution attestation is unavailable"), nil
|
||||
}
|
||||
out.ReleaseStatus = releaseStatus
|
||||
out.Distribution = rustDistributionHealth(releaseStatus)
|
||||
|
||||
// Catalog / resource discovery
|
||||
var snapshot *SnapshotSummary
|
||||
var resourceRoot string
|
||||
resourceRoot := attestation.ResourceRoot
|
||||
catalogAvailabilityKnown := false
|
||||
catalogAvailable := false
|
||||
|
||||
@@ -429,18 +439,31 @@ func DiscoverAndIndex(ctx context.Context, backend Backend, resourceRootOverride
|
||||
}
|
||||
if snapshot != nil &&
|
||||
snapshot.VersionID != "" &&
|
||||
out.ReleaseStatus.OfficialCurrentReleaseID != "" &&
|
||||
snapshot.VersionID != out.ReleaseStatus.OfficialCurrentReleaseID {
|
||||
attestation.ReleaseID != "" &&
|
||||
snapshot.VersionID != attestation.ReleaseID {
|
||||
return emptyRPCResult(
|
||||
out,
|
||||
snapshot,
|
||||
fmt.Sprintf(
|
||||
"release.status current ID %q does not match catalog current ID %q",
|
||||
out.ReleaseStatus.OfficialCurrentReleaseID,
|
||||
"release.attestation current ID %q does not match catalog current ID %q",
|
||||
attestation.ReleaseID,
|
||||
snapshot.VersionID,
|
||||
),
|
||||
), nil
|
||||
}
|
||||
if resourceRoot != "" &&
|
||||
attestation.ResourceRoot != "" &&
|
||||
resourceRoot != attestation.ResourceRoot {
|
||||
return emptyRPCResult(
|
||||
out,
|
||||
snapshot,
|
||||
fmt.Sprintf(
|
||||
"catalog current root %q does not match attestation root %q",
|
||||
resourceRoot,
|
||||
attestation.ResourceRoot,
|
||||
),
|
||||
), nil
|
||||
}
|
||||
if catalogAvailabilityKnown && !catalogAvailable {
|
||||
return emptyRPCResult(out, snapshot, "catalog.status available=false; no published release"), nil
|
||||
}
|
||||
@@ -485,7 +508,7 @@ func DiscoverAndIndex(ctx context.Context, backend Backend, resourceRootOverride
|
||||
}
|
||||
}
|
||||
|
||||
entries, manifestVersion, rootFromManifest, err := fetchAllManifestEntries(ctx, backend)
|
||||
entries, manifestVersion, rootFromManifest, err := fetchAllManifestEntries(ctx, backend, attestation)
|
||||
if err != nil {
|
||||
out.Warnings = append(out.Warnings, fmt.Sprintf("resource.manifest: %v", err))
|
||||
// Without the RPC manifest there is no evidence that the local
|
||||
@@ -498,6 +521,17 @@ func DiscoverAndIndex(ctx context.Context, backend Backend, resourceRootOverride
|
||||
), nil
|
||||
}
|
||||
if rootFromManifest != "" {
|
||||
if attestation.ResourceRoot != "" && rootFromManifest != attestation.ResourceRoot {
|
||||
return emptyRPCResult(
|
||||
out,
|
||||
snapshot,
|
||||
fmt.Sprintf(
|
||||
"resource.manifest resource root %q does not match attestation root %q",
|
||||
rootFromManifest,
|
||||
attestation.ResourceRoot,
|
||||
),
|
||||
), nil
|
||||
}
|
||||
resourceRoot = rootFromManifest
|
||||
}
|
||||
if resourceRootOverride != "" {
|
||||
@@ -530,6 +564,13 @@ func DiscoverAndIndex(ctx context.Context, backend Backend, resourceRootOverride
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// AttestationBackend exposes the lightweight current official health proof.
|
||||
// It is intentionally separate from ReleaseStatusBackend because health
|
||||
// refreshes must not require the historical release scan.
|
||||
type AttestationBackend interface {
|
||||
ReleaseAttestation(ctx context.Context) (*backendrpc.DistributionAttestation, error)
|
||||
}
|
||||
|
||||
func emptyRPCResult(out *DiscoverResult, snapshot *SnapshotSummary, warning string) *DiscoverResult {
|
||||
distribution := out.Distribution
|
||||
if distribution.Source == "" {
|
||||
@@ -553,52 +594,48 @@ func emptyRPCResult(out *DiscoverResult, snapshot *SnapshotSummary, warning stri
|
||||
return out
|
||||
}
|
||||
|
||||
func rustDistributionHealth(report *backendrpc.ReleaseStatusReport) DistributionHealth {
|
||||
func rustAttestationHealth(report *backendrpc.DistributionAttestation) DistributionHealth {
|
||||
health := DistributionHealth{
|
||||
Source: "rust_release_status",
|
||||
Status: "blocked",
|
||||
StatusCode: "distribution.blocked",
|
||||
IntegrityStatus: "unknown",
|
||||
Source: "rust_release_attestation",
|
||||
Status: "unavailable",
|
||||
StatusCode: "distribution.attestation_unavailable",
|
||||
IntegrityStatus: "unavailable",
|
||||
}
|
||||
if report == nil {
|
||||
return health
|
||||
}
|
||||
health.Ready = report.OfficialDistributionReady
|
||||
health.Ready = report.Ready
|
||||
health.Channel = report.Channel
|
||||
health.ReleaseID = report.ReleaseID
|
||||
health.ResourceRoot = report.ResourceRoot
|
||||
health.PublicationIdentity = report.PublicationIdentity
|
||||
health.MappingIdentity = report.MappingIdentity
|
||||
health.ManifestIdentity = report.ManifestIdentity
|
||||
health.EntryCount = report.EntryCount
|
||||
health.VerificationGeneration = report.VerificationGeneration
|
||||
health.VerifiedAt = report.VerifiedAt
|
||||
health.Status = report.Status
|
||||
health.StatusCode = report.StatusCode
|
||||
health.IntegrityStatus = report.IntegrityStatus
|
||||
health.Diagnostics = append([]string(nil), report.Diagnostics...)
|
||||
if health.Status == "" {
|
||||
if health.Ready {
|
||||
health.Status = "ready"
|
||||
} else {
|
||||
health.Status = "blocked"
|
||||
}
|
||||
health.Status = "unavailable"
|
||||
}
|
||||
if health.StatusCode == "" {
|
||||
if health.Ready {
|
||||
health.StatusCode = "distribution.ready"
|
||||
} else {
|
||||
health.StatusCode = "distribution.blocked"
|
||||
}
|
||||
health.StatusCode = "distribution.attestation_unavailable"
|
||||
}
|
||||
for _, release := range report.Releases {
|
||||
if release.Channel == "official" && release.Current {
|
||||
health.IntegrityStatus = release.DistributionIntegrityStatus
|
||||
health.Diagnostics = append([]string(nil), release.Diagnostics...)
|
||||
break
|
||||
}
|
||||
}
|
||||
if health.IntegrityStatus == "valid" && !health.Ready {
|
||||
health.IntegrityStatus = "invalid"
|
||||
if health.IntegrityStatus == "" {
|
||||
health.IntegrityStatus = "unavailable"
|
||||
}
|
||||
return health
|
||||
}
|
||||
|
||||
func unavailableRustDistributionHealth() DistributionHealth {
|
||||
return DistributionHealth{
|
||||
Source: "rust_release_status",
|
||||
Source: "rust_release_attestation",
|
||||
Status: "unavailable",
|
||||
StatusCode: "distribution.health_unavailable",
|
||||
IntegrityStatus: "unknown",
|
||||
StatusCode: "distribution.attestation_unavailable",
|
||||
IntegrityStatus: "unavailable",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -675,25 +712,85 @@ func parseCatalogAvailability(raw json.RawMessage) (bool, bool) {
|
||||
return *payload.Available, true
|
||||
}
|
||||
|
||||
func fetchAllManifestEntries(ctx context.Context, backend Backend) ([]manifestEntry, int, string, error) {
|
||||
const pageSize = 500
|
||||
func fetchAllManifestEntries(
|
||||
ctx context.Context,
|
||||
backend Backend,
|
||||
attestation *backendrpc.DistributionAttestation,
|
||||
) ([]manifestEntry, int, string, error) {
|
||||
return fetchAllManifestEntriesWithPageSize(ctx, backend, attestation, 500)
|
||||
}
|
||||
|
||||
func fetchAllManifestEntriesWithPageSize(
|
||||
ctx context.Context,
|
||||
backend Backend,
|
||||
attestation *backendrpc.DistributionAttestation,
|
||||
pageSize int,
|
||||
) ([]manifestEntry, int, string, error) {
|
||||
if pageSize <= 0 {
|
||||
return nil, 0, "", fmt.Errorf("manifest page size must be positive")
|
||||
}
|
||||
offset := 0
|
||||
var all []manifestEntry
|
||||
var version int
|
||||
var root string
|
||||
var root, releaseID, publicationIdentity, mappingIdentity, manifestIdentity string
|
||||
total := -1
|
||||
for {
|
||||
page, err := backend.ResourceManifest(ctx, offset, pageSize)
|
||||
page, err := backend.ResourceManifest(ctx, backendrpc.ResourceManifestParams{
|
||||
ReleaseID: attestation.ReleaseID,
|
||||
ExpectedPublicationIdentity: attestation.PublicationIdentity,
|
||||
ExpectedManifestIdentity: attestation.ManifestIdentity,
|
||||
Offset: offset,
|
||||
Limit: pageSize,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, "", err
|
||||
}
|
||||
if page == nil {
|
||||
return nil, 0, "", fmt.Errorf("resource.manifest returned nil page")
|
||||
}
|
||||
if !page.Available {
|
||||
return nil, 0, "", fmt.Errorf("resource.manifest available=false")
|
||||
}
|
||||
if root == "" {
|
||||
root = page.ResourceRoot
|
||||
}
|
||||
if version == 0 {
|
||||
releaseID = page.ReleaseID
|
||||
publicationIdentity = page.PublicationIdentity
|
||||
mappingIdentity = page.MappingIdentity
|
||||
manifestIdentity = page.ManifestIdentity
|
||||
version = page.ManifestVersion
|
||||
total = page.TotalEntries
|
||||
} else if page.ResourceRoot != root ||
|
||||
page.ReleaseID != releaseID ||
|
||||
page.PublicationIdentity != publicationIdentity ||
|
||||
page.MappingIdentity != mappingIdentity ||
|
||||
page.ManifestIdentity != manifestIdentity ||
|
||||
page.ManifestVersion != version ||
|
||||
page.TotalEntries != total {
|
||||
return nil, 0, "", fmt.Errorf("resource.manifest page identity or total changed")
|
||||
}
|
||||
if page.Offset != offset {
|
||||
return nil, 0, "", fmt.Errorf(
|
||||
"resource.manifest page offset mismatch: requested=%d actual=%d",
|
||||
offset,
|
||||
page.Offset,
|
||||
)
|
||||
}
|
||||
if page.Limit <= 0 || page.Limit > pageSize {
|
||||
return nil, 0, "", fmt.Errorf("resource.manifest page limit is unreasonable: %d", page.Limit)
|
||||
}
|
||||
if page.TotalEntries < 0 || len(page.Entries) > page.Limit {
|
||||
return nil, 0, "", fmt.Errorf("resource.manifest page entry count is unreasonable")
|
||||
}
|
||||
if total < 0 || offset > total || offset+len(page.Entries) > total {
|
||||
return nil, 0, "", fmt.Errorf("resource.manifest page exceeds declared total")
|
||||
}
|
||||
if attestation != nil && (releaseID != attestation.ReleaseID ||
|
||||
root != attestation.ResourceRoot ||
|
||||
publicationIdentity != attestation.PublicationIdentity ||
|
||||
mappingIdentity != attestation.MappingIdentity ||
|
||||
manifestIdentity != attestation.ManifestIdentity ||
|
||||
total != attestation.EntryCount) {
|
||||
return nil, 0, "", fmt.Errorf("resource.manifest page does not match attestation")
|
||||
}
|
||||
for _, e := range page.Entries {
|
||||
var bytes uint64
|
||||
@@ -708,9 +805,12 @@ func fetchAllManifestEntries(ctx context.Context, backend Backend) ([]manifestEn
|
||||
})
|
||||
}
|
||||
offset += len(page.Entries)
|
||||
if len(page.Entries) == 0 || offset >= page.TotalEntries {
|
||||
if offset == total {
|
||||
break
|
||||
}
|
||||
if len(page.Entries) == 0 || len(page.Entries) < page.Limit {
|
||||
return nil, 0, "", fmt.Errorf("resource.manifest page has a gap before total")
|
||||
}
|
||||
}
|
||||
return all, version, root, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user