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-15 21:13:52 +08:00
parent 13b0bd5b45
commit 99355effe4
29 changed files with 1137 additions and 189 deletions
+10 -1
View File
@@ -279,7 +279,7 @@ func TestRefreshCurrentReleaseHealthTransitionsAndClearsFailure(t *testing.T) {
t.Fatal(err)
}
if !server.index().Summary().Ready {
t.Fatal("initial release is not ready")
t.Fatalf("initial release is not ready: %+v", server.index().Summary())
}
get := func() *httptest.ResponseRecorder {
recorder := httptest.NewRecorder()
@@ -917,6 +917,10 @@ func (f *fakeBackend) ReleaseAttestation(ctx context.Context) (*backendrpc.Distr
break
}
}
if integrity == "valid" {
integrity = "verified"
}
verifiedAt := uint64(time.Now().Unix())
return &backendrpc.DistributionAttestation{
Available: true,
Channel: "official",
@@ -936,6 +940,8 @@ func (f *fakeBackend) ReleaseAttestation(ctx context.Context) (*backendrpc.Distr
StatusCode: releaseStatus.StatusCode,
Ready: releaseStatus.OfficialDistributionReady,
VerificationGeneration: 1,
VerifiedAt: &verifiedAt,
MaxAgeSeconds: 7260,
}, nil
}
func (f *fakeBackend) ResourceState(ctx context.Context) (*backendrpc.ResourceState, error) {
@@ -953,6 +959,9 @@ func (f *fakeBackend) ResourceManifest(ctx context.Context, params backendrpc.Re
}
f.manifestParams = append(f.manifestParams, params)
page := *f.manifest
if page.Channel == "" {
page.Channel = "official"
}
if page.ReleaseID == "" {
page.ReleaseID = params.ReleaseID
}
+1
View File
@@ -99,6 +99,7 @@ func TestRustContractFixturesPreserveGoMirror(t *testing.T) {
attestation.ReleaseID != "${VERSION_ID}" ||
attestation.ManifestIdentity != "${MANIFEST_IDENTITY}" ||
attestation.VerificationGeneration != 7 ||
attestation.MaxAgeSeconds != 7260 ||
attestation.VerifiedAt == nil || *attestation.VerifiedAt != 1000 {
t.Fatalf("attestation=%+v", attestation)
}
+45 -1
View File
@@ -4,11 +4,13 @@ import (
"context"
"encoding/json"
"testing"
"time"
"bat-api/internal/backendrpc"
)
func testAttestation(root, releaseID, integrity string, ready bool) *backendrpc.DistributionAttestation {
verifiedAt := uint64(time.Now().Unix())
return &backendrpc.DistributionAttestation{
Available: true,
Channel: "official",
@@ -23,6 +25,8 @@ func testAttestation(root, releaseID, integrity string, ready bool) *backendrpc.
StatusCode: "distribution." + integrity,
Ready: ready,
VerificationGeneration: 4,
VerifiedAt: &verifiedAt,
MaxAgeSeconds: 7260,
}
}
@@ -85,10 +89,30 @@ func TestFetchAllManifestEntriesRejectsMixedPages(t *testing.T) {
name: "manifest identity",
mutate: func(page *backendrpc.ResourceManifestPage) { page.ManifestIdentity = "manifest-b" },
},
{
name: "generation",
mutate: func(page *backendrpc.ResourceManifestPage) { page.Generation = 5 },
},
{
name: "publication identity",
mutate: func(page *backendrpc.ResourceManifestPage) { page.PublicationIdentity = "publication-b" },
},
{
name: "mapping identity",
mutate: func(page *backendrpc.ResourceManifestPage) { page.MappingIdentity = "mapping-b" },
},
{
name: "manifest version",
mutate: func(page *backendrpc.ResourceManifestPage) { page.ManifestVersion = 2 },
},
{
name: "total",
mutate: func(page *backendrpc.ResourceManifestPage) { page.TotalEntries = 4 },
},
{
name: "limit",
mutate: func(page *backendrpc.ResourceManifestPage) { page.Limit = 1 },
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
@@ -134,7 +158,8 @@ func TestFetchAllManifestEntriesAcceptsMatchingGeneration(t *testing.T) {
}
if len(backend.params) != 2 ||
backend.params[1].ReleaseID != attestation.ReleaseID ||
backend.params[1].ExpectedManifestIdentity != attestation.ManifestIdentity {
backend.params[1].ExpectedManifestIdentity != attestation.ManifestIdentity ||
backend.params[1].ExpectedVerificationGeneration != attestation.VerificationGeneration {
t.Fatalf("params=%+v", backend.params)
}
}
@@ -239,3 +264,22 @@ func TestStaleOrInvalidMatchingAttestationNeverReadiesIndex(t *testing.T) {
})
}
}
func TestExpiredReadyAttestationNeverReadiesIndex(t *testing.T) {
root := fixtureRoot(t)
backend := fixtureRPCBackend(t, root)
attestation := testAttestation(root, "official-fixture", "verified", true)
expired := uint64(time.Now().Unix()) - attestation.MaxAgeSeconds - 1
attestation.VerifiedAt = &expired
backend.attestation = attestation
result, err := DiscoverAndIndex(context.Background(), backend, "")
if err != nil {
t.Fatal(err)
}
if result.Index == nil || result.Index.Summary().Ready {
t.Fatalf("expired attestation unexpectedly ready: %+v", result.Index.Summary())
}
if len(backend.manifestParams) != 0 {
t.Fatalf("manifest should not be fetched for expired attestation: %+v", backend.manifestParams)
}
}
+5 -1
View File
@@ -61,6 +61,7 @@ type GameMainConfigSummary struct {
// attestation. The local manifest checks only establish that this process has
// a complete, safe read snapshot; they do not replace Rust's verifier.
type DistributionHealth struct {
Available bool `json:"available"`
Ready bool `json:"ready"`
Source string `json:"source"`
Channel string `json:"channel,omitempty"`
@@ -72,6 +73,7 @@ type DistributionHealth struct {
EntryCount int `json:"entry_count,omitempty"`
VerificationGeneration uint64 `json:"verification_generation,omitempty"`
VerifiedAt *uint64 `json:"verified_at,omitempty"`
MaxAgeSeconds uint64 `json:"max_age_seconds,omitempty"`
Status string `json:"status,omitempty"`
StatusCode string `json:"status_code,omitempty"`
IntegrityStatus string `json:"integrity_status,omitempty"`
@@ -130,7 +132,8 @@ func (idx *ReleaseIndex) Summary() ReleaseSummary {
if distribution.Source == "" {
distribution.Ready = localComplete && idx.Source != "rpc" && idx.Source != "rpc+local_manifest"
} else {
distribution.Ready = distribution.Ready &&
distribution.Ready = distribution.Available &&
distribution.Ready &&
localComplete &&
(idx.Source != "rpc" ||
(distribution.ManifestIdentity != "" &&
@@ -310,6 +313,7 @@ func LoadIndexFromResourceRoot(resourceRoot string) (*ReleaseIndex, error) {
manifest.Version,
entries,
DistributionHealth{
Available: true,
Ready: true,
Source: "resource_root_override",
Status: "ready",
+65 -13
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"os"
"path/filepath"
"time"
"bat-api/internal/backendrpc"
)
@@ -417,8 +418,17 @@ func DiscoverAndIndex(ctx context.Context, backend Backend, resourceRootOverride
}
out.Attestation = attestation
out.Distribution = rustAttestationHealth(attestation)
if !attestation.Available {
return emptyRPCResult(out, nil, "Rust current official distribution attestation is unavailable"), nil
if !attestation.Available ||
!attestation.Ready ||
attestation.Channel != "official" ||
attestation.IntegrityStatus != "verified" ||
attestation.VerificationGeneration == 0 ||
!attestationIsFresh(attestation) {
return emptyRPCResult(
out,
nil,
"Rust current official distribution attestation is unavailable or not ready",
), nil
}
// Catalog / resource discovery
@@ -604,6 +614,7 @@ func rustAttestationHealth(report *backendrpc.DistributionAttestation) Distribut
if report == nil {
return health
}
health.Available = report.Available
health.Ready = report.Ready
health.Channel = report.Channel
health.ReleaseID = report.ReleaseID
@@ -614,6 +625,7 @@ func rustAttestationHealth(report *backendrpc.DistributionAttestation) Distribut
health.EntryCount = report.EntryCount
health.VerificationGeneration = report.VerificationGeneration
health.VerifiedAt = report.VerifiedAt
health.MaxAgeSeconds = report.MaxAgeSeconds
health.Status = report.Status
health.StatusCode = report.StatusCode
health.IntegrityStatus = report.IntegrityStatus
@@ -639,6 +651,18 @@ func unavailableRustDistributionHealth() DistributionHealth {
}
}
func attestationIsFresh(attestation *backendrpc.DistributionAttestation) bool {
if attestation == nil || attestation.MaxAgeSeconds == 0 || attestation.VerifiedAt == nil {
return false
}
now := uint64(time.Now().Unix())
age := uint64(0)
if now > *attestation.VerifiedAt {
age = now - *attestation.VerifiedAt
}
return age <= attestation.MaxAgeSeconds
}
func snapshotWithDistributionHealth(snapshot *SnapshotSummary, health DistributionHealth) *SnapshotSummary {
if snapshot == nil {
return nil
@@ -729,18 +753,31 @@ func fetchAllManifestEntriesWithPageSize(
if pageSize <= 0 {
return nil, 0, "", fmt.Errorf("manifest page size must be positive")
}
if attestation == nil {
return nil, 0, "", fmt.Errorf("resource.manifest requires a Rust attestation")
}
if !attestation.Available ||
!attestation.Ready ||
attestation.Channel != "official" ||
attestation.IntegrityStatus != "verified" ||
attestation.VerificationGeneration == 0 ||
!attestationIsFresh(attestation) {
return nil, 0, "", fmt.Errorf("resource.manifest attestation is not ready")
}
offset := 0
var all []manifestEntry
var version int
var root, releaseID, publicationIdentity, mappingIdentity, manifestIdentity string
var version, limit int
var channel, root, releaseID, publicationIdentity, mappingIdentity, manifestIdentity string
var generation uint64
total := -1
for {
page, err := backend.ResourceManifest(ctx, backendrpc.ResourceManifestParams{
ReleaseID: attestation.ReleaseID,
ExpectedPublicationIdentity: attestation.PublicationIdentity,
ExpectedManifestIdentity: attestation.ManifestIdentity,
Offset: offset,
Limit: pageSize,
ReleaseID: attestation.ReleaseID,
ExpectedPublicationIdentity: attestation.PublicationIdentity,
ExpectedManifestIdentity: attestation.ManifestIdentity,
ExpectedVerificationGeneration: attestation.VerificationGeneration,
Offset: offset,
Limit: pageSize,
})
if err != nil {
return nil, 0, "", err
@@ -753,19 +790,25 @@ func fetchAllManifestEntriesWithPageSize(
}
if root == "" {
root = page.ResourceRoot
channel = page.Channel
releaseID = page.ReleaseID
publicationIdentity = page.PublicationIdentity
mappingIdentity = page.MappingIdentity
manifestIdentity = page.ManifestIdentity
generation = page.Generation
version = page.ManifestVersion
total = page.TotalEntries
limit = page.Limit
} else if page.ResourceRoot != root ||
page.Channel != channel ||
page.ReleaseID != releaseID ||
page.PublicationIdentity != publicationIdentity ||
page.MappingIdentity != mappingIdentity ||
page.ManifestIdentity != manifestIdentity ||
page.Generation != generation ||
page.ManifestVersion != version ||
page.TotalEntries != total {
page.TotalEntries != total ||
page.Limit != limit {
return nil, 0, "", fmt.Errorf("resource.manifest page identity or total changed")
}
if page.Offset != offset {
@@ -775,7 +818,7 @@ func fetchAllManifestEntriesWithPageSize(
page.Offset,
)
}
if page.Limit <= 0 || page.Limit > pageSize {
if 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 {
@@ -784,12 +827,14 @@ func fetchAllManifestEntriesWithPageSize(
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 ||
if channel != attestation.Channel ||
releaseID != attestation.ReleaseID ||
root != attestation.ResourceRoot ||
publicationIdentity != attestation.PublicationIdentity ||
mappingIdentity != attestation.MappingIdentity ||
manifestIdentity != attestation.ManifestIdentity ||
total != attestation.EntryCount) {
generation != attestation.VerificationGeneration ||
total != attestation.EntryCount {
return nil, 0, "", fmt.Errorf("resource.manifest page does not match attestation")
}
for _, e := range page.Entries {
@@ -812,5 +857,12 @@ func fetchAllManifestEntriesWithPageSize(
return nil, 0, "", fmt.Errorf("resource.manifest page has a gap before total")
}
}
if len(all) != total {
return nil, 0, "", fmt.Errorf(
"resource.manifest final entry count mismatch: entries=%d total=%d",
len(all),
total,
)
}
return all, version, root, nil
}
+4 -2
View File
@@ -8,8 +8,10 @@ contract fixture。JSON 由 Rust 代码路径产出后归一化,只替换本
- `catalog.status` 可用与不可用响应。
- `resource.manifest` 第一页分页响应,包含 release/publication/mapping/manifest
identity 和 generation 绑定字段
- `release.attestation` 当前 official health/publication proof
identity 和 generation 绑定字段;生产请求必须回传 attestation 的
`expected_verification_generation`
- `release.attestation` 当前 official health/publication proof,默认 fixture freshness
window 为 7260 秒(默认 3600 秒验证周期和 60 秒失败重试周期)。
- 对应 release 的 `official-sync-snapshot.json`
- `launcher_metadata``game_main_config_bootstrap` 的 Go mirror 解码。
- Rust Glossary V1 query 响应,覆盖 alias、approved review、source provenance 和完整 history。
+1 -1
View File
@@ -6,7 +6,7 @@
"integrity_status": "verified",
"manifest_identity": "${MANIFEST_IDENTITY}",
"mapping_identity": "${MAPPING_IDENTITY}",
"max_age_seconds": 900,
"max_age_seconds": 7260,
"publication_identity": "${PUBLICATION_IDENTITY}",
"ready": true,
"release_id": "${VERSION_ID}",