221 lines
8.2 KiB
Go
221 lines
8.2 KiB
Go
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
package releaseartifact
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const nativeReceiptMaximumAge = 30 * 24 * time.Hour
|
|
|
|
var runnerVersionPattern = regexp.MustCompile(`^v(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)$`)
|
|
|
|
type nativeLane struct {
|
|
directory string
|
|
goos string
|
|
goarch string
|
|
goVersion string
|
|
runner string
|
|
}
|
|
|
|
var requiredNativeLanes = []nativeLane{
|
|
{directory: "darwin-arm64-go1.26.7", goos: "darwin", goarch: "arm64", goVersion: "go1.26.7", runner: "macbook-air-himesan-darwin-arm64"},
|
|
{directory: "darwin-arm64-go1.27.0", goos: "darwin", goarch: "arm64", goVersion: "go1.27.0", runner: "macbook-air-himesan-darwin-arm64"},
|
|
{directory: "linux-amd64-go1.26.7", goos: "linux", goarch: "amd64", goVersion: "go1.26.7", runner: "cliff-himesan-linux-amd64"},
|
|
{directory: "linux-amd64-go1.27.0", goos: "linux", goarch: "amd64", goVersion: "go1.27.0", runner: "cliff-himesan-linux-amd64"},
|
|
}
|
|
|
|
var requiredNativeGates = []string{
|
|
"build", "consumer", "contracts", "fuzz", "generation", "package",
|
|
"public-snapshot", "race", "test", "vet", "vulnerability",
|
|
}
|
|
|
|
// NativeReceiptExpectation identifies the exact source verified by all native
|
|
// runner lanes.
|
|
type NativeReceiptExpectation struct {
|
|
Repository string
|
|
Commit string
|
|
Tree string
|
|
}
|
|
|
|
// NativeReceiptSummary is safe to include in human release evidence.
|
|
type NativeReceiptSummary struct {
|
|
SchemaVersion string `json:"schema_version"`
|
|
Repository string `json:"repository"`
|
|
Commit string `json:"commit"`
|
|
Tree string `json:"tree"`
|
|
GeneratedDigest string `json:"generated_output_sha256"`
|
|
Lanes []NativeLaneSummary `json:"lanes"`
|
|
}
|
|
|
|
// NativeLaneSummary records one verified native receipt without local paths.
|
|
type NativeLaneSummary struct {
|
|
Platform string `json:"platform"`
|
|
GoVersion string `json:"go_version"`
|
|
RunnerVersion string `json:"runner_version"`
|
|
ArtifactSHA256 string `json:"unsigned_artifact_sha256"`
|
|
ReceiptSHA256 string `json:"receipt_sha256"`
|
|
CompletedAt string `json:"completed_at"`
|
|
}
|
|
|
|
// VerifyNativeReceiptSet requires all four maintained target/toolchain lanes,
|
|
// validates their checksum sidecars, and proves identical generated output.
|
|
func VerifyNativeReceiptSet(directory string, expected NativeReceiptExpectation) (NativeReceiptSummary, error) {
|
|
if strings.TrimSpace(expected.Repository) == "" || strings.ContainsAny(expected.Repository, "\r\n\x00") {
|
|
return NativeReceiptSummary{}, errors.New("native receipt repository is required and must be one line")
|
|
}
|
|
if !gitObjectPattern.MatchString(expected.Commit) || !gitObjectPattern.MatchString(expected.Tree) {
|
|
return NativeReceiptSummary{}, errors.New("native receipt commit and tree must be full lowercase Git object identities")
|
|
}
|
|
if err := validateNativeDirectory(directory); err != nil {
|
|
return NativeReceiptSummary{}, err
|
|
}
|
|
|
|
summary := NativeReceiptSummary{
|
|
SchemaVersion: "himesan.native-receipt-set.v1",
|
|
Repository: expected.Repository, Commit: expected.Commit, Tree: expected.Tree,
|
|
}
|
|
for _, lane := range requiredNativeLanes {
|
|
receipt, receiptDigest, err := readNativeLane(filepath.Join(directory, lane.directory))
|
|
if err != nil {
|
|
return NativeReceiptSummary{}, fmt.Errorf("verify native lane %s: %w", lane.directory, err)
|
|
}
|
|
if receipt.SchemaVersion != receiptSchema || receipt.Repository != expected.Repository ||
|
|
receipt.Commit != expected.Commit || receipt.Tree != expected.Tree ||
|
|
receipt.GOOS != lane.goos || receipt.GOARCH != lane.goarch ||
|
|
receipt.GoVersion != lane.goVersion || receipt.RunnerName != lane.runner {
|
|
return NativeReceiptSummary{}, fmt.Errorf("native lane %s does not match its source, platform, toolchain, or runner", lane.directory)
|
|
}
|
|
if !runnerVersionPattern.MatchString(receipt.RunnerVersion) || !digestPattern.MatchString(receipt.UnsignedArtifactSHA) {
|
|
return NativeReceiptSummary{}, fmt.Errorf("native lane %s has an invalid runner or artifact identity", lane.directory)
|
|
}
|
|
completedAt, err := time.Parse(time.RFC3339, receipt.CompletedAt)
|
|
if err != nil || completedAt.After(time.Now().UTC().Add(5*time.Minute)) || time.Since(completedAt) > nativeReceiptMaximumAge {
|
|
return NativeReceiptSummary{}, fmt.Errorf("native lane %s is not fresh, completed, RFC3339 evidence", lane.directory)
|
|
}
|
|
if err := requireNativeGates(receipt.SuccessfulGates); err != nil {
|
|
return NativeReceiptSummary{}, fmt.Errorf("native lane %s: %w", lane.directory, err)
|
|
}
|
|
if summary.GeneratedDigest == "" {
|
|
summary.GeneratedDigest = receipt.GeneratedDigest
|
|
} else if summary.GeneratedDigest != receipt.GeneratedDigest {
|
|
return NativeReceiptSummary{}, errors.New("native lanes did not produce identical generated output")
|
|
}
|
|
summary.Lanes = append(summary.Lanes, NativeLaneSummary{
|
|
Platform: lane.goos + "/" + lane.goarch, GoVersion: lane.goVersion,
|
|
RunnerVersion: receipt.RunnerVersion, ArtifactSHA256: receipt.UnsignedArtifactSHA,
|
|
ReceiptSHA256: receiptDigest, CompletedAt: receipt.CompletedAt,
|
|
})
|
|
}
|
|
return summary, nil
|
|
}
|
|
|
|
func validateNativeDirectory(directory string) error {
|
|
info, err := os.Lstat(directory)
|
|
if err != nil {
|
|
return fmt.Errorf("inspect native evidence directory: %w", err)
|
|
}
|
|
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
|
|
return errors.New("native evidence must be a real directory")
|
|
}
|
|
entries, err := os.ReadDir(directory)
|
|
if err != nil {
|
|
return fmt.Errorf("read native evidence directory: %w", err)
|
|
}
|
|
expected := make([]string, 0, len(requiredNativeLanes))
|
|
for _, lane := range requiredNativeLanes {
|
|
expected = append(expected, lane.directory)
|
|
}
|
|
observed := make([]string, 0, len(entries))
|
|
for _, entry := range entries {
|
|
observed = append(observed, entry.Name())
|
|
}
|
|
sort.Strings(expected)
|
|
sort.Strings(observed)
|
|
if !equalStrings(expected, observed) {
|
|
return fmt.Errorf("native evidence directories = %v, want exactly %v", observed, expected)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func readNativeLane(directory string) (Receipt, string, error) {
|
|
info, err := os.Lstat(directory)
|
|
if err != nil {
|
|
return Receipt{}, "", err
|
|
}
|
|
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
|
|
return Receipt{}, "", errors.New("lane must be a real directory")
|
|
}
|
|
entries, err := os.ReadDir(directory)
|
|
if err != nil {
|
|
return Receipt{}, "", err
|
|
}
|
|
if len(entries) != 2 || entries[0].Name() != "TEND-CI-VERIFICATION.json" || entries[1].Name() != "TEND-CI-VERIFICATION.json.sha256" {
|
|
return Receipt{}, "", errors.New("lane must contain only its receipt and checksum sidecar")
|
|
}
|
|
receiptPath := filepath.Join(directory, "TEND-CI-VERIFICATION.json")
|
|
contents, err := readRegularFile(receiptPath, maximumEvidenceSize)
|
|
if err != nil || len(contents) == 0 {
|
|
return Receipt{}, "", errors.New("receipt must be a non-empty bounded regular file")
|
|
}
|
|
receiptDigest := digest(contents)
|
|
sidecar, err := readRegularFile(receiptPath+".sha256", 512)
|
|
if err != nil {
|
|
return Receipt{}, "", fmt.Errorf("read receipt checksum: %w", err)
|
|
}
|
|
expectedSidecar := receiptDigest + " TEND-CI-VERIFICATION.json\n"
|
|
if string(sidecar) != expectedSidecar {
|
|
return Receipt{}, "", errors.New("receipt checksum sidecar does not match")
|
|
}
|
|
decoder := json.NewDecoder(bytes.NewReader(contents))
|
|
decoder.DisallowUnknownFields()
|
|
var receipt Receipt
|
|
if err := decoder.Decode(&receipt); err != nil {
|
|
return Receipt{}, "", fmt.Errorf("decode receipt: %w", err)
|
|
}
|
|
if err := requireJSONEOF(decoder); err != nil {
|
|
return Receipt{}, "", fmt.Errorf("decode receipt: %w", err)
|
|
}
|
|
if err := validateReceipt(receipt); err != nil {
|
|
return Receipt{}, "", err
|
|
}
|
|
return receipt, receiptDigest, nil
|
|
}
|
|
|
|
func requireNativeGates(gates []string) error {
|
|
observed := make(map[string]bool, len(gates))
|
|
for _, gate := range gates {
|
|
if observed[gate] {
|
|
return fmt.Errorf("successful gate %q is duplicated", gate)
|
|
}
|
|
observed[gate] = true
|
|
}
|
|
for _, gate := range requiredNativeGates {
|
|
if !observed[gate] {
|
|
return fmt.Errorf("required successful gate %q is missing", gate)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func equalStrings(left, right []string) bool {
|
|
if len(left) != len(right) {
|
|
return false
|
|
}
|
|
for index := range left {
|
|
if left[index] != right[index] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|