Prepare Sandwich Hime v1 release candidate source

This commit is contained in:
2026-08-23 16:25:02 -04:00
parent c11552b87a
commit efd5b997ed
64 changed files with 5276 additions and 156 deletions
+423
View File
@@ -0,0 +1,423 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Package releaseartifact creates deterministic Hime-san release archives and
// checksummed native-platform verification receipts.
package releaseartifact
import (
"archive/tar"
"compress/gzip"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"time"
)
const receiptSchema = "himesan.native-verification.v1"
var (
releaseVersionPattern = regexp.MustCompile(`^v(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$`)
gitObjectPattern = regexp.MustCompile(`^[0-9a-f]{40,64}$`)
digestPattern = regexp.MustCompile(`^[0-9a-f]{64}$`)
goVersionPattern = regexp.MustCompile(`^go(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)$`)
platformPattern = regexp.MustCompile(`^[a-z0-9]+$`)
)
// PackageOptions contains the reviewed inputs to one release archive.
type PackageOptions struct {
Version string
Commit string
Tree string
GoVersion string
GOOS string
GOARCH string
BinaryPath string
LicensePath string
ReleaseNotes string
OutputDirectory string
SourceDateEpoch int64
}
// PackageResult identifies the immutable unsigned release archive.
type PackageResult struct {
ArchivePath string `json:"archive_path"`
SHA256 string `json:"sha256"`
}
type buildInfo struct {
SchemaVersion int `json:"schema_version"`
Version string `json:"version"`
Commit string `json:"commit"`
Tree string `json:"tree"`
GoVersion string `json:"go_version"`
GOOS string `json:"goos"`
GOARCH string `json:"goarch"`
BinarySHA256 string `json:"binary_sha256"`
UnsignedBinarySHA256 string `json:"unsigned_binary_sha256,omitempty"`
UnsignedArchiveSHA256 string `json:"unsigned_archive_sha256,omitempty"`
SigningIdentifier string `json:"signing_identifier,omitempty"`
}
type spdxDocument struct {
SPDXVersion string `json:"spdxVersion"`
DataLicense string `json:"dataLicense"`
SPDXID string `json:"SPDXID"`
Name string `json:"name"`
DocumentNamespace string `json:"documentNamespace"`
CreationInfo creationInfo `json:"creationInfo"`
Packages []spdxPackage `json:"packages"`
}
type creationInfo struct {
Created string `json:"created"`
Creators []string `json:"creators"`
}
type spdxPackage struct {
Name string `json:"name"`
SPDXID string `json:"SPDXID"`
VersionInfo string `json:"versionInfo"`
DownloadLocation string `json:"downloadLocation"`
FilesAnalyzed bool `json:"filesAnalyzed"`
LicenseConcluded string `json:"licenseConcluded"`
LicenseDeclared string `json:"licenseDeclared"`
Checksums []spdxChecksum `json:"checksums"`
}
type spdxChecksum struct {
Algorithm string `json:"algorithm"`
ChecksumValue string `json:"checksumValue"`
}
// Receipt describes one completed native verification run.
type Receipt struct {
SchemaVersion string `json:"schema_version"`
Repository string `json:"repository"`
Commit string `json:"commit"`
Tree string `json:"tree"`
GOOS string `json:"goos"`
GOARCH string `json:"goarch"`
GoVersion string `json:"go_version"`
RunnerVersion string `json:"runner_version"`
RunnerName string `json:"runner_name"`
GeneratedDigest string `json:"generated_output_sha256"`
UnsignedArtifactSHA string `json:"unsigned_artifact_sha256,omitempty"`
SuccessfulGates []string `json:"successful_gates"`
CompletedAt string `json:"completed_at"`
}
type archiveMember struct {
name string
mode int64
data []byte
}
// Package creates a byte-reproducible unsigned tar.gz and its SHA-256 sidecar.
func Package(options PackageOptions) (PackageResult, error) {
if err := validatePackageOptions(options); err != nil {
return PackageResult{}, err
}
binary, err := os.ReadFile(options.BinaryPath)
if err != nil {
return PackageResult{}, fmt.Errorf("read binary: %w", err)
}
license, err := os.ReadFile(options.LicensePath)
if err != nil {
return PackageResult{}, fmt.Errorf("read license: %w", err)
}
releaseNotes, err := os.ReadFile(options.ReleaseNotes)
if err != nil {
return PackageResult{}, fmt.Errorf("read release notes: %w", err)
}
binaryDigest := digest(binary)
infoBytes, err := indentedJSON(buildInfo{
SchemaVersion: 1,
Version: options.Version,
Commit: options.Commit,
Tree: options.Tree,
GoVersion: options.GoVersion,
GOOS: options.GOOS,
GOARCH: options.GOARCH,
BinarySHA256: binaryDigest,
})
if err != nil {
return PackageResult{}, err
}
created := time.Unix(options.SourceDateEpoch, 0).UTC().Format(time.RFC3339)
sbomBytes, err := indentedJSON(spdxDocument{
SPDXVersion: "SPDX-2.3",
DataLicense: "CC0-1.0",
SPDXID: "SPDXRef-DOCUMENT",
Name: "himesan-" + options.Version + "-" + options.GOOS + "-" + options.GOARCH,
DocumentNamespace: "https://sandwichhime.com/spdx/himesan/" + options.Version + "/" + options.Commit + "/" + options.GOOS + "-" + options.GOARCH,
CreationInfo: creationInfo{
Created: created,
Creators: []string{"Organization: Gamertan"},
},
Packages: []spdxPackage{{
Name: "himesan",
SPDXID: "SPDXRef-Package-himesan",
VersionInfo: options.Version,
DownloadLocation: "NOASSERTION",
FilesAnalyzed: false,
LicenseConcluded: "AGPL-3.0-only",
LicenseDeclared: "AGPL-3.0-only",
Checksums: []spdxChecksum{{
Algorithm: "SHA256",
ChecksumValue: binaryDigest,
}},
}},
})
if err != nil {
return PackageResult{}, err
}
members := []archiveMember{
{name: "BUILDINFO.json", mode: 0o444, data: infoBytes},
{name: "LICENSE", mode: 0o444, data: license},
{name: "RELEASE.md", mode: 0o444, data: releaseNotes},
{name: "SBOM.spdx.json", mode: 0o444, data: sbomBytes},
{name: "himesan", mode: 0o555, data: binary},
}
checksumLines := make([]string, 0, len(members))
for _, member := range members {
checksumLines = append(checksumLines, digest(member.data)+" "+member.name)
}
members = append(members, archiveMember{
name: "SHA256SUMS", mode: 0o444,
data: []byte(strings.Join(checksumLines, "\n") + "\n"),
})
sort.Slice(members, func(i, j int) bool { return members[i].name < members[j].name })
if err := os.MkdirAll(options.OutputDirectory, 0o755); err != nil {
return PackageResult{}, fmt.Errorf("create output directory: %w", err)
}
base := "himesan-" + strings.TrimPrefix(options.Version, "v") + "-" + options.GOOS + "-" + options.GOARCH
archivePath := filepath.Join(options.OutputDirectory, base+".tar.gz")
if err := writeArchive(archivePath, base, members, time.Unix(options.SourceDateEpoch, 0).UTC()); err != nil {
return PackageResult{}, err
}
archive, err := os.ReadFile(archivePath)
if err != nil {
return PackageResult{}, fmt.Errorf("read completed archive: %w", err)
}
archiveDigest := digest(archive)
if err := writeExclusive(archivePath+".sha256", []byte(archiveDigest+" "+filepath.Base(archivePath)+"\n"), 0o444); err != nil {
return PackageResult{}, fmt.Errorf("write archive checksum: %w", err)
}
return PackageResult{ArchivePath: archivePath, SHA256: archiveDigest}, nil
}
// WriteReceipt validates, canonicalizes, and writes a receipt plus SHA sidecar.
func WriteReceipt(path string, receipt Receipt) (string, error) {
receipt.SchemaVersion = receiptSchema
if err := validateReceipt(receipt); err != nil {
return "", err
}
sort.Strings(receipt.SuccessfulGates)
contents, err := indentedJSON(receipt)
if err != nil {
return "", err
}
if err := writeExclusive(path, contents, 0o444); err != nil {
return "", fmt.Errorf("write receipt: %w", err)
}
checksum := digest(contents)
if err := writeExclusive(path+".sha256", []byte(checksum+" "+filepath.Base(path)+"\n"), 0o444); err != nil {
return "", fmt.Errorf("write receipt checksum: %w", err)
}
return checksum, nil
}
// DigestFiles returns a stable digest over sorted names and file contents.
func DigestFiles(paths []string) (string, error) {
if len(paths) == 0 {
return "", errors.New("at least one generated file is required")
}
sorted := append([]string(nil), paths...)
sort.Strings(sorted)
hash := sha256.New()
for _, path := range sorted {
contents, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("read %s: %w", path, err)
}
_, _ = io.WriteString(hash, filepath.ToSlash(path))
_, _ = hash.Write([]byte{0})
_, _ = hash.Write(contents)
_, _ = hash.Write([]byte{0})
}
return hex.EncodeToString(hash.Sum(nil)), nil
}
func validatePackageOptions(options PackageOptions) error {
for name, value := range map[string]string{
"version": options.Version, "commit": options.Commit, "tree": options.Tree,
"go version": options.GoVersion, "GOOS": options.GOOS, "GOARCH": options.GOARCH,
"binary": options.BinaryPath, "license": options.LicensePath,
"release notes": options.ReleaseNotes, "output directory": options.OutputDirectory,
} {
if strings.TrimSpace(value) == "" {
return fmt.Errorf("%s is required", name)
}
}
if options.SourceDateEpoch <= 0 {
return errors.New("source date epoch must be positive")
}
if !releaseVersionPattern.MatchString(options.Version) {
return errors.New("version must be a canonical v-prefixed semantic version without build metadata")
}
if !gitObjectPattern.MatchString(options.Commit) || !gitObjectPattern.MatchString(options.Tree) {
return errors.New("commit and tree must be full lowercase Git object identities")
}
if !goVersionPattern.MatchString(options.GoVersion) {
return errors.New("Go version must be a complete goX.Y.Z toolchain identity")
}
if !platformPattern.MatchString(options.GOOS) || !platformPattern.MatchString(options.GOARCH) {
return errors.New("GOOS and GOARCH must contain only lowercase letters and digits")
}
return nil
}
func validateReceipt(receipt Receipt) error {
for name, value := range map[string]string{
"repository": receipt.Repository, "commit": receipt.Commit, "tree": receipt.Tree,
"GOOS": receipt.GOOS, "GOARCH": receipt.GOARCH, "Go version": receipt.GoVersion,
"runner version": receipt.RunnerVersion, "runner name": receipt.RunnerName,
"generated digest": receipt.GeneratedDigest, "completed at": receipt.CompletedAt,
} {
if strings.TrimSpace(value) == "" {
return fmt.Errorf("%s is required", name)
}
}
if len(receipt.SuccessfulGates) == 0 {
return errors.New("at least one successful gate is required")
}
if !gitObjectPattern.MatchString(receipt.Commit) || !gitObjectPattern.MatchString(receipt.Tree) {
return errors.New("commit and tree must be full lowercase Git object identities")
}
if !goVersionPattern.MatchString(receipt.GoVersion) {
return errors.New("Go version must be a complete goX.Y.Z toolchain identity")
}
if !platformPattern.MatchString(receipt.GOOS) || !platformPattern.MatchString(receipt.GOARCH) {
return errors.New("GOOS and GOARCH must contain only lowercase letters and digits")
}
if !digestPattern.MatchString(receipt.GeneratedDigest) {
return errors.New("generated-output digest must be a lowercase SHA-256")
}
if receipt.UnsignedArtifactSHA != "" && !digestPattern.MatchString(receipt.UnsignedArtifactSHA) {
return errors.New("unsigned-artifact digest must be a lowercase SHA-256")
}
if _, err := time.Parse(time.RFC3339, receipt.CompletedAt); err != nil {
return fmt.Errorf("completed at must be RFC3339: %w", err)
}
return nil
}
func writeArchive(path, root string, members []archiveMember, modified time.Time) error {
file, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
if err != nil {
return fmt.Errorf("create archive: %w", err)
}
success := false
defer func() {
_ = file.Close()
if !success {
_ = os.Remove(path)
}
}()
gzipWriter := gzip.NewWriter(file)
gzipWriter.Header.ModTime = modified
gzipWriter.Header.OS = 255
tarWriter := tar.NewWriter(gzipWriter)
for _, member := range members {
header := &tar.Header{
Name: root + "/" + member.name,
Mode: member.mode,
Size: int64(len(member.data)),
ModTime: modified,
AccessTime: time.Time{},
ChangeTime: time.Time{},
Uid: 0,
Gid: 0,
Uname: "",
Gname: "",
Format: tar.FormatUSTAR,
}
if err := tarWriter.WriteHeader(header); err != nil {
return fmt.Errorf("write archive header: %w", err)
}
if _, err := tarWriter.Write(member.data); err != nil {
return fmt.Errorf("write archive member: %w", err)
}
}
if err := tarWriter.Close(); err != nil {
return fmt.Errorf("close tar stream: %w", err)
}
if err := gzipWriter.Close(); err != nil {
return fmt.Errorf("close gzip stream: %w", err)
}
if err := file.Sync(); err != nil {
return fmt.Errorf("sync archive: %w", err)
}
if err := file.Close(); err != nil {
return fmt.Errorf("close archive: %w", err)
}
if err := os.Chmod(path, 0o444); err != nil {
return fmt.Errorf("set archive permissions: %w", err)
}
success = true
return nil
}
func writeExclusive(path string, contents []byte, mode os.FileMode) error {
file, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, mode)
if err != nil {
return err
}
success := false
defer func() {
_ = file.Close()
if !success {
_ = os.Remove(path)
}
}()
if _, err := file.Write(contents); err != nil {
return err
}
if err := file.Sync(); err != nil {
return err
}
if err := file.Close(); err != nil {
return err
}
if err := os.Chmod(path, mode); err != nil {
return err
}
success = true
return nil
}
func indentedJSON(value any) ([]byte, error) {
contents, err := json.MarshalIndent(value, "", " ")
if err != nil {
return nil, fmt.Errorf("encode JSON: %w", err)
}
return append(contents, '\n'), nil
}
func digest(contents []byte) string {
sum := sha256.Sum256(contents)
return hex.EncodeToString(sum[:])
}