Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: allow to specify custom signatureKey in the config.ini #1024

Merged
merged 15 commits into from
Mar 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 81 additions & 77 deletions conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package main

import (
"bytes"
"crypto/rsa"
"encoding/json"
"errors"
"fmt"
Expand Down Expand Up @@ -79,111 +80,114 @@ type Upload struct {

var uploadStatusStr = "ProgrammerStatus"

func uploadHandler(c *gin.Context) {
data := new(Upload)
if err := c.BindJSON(data); err != nil {
c.String(http.StatusBadRequest, fmt.Sprintf("err with the payload. %v", err.Error()))
return
}

log.Printf("%+v %+v %+v %+v %+v %+v", data.Port, data.Board, data.Rewrite, data.Commandline, data.Extra, data.Filename)

if data.Port == "" {
c.String(http.StatusBadRequest, "port is required")
return
}

if data.Board == "" {
c.String(http.StatusBadRequest, "board is required")
log.Error("board is required")
return
}

if !data.Extra.Network {
if data.Signature == "" {
c.String(http.StatusBadRequest, "signature is required")
func uploadHandler(pubKey *rsa.PublicKey) func(*gin.Context) {
return func(c *gin.Context) {
data := new(Upload)
if err := c.BindJSON(data); err != nil {
c.String(http.StatusBadRequest, fmt.Sprintf("err with the payload. %v", err.Error()))
return
}

if data.Commandline == "" {
c.String(http.StatusBadRequest, "commandline is required for local board")
log.Printf("%+v %+v %+v %+v %+v %+v", data.Port, data.Board, data.Rewrite, data.Commandline, data.Extra, data.Filename)

if data.Port == "" {
c.String(http.StatusBadRequest, "port is required")
return
}

err := utilities.VerifyInput(data.Commandline, data.Signature)

if err != nil {
c.String(http.StatusBadRequest, "signature is invalid")
if data.Board == "" {
c.String(http.StatusBadRequest, "board is required")
log.Error("board is required")
return
}
}

buffer := bytes.NewBuffer(data.Hex)
if !data.Extra.Network {
if data.Signature == "" {
c.String(http.StatusBadRequest, "signature is required")
return
}

filePath, err := utilities.SaveFileonTempDir(data.Filename, buffer)
if err != nil {
c.String(http.StatusBadRequest, err.Error())
return
}
if data.Commandline == "" {
c.String(http.StatusBadRequest, "commandline is required for local board")
return
}

tmpdir, err := os.MkdirTemp("", "extrafiles")
if err != nil {
c.String(http.StatusBadRequest, err.Error())
return
}
err := utilities.VerifyInput(data.Commandline, data.Signature, pubKey)

for _, extraFile := range data.ExtraFiles {
path, err := utilities.SafeJoin(tmpdir, extraFile.Filename)
if err != nil {
c.String(http.StatusBadRequest, err.Error())
return
if err != nil {
log.WithField("err", err).Error("Error verifying the command")
c.String(http.StatusBadRequest, "signature is invalid")
return
}
}
log.Printf("Saving %s on %s", extraFile.Filename, path)

err = os.MkdirAll(filepath.Dir(path), 0744)
if err != nil {
c.String(http.StatusBadRequest, err.Error())
return
}
buffer := bytes.NewBuffer(data.Hex)

err = os.WriteFile(path, extraFile.Hex, 0644)
filePath, err := utilities.SaveFileonTempDir(data.Filename, buffer)
if err != nil {
c.String(http.StatusBadRequest, err.Error())
return
}
}

if data.Rewrite != "" {
data.Board = data.Rewrite
}

go func() {
// Resolve commandline
commandline, err := upload.PartiallyResolve(data.Board, filePath, tmpdir, data.Commandline, data.Extra, Tools)
tmpdir, err := os.MkdirTemp("", "extrafiles")
if err != nil {
send(map[string]string{uploadStatusStr: "Error", "Msg": err.Error()})
c.String(http.StatusBadRequest, err.Error())
return
}

l := PLogger{Verbose: true}

// Upload
if data.Extra.Network {
err = errors.New("network upload is not supported anymore, pease use OTA instead")
} else {
send(map[string]string{uploadStatusStr: "Starting", "Cmd": "Serial"})
err = upload.Serial(data.Port, commandline, data.Extra, l)
for _, extraFile := range data.ExtraFiles {
path, err := utilities.SafeJoin(tmpdir, extraFile.Filename)
if err != nil {
c.String(http.StatusBadRequest, err.Error())
return
}
log.Printf("Saving %s on %s", extraFile.Filename, path)

err = os.MkdirAll(filepath.Dir(path), 0744)
if err != nil {
c.String(http.StatusBadRequest, err.Error())
return
}

err = os.WriteFile(path, extraFile.Hex, 0644)
if err != nil {
c.String(http.StatusBadRequest, err.Error())
return
}
}

// Handle result
if err != nil {
send(map[string]string{uploadStatusStr: "Error", "Msg": err.Error()})
return
if data.Rewrite != "" {
data.Board = data.Rewrite
}
send(map[string]string{uploadStatusStr: "Done", "Flash": "Ok"})
}()

c.String(http.StatusAccepted, "")
go func() {
// Resolve commandline
commandline, err := upload.PartiallyResolve(data.Board, filePath, tmpdir, data.Commandline, data.Extra, Tools)
if err != nil {
send(map[string]string{uploadStatusStr: "Error", "Msg": err.Error()})
return
}

l := PLogger{Verbose: true}

// Upload
if data.Extra.Network {
err = errors.New("network upload is not supported anymore, pease use OTA instead")
} else {
send(map[string]string{uploadStatusStr: "Starting", "Cmd": "Serial"})
err = upload.Serial(data.Port, commandline, data.Extra, l)
}

// Handle result
if err != nil {
send(map[string]string{uploadStatusStr: "Error", "Msg": err.Error()})
return
}
send(map[string]string{uploadStatusStr: "Done", "Flash": "Ok"})
}()

c.String(http.StatusAccepted, "")
}
}

// PLogger sends the info from the upload to the websocket
Expand Down
13 changes: 10 additions & 3 deletions globals/globals.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,15 @@

package globals

// DefaultIndexURL is the default index url
var (
// SignatureKey is the public key used to verify commands and url sent by the builder
SignatureKey = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvc0yZr1yUSen7qmE3cxF\nIE12rCksDnqR+Hp7o0nGi9123eCSFcJ7CkIRC8F+8JMhgI3zNqn4cUEn47I3RKD1\nZChPUCMiJCvbLbloxfdJrUi7gcSgUXrlKQStOKF5Iz7xv1M4XOP3JtjXLGo3EnJ1\npFgdWTOyoSrA8/w1rck4c/ISXZSinVAggPxmLwVEAAln6Itj6giIZHKvA2fL2o8z\nCeK057Lu8X6u2CG8tRWSQzVoKIQw/PKK6CNXCAy8vo4EkXudRutnEYHEJlPkVgPn\n2qP06GI+I+9zKE37iqj0k1/wFaCVXHXIvn06YrmjQw6I0dDj/60Wvi500FuRVpn9\ntwIDAQAB\n-----END PUBLIC KEY-----"
// ArduinoSignaturePubKey is the public key used to verify commands and url sent by the builder
ArduinoSignaturePubKey = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvc0yZr1yUSen7qmE3cxF
IE12rCksDnqR+Hp7o0nGi9123eCSFcJ7CkIRC8F+8JMhgI3zNqn4cUEn47I3RKD1
ZChPUCMiJCvbLbloxfdJrUi7gcSgUXrlKQStOKF5Iz7xv1M4XOP3JtjXLGo3EnJ1
pFgdWTOyoSrA8/w1rck4c/ISXZSinVAggPxmLwVEAAln6Itj6giIZHKvA2fL2o8z
CeK057Lu8X6u2CG8tRWSQzVoKIQw/PKK6CNXCAy8vo4EkXudRutnEYHEJlPkVgPn
2qP06GI+I+9zKE37iqj0k1/wFaCVXHXIvn06YrmjQw6I0dDj/60Wvi500FuRVpn9
twIDAQAB
-----END PUBLIC KEY-----`
)
16 changes: 12 additions & 4 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ var (
logDump = iniConf.String("log", "off", "off = (default)")
origins = iniConf.String("origins", "", "Allowed origin list for CORS")
portsFilterRegexp = iniConf.String("regex", "usb|acm|com", "Regular expression to filter serial port list")
signatureKey = iniConf.String("signatureKey", globals.SignatureKey, "Pem-encoded public key to verify signed commandlines")
signatureKey = iniConf.String("signatureKey", globals.ArduinoSignaturePubKey, "Pem-encoded public key to verify signed commandlines")
updateURL = iniConf.String("updateUrl", "", "")
verbose = iniConf.Bool("v", true, "show debug logging")
crashreport = iniConf.Bool("crashreport", false, "enable crashreport logging")
Expand Down Expand Up @@ -278,9 +278,17 @@ func loop() {
}
}

if signatureKey == nil || len(*signatureKey) == 0 {
log.Panicf("signature public key should be set")
}
signaturePubKey, err := utilities.ParseRsaPublicKey([]byte(*signatureKey))
if err != nil {
log.Panicf("cannot parse signature key '%s'. %s", *signatureKey, err)
}

// Instantiate Index and Tools
Index = index.Init(*indexURL, config.GetDataDir())
Tools = tools.New(config.GetDataDir(), Index, logger)
Tools = tools.New(config.GetDataDir(), Index, logger, signaturePubKey)

// see if we are supposed to wait 5 seconds
if *isLaunchSelf {
Expand Down Expand Up @@ -454,7 +462,7 @@ func loop() {
r.LoadHTMLFiles("templates/nofirefox.html")

r.GET("/", homeHandler)
r.POST("/upload", uploadHandler)
r.POST("/upload", uploadHandler(signaturePubKey))
r.GET("/socket.io/", socketHandler)
r.POST("/socket.io/", socketHandler)
r.Handle("WS", "/socket.io/", socketHandler)
Expand All @@ -464,7 +472,7 @@ func loop() {
r.POST("/update", updateHandler)

// Mount goa handlers
goa := v2.Server(config.GetDataDir().String(), Index)
goa := v2.Server(config.GetDataDir().String(), Index, signaturePubKey)
r.Any("/v2/*path", gin.WrapH(goa))

go func() {
Expand Down
10 changes: 6 additions & 4 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@ import (

"github.com/arduino/arduino-create-agent/config"
"github.com/arduino/arduino-create-agent/gen/tools"
"github.com/arduino/arduino-create-agent/globals"
"github.com/arduino/arduino-create-agent/index"
"github.com/arduino/arduino-create-agent/upload"
"github.com/arduino/arduino-create-agent/utilities"
v2 "github.com/arduino/arduino-create-agent/v2"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
Expand All @@ -54,7 +56,7 @@ func TestValidSignatureKey(t *testing.T) {

func TestUploadHandlerAgainstEvilFileNames(t *testing.T) {
r := gin.New()
r.POST("/", uploadHandler)
r.POST("/", uploadHandler(utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey))))
ts := httptest.NewServer(r)

uploadEvilFileName := Upload{
Expand Down Expand Up @@ -90,7 +92,7 @@ func TestUploadHandlerAgainstEvilFileNames(t *testing.T) {

func TestUploadHandlerAgainstBase64WithoutPaddingMustFail(t *testing.T) {
r := gin.New()
r.POST("/", uploadHandler)
r.POST("/", uploadHandler(utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey))))
ts := httptest.NewServer(r)
defer ts.Close()

Expand Down Expand Up @@ -119,7 +121,7 @@ func TestInstallToolV2(t *testing.T) {
Index := index.Init(indexURL, config.GetDataDir())

r := gin.New()
goa := v2.Server(config.GetDataDir().String(), Index)
goa := v2.Server(config.GetDataDir().String(), Index, utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey)))
r.Any("/v2/*path", gin.WrapH(goa))
ts := httptest.NewServer(r)

Expand Down Expand Up @@ -213,7 +215,7 @@ func TestInstalledHead(t *testing.T) {
Index := index.Init(indexURL, config.GetDataDir())

r := gin.New()
goa := v2.Server(config.GetDataDir().String(), Index)
goa := v2.Server(config.GetDataDir().String(), Index, utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey)))
r.Any("/v2/*path", gin.WrapH(goa))
ts := httptest.NewServer(r)

Expand Down
6 changes: 4 additions & 2 deletions tools/download_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import (
"testing"
"time"

"github.com/arduino/arduino-create-agent/globals"
"github.com/arduino/arduino-create-agent/index"
"github.com/arduino/arduino-create-agent/utilities"
"github.com/arduino/arduino-create-agent/v2/pkgs"
"github.com/arduino/go-paths-helper"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -128,7 +130,7 @@ func TestDownload(t *testing.T) {
IndexFile: *paths.New("testdata", "test_tool_index.json"),
LastRefresh: time.Now(),
}
testTools := New(tempDirPath, &testIndex, func(msg string) { t.Log(msg) })
testTools := New(tempDirPath, &testIndex, func(msg string) { t.Log(msg) }, utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey)))

for _, tc := range testCases {
t.Run(tc.name+"-"+tc.version, func(t *testing.T) {
Expand Down Expand Up @@ -175,7 +177,7 @@ func TestCorruptedInstalled(t *testing.T) {
defer fileJSON.Close()
_, err = fileJSON.Write([]byte("Hello"))
require.NoError(t, err)
testTools := New(tempDirPath, &testIndex, func(msg string) { t.Log(msg) })
testTools := New(tempDirPath, &testIndex, func(msg string) { t.Log(msg) }, utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey)))
// Download the tool
err = testTools.Download("arduino-test", "avrdude", "6.3.0-arduino17", "keep")
require.NoError(t, err)
Expand Down
5 changes: 3 additions & 2 deletions tools/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package tools

import (
"crypto/rsa"
"encoding/json"
"path/filepath"
"strings"
Expand Down Expand Up @@ -55,14 +56,14 @@ type Tools struct {
// The New functions accept the directory to use to host the tools,
// an index (used to download the tools),
// and a logger to log the operations
func New(directory *paths.Path, index *index.Resource, logger func(msg string)) *Tools {
func New(directory *paths.Path, index *index.Resource, logger func(msg string), signPubKey *rsa.PublicKey) *Tools {
t := &Tools{
directory: directory,
index: index,
logger: logger,
installed: map[string]string{},
mutex: sync.RWMutex{},
tools: pkgs.New(index, directory.String(), "replace"),
tools: pkgs.New(index, directory.String(), "replace", signPubKey),
}
_ = t.readMap()
return t
Expand Down
Loading
Loading