11package update
22
33import (
4+ "bufio"
5+ "bytes"
46 "context"
7+ "crypto/sha256"
8+ "crypto/subtle"
9+ "encoding/hex"
510 "fmt"
11+ "io"
612 "net/http"
713 "runtime"
814 "strings"
@@ -17,6 +23,8 @@ import (
1723
1824var urlGitubReleases = "https://github.com/ovh/venom/releases"
1925
26+ const checksumsAssetName = "checksums.txt"
27+
2028// Cmd update
2129var Cmd = & cobra.Command {
2230 Use : "update" ,
@@ -27,7 +35,9 @@ var Cmd = &cobra.Command{
2735 },
2836}
2937
30- func getURLArtifactFromGithub () string {
38+ // releaseAssets returns, for the latest release, the download URL of the
39+ // binary for the current OS/arch and the URL of the checksums.txt file.
40+ func releaseAssets () (binaryURL , checksumsURL , binaryAssetName string ) {
3141 client := github .NewClient (nil )
3242 release , resp , err := client .Repositories .GetLatestRelease (context .TODO (), "ovh" , "venom" )
3343 if err != nil {
@@ -38,21 +48,89 @@ func getURLArtifactFromGithub() string {
3848 cmd .Exit ("you already have the latest release: %s" , * release .TagName )
3949 }
4050
41- if len (release .Assets ) > 0 {
42- for _ , asset := range release .Assets {
43- assetName := strings .ReplaceAll (* asset .Name , "." , "-" )
44- current := fmt .Sprintf ("venom-%s-%s" , runtime .GOOS , runtime .GOARCH )
45- if assetName == current {
46- return * asset .BrowserDownloadURL
47- }
51+ current := fmt .Sprintf ("venom-%s-%s" , runtime .GOOS , runtime .GOARCH )
52+ for _ , asset := range release .Assets {
53+ normalised := strings .ReplaceAll (* asset .Name , "." , "-" )
54+ if normalised == current {
55+ binaryURL = * asset .BrowserDownloadURL
56+ binaryAssetName = * asset .Name
57+ }
58+ if * asset .Name == checksumsAssetName {
59+ checksumsURL = * asset .BrowserDownloadURL
4860 }
4961 }
5062
51- const text = `Invalid Artifacts on latest release. Please try again in few minutes.
63+ if binaryURL == "" {
64+ const text = `Invalid Artifacts on latest release. Please try again in few minutes.
5265If the problem persists, please open an issue on https://github.com/ovh/venom/issues
5366`
54- cmd .Exit (text )
55- return ""
67+ cmd .Exit (text )
68+ }
69+ return binaryURL , checksumsURL , binaryAssetName
70+ }
71+
72+ // fetchExpectedChecksum downloads checksums.txt from the release and
73+ // returns the expected SHA256 (hex) for assetName. The checksums.txt
74+ // format is the standard `sha256sum` output: "<hex> <filename>" lines.
75+ func fetchExpectedChecksum (checksumsURL , assetName string ) (string , error ) {
76+ if checksumsURL == "" {
77+ return "" , fmt .Errorf ("this release does not publish %s; refusing to update for security reasons" , checksumsAssetName )
78+ }
79+ resp , err := http .Get (checksumsURL )
80+ if err != nil {
81+ return "" , fmt .Errorf ("failed to fetch %s: %w" , checksumsAssetName , err )
82+ }
83+ defer resp .Body .Close ()
84+ if resp .StatusCode != http .StatusOK {
85+ return "" , fmt .Errorf ("failed to fetch %s: HTTP %d" , checksumsAssetName , resp .StatusCode )
86+ }
87+
88+ scanner := bufio .NewScanner (resp .Body )
89+ for scanner .Scan () {
90+ line := strings .TrimSpace (scanner .Text ())
91+ if line == "" || strings .HasPrefix (line , "#" ) {
92+ continue
93+ }
94+ // Accept both "<hex> <name>" (two spaces) and "<hex> <name>"
95+ fields := strings .Fields (line )
96+ if len (fields ) != 2 {
97+ continue
98+ }
99+ // The filename in checksums.txt may be a relative path; match on basename.
100+ name := fields [1 ]
101+ if idx := strings .LastIndexAny (name , "/\\ " ); idx >= 0 {
102+ name = name [idx + 1 :]
103+ }
104+ if name == assetName {
105+ return strings .ToLower (fields [0 ]), nil
106+ }
107+ }
108+ if err := scanner .Err (); err != nil {
109+ return "" , fmt .Errorf ("error parsing %s: %w" , checksumsAssetName , err )
110+ }
111+ return "" , fmt .Errorf ("no checksum entry for asset %q in %s" , assetName , checksumsAssetName )
112+ }
113+
114+ // downloadAndVerify reads the response body fully into memory, verifies its
115+ // SHA256 against expectedHex (constant-time compare), and returns a Reader
116+ // suitable for update.Apply. Memory cost is acceptable: venom binaries are
117+ // only a few tens of MB.
118+ func downloadAndVerify (body io.Reader , expectedHex string ) (io.Reader , error ) {
119+ hasher := sha256 .New ()
120+ var buf bytes.Buffer
121+ if _ , err := io .Copy (io .MultiWriter (& buf , hasher ), body ); err != nil {
122+ return nil , fmt .Errorf ("failed to download binary: %w" , err )
123+ }
124+ got := hex .EncodeToString (hasher .Sum (nil ))
125+ expected , err := hex .DecodeString (expectedHex )
126+ if err != nil {
127+ return nil , fmt .Errorf ("invalid expected checksum %q: %w" , expectedHex , err )
128+ }
129+ gotBytes := hasher .Sum (nil )
130+ if subtle .ConstantTimeCompare (gotBytes , expected ) != 1 {
131+ return nil , fmt .Errorf ("checksum mismatch: expected %s, got %s" , expectedHex , got )
132+ }
133+ return & buf , nil
56134}
57135
58136func getContentType (resp * http.Response ) string {
@@ -65,27 +143,37 @@ func getContentType(resp *http.Response) string {
65143}
66144
67145func doUpdate () {
68- url := getURLArtifactFromGithub ()
69- fmt .Printf ("Url to update venom: %s\n " , url )
146+ binaryURL , checksumsURL , assetName := releaseAssets ()
147+ fmt .Printf ("Url to update venom: %s\n " , binaryURL )
70148
71- resp , err := http . Get ( url )
149+ expected , err := fetchExpectedChecksum ( checksumsURL , assetName )
72150 if err != nil {
73- cmd .Exit ("Error when downloading venom from url %s: %v \n " , url , err )
151+ cmd .Exit ("%s \n Download the binary manually from %s if needed. \n " , err . Error (), urlGitubReleases )
74152 }
75153
154+ resp , err := http .Get (binaryURL )
155+ if err != nil {
156+ cmd .Exit ("Error when downloading venom from url %s: %v\n " , binaryURL , err )
157+ }
158+ defer resp .Body .Close ()
159+
76160 if contentType := getContentType (resp ); contentType != "application/octet-stream" {
77- fmt .Printf ("Url: %s\n " , url )
161+ fmt .Printf ("Url: %s\n " , binaryURL )
78162 cmd .Exit ("Invalid Binary (Content-Type: %s). Please try again or download it manually from %s\n " , contentType , urlGitubReleases )
79163 }
80164
81165 if resp .StatusCode != 200 {
82- cmd .Exit ("Error http code: %d, url called: %s\n " , resp .StatusCode , url )
166+ cmd .Exit ("Error http code: %d, url called: %s\n " , resp .StatusCode , binaryURL )
83167 }
84168
85- fmt .Printf ("Getting latest release from: %s ...\n " , url )
86- defer resp .Body .Close ()
87- if err = update .Apply (resp .Body , update.Options {}); err != nil {
88- cmd .Exit ("Error when updating venom from url: %s err:%s\n " , url , err .Error ())
169+ fmt .Printf ("Getting latest release from: %s ...\n " , binaryURL )
170+ verified , err := downloadAndVerify (resp .Body , expected )
171+ if err != nil {
172+ cmd .Exit ("Error when verifying venom binary from url: %s err:%s\n " , binaryURL , err .Error ())
173+ }
174+
175+ if err = update .Apply (verified , update.Options {}); err != nil {
176+ cmd .Exit ("Error when updating venom from url: %s err:%s\n " , binaryURL , err .Error ())
89177 }
90178 fmt .Println ("Update done." )
91179}
0 commit comments