package firmwareconformance import ( "bytes" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "io" "regexp" "strings" "time" "github.com/google/uuid" ) const ( DeviceEvidenceSchema = "firmware-device-evidence-v1" ReportSchema = "firmware-conformance-report-v1" ) var digestPattern = regexp.MustCompile(`^[0-9a-f]{64}$`) type CloudRollout struct { ID string `json:"id"` ReleaseID string `json:"release_id"` Status string `json:"status"` Assignments []CloudAssignment `json:"assignments"` } type CloudAssignment struct { DeviceID string `json:"device_id"` DeviceModel string `json:"device_model"` PreviousVersion string `json:"previous_version"` Status string `json:"status"` Events []CloudEvent `json:"events"` } type CloudEvent struct { ID string `json:"id"` CommandID string `json:"command_id"` CommandStatus string `json:"command_status"` AssignmentStatus string `json:"assignment_status"` Phase string `json:"phase"` ProgressPercent *int `json:"progress_percent"` ResultCode string `json:"result_code"` ActiveSlot string `json:"active_slot"` TargetSlot string `json:"target_slot"` PreviousSlot string `json:"previous_slot"` FailedSlot string `json:"failed_slot"` BootAttemptID string `json:"boot_attempt_id"` RollbackApplied *bool `json:"rollback_applied"` RollbackVersion string `json:"rollback_version"` InstalledVersion string `json:"installed_version"` VerifiedSHA256 string `json:"verified_sha256"` VerifiedSigningKeyID string `json:"verified_signing_key_id"` ResultDigest string `json:"result_digest"` ReportedAt time.Time `json:"reported_at"` } type DeviceEvidence struct { SchemaVersion string `json:"schema_version"` EvidenceSource string `json:"evidence_source"` CandidateVersion string `json:"candidate_version"` BuildDigest string `json:"build_digest"` DeviceID string `json:"device_id"` DeviceModel string `json:"device_model"` HardwareRevision string `json:"hardware_revision"` ReleaseID string `json:"release_id"` CommandID string `json:"command_id"` Scenario string `json:"scenario"` BootloaderVersion string `json:"bootloader_version"` PartitionTableSHA256 string `json:"partition_table_sha256"` SecureBootEnabled bool `json:"secure_boot_enabled"` ArtifactSHA256 string `json:"artifact_sha256"` SigningKeyID string `json:"signing_key_id"` InitialSlot string `json:"initial_slot"` TargetSlot string `json:"target_slot"` FinalSlot string `json:"final_slot"` InitialVersion string `json:"initial_version"` TargetVersion string `json:"target_version"` FinalVersion string `json:"final_version"` BootAttemptID string `json:"boot_attempt_id"` CollectedAt time.Time `json:"collected_at"` Events []DeviceEvent `json:"events"` } type DeviceEvent struct { Phase string `json:"phase"` OccurredAt time.Time `json:"occurred_at"` ActiveSlot string `json:"active_slot,omitempty"` TargetSlot string `json:"target_slot,omitempty"` ResultCode string `json:"result_code,omitempty"` BootAttemptID string `json:"boot_attempt_id,omitempty"` } type Check struct { Code string `json:"code"` Status string `json:"status"` Detail string `json:"detail"` } type Report struct { SchemaVersion string `json:"schema_version"` Result string `json:"result"` GeneratedAt time.Time `json:"generated_at"` CandidateVersion string `json:"candidate_version"` BuildDigest string `json:"build_digest"` EvidenceScope string `json:"evidence_scope"` ExternalValidationRequired bool `json:"external_validation_required"` DeviceID string `json:"device_id"` ReleaseID string `json:"release_id"` RolloutID string `json:"rollout_id"` CommandID string `json:"command_id"` Scenario string `json:"scenario"` CloudEventCount int `json:"cloud_event_count"` DeviceEventCount int `json:"device_event_count"` InputSHA256 map[string]string `json:"input_sha256"` Checks []Check `json:"checks"` Disclaimer string `json:"disclaimer"` } func ParseCloud(content []byte) (CloudRollout, error) { var envelope struct { Data json.RawMessage `json:"data"` } if err := json.Unmarshal(content, &envelope); err != nil { return CloudRollout{}, fmt.Errorf("decode cloud rollout: %w", err) } payload := content if len(envelope.Data) > 0 && string(envelope.Data) != "null" { payload = envelope.Data } var rollout CloudRollout if err := json.Unmarshal(payload, &rollout); err != nil { return CloudRollout{}, fmt.Errorf("decode cloud rollout data: %w", err) } if rollout.ID == "" || rollout.ReleaseID == "" { return CloudRollout{}, errors.New("cloud rollout is missing id or release_id") } return rollout, nil } func ParseDeviceEvidence(content []byte) (DeviceEvidence, error) { var evidence DeviceEvidence decoder := json.NewDecoder(bytes.NewReader(content)) decoder.DisallowUnknownFields() if err := decoder.Decode(&evidence); err != nil { return DeviceEvidence{}, fmt.Errorf("decode device evidence: %w", err) } var trailing any if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) { return DeviceEvidence{}, errors.New("device evidence contains trailing JSON data") } return evidence, nil } // ParseReport reads the exact report format emitted by cmd/firmware-conformance. // Strict decoding prevents an unreviewed producer extension from being treated as // release evidence by downstream packaging tools. func ParseReport(content []byte) (Report, error) { var report Report decoder := json.NewDecoder(bytes.NewReader(content)) decoder.DisallowUnknownFields() if err := decoder.Decode(&report); err != nil { return Report{}, fmt.Errorf("decode firmware conformance report: %w", err) } var trailing any if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) { return Report{}, errors.New("firmware conformance report contains trailing JSON data") } return report, nil } func Evaluate(cloud CloudRollout, device DeviceEvidence, generatedAt time.Time) Report { report := Report{ SchemaVersion: ReportSchema, Result: "PASS", GeneratedAt: generatedAt.UTC(), CandidateVersion: device.CandidateVersion, BuildDigest: device.BuildDigest, EvidenceScope: "PROTOCOL_SIMULATION", ExternalValidationRequired: true, DeviceID: device.DeviceID, ReleaseID: device.ReleaseID, RolloutID: cloud.ID, CommandID: device.CommandID, Scenario: device.Scenario, CloudEventCount: 0, DeviceEventCount: len(device.Events), InputSHA256: map[string]string{}, Checks: []Check{}, Disclaimer: "一致性通过只证明所提供设备证据与云端固件时间线相互匹配;不替代真实Bootloader、闪存掉电一致性、硬件安全或生产发布验收。", } if device.EvidenceSource == "HARDWARE" { report.EvidenceScope = "HARDWARE_CAPTURE" } add := func(code string, ok bool, detail string) { status := "PASS" if !ok { status = "FAIL" report.Result = "FAIL" } report.Checks = append(report.Checks, Check{Code: code, Status: status, Detail: detail}) } add("DEVICE_EVIDENCE_SCHEMA", device.SchemaVersion == DeviceEvidenceSchema, "设备证据必须使用"+DeviceEvidenceSchema) add("EVIDENCE_SOURCE", device.EvidenceSource == "SIMULATOR" || device.EvidenceSource == "HARDWARE", "证据来源必须明确为SIMULATOR或HARDWARE") buildOK := strings.TrimSpace(device.CandidateVersion) != "" && strings.HasPrefix(device.BuildDigest, "sha256:") && digestPattern.MatchString(strings.TrimPrefix(device.BuildDigest, "sha256:")) add("CANDIDATE_IDENTITY", buildOK, "候选版本和sha256构建摘要必须完整") identityOK := validUUID(device.DeviceID) && validUUID(device.ReleaseID) && validUUID(device.CommandID) && device.DeviceModel != "" && device.HardwareRevision != "" add("DEVICE_IDENTITY", identityOK, "设备、发布和命令必须使用公开UUID,并提供型号与硬件版本") platformOK := device.BootloaderVersion != "" && digestPattern.MatchString(device.PartitionTableSHA256) add("DEVICE_PLATFORM_METADATA", platformOK, fmt.Sprintf("Bootloader=%s,安全启动=%t,分区表摘要必须为SHA-256", device.BootloaderVersion, device.SecureBootEnabled)) artifactOK := digestPattern.MatchString(device.ArtifactSHA256) && device.SigningKeyID != "" add("ARTIFACT_IDENTITY", artifactOK, "设备证据必须绑定完整制品摘要和签名密钥编号") assignment, found := findAssignment(cloud.Assignments, device.DeviceID) add("CLOUD_ASSIGNMENT", found, "云端批次必须包含目标设备且只能匹配一条分配") add("RELEASE_BINDING", cloud.ReleaseID == device.ReleaseID, "设备证据发布编号必须与云端批次一致") if found { report.CloudEventCount = len(assignment.Events) } cloudArtifactBound := false if found { for _, event := range assignment.Events { if event.VerifiedSHA256 == device.ArtifactSHA256 && event.VerifiedSigningKeyID == device.SigningKeyID { cloudArtifactBound = true break } } } add("CLOUD_ARTIFACT_BINDING", cloudArtifactBound, "云端阶段时间线必须包含与设备一致的已验证摘要和签名密钥") commandBound := found && len(assignment.Events) > 0 if commandBound { for _, event := range assignment.Events { if event.CommandID != device.CommandID { commandBound = false break } } } add("COMMAND_BINDING", commandBound, "全部云端阶段事件必须绑定同一设备命令") cloudTimelineOK := found && validateCloudTimeline(assignment.Events) add("CLOUD_TIMELINE_INTEGRITY", cloudTimelineOK, "事件编号、结果摘要哈希、接收时间和顺序必须有效") deviceTimelineOK := validateDeviceTimeline(device.Events, device.CollectedAt) add("DEVICE_TIMELINE_INTEGRITY", deviceTimelineOK, "设备事件必须按时间升序且不晚于证据采集时间") terminalPhase, scenarioOK := scenarioTerminalPhase(device.Scenario) add("SCENARIO_SUPPORTED", scenarioOK, "场景必须为AB_SUCCESS、AB_ROLLBACK、POWER_LOSS_AFTER_STAGE或POWER_LOSS_DURING_BOOT") required := []string{"staging", "reboot_pending", "boot_verifying", terminalPhase} cloudPhasesOK := scenarioOK && found && containsPhaseSequence(cloudPhases(assignment.Events), required) deviceRequired := append([]string{}, required...) if device.Scenario == "POWER_LOSS_AFTER_STAGE" { deviceRequired = []string{"staging", "power_loss_after_stage", "reboot_pending", "boot_verifying", terminalPhase} } if device.Scenario == "POWER_LOSS_DURING_BOOT" { deviceRequired = []string{"staging", "reboot_pending", "boot_verifying", "power_loss_during_boot", "boot_verifying", terminalPhase} } devicePhasesOK := scenarioOK && containsPhaseSequence(devicePhases(device.Events), deviceRequired) add("CLOUD_PHASE_SEQUENCE", cloudPhasesOK, "云端必须按顺序留存写入、待重启、健康确认和终态") add("DEVICE_PHASE_SEQUENCE", devicePhasesOK, "设备侧必须提供与场景一致的本地阶段顺序") bootAttemptOK := found && device.BootAttemptID != "" if bootAttemptOK { seen := false for _, event := range assignment.Events { if event.BootAttemptID == "" { continue } seen = true if event.BootAttemptID != device.BootAttemptID { bootAttemptOK = false } } bootAttemptOK = bootAttemptOK && seen } add("BOOT_ATTEMPT_BINDING", bootAttemptOK, "设备和云端必须引用同一启动尝试编号") phaseTimeOK, maxSkew := phaseTimeCorrelation(assignment.Events, device.Events) add("PHASE_TIME_CORRELATION", found && phaseTimeOK, fmt.Sprintf("同名设备/云端阶段最大时间差为%s,允许10分钟", maxSkew.Round(time.Second))) slotOK := device.InitialSlot != "" && device.TargetSlot != "" && device.InitialSlot != device.TargetSlot versionOK := device.InitialVersion != "" && device.TargetVersion != "" && device.FinalVersion != "" outcomeOK := false if found && scenarioOK && len(assignment.Events) > 0 { finalEvent := assignment.Events[len(assignment.Events)-1] if terminalPhase == "rolled_back" { outcomeOK = assignment.Status == "FAILED" && finalEvent.CommandStatus == "FAILED" && finalEvent.Phase == "rolled_back" && finalEvent.RollbackApplied != nil && *finalEvent.RollbackApplied && finalEvent.RollbackVersion == device.InitialVersion && device.FinalSlot == device.InitialSlot && device.FinalVersion == device.InitialVersion slotOK = slotOK && device.FinalSlot == device.InitialSlot versionOK = versionOK && device.FinalVersion == device.InitialVersion } else { outcomeOK = assignment.Status == "CONFIRMED" && finalEvent.CommandStatus == "SUCCEEDED" && finalEvent.Phase == "confirmed" && finalEvent.InstalledVersion == device.TargetVersion && finalEvent.VerifiedSHA256 == device.ArtifactSHA256 && finalEvent.VerifiedSigningKeyID == device.SigningKeyID && device.FinalSlot == device.TargetSlot && device.FinalVersion == device.TargetVersion slotOK = slotOK && device.FinalSlot == device.TargetSlot versionOK = versionOK && device.FinalVersion == device.TargetVersion } } add("SLOT_TRANSITION", slotOK, "目标分区必须不同于初始分区,成功或回退后的活动分区必须符合场景") add("VERSION_TRANSITION", versionOK, "最终版本必须与成功升级或自动回退结果一致") add("TERMINAL_OUTCOME", outcomeOK, "云端命令、分配终态、设备分区和最终版本必须一致") return report } func SHA256(content []byte) string { digest := sha256.Sum256(content) return hex.EncodeToString(digest[:]) } func validUUID(value string) bool { _, err := uuid.Parse(value) return err == nil } func findAssignment(items []CloudAssignment, deviceID string) (CloudAssignment, bool) { var result CloudAssignment count := 0 for _, item := range items { if item.DeviceID == deviceID { result = item count++ } } return result, count == 1 } func validateCloudTimeline(events []CloudEvent) bool { if len(events) == 0 { return false } for index, event := range events { if !validUUID(event.ID) || !validUUID(event.CommandID) || !digestPattern.MatchString(event.ResultDigest) || event.ReportedAt.IsZero() { return false } if index > 0 && event.ReportedAt.Before(events[index-1].ReportedAt) { return false } } return true } func validateDeviceTimeline(events []DeviceEvent, collectedAt time.Time) bool { if len(events) == 0 || collectedAt.IsZero() { return false } for index, event := range events { if event.Phase == "" || event.OccurredAt.IsZero() || event.OccurredAt.After(collectedAt) { return false } if index > 0 && event.OccurredAt.Before(events[index-1].OccurredAt) { return false } } return true } func scenarioTerminalPhase(scenario string) (string, bool) { switch scenario { case "AB_SUCCESS", "POWER_LOSS_AFTER_STAGE", "POWER_LOSS_DURING_BOOT": return "confirmed", true case "AB_ROLLBACK": return "rolled_back", true default: return "", false } } func cloudPhases(events []CloudEvent) []string { result := make([]string, 0, len(events)) for _, event := range events { result = append(result, event.Phase) } return result } func devicePhases(events []DeviceEvent) []string { result := make([]string, 0, len(events)) for _, event := range events { result = append(result, event.Phase) } return result } func containsPhaseSequence(actual, required []string) bool { position := 0 for _, value := range actual { if position < len(required) && value == required[position] { position++ } } return position == len(required) } func phaseTimeCorrelation(cloud []CloudEvent, device []DeviceEvent) (bool, time.Duration) { maximum := time.Duration(0) matched := 0 for _, local := range device { closest := time.Duration(1<<63 - 1) for _, remote := range cloud { if remote.Phase != local.Phase { continue } difference := remote.ReportedAt.Sub(local.OccurredAt) if difference < 0 { difference = -difference } if difference < closest { closest = difference } } if closest == time.Duration(1<<63-1) { continue } matched++ if closest > maximum { maximum = closest } } return matched >= 4 && maximum <= 10*time.Minute, maximum }