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}