sigstore.go

  1package update
  2
  3import (
  4	"context"
  5	"crypto/sha256"
  6	"encoding/json"
  7	"fmt"
  8	"io"
  9	"net/http"
 10
 11	"github.com/sigstore/sigstore-go/pkg/bundle"
 12	"github.com/sigstore/sigstore-go/pkg/root"
 13	"github.com/sigstore/sigstore-go/pkg/verify"
 14)
 15
 16const (
 17	maxBundleSize = 10 * 1024 * 1024 // 10MB
 18)
 19
 20// verifySigstoreBundle verifies the checksums.txt file using its sigstore bundle.
 21// The bundle contains the signature, certificate, and transparency log entries
 22// needed to cryptographically verify the checksums file was signed by the expected
 23// identity (GitHub Actions workflow for charmbracelet/crush).
 24func verifySigstoreBundle(ctx context.Context, client *http.Client, checksumsContent []byte, bundleAsset *Asset) error {
 25	bundleData, err := downloadSigstoreBundle(ctx, client, bundleAsset)
 26	if err != nil {
 27		return fmt.Errorf("failed to download sigstore bundle: %w", err)
 28	}
 29
 30	b := &bundle.Bundle{}
 31	if err := b.UnmarshalJSON(bundleData); err != nil {
 32		return fmt.Errorf("failed to parse sigstore bundle: %w", err)
 33	}
 34
 35	trustedRoot, err := root.FetchTrustedRoot()
 36	if err != nil {
 37		return fmt.Errorf("failed to get Sigstore trusted root: %w", err)
 38	}
 39
 40	verifierConfig := []verify.VerifierOption{
 41		verify.WithObserverTimestamps(1),
 42		verify.WithTransparencyLog(1),
 43	}
 44
 45	sev, err := verify.NewVerifier(root.TrustedMaterialCollection{trustedRoot}, verifierConfig...)
 46	if err != nil {
 47		return fmt.Errorf("failed to create sigstore verifier: %w", err)
 48	}
 49
 50	certID, err := verify.NewShortCertificateIdentity(
 51		"https://token.actions.githubusercontent.com",
 52		"",
 53		"https://github.com/charmbracelet/meta/.github/workflows/goreleaser.yml@refs/heads/main",
 54		"",
 55	)
 56	if err != nil {
 57		return fmt.Errorf("failed to create certificate identity: %w", err)
 58	}
 59
 60	digest := sha256.Sum256(checksumsContent)
 61	policy := verify.NewPolicy(
 62		verify.WithArtifactDigest("sha256", digest[:]),
 63		verify.WithCertificateIdentity(certID),
 64	)
 65
 66	_, err = sev.Verify(b, policy)
 67	if err != nil {
 68		return fmt.Errorf("sigstore verification failed: %w", err)
 69	}
 70
 71	return nil
 72}
 73
 74func downloadSigstoreBundle(ctx context.Context, client *http.Client, asset *Asset) ([]byte, error) {
 75	req, err := http.NewRequestWithContext(ctx, "GET", asset.BrowserDownloadURL, nil)
 76	if err != nil {
 77		return nil, err
 78	}
 79	req.Header.Set("User-Agent", userAgent())
 80
 81	resp, err := client.Do(req)
 82	if err != nil {
 83		return nil, err
 84	}
 85	defer resp.Body.Close()
 86
 87	if resp.StatusCode != http.StatusOK {
 88		return nil, fmt.Errorf("failed to download bundle with status %d", resp.StatusCode)
 89	}
 90
 91	limitedBody := io.LimitReader(resp.Body, maxBundleSize+1)
 92	data, err := io.ReadAll(limitedBody)
 93	if err != nil {
 94		return nil, err
 95	}
 96	if int64(len(data)) > maxBundleSize {
 97		return nil, fmt.Errorf("sigstore bundle exceeds maximum size of %d bytes", maxBundleSize)
 98	}
 99
100	if !json.Valid(data) {
101		return nil, fmt.Errorf("sigstore bundle is not valid JSON")
102	}
103
104	return data, nil
105}