mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 06:34:54 +08:00
fix(release): 完成分发证明代际绑定与质量门禁收口
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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}",
|
||||
|
||||
@@ -879,11 +879,12 @@ type ResourceManifestEntry struct {
|
||||
|
||||
// ResourceManifestParams binds every page to one attested official release.
|
||||
type ResourceManifestParams struct {
|
||||
ReleaseID string `json:"release_id,omitempty"`
|
||||
ExpectedPublicationIdentity string `json:"expected_publication_identity,omitempty"`
|
||||
ExpectedManifestIdentity string `json:"expected_manifest_identity,omitempty"`
|
||||
Offset int `json:"offset"`
|
||||
Limit int `json:"limit"`
|
||||
ReleaseID string `json:"release_id,omitempty"`
|
||||
ExpectedPublicationIdentity string `json:"expected_publication_identity,omitempty"`
|
||||
ExpectedManifestIdentity string `json:"expected_manifest_identity,omitempty"`
|
||||
ExpectedVerificationGeneration uint64 `json:"expected_verification_generation"`
|
||||
Offset int `json:"offset"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
type ResourceManifestPage struct {
|
||||
@@ -895,7 +896,7 @@ type ResourceManifestPage struct {
|
||||
PublicationIdentity string `json:"publication_identity,omitempty"`
|
||||
MappingIdentity string `json:"mapping_identity,omitempty"`
|
||||
ManifestIdentity string `json:"manifest_identity,omitempty"`
|
||||
Generation uint64 `json:"generation,omitempty"`
|
||||
Generation uint64 `json:"generation"`
|
||||
TotalEntries int `json:"total_entries,omitempty"`
|
||||
Offset int `json:"offset,omitempty"`
|
||||
Limit int `json:"limit,omitempty"`
|
||||
|
||||
@@ -273,7 +273,7 @@ func TestReleaseAttestationMirrorsCurrentOfficialHealth(t *testing.T) {
|
||||
"ready": true,
|
||||
"verification_generation": 7,
|
||||
"verified_at": 1234,
|
||||
"max_age_seconds": 900,
|
||||
"max_age_seconds": 7260,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -302,6 +302,7 @@ func TestResourceManifestSendsAttestedGenerationParams(t *testing.T) {
|
||||
if params.ReleaseID != "official-a" ||
|
||||
params.ExpectedPublicationIdentity != "odp-v1-publication-a" ||
|
||||
params.ExpectedManifestIdentity != "manifest-a" ||
|
||||
params.ExpectedVerificationGeneration != 7 ||
|
||||
params.Offset != 1 || params.Limit != 100 {
|
||||
t.Fatalf("params = %#v", params)
|
||||
}
|
||||
@@ -329,11 +330,12 @@ func TestResourceManifestSendsAttestedGenerationParams(t *testing.T) {
|
||||
})
|
||||
|
||||
page, err := client.ResourceManifest(context.Background(), ResourceManifestParams{
|
||||
ReleaseID: "official-a",
|
||||
ExpectedPublicationIdentity: "odp-v1-publication-a",
|
||||
ExpectedManifestIdentity: "manifest-a",
|
||||
Offset: 1,
|
||||
Limit: 100,
|
||||
ReleaseID: "official-a",
|
||||
ExpectedPublicationIdentity: "odp-v1-publication-a",
|
||||
ExpectedManifestIdentity: "manifest-a",
|
||||
ExpectedVerificationGeneration: 7,
|
||||
Offset: 1,
|
||||
Limit: 100,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ResourceManifest error: %v", err)
|
||||
|
||||
Reference in New Issue
Block a user