Skip to content

Commit 436da91

Browse files
authored
feature/security: Implement image signature verification (#1143)
1 parent b2e0f1d commit 436da91

File tree

6 files changed

+320
-75
lines changed

6 files changed

+320
-75
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ All notable changes to `src-cli` are documented in this file.
1111

1212
## Unreleased
1313

14+
## 6.0.1
15+
16+
- Container signature verification support: Container signatures can now be verified for Sourcegraph releases after 5.11.4013 using `src signature verify -v <release>` [#1143](https://github.com/sourcegraph/src-cli/pull/1143)
17+
1418
## 5.11.1
1519

1620
- Update x/net to fix CVE-2024-45338

cmd/src/sbom.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
var sbomCommands commander
99

1010
func init() {
11-
usage := `'src sbom' fetches and verified SBOM (Software Bill of Materials) data for Sourcegraph containers.
11+
usage := `'src sbom' fetches and verifies SBOM (Software Bill of Materials) data for Sourcegraph containers.
1212
1313
Usage:
1414

cmd/src/sbom_fetch.go

+7-74
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,23 @@
11
package main
22

33
import (
4-
"bufio"
54
"bytes"
65
"encoding/base64"
76
"encoding/json"
87
"flag"
98
"fmt"
10-
"net/http"
119
"os"
1210
"os/exec"
1311
"path/filepath"
1412
"strings"
1513
"unicode"
1614

17-
"github.com/grafana/regexp"
18-
"github.com/sourcegraph/sourcegraph/lib/errors"
1915
"github.com/sourcegraph/sourcegraph/lib/output"
2016

2117
"github.com/sourcegraph/src-cli/internal/cmderrors"
2218
)
2319

24-
type sbomConfig struct {
25-
publicKey string
26-
outputDir string
27-
version string
28-
internalRelease bool
29-
insecureIgnoreTransparencyLog bool
30-
}
31-
32-
const publicKey = "https://storage.googleapis.com/sourcegraph-release-sboms/keys/cosign_keyring-cosign-1.pub"
33-
const imageListBaseURL = "https://storage.googleapis.com/sourcegraph-release-sboms"
34-
const imageListFilename = "release-image-list.txt"
20+
const sbomPublicKey = "https://storage.googleapis.com/sourcegraph-release-sboms/keys/cosign_keyring-cosign-1.pub"
3521

3622
func init() {
3723
usage := `
@@ -55,8 +41,8 @@ Examples:
5541
insecureIgnoreTransparencyLogFlag := flagSet.Bool("insecure-ignore-tlog", false, "Disable transparency log verification. Defaults to false.")
5642

5743
handler := func(args []string) error {
58-
c := sbomConfig{
59-
publicKey: publicKey,
44+
c := cosignConfig{
45+
publicKey: sbomPublicKey,
6046
}
6147

6248
if err := flagSet.Parse(args); err != nil {
@@ -155,7 +141,7 @@ Examples:
155141
})
156142
}
157143

158-
func (c sbomConfig) getSBOMForImageVersion(image string, version string) (string, error) {
144+
func (c cosignConfig) getSBOMForImageVersion(image string, version string) (string, error) {
159145
hash, err := getImageDigest(image, version)
160146
if err != nil {
161147
return "", err
@@ -169,51 +155,7 @@ func (c sbomConfig) getSBOMForImageVersion(image string, version string) (string
169155
return sbom, nil
170156
}
171157

172-
func verifyCosign() error {
173-
_, err := exec.LookPath("cosign")
174-
if err != nil {
175-
return errors.New("SBOM verification requires 'cosign' to be installed and available in $PATH. See https://docs.sigstore.dev/cosign/system_config/installation/")
176-
}
177-
return nil
178-
}
179-
180-
func (c sbomConfig) getImageList() ([]string, error) {
181-
imageReleaseListURL := c.getImageReleaseListURL()
182-
183-
resp, err := http.Get(imageReleaseListURL)
184-
if err != nil {
185-
return nil, fmt.Errorf("failed to fetch image list: %w", err)
186-
}
187-
defer resp.Body.Close()
188-
189-
if resp.StatusCode != http.StatusOK {
190-
// Compare version number against a regex that matches versions up to and including 5.8.0
191-
versionRegex := regexp.MustCompile(`^v?[0-5]\.([0-7]\.[0-9]+|8\.0)$`)
192-
if versionRegex.MatchString(c.version) {
193-
return nil, fmt.Errorf("unsupported version %s: SBOMs are only available for Sourcegraph releases after 5.8.0", c.version)
194-
}
195-
return nil, fmt.Errorf("failed to fetch list of images - check that %s is a valid Sourcegraph release: HTTP status %d", c.version, resp.StatusCode)
196-
}
197-
198-
scanner := bufio.NewScanner(resp.Body)
199-
var images []string
200-
for scanner.Scan() {
201-
image := strings.TrimSpace(scanner.Text())
202-
if image != "" {
203-
// Strip off a version suffix if present
204-
parts := strings.SplitN(image, ":", 2)
205-
images = append(images, parts[0])
206-
}
207-
}
208-
209-
if err := scanner.Err(); err != nil {
210-
return nil, fmt.Errorf("error reading image list: %w", err)
211-
}
212-
213-
return images, nil
214-
}
215-
216-
func (c sbomConfig) getSBOMForImageHash(image string, hash string) (string, error) {
158+
func (c cosignConfig) getSBOMForImageHash(image string, hash string) (string, error) {
217159
tempDir, err := os.MkdirTemp("", "sbom-")
218160
if err != nil {
219161
return "", fmt.Errorf("failed to create temporary directory: %w", err)
@@ -224,7 +166,7 @@ func (c sbomConfig) getSBOMForImageHash(image string, hash string) (string, erro
224166

225167
cosignArgs := []string{
226168
"verify-attestation",
227-
"--key", publicKey,
169+
"--key", c.publicKey,
228170
"--type", "cyclonedx",
229171
fmt.Sprintf("%s@%s", image, hash),
230172
"--output-file", outputFile,
@@ -297,7 +239,7 @@ func extractSBOM(attestationBytes []byte) (string, error) {
297239
return string(predicate), nil
298240
}
299241

300-
func (c sbomConfig) storeSBOM(sbom string, image string) error {
242+
func (c cosignConfig) storeSBOM(sbom string, image string) error {
301243
// Make the image name safe for use as a filename
302244
safeImageName := strings.Map(func(r rune) rune {
303245
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '-' || r == '_' {
@@ -321,12 +263,3 @@ func (c sbomConfig) storeSBOM(sbom string, image string) error {
321263

322264
return nil
323265
}
324-
325-
// getImageReleaseListURL returns the URL for the list of images in a release, based on the version and whether it's an internal release.
326-
func (c *sbomConfig) getImageReleaseListURL() string {
327-
if c.internalRelease {
328-
return fmt.Sprintf("%s/release-internal/%s/%s", imageListBaseURL, c.version, imageListFilename)
329-
} else {
330-
return fmt.Sprintf("%s/release/%s/%s", imageListBaseURL, c.version, imageListFilename)
331-
}
332-
}

cmd/src/sbom_utils.go

+70
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,33 @@
11
package main
22

3+
// Utility functions used by the SBOM and Signature commands.
4+
35
import (
6+
"bufio"
47
"encoding/json"
58
"fmt"
69
"io"
710
"net/http"
811
"os/exec"
912
"path"
13+
"regexp"
1014
"strings"
1115
"time"
16+
17+
"github.com/sourcegraph/sourcegraph/lib/errors"
1218
)
1319

20+
const imageListBaseURL = "https://storage.googleapis.com/sourcegraph-release-sboms"
21+
const imageListFilename = "release-image-list.txt"
22+
23+
type cosignConfig struct {
24+
publicKey string
25+
outputDir string
26+
version string
27+
internalRelease bool
28+
insecureIgnoreTransparencyLog bool
29+
}
30+
1431
// TokenResponse represents the JSON response from dockerHub's token service
1532
type dockerHubTokenResponse struct {
1633
Token string `json:"token"`
@@ -200,3 +217,56 @@ func getOutputDir(parentDir, version string) string {
200217
func sanitizeVersion(version string) string {
201218
return strings.TrimPrefix(version, "v")
202219
}
220+
221+
func verifyCosign() error {
222+
_, err := exec.LookPath("cosign")
223+
if err != nil {
224+
return errors.New("SBOM verification requires 'cosign' to be installed and available in $PATH. See https://docs.sigstore.dev/cosign/system_config/installation/")
225+
}
226+
return nil
227+
}
228+
229+
func (c cosignConfig) getImageList() ([]string, error) {
230+
imageReleaseListURL := c.getImageReleaseListURL()
231+
232+
resp, err := http.Get(imageReleaseListURL)
233+
if err != nil {
234+
return nil, fmt.Errorf("failed to fetch image list: %w", err)
235+
}
236+
defer resp.Body.Close()
237+
238+
if resp.StatusCode != http.StatusOK {
239+
// Compare version number against a regex that matches versions up to and including 5.8.0
240+
versionRegex := regexp.MustCompile(`^v?[0-5]\.([0-7]\.[0-9]+|8\.0)$`)
241+
if versionRegex.MatchString(c.version) {
242+
return nil, fmt.Errorf("unsupported version %s: SBOMs are only available for Sourcegraph releases after 5.8.0", c.version)
243+
}
244+
return nil, fmt.Errorf("failed to fetch list of images - check that %s is a valid Sourcegraph release: HTTP status %d", c.version, resp.StatusCode)
245+
}
246+
247+
scanner := bufio.NewScanner(resp.Body)
248+
var images []string
249+
for scanner.Scan() {
250+
image := strings.TrimSpace(scanner.Text())
251+
if image != "" {
252+
// Strip off a version suffix if present
253+
parts := strings.SplitN(image, ":", 2)
254+
images = append(images, parts[0])
255+
}
256+
}
257+
258+
if err := scanner.Err(); err != nil {
259+
return nil, fmt.Errorf("error reading image list: %w", err)
260+
}
261+
262+
return images, nil
263+
}
264+
265+
// getImageReleaseListURL returns the URL for the list of images in a release, based on the version and whether it's an internal release.
266+
func (c *cosignConfig) getImageReleaseListURL() string {
267+
if c.internalRelease {
268+
return fmt.Sprintf("%s/release-internal/%s/%s", imageListBaseURL, c.version, imageListFilename)
269+
} else {
270+
return fmt.Sprintf("%s/release/%s/%s", imageListBaseURL, c.version, imageListFilename)
271+
}
272+
}

cmd/src/signature.go

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
)
7+
8+
var signatureCommands commander
9+
10+
func init() {
11+
usage := `'src signature' verifies published signatures for Sourcegraph containers.
12+
13+
Usage:
14+
15+
src signature command [command options]
16+
17+
The commands are:
18+
19+
verify verify signatures for a Sourcegraph release
20+
`
21+
flagSet := flag.NewFlagSet("signature", flag.ExitOnError)
22+
handler := func(args []string) error {
23+
signatureCommands.run(flagSet, "src signature", usage, args)
24+
return nil
25+
}
26+
27+
// Register the command.
28+
commands = append(commands, &command{
29+
flagSet: flagSet,
30+
aliases: []string{"signature", "sig"},
31+
handler: handler,
32+
usageFunc: func() {
33+
fmt.Println(usage)
34+
},
35+
})
36+
}

0 commit comments

Comments
 (0)