Skip to content

Commit 552564b

Browse files
umbynossilvanocerzacmaglie
authored
Add functions to download and load indexes, tools and sketches (#49)
* add first draft of index download * DownloadIndex takes the url as parameter now * minor enhancements * added extensions and removed a warning, added a simple test * TestDownloadIndex should be complete, applied suggestions by Silvano * `DownloadIndex` func should be complete. Tests are fine and we download gzipped version of the index * add first implementation of module_firmware_index struct to parse json * general enhancements, fix `LoadIndex`, add test * fixed circular include, uniformed test like in cli (index_test.go) * started working on cli.go (WIP) * enhancements * make fwUploaderPath global * enhancements pt2 * implement some (hopefully) useful functions and relative tests * enhance firmwareindex.go * move testdata and manually patch url and module for testing and add package_index.json * add first implementation of download package. DownloadTool is still not working. Also move Download function in correct package * latest enhancements with Silvano * Fix rice box name * Add functions to download and load indexes * Remove commented code * Simplify DownloadIndex function * Fix index URLs to use https Co-authored-by: Cristian Maglie <[email protected]> Co-authored-by: Silvano Cerza <[email protected]> Co-authored-by: Silvano Cerza <[email protected]> Co-authored-by: Cristian Maglie <[email protected]>
1 parent 2543546 commit 552564b

20 files changed

+21007
-10
lines changed

cli/cli.go

-2
Original file line numberDiff line numberDiff line change
@@ -210,9 +210,7 @@ func preRun(cmd *cobra.Command, args []string) {
210210
logrus.SetLevel(lvl)
211211
}
212212

213-
//
214213
// Prepare the Feedback system
215-
//
216214

217215
// normalize the format strings
218216
outputFormat = strings.ToLower(outputFormat)

cli/globals/globals.go

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
FirmwareUploader
3+
Copyright (c) 2021 Arduino LLC. All right reserved.
4+
5+
This library is free software; you can redistribute it and/or
6+
modify it under the terms of the GNU Lesser General Public
7+
License as published by the Free Software Foundation; either
8+
version 2.1 of the License, or (at your option) any later version.
9+
10+
This library is distributed in the hope that it will be useful,
11+
but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
Lesser General Public License for more details.
14+
15+
You should have received a copy of the GNU Lesser General Public
16+
License along with this library; if not, write to the Free Software
17+
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18+
*/
19+
20+
package globals
21+
22+
import "github.com/arduino/go-paths-helper"
23+
24+
var (
25+
PackageIndexGZURL = "https://downloads.arduino.cc/packages/package_index.json.gz"
26+
ModuleFirmwareIndexGZURL = "https://downloads.arduino.cc/arduino-fwuploader/boards/module_firmware_index.json.gz"
27+
FwUploaderPath = paths.TempDir().Join("fwuploader")
28+
)

go.mod

+7-4
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@ go 1.14
66
replace go.bug.st/serial => github.com/cmaglie/go-serial v0.0.0-20200923162623-b214c147e37e
77

88
require (
9-
github.com/arduino/arduino-cli v0.0.0-20210422154105-5aa424818026
10-
github.com/arduino/go-paths-helper v1.4.0
11-
github.com/mattn/go-colorable v0.1.8 // indirect
9+
github.com/arduino/arduino-cli v0.0.0-20210603144340-aef5a54882fa
10+
github.com/arduino/go-paths-helper v1.6.0
11+
github.com/cmaglie/go.rice v1.0.3
12+
github.com/mattn/go-colorable v0.1.8
1213
github.com/pkg/errors v0.9.1
13-
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 // indirect
14+
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5
1415
github.com/sirupsen/logrus v1.8.1
1516
github.com/spf13/cobra v1.1.3
1617
github.com/stretchr/testify v1.6.1
18+
go.bug.st/downloader/v2 v2.1.1
19+
go.bug.st/relaxed-semver v0.0.0-20190922224835-391e10178d18
1720
go.bug.st/serial v1.1.2
1821
)

go.sum

+37-4
Large diffs are not rendered by default.

indexes/download/download.go

+325
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
/*
2+
FirmwareUploader
3+
Copyright (c) 2021 Arduino LLC. All right reserved.
4+
5+
This library is free software; you can redistribute it and/or
6+
modify it under the terms of the GNU Lesser General Public
7+
License as published by the Free Software Foundation; either
8+
version 2.1 of the License, or (at your option) any later version.
9+
10+
This library is distributed in the hope that it will be useful,
11+
but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
Lesser General Public License for more details.
14+
15+
You should have received a copy of the GNU Lesser General Public
16+
License along with this library; if not, write to the Free Software
17+
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18+
*/
19+
20+
package download
21+
22+
import (
23+
"bytes"
24+
"crypto"
25+
"encoding/hex"
26+
"fmt"
27+
"hash"
28+
"io"
29+
"math/rand"
30+
"net/url"
31+
"path"
32+
"strings"
33+
34+
"github.com/arduino/FirmwareUploader/cli/globals"
35+
"github.com/arduino/FirmwareUploader/indexes/firmwareindex"
36+
"github.com/arduino/arduino-cli/arduino/cores"
37+
"github.com/arduino/arduino-cli/arduino/cores/packageindex"
38+
"github.com/arduino/arduino-cli/arduino/security"
39+
"github.com/arduino/arduino-cli/arduino/utils"
40+
"github.com/arduino/go-paths-helper"
41+
rice "github.com/cmaglie/go.rice"
42+
"github.com/sirupsen/logrus"
43+
"go.bug.st/downloader/v2"
44+
)
45+
46+
func DownloadTool(toolRelease *cores.ToolRelease) (*paths.Path, error) {
47+
resource := toolRelease.GetCompatibleFlavour()
48+
installDir := globals.FwUploaderPath.Join(
49+
"tools",
50+
toolRelease.Tool.Name,
51+
toolRelease.Version.String())
52+
installDir.MkdirAll()
53+
downloadsDir := globals.FwUploaderPath.Join("downloads")
54+
archivePath := downloadsDir.Join(resource.ArchiveFileName)
55+
archivePath.Parent().MkdirAll()
56+
if err := archivePath.WriteFile(nil); err != nil {
57+
logrus.Error(err)
58+
return nil, err
59+
}
60+
d, err := downloader.Download(archivePath.String(), resource.URL)
61+
if err != nil {
62+
logrus.Error(err)
63+
return nil, err
64+
}
65+
if err := Download(d); err != nil {
66+
logrus.Error(err)
67+
return nil, err
68+
}
69+
if err := resource.Install(downloadsDir, paths.TempDir(), installDir); err != nil {
70+
logrus.Error(err)
71+
return nil, err
72+
}
73+
return installDir, nil
74+
}
75+
76+
func DownloadFirmware(firmware *firmwareindex.IndexFirmware) (*paths.Path, error) {
77+
firmwarePath := globals.FwUploaderPath.Join(
78+
"firmwares",
79+
firmware.Module,
80+
firmware.Version,
81+
path.Base(firmware.URL))
82+
firmwarePath.Parent().MkdirAll()
83+
if err := firmwarePath.WriteFile(nil); err != nil {
84+
logrus.Error(err)
85+
return nil, err
86+
}
87+
d, err := downloader.Download(firmwarePath.String(), firmware.URL)
88+
if err != nil {
89+
logrus.Error(err)
90+
return nil, err
91+
}
92+
if err := Download(d); err != nil {
93+
logrus.Error(err)
94+
return nil, err
95+
}
96+
if err := VerifyFileChecksum(firmware.Checksum, firmwarePath); err != nil {
97+
logrus.Error(err)
98+
return nil, err
99+
}
100+
size, _ := firmware.Size.Int64()
101+
if err := VerifyFileSize(size, firmwarePath); err != nil {
102+
logrus.Error(err)
103+
return nil, err
104+
}
105+
return firmwarePath, nil
106+
}
107+
108+
func DownloadLoaderSketch(loader *firmwareindex.IndexLoaderSketch) (*paths.Path, error) {
109+
loaderPath := globals.FwUploaderPath.Join(
110+
"loader",
111+
path.Base(loader.URL))
112+
loaderPath.Parent().MkdirAll()
113+
if err := loaderPath.WriteFile(nil); err != nil {
114+
logrus.Error(err)
115+
return nil, err
116+
}
117+
d, err := downloader.Download(loaderPath.String(), loader.URL)
118+
if err != nil {
119+
logrus.Error(err)
120+
return nil, err
121+
}
122+
if err := Download(d); err != nil {
123+
logrus.Error(err)
124+
return nil, err
125+
}
126+
if err := VerifyFileChecksum(loader.Checksum, loaderPath); err != nil {
127+
logrus.Error(err)
128+
return nil, err
129+
}
130+
size, _ := loader.Size.Int64()
131+
if err := VerifyFileSize(size, loaderPath); err != nil {
132+
logrus.Error(err)
133+
return nil, err
134+
}
135+
return loaderPath, nil
136+
}
137+
138+
// Download will take a downloader.Downloader as parameter. It will Download the file specified in the downloader
139+
func Download(d *downloader.Downloader) error {
140+
if d == nil {
141+
// This signal means that the file is already downloaded
142+
return nil
143+
}
144+
if err := d.Run(); err != nil {
145+
return fmt.Errorf("failed to download file from %s : %s", d.URL, err)
146+
}
147+
// The URL is not reachable for some reason
148+
if d.Resp.StatusCode >= 400 && d.Resp.StatusCode <= 599 {
149+
return fmt.Errorf(d.Resp.Status)
150+
}
151+
return nil
152+
}
153+
154+
// taken and adapted from https://github.com/arduino/arduino-cli/blob/59b6277a4d6731a1c1579d43aef6df2a46a771d5/arduino/resources/checksums.go
155+
func VerifyFileChecksum(checksum string, filePath *paths.Path) error {
156+
if checksum == "" {
157+
return fmt.Errorf("missing checksum for: %s", filePath)
158+
}
159+
split := strings.SplitN(checksum, ":", 2)
160+
if len(split) != 2 {
161+
return fmt.Errorf("invalid checksum format: %s", checksum)
162+
}
163+
digest, err := hex.DecodeString(split[1])
164+
if err != nil {
165+
return fmt.Errorf("invalid hash '%s': %s", split[1], err)
166+
}
167+
168+
// names based on: https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#MessageDigest
169+
var algo hash.Hash
170+
switch split[0] {
171+
case "SHA-256":
172+
algo = crypto.SHA256.New()
173+
case "SHA-1":
174+
algo = crypto.SHA1.New()
175+
case "MD5":
176+
algo = crypto.MD5.New()
177+
default:
178+
return fmt.Errorf("unsupported hash algorithm: %s", split[0])
179+
}
180+
181+
file, err := filePath.Open()
182+
if err != nil {
183+
return fmt.Errorf("opening file: %s", err)
184+
}
185+
defer file.Close()
186+
if _, err := io.Copy(algo, file); err != nil {
187+
return fmt.Errorf("computing hash: %s", err)
188+
}
189+
if bytes.Compare(algo.Sum(nil), digest) != 0 {
190+
return fmt.Errorf("archive hash differs from hash in index")
191+
}
192+
193+
return nil
194+
}
195+
196+
// taken and adapted from https://github.com/arduino/arduino-cli/blob/59b6277a4d6731a1c1579d43aef6df2a46a771d5/arduino/resources/checksums.go
197+
func VerifyFileSize(size int64, filePath *paths.Path) error {
198+
info, err := filePath.Stat()
199+
if err != nil {
200+
return fmt.Errorf("getting archive info: %s", err)
201+
}
202+
if info.Size() != size {
203+
return fmt.Errorf("fetched archive size differs from size specified in index")
204+
}
205+
206+
return nil
207+
}
208+
209+
// DownloadIndex will download the index in the os temp directory
210+
func DownloadIndex(indexURL string) (*paths.Path, error) {
211+
indexArchiveURL, err := utils.URLParse(indexURL)
212+
if err != nil {
213+
return nil, fmt.Errorf("unable to parse URL %s: %s", indexURL, err)
214+
}
215+
// Create a directory to store temporary files before verifying them and copying to
216+
// their final directory
217+
tempDir, err := paths.MkTempDir("", fmt.Sprintf("%d", rand.Int()))
218+
if err != nil {
219+
return nil, err
220+
}
221+
defer tempDir.Remove()
222+
223+
// Download index
224+
tmpGZIndex := tempDir.Join("index.gz")
225+
d, err := downloader.Download(tmpGZIndex.String(), indexArchiveURL.String())
226+
if err != nil {
227+
return nil, fmt.Errorf("downloading index %s: %s", indexURL, err)
228+
}
229+
if err := Download(d); err != nil || d.Error() != nil {
230+
return nil, fmt.Errorf("downloading index %s: %s %s", indexArchiveURL, d.Error(), err)
231+
}
232+
233+
// Extract the index to temporary file
234+
tmpIndex := tempDir.Join("index.json")
235+
if err := paths.GUnzip(tmpGZIndex, tmpIndex); err != nil {
236+
return nil, fmt.Errorf("unzipping %s", indexArchiveURL)
237+
}
238+
239+
// Download signature file
240+
signatureURL, err := url.Parse(strings.ReplaceAll(indexURL, ".gz", ".sig"))
241+
if err != nil {
242+
return nil, fmt.Errorf("unable to parse URL %s: %s", signatureURL, err)
243+
}
244+
tmpSignature := tempDir.Join("index.json.sig")
245+
246+
d, err = downloader.Download(tmpSignature.String(), signatureURL.String())
247+
if err != nil {
248+
return nil, fmt.Errorf("downloading index signature %s: %s", signatureURL, err)
249+
}
250+
indexSigPath := globals.FwUploaderPath.Join(path.Base(signatureURL.Path))
251+
if err := Download(d); err != nil || d.Error() != nil {
252+
return nil, fmt.Errorf("downloading index signature %s: %s %s", indexArchiveURL, d.Error(), err)
253+
}
254+
if err := verifyIndex(tmpIndex, indexArchiveURL); err != nil {
255+
return nil, fmt.Errorf("signature verification failed: %s", err)
256+
}
257+
if err := globals.FwUploaderPath.MkdirAll(); err != nil { //does not overwrite if dir already present
258+
return nil, fmt.Errorf("can't create data directory %s: %s", globals.FwUploaderPath, err)
259+
}
260+
indexPath := globals.FwUploaderPath.Join(path.Base(strings.ReplaceAll(indexArchiveURL.Path, ".gz", "")))
261+
if err := tmpIndex.CopyTo(indexPath); err != nil { //does overwrite
262+
return nil, fmt.Errorf("saving downloaded index %s: %s", indexArchiveURL, err)
263+
}
264+
if err := tmpSignature.CopyTo(indexSigPath); err != nil { //does overwrite
265+
return nil, fmt.Errorf("saving downloaded index signature: %s", err)
266+
}
267+
return indexPath, nil
268+
}
269+
270+
// verifyIndex verifies if the signature is correct and the index is parsable.
271+
func verifyIndex(indexPath *paths.Path, URL *url.URL) error {
272+
var valid bool
273+
var err error
274+
index := path.Base(URL.Path)
275+
signaturePath := paths.New(fmt.Sprintf("%s.sig", indexPath))
276+
if index == "package_index.json.gz" {
277+
valid, err = verifyPackageIndex(indexPath, signaturePath)
278+
} else if index == "module_firmware_index.json.gz" {
279+
valid, err = verifyModuleFirmwareIndex(indexPath, signaturePath)
280+
} else {
281+
return fmt.Errorf("index %s not supported", URL.Path)
282+
}
283+
284+
if err != nil {
285+
return fmt.Errorf("signature verification error: %s for index %s", err, URL)
286+
}
287+
if !valid {
288+
return fmt.Errorf(`index "%s" has an invalid signature`, URL)
289+
}
290+
return nil
291+
}
292+
293+
func verifyPackageIndex(indexPath, signaturePath *paths.Path) (bool, error) {
294+
valid, _, err := security.VerifyArduinoDetachedSignature(indexPath, signaturePath)
295+
if err != nil {
296+
return valid, err
297+
}
298+
// the signature verification is already done above
299+
if _, err := packageindex.LoadIndex(indexPath); err != nil {
300+
return valid, fmt.Errorf("invalid package index: %s", err)
301+
}
302+
return valid, nil
303+
}
304+
305+
func verifyModuleFirmwareIndex(indexPath, signaturePath *paths.Path) (bool, error) {
306+
keysBox, err := rice.FindBox("gpg_keys")
307+
if err != nil {
308+
return false, fmt.Errorf("could not find bundled signature keys: %s", err)
309+
}
310+
key, err := keysBox.Open("module_firmware_index_public.gpg.key")
311+
if err != nil {
312+
return false, fmt.Errorf("could not find bundled signature keys: %s", err)
313+
}
314+
valid, _, err := security.VerifySignature(indexPath, signaturePath, key)
315+
if err != nil {
316+
return valid, nil
317+
}
318+
// the signature verification is already done above
319+
_, err = firmwareindex.LoadIndexNoSign(indexPath)
320+
if err != nil {
321+
return valid, nil
322+
}
323+
324+
return valid, nil
325+
}

0 commit comments

Comments
 (0)