Skip to content

chore: implement signing of extensions #79

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 22 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from 18 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
2 changes: 1 addition & 1 deletion .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "~1.19"
go-version: "~1.22"

- name: Get Go cache paths
id: go-cache-paths
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "~1.19"
go-version: "~1.22"
- name: golangci-lint
uses: golangci/[email protected]
with:
version: v1.48.0
version: v1.58.0
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "~1.19"
go-version: "~1.22"

- name: Echo Go Cache Paths
id: go-cache-paths
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
bin
coverage
extensions
.idea
.idea
2 changes: 1 addition & 1 deletion api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ func New(options *Options) *API {
r.Post("/api/extensionquery", api.extensionQuery)

// Endpoint for getting an extension's files or the extension zip.
r.Mount("/files", http.StripPrefix("/files", options.Storage.FileServer()))
r.Mount("/files", http.StripPrefix("/files", storage.HTTPFileServer(options.Storage)))

// VS Code can use the files in the response to get file paths but it will
// sometimes ignore that and use requests to /assets with hardcoded types to
Expand Down
2 changes: 1 addition & 1 deletion api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ func TestAPI(t *testing.T) {
Response: "foobar",
},
{
Name: "FileAPI",
Name: "FileAPINotExists",
Path: "/files/nonexistent",
Status: http.StatusNotFound,
},
Expand Down
31 changes: 3 additions & 28 deletions cli/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,12 @@ import (
"github.com/spf13/cobra"
"golang.org/x/xerrors"

"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"

"github.com/coder/code-marketplace/storage"
"github.com/coder/code-marketplace/util"
)

func add() *cobra.Command {
var (
artifactory string
extdir string
repo string
)

addFlags, opts := serverFlags()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oo yeah very nice

cmd := &cobra.Command{
Use: "add <source>",
Short: "Add an extension to the marketplace",
Expand All @@ -37,21 +29,7 @@ func add() *cobra.Command {
ctx, cancel := context.WithCancel(cmd.Context())
defer cancel()

verbose, err := cmd.Flags().GetBool("verbose")
if err != nil {
return err
}
logger := slog.Make(sloghuman.Sink(cmd.ErrOrStderr()))
if verbose {
logger = logger.Leveled(slog.LevelDebug)
}

store, err := storage.NewStorage(ctx, &storage.Options{
Artifactory: artifactory,
ExtDir: extdir,
Logger: logger,
Repo: repo,
})
store, err := storage.NewStorage(ctx, opts)
if err != nil {
return err
}
Expand Down Expand Up @@ -98,10 +76,7 @@ func add() *cobra.Command {
return nil
},
}

cmd.Flags().StringVar(&extdir, "extensions-dir", "", "The path to extensions.")
cmd.Flags().StringVar(&artifactory, "artifactory", "", "Artifactory server URL.")
cmd.Flags().StringVar(&repo, "repo", "", "Artifactory repository.")
addFlags(cmd)

return cmd
}
Expand Down
29 changes: 4 additions & 25 deletions cli/remove.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,15 @@ import (
"github.com/spf13/cobra"
"golang.org/x/xerrors"

"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"

"github.com/coder/code-marketplace/storage"
"github.com/coder/code-marketplace/util"
)

func remove() *cobra.Command {
var (
all bool
artifactory string
extdir string
repo string
all bool
)
addFlags, opts := serverFlags()

cmd := &cobra.Command{
Use: "remove <id>",
Expand All @@ -37,21 +32,7 @@ func remove() *cobra.Command {
ctx, cancel := context.WithCancel(cmd.Context())
defer cancel()

verbose, err := cmd.Flags().GetBool("verbose")
if err != nil {
return err
}
logger := slog.Make(sloghuman.Sink(cmd.ErrOrStderr()))
if verbose {
logger = logger.Leveled(slog.LevelDebug)
}

store, err := storage.NewStorage(ctx, &storage.Options{
Artifactory: artifactory,
ExtDir: extdir,
Logger: logger,
Repo: repo,
})
store, err := storage.NewStorage(ctx, opts)
if err != nil {
return err
}
Expand Down Expand Up @@ -120,9 +101,7 @@ func remove() *cobra.Command {
}

cmd.Flags().BoolVar(&all, "all", false, "Whether to delete all versions of the extension.")
cmd.Flags().StringVar(&extdir, "extensions-dir", "", "The path to extensions.")
cmd.Flags().StringVar(&artifactory, "artifactory", "", "Artifactory server URL.")
cmd.Flags().StringVar(&repo, "repo", "", "Artifactory repository.")
addFlags(cmd)

return cmd
}
5 changes: 3 additions & 2 deletions cli/root.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package cli

import (
"github.com/spf13/cobra"
"strings"

"github.com/spf13/cobra"
)

func Root() *cobra.Command {
Expand All @@ -16,7 +17,7 @@ func Root() *cobra.Command {
}, "\n"),
}

cmd.AddCommand(add(), remove(), server(), version())
cmd.AddCommand(add(), remove(), server(), version(), signature())

cmd.PersistentFlags().BoolP("verbose", "v", false, "Enable verbose output")

Expand Down
78 changes: 52 additions & 26 deletions cli/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,64 @@ import (

"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
"github.com/coder/code-marketplace/extensionsign"

"github.com/coder/code-marketplace/api"
"github.com/coder/code-marketplace/database"
"github.com/coder/code-marketplace/storage"
)

func serverFlags() (addFlags func(cmd *cobra.Command), opts *storage.Options) {
opts = &storage.Options{}
var sign bool
return func(cmd *cobra.Command) {
cmd.Flags().StringVar(&opts.ExtDir, "extensions-dir", "", "The path to extensions.")
cmd.Flags().StringVar(&opts.Artifactory, "artifactory", "", "Artifactory server URL.")
cmd.Flags().StringVar(&opts.Repo, "repo", "", "Artifactory repository.")
cmd.Flags().DurationVar(&opts.ListCacheDuration, "list-cache-duration", time.Minute, "The duration of the extension cache.")
cmd.Flags().BoolVar(&sign, "sign", false, "Sign extensions.")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sort of a similar thought process here, I know it is hidden at the moment, but remove with --sign might not make sense, I think? Also not sure about add, still need to go through the rest of the PR, but we are signing on demand right? So only with server? I am assuming we just always create the signature manifest on add.

Mostly thinking of it in terms of what a user might see and think when they run subcommand --help.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right it does not make the most sense, but it is hidden. The sign flag will be removed though, replaced by something that will parse a key or cert.

See the flags in my other PR:

cmd.Flags().StringArrayVar(&certificates, "certs", []string{}, "The path to certificates that match the signing key.")
cmd.Flags().StringVar(&signingKeyFile, "key", "", "The path to signing key file in PEM format.")
cmd.Flags().BoolVar(&opts.SaveSigZips, "save-sigs", false, "Save signed extensions to disk for debugging.")

I do have a flag to save the signature to disk. Makes it easier to debug

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to leave sign for now. I intend to fix it before being unhidden, anyone using it will be broken when I change things.

When I fix it, I'll make the flags make sense.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fabulous!

_ = cmd.Flags().MarkHidden("sign") // This flag needs to import a key, not just be a bool

var before func(cmd *cobra.Command, args []string) error
if cmd.PreRunE != nil {
before = cmd.PreRunE
}
if cmd.PreRun != nil {
beforeNoE := cmd.PreRun
before = func(cmd *cobra.Command, args []string) error {
beforeNoE(cmd, args)
return nil
}
}

cmd.PreRunE = func(cmd *cobra.Command, args []string) error {
opts.Logger = cmdLogger(cmd)
if before != nil {
return before(cmd, args)
}
if sign { // TODO: Remove this for an actual key import
opts.Signer, _ = extensionsign.GenerateKey()
}
return nil
}
}, opts
}

func cmdLogger(cmd *cobra.Command) slog.Logger {
verbose, _ := cmd.Flags().GetBool("verbose")
logger := slog.Make(sloghuman.Sink(cmd.ErrOrStderr()))
if verbose {
logger = logger.Leveled(slog.LevelDebug)
}
return logger
}

func server() *cobra.Command {
var (
address string
artifactory string
extdir string
repo string
listcacheduration time.Duration
maxpagesize int
address string
maxpagesize int
)
addFlags, opts := serverFlags()

cmd := &cobra.Command{
Use: "server",
Expand All @@ -41,26 +84,12 @@ func server() *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error {
ctx, cancel := context.WithCancel(cmd.Context())
defer cancel()
logger := opts.Logger

notifyCtx, notifyStop := signal.NotifyContext(ctx, interruptSignals...)
defer notifyStop()

verbose, err := cmd.Flags().GetBool("verbose")
if err != nil {
return err
}
logger := slog.Make(sloghuman.Sink(cmd.ErrOrStderr()))
if verbose {
logger = logger.Leveled(slog.LevelDebug)
}

store, err := storage.NewStorage(ctx, &storage.Options{
Artifactory: artifactory,
ExtDir: extdir,
Logger: logger,
Repo: repo,
ListCacheDuration: listcacheduration,
})
store, err := storage.NewStorage(ctx, opts)
if err != nil {
return err
}
Expand Down Expand Up @@ -137,12 +166,9 @@ func server() *cobra.Command {
},
}

cmd.Flags().StringVar(&extdir, "extensions-dir", "", "The path to extensions.")
cmd.Flags().IntVar(&maxpagesize, "max-page-size", api.MaxPageSizeDefault, "The maximum number of pages to request")
cmd.Flags().StringVar(&artifactory, "artifactory", "", "Artifactory server URL.")
cmd.Flags().StringVar(&repo, "repo", "", "Artifactory repository.")
cmd.Flags().StringVar(&address, "address", "127.0.0.1:3001", "The address on which to serve the marketplace API.")
cmd.Flags().DurationVar(&listcacheduration, "list-cache-duration", time.Minute, "The duration of the extension cache.")
addFlags(cmd)

return cmd
}
63 changes: 63 additions & 0 deletions cli/signature.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package cli

import (
"fmt"
"os"

"github.com/spf13/cobra"
"golang.org/x/xerrors"

"github.com/coder/code-marketplace/extensionsign"
)

func signature() *cobra.Command {
cmd := &cobra.Command{
Use: "signature",
Short: "Commands for debugging and working with signatures.",
Hidden: true, // Debugging tools
Aliases: []string{"sig", "sigs", "signatures"},
}
cmd.AddCommand(compareSignatureSigZips())
return cmd
}

func compareSignatureSigZips() *cobra.Command {
cmd := &cobra.Command{
Use: "compare",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
decode := func(path string) (extensionsign.SignatureManifest, error) {
data, err := os.ReadFile(path)
if err != nil {
return extensionsign.SignatureManifest{}, xerrors.Errorf("read %q: %w", args[0], err)
}

sig, err := extensionsign.ExtractSignatureManifest(data)
if err != nil {
return extensionsign.SignatureManifest{}, xerrors.Errorf("unmarshal %q: %w", path, err)
}
return sig, nil
}

a, err := decode(args[0])
if err != nil {
return err
}
b, err := decode(args[1])
if err != nil {
return err
}

_, _ = fmt.Fprintf(os.Stdout, "Signature A:%s\n", a)
_, _ = fmt.Fprintf(os.Stdout, "Signature B:%s\n", b)
err = a.Equal(b)
if err != nil {
return err
}

_, _ = fmt.Fprintf(os.Stdout, "Signatures are equal\n")
return nil
},
}
return cmd
}
2 changes: 2 additions & 0 deletions extensionsign/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package extensionsign is a Go implementation of https://github.com/filiptronicek/node-ovsx-sign
package extensionsign
14 changes: 14 additions & 0 deletions extensionsign/key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package extensionsign

import (
"crypto/ed25519"
"crypto/rand"
)

func GenerateKey() (ed25519.PrivateKey, error) {
_, private, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, err
}
return private, nil
}
Loading
Loading