Skip to content
Open
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
1 change: 1 addition & 0 deletions .github/workflows/contrib-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ jobs:
"Type" : "noauth"
}
EOF
./contrib/dev-shell go run github.com/foundriesio/update-server/cmd/server --datadir .compose-server-data tuf-init

- name: Test server
run: |
Expand Down
35 changes: 33 additions & 2 deletions cli/api/updates.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ package api

import (
"io"
"net/url"
"strconv"

models "github.com/foundriesio/update-server/storage/api"
)
Expand Down Expand Up @@ -53,8 +55,37 @@ func (u UpdatesApi) TailRollout(tag, updateName, rollout string) (io.ReadCloser,
return u.api.GetStream(endpoint)
}

func (u UpdatesApi) CreateUpdate(tag, updateName string, body io.Reader) error {
endpoint := "/v1/updates/" + tag + "/" + updateName
// CreateUpdateOptions captures the optional TUF target overrides that can be
// supplied when creating an update.
type CreateUpdateOptions struct {
Version int // Override the target version (AppVersion)
Name string // Override the target name
OstreeHash string // Override the ostree hash
Apps map[string]string // Override docker compose apps (name -> sha256)
}

func (o CreateUpdateOptions) query() string {
values := url.Values{}
if o.Version != 0 {
values.Set("version", strconv.Itoa(o.Version))
}
if o.Name != "" {
values.Set("name", o.Name)
}
if o.OstreeHash != "" {
values.Set("ostree-hash", o.OstreeHash)
}
for name, hash := range o.Apps {
values.Add("apps", name+"="+hash)
}
if len(values) == 0 {
return ""
}
return "?" + values.Encode()
}

func (u UpdatesApi) CreateUpdate(tag, updateName string, opts CreateUpdateOptions, body io.Reader) error {
endpoint := "/v1/updates/" + tag + "/" + updateName + opts.query()
_, err := u.api.Post(endpoint, body, HttpHeader("Content-Type", "application/x-tar"), HttpHeader("Content-Encoding", "gzip"))
return err
}
50 changes: 47 additions & 3 deletions cli/subcommands/updates/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package updates
import (
"fmt"
"os"
"strings"

"github.com/spf13/cobra"

Expand All @@ -19,17 +20,60 @@ var createCmd = &cobra.Command{
Long: `Create an update on Update server by uploading the offline update found in the directory.`,
Args: cobra.ExactArgs(3),
RunE: func(cmd *cobra.Command, args []string) error {
opts, err := createOptions(cmd)
if err != nil {
return err
}
a := api.CtxGetApi(cmd.Context())
cobra.CheckErr(createUpdate(a.Updates(), args[0], args[1], args[2]))
cobra.CheckErr(createUpdate(a.Updates(), args[0], args[1], args[2], opts))
return nil
},
}

func init() {
flags := createCmd.Flags()
flags.Int("version", 0, "Override the target version (AppVersion)")
flags.String("name", "", "Override the target name")
flags.String("ostree-hash", "", "Override the ostree hash")
flags.StringSlice("apps", nil, "Override docker compose apps as name=sha256 (repeatable or comma separated)")
UpdatesCmd.AddCommand(createCmd)
}

func createUpdate(updates api.UpdatesApi, tag, updateName, path string) error {
func createOptions(cmd *cobra.Command) (api.CreateUpdateOptions, error) {
flags := cmd.Flags()
var opts api.CreateUpdateOptions
var err error
if opts.Version, err = flags.GetInt("version"); err != nil {
return opts, err
}
if opts.Name, err = flags.GetString("name"); err != nil {
return opts, err
}
if opts.OstreeHash, err = flags.GetString("ostree-hash"); err != nil {
return opts, err
}
apps, err := flags.GetStringSlice("apps")
if err != nil {
return opts, err
}
for _, pair := range apps {
name, hash, ok := strings.Cut(pair, "=")
if !ok {
return opts, fmt.Errorf("invalid --apps value '%s', expected name=sha256", pair)
}
name = strings.TrimSpace(name)
if name == "" {
return opts, fmt.Errorf("invalid --apps value '%s', expected name=sha256", pair)
}
if opts.Apps == nil {
opts.Apps = make(map[string]string)
}
opts.Apps[name] = strings.TrimSpace(hash)
}
return opts, nil
}

func createUpdate(updates api.UpdatesApi, tag, updateName, path string, opts api.CreateUpdateOptions) error {
if stat, err := os.Stat(path); err != nil {
return fmt.Errorf("failed to stat directory '%s': %w", path, err)
} else if !stat.Mode().IsDir() {
Expand All @@ -45,7 +89,7 @@ func createUpdate(updates api.UpdatesApi, tag, updateName, path string) error {
// See cli/subcommands/configs/upload.go for a comment about how reporter works.
go progress.Report("Uploaded:", stop, done)

err := updates.CreateUpdate(tag, updateName, reader)
err := updates.CreateUpdate(tag, updateName, opts, reader)
stop <- err == nil
<-done
return err
Expand Down
3 changes: 3 additions & 0 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type CommonArgs struct {
Csr *CsrCmd `arg:"subcommand:create-csr" help:"Create a TLS certificate signing request for this server"`
SignCsr *CsrSignCmd `arg:"subcommand:sign-csr" help:"Create the TLS certificate from the signing request"`
Serve *ServeCmd `arg:"subcommand:serve" help:"Run the REST API and device-gateway services"`
TufInit *TufInitCmd `arg:"subcommand:tuf-init" help:"Initialize TUF keys and root metadata for this server"`
UserAdd *UserAddCmd `arg:"subcommand:user-add" help:"Add a new user if local authentication is enabled"`
Version *VersionCmd `arg:"subcommand:version" help:"Print the version of the program"`

Expand All @@ -48,6 +49,8 @@ func main() {
err = args.SignCsr.Run(args)
case args.Serve != nil:
err = args.Serve.Run(args)
case args.TufInit != nil:
err = args.TufInit.Run(args)
case args.AuthInit != nil:
err = args.AuthInit.Run(args)
case args.UserAdd != nil:
Expand Down
3 changes: 3 additions & 0 deletions cmd/server/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ func (c *ServeCmd) Run(args CommonArgs) error {
if err != nil {
return fmt.Errorf("failed to load filesystem: %w", err)
}
if err := fs.Tuf.LoadTuf(); err != nil {
return fmt.Errorf("failed to load TUF (run tuf-init first): %w", err)
}
db, err := storage.NewDb(fs.Config.DbFile())
if err != nil {
return fmt.Errorf("failed to load database: %w", err)
Expand Down
1 change: 1 addition & 0 deletions cmd/server/serve_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ func TestServe(t *testing.T) {
fs, err := storage.NewFs(common.DataDir)
require.Nil(t, err)
require.Nil(t, fs.Auth.InitHmacSecret())
require.Nil(t, fs.Tuf.InitTuf())
authConfig := storage.AuthConfig{
Type: "noauth",
}
Expand Down
18 changes: 18 additions & 0 deletions cmd/server/tuf_init.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
// SPDX-License-Identifier: BSD-3-Clause-Clear

package main

import (
"github.com/foundriesio/update-server/storage"
)

type TufInitCmd struct{}

func (c TufInitCmd) Run(args CommonArgs) error {
fs, err := storage.NewFs(args.DataDir)
if err != nil {
return err
}
return fs.Tuf.InitTuf()
}
3 changes: 3 additions & 0 deletions contrib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ communicate with. In order to use this you must first:

$ go run github.com/foundriesio/update-server/cmd/server \
--datadir .compose-server-data auth-init

$ go run github.com/foundriesio/update-server/cmd/server \
--datadir .compose-server-data tuf-init
```

## gen-certs.sh / fake-device.py
Expand Down
9 changes: 9 additions & 0 deletions docs/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,15 @@ tokens and web sessions, as well as the "noauth" provider.
./fioserver --datadir=./datadir auth-init --test
```

## Initialize TUF

Before the server can sign and manage TUF metadata, the TUF keys and
root metadata must be initialized:

```
./fioserver --datadir=./datadir tuf-init
```

## Run the Server

`./fioserv serve --datadir=datadir`
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/labstack/gommon v0.5.0
github.com/mattn/go-sqlite3 v1.14.32
github.com/pelletier/go-toml v1.9.5
github.com/secure-systems-lab/go-securesystemslib v0.11.0
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
golang.org/x/crypto v0.50.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCko
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/secure-systems-lab/go-securesystemslib v0.11.0 h1:iuCR9kcMFD4QurdKrGvPLoKZLv9YvwPYVr0473BdtFs=
github.com/secure-systems-lab/go-securesystemslib v0.11.0/go.mod h1:+PMOTjUGwHj2vcZ+TFKlb1tXRbrdWE1LYDT5i9JC80Q=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
Expand Down
5 changes: 5 additions & 0 deletions server/ui/api/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,9 @@ func RegisterHandlers(e *echo.Echo, storage *storage.Storage, a auth.Provider) {
upd.PUT("/:tag/:update/rollouts/:rollout", h.rolloutPut, requireScope(users.ScopeUpdatesRU))
upd.GET("/:tag/:update/rollouts/:rollout/tail", h.rolloutTail, requireScope(users.ScopeUpdatesR))
upd.GET("/:tag/:update/tail", h.updateTail, requireScope(users.ScopeUpdatesR))

// TUF root metadata. The static "root.json" route returns the latest
// version; "<n>.root.json" returns a specific version.
g.GET("/tuf/root.json", h.tufRootLatest, requireScope(users.ScopeUpdatesR))
g.GET("/tuf/:version", h.tufRootVersion, requireScope(users.ScopeUpdatesR))
}
57 changes: 57 additions & 0 deletions server/ui/api/handlers_tuf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
// SPDX-License-Identifier: BSD-3-Clause-Clear

package api

import (
"errors"
"net/http"
"os"
"strconv"
"strings"

"github.com/labstack/echo/v4"
)

const rootJsonSuffix = ".root.json"

// @Summary Returns the latest TUF root metadata
// @Description Requires scope: updates:read
// @Tags TUF
// @Produce json
// @Success 200
// @Router /tuf/root.json [get]
func (h handlers) tufRootLatest(c echo.Context) error {
return h.writeTufRoot(c, 0)
}

// @Summary Returns a specific version of the TUF root metadata
// @Description Requires scope: updates:read
// @Tags TUF
// @Produce json
// @Success 200
// @Param version path string true "Root metadata file name, e.g. 3.root.json"
// @Router /tuf/{version}.root.json [get]
func (h handlers) tufRootVersion(c echo.Context) error {
name := c.Param("version")
digits, found := strings.CutSuffix(name, rootJsonSuffix)
if !found {
return EchoError(c, nil, http.StatusNotFound, "not found")
}
version, err := strconv.Atoi(digits)
if err != nil || version < 1 {
return EchoError(c, err, http.StatusNotFound, "invalid root metadata version")
}
return h.writeTufRoot(c, version)
}

func (h handlers) writeTufRoot(c echo.Context, version int) error {
data, err := h.storage.GetTufRoot(version)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return EchoError(c, err, http.StatusNotFound, "root metadata not found")
}
return EchoError(c, err, http.StatusInternalServerError, "failed to read root metadata")
}
return c.JSONBlob(http.StatusOK, data)
}
47 changes: 47 additions & 0 deletions server/ui/api/handlers_tuf_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
// SPDX-License-Identifier: BSD-3-Clause-Clear

package api

import (
"encoding/json"
"testing"

"github.com/stretchr/testify/require"

"github.com/foundriesio/update-server/storage/tuf"
"github.com/foundriesio/update-server/storage/users"
)

func TestApiTufRoot(t *testing.T) {
tc := NewTestClient(t)

// Requires the updates:read scope.
tc.GET("/tuf/root.json", 403)
tc.GET("/tuf/1.root.json", 403)
tc.u.AllowedScopes = users.ScopeUpdatesR

// Before TUF is initialized there is no root metadata.
tc.GET("/tuf/root.json", 404)

require.Nil(t, tc.fs.Auth.InitHmacSecret())
require.Nil(t, tc.fs.Tuf.InitTuf())

// The latest root.json is returned and is valid v1 root metadata.
var root tuf.AtsTufRoot
require.Nil(t, json.Unmarshal(tc.GET("/tuf/root.json", 200), &root))
require.Equal(t, "Root", root.Signed.Type)
require.Equal(t, 1, root.Signed.Version)
require.Len(t, root.Signatures, 1)

// The explicit version returns the same document.
var byVersion tuf.AtsTufRoot
require.Nil(t, json.Unmarshal(tc.GET("/tuf/1.root.json", 200), &byVersion))
require.Equal(t, root.Signed.Version, byVersion.Signed.Version)
require.Equal(t, root.Signatures, byVersion.Signatures)

// Unknown versions and malformed names are 404.
tc.GET("/tuf/2.root.json", 404)
tc.GET("/tuf/not-a-version", 404)
tc.GET("/tuf/0.root.json", 404)
}
Loading
Loading