Skip to content

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

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

Merged
merged 15 commits into from
Mar 27, 2025
Merged
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
@@ -19,6 +19,7 @@ package main

import (
"bytes"
"crypto/rsa"
"encoding/json"
"errors"
"fmt"
@@ -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
13 changes: 10 additions & 3 deletions globals/globals.go
Original file line number Diff line number Diff line change
@@ -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
@@ -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")
@@ -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 {
@@ -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)
@@ -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() {
10 changes: 6 additions & 4 deletions main_test.go
Original file line number Diff line number Diff line change
@@ -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"
@@ -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{
@@ -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()

@@ -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)

@@ -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)

6 changes: 4 additions & 2 deletions tools/download_test.go
Original file line number Diff line number Diff line change
@@ -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"
@@ -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) {
@@ -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)
5 changes: 3 additions & 2 deletions tools/tools.go
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@
package tools

import (
"crypto/rsa"
"encoding/json"
"path/filepath"
"strings"
@@ -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
45 changes: 32 additions & 13 deletions utilities/utilities.go
Original file line number Diff line number Diff line change
@@ -30,8 +30,6 @@ import (
"os/exec"
"path/filepath"
"strings"

"github.com/arduino/arduino-create-agent/globals"
)

// SaveFileonTempDir creates a temp directory and saves the file data as the
@@ -131,23 +129,44 @@ func SafeJoin(parent, subdir string) (string, error) {
return res, nil
}

// VerifyInput will verify an input against a signature
// VerifyInput will verify an input against a signature using the public key.
// A valid signature is indicated by returning a nil error.
func VerifyInput(input string, signature string) error {
func VerifyInput(input string, signature string, pubKey *rsa.PublicKey) error {
sign, _ := hex.DecodeString(signature)
block, _ := pem.Decode([]byte(globals.SignatureKey))
h := sha256.New()
h.Write([]byte(input))
d := h.Sum(nil)
return rsa.VerifyPKCS1v15(pubKey, crypto.SHA256, d, sign)
}

// ParseRsaPublicKey parses a public key in PEM format and returns the rsa.PublicKey object.
// Returns an error if the key is invalid.
func ParseRsaPublicKey(key []byte) (*rsa.PublicKey, error) {
block, _ := pem.Decode(key)
if block == nil {
return errors.New("invalid key")
return nil, errors.New("invalid key")
}
key, err := x509.ParsePKIXPublicKey(block.Bytes)

parsedKey, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return err
return nil, err
}
rsaKey := key.(*rsa.PublicKey)
h := sha256.New()
h.Write([]byte(input))
d := h.Sum(nil)
return rsa.VerifyPKCS1v15(rsaKey, crypto.SHA256, d, sign)

publicKey, ok := parsedKey.(*rsa.PublicKey)
if !ok {
return nil, fmt.Errorf("not an rsa key")
}
return publicKey, nil
}

// MustParseRsaPublicKey parses a public key in PEM format and returns the rsa.PublicKey object.
// Panics if the key is invalid.
func MustParseRsaPublicKey(key []byte) *rsa.PublicKey {
parsedKey, err := ParseRsaPublicKey(key)
if err != nil {
panic(err)
}
return parsedKey
}

// UserPrompt executes an osascript and returns the pressed button
5 changes: 3 additions & 2 deletions v2/http.go
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@ package v2

import (
"context"
"crypto/rsa"
"encoding/json"
"net/http"

@@ -31,7 +32,7 @@ import (
)

// Server is the actual server
func Server(directory string, index *index.Resource) http.Handler {
func Server(directory string, index *index.Resource, pubKey *rsa.PublicKey) http.Handler {
mux := goahttp.NewMuxer()

// Instantiate logger
@@ -40,7 +41,7 @@ func Server(directory string, index *index.Resource) http.Handler {
logAdapter := LogAdapter{Logger: logger}

// Mount tools
toolsSvc := pkgs.New(index, directory, "replace")
toolsSvc := pkgs.New(index, directory, "replace", pubKey)
toolsEndpoints := toolssvc.NewEndpoints(toolsSvc)
toolsServer := toolssvr.New(toolsEndpoints, mux, CustomRequestDecoder, goahttp.ResponseEncoder, errorHandler(logger), nil)
toolssvr.Mount(mux, toolsServer)
27 changes: 15 additions & 12 deletions v2/pkgs/tools.go
Original file line number Diff line number Diff line change
@@ -18,6 +18,7 @@ package pkgs
import (
"bytes"
"context"
"crypto/rsa"
"crypto/sha256"
"encoding/hex"
"encoding/json"
@@ -58,23 +59,25 @@ var (
//
// It requires an Index Resource to search for tools
type Tools struct {
index *index.Resource
folder string
behaviour string
installed map[string]string
mutex sync.RWMutex
index *index.Resource
folder string
behaviour string
installed map[string]string
mutex sync.RWMutex
verifySignaturePubKey *rsa.PublicKey // public key used to verify the signature of a command sent to the boards
}

// New will return a Tool object, allowing the caller to execute operations on it.
// The New function will accept an index as parameter (used to download the indexes)
// and a folder used to download the indexes
func New(index *index.Resource, folder, behaviour string) *Tools {
func New(index *index.Resource, folder, behaviour string, verifySignaturePubKey *rsa.PublicKey) *Tools {
t := &Tools{
index: index,
folder: folder,
behaviour: behaviour,
installed: map[string]string{},
mutex: sync.RWMutex{},
index: index,
folder: folder,
behaviour: behaviour,
installed: map[string]string{},
mutex: sync.RWMutex{},
verifySignaturePubKey: verifySignaturePubKey,
}
t.readInstalled()
return t
@@ -166,7 +169,7 @@ func (t *Tools) Install(ctx context.Context, payload *tools.ToolPayload) (*tools

//if URL is defined and is signed we verify the signature and override the name, payload, version parameters
if payload.URL != nil && payload.Signature != nil && payload.Checksum != nil {
err := utilities.VerifyInput(*payload.URL, *payload.Signature)
err := utilities.VerifyInput(*payload.URL, *payload.Signature, t.verifySignaturePubKey)
if err != nil {
return nil, err
}
10 changes: 6 additions & 4 deletions v2/pkgs/tools_test.go
Original file line number Diff line number Diff line change
@@ -25,7 +25,9 @@ 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/utilities"
"github.com/arduino/arduino-create-agent/v2/pkgs"
"github.com/arduino/go-paths-helper"
"github.com/stretchr/testify/require"
@@ -45,7 +47,7 @@ func TestTools(t *testing.T) {
// Instantiate Index
Index := index.Init(indexURL, config.GetDataDir())

service := pkgs.New(Index, tmp, "replace")
service := pkgs.New(Index, tmp, "replace", utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey)))

ctx := context.Background()

@@ -126,7 +128,7 @@ func TestEvilFilename(t *testing.T) {
// Instantiate Index
Index := index.Init(indexURL, config.GetDataDir())

service := pkgs.New(Index, tmp, "replace")
service := pkgs.New(Index, tmp, "replace", utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey)))

ctx := context.Background()

@@ -195,7 +197,7 @@ func TestInstalledHead(t *testing.T) {
// Instantiate Index
Index := index.Init(indexURL, config.GetDataDir())

service := pkgs.New(Index, tmp, "replace")
service := pkgs.New(Index, tmp, "replace", utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey)))

ctx := context.Background()

@@ -216,7 +218,7 @@ func TestInstall(t *testing.T) {
LastRefresh: time.Now(),
}

tool := pkgs.New(testIndex, tmp, "replace")
tool := pkgs.New(testIndex, tmp, "replace", utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey)))

ctx := context.Background()