Skip to content

Commit f35db41

Browse files
authored
chore: implement signing of extensions (#79)
* chore: begin work implementing vsix signatures, manifest matching Based on https://github.com/filiptronicek/node-ovsx-sign
1 parent 2c12582 commit f35db41

27 files changed

+716
-147
lines changed

.github/workflows/build.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
- uses: actions/checkout@v4
1717
- uses: actions/setup-go@v5
1818
with:
19-
go-version: "~1.19"
19+
go-version: "~1.22"
2020

2121
- name: Get Go cache paths
2222
id: go-cache-paths

.github/workflows/lint.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ jobs:
2929
- uses: actions/checkout@v4
3030
- uses: actions/setup-go@v5
3131
with:
32-
go-version: "~1.19"
32+
go-version: "~1.22"
3333
- name: golangci-lint
3434
uses: golangci/[email protected]
3535
with:
36-
version: v1.48.0
36+
version: v1.58.0

.github/workflows/test.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ jobs:
3535
- uses: actions/checkout@v4
3636
- uses: actions/setup-go@v5
3737
with:
38-
go-version: "~1.19"
38+
go-version: "~1.22"
3939

4040
- name: Echo Go Cache Paths
4141
id: go-cache-paths

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
bin
33
coverage
44
extensions
5-
.idea
5+
.idea

api/api.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ func New(options *Options) *API {
112112
r.Post("/api/extensionquery", api.extensionQuery)
113113

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

117117
// VS Code can use the files in the response to get file paths but it will
118118
// sometimes ignore that and use requests to /assets with hardcoded types to

api/api_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ func TestAPI(t *testing.T) {
171171
Response: "foobar",
172172
},
173173
{
174-
Name: "FileAPI",
174+
Name: "FileAPINotExists",
175175
Path: "/files/nonexistent",
176176
Status: http.StatusNotFound,
177177
},

cli/add.go

+3-28
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,12 @@ import (
1010
"github.com/spf13/cobra"
1111
"golang.org/x/xerrors"
1212

13-
"cdr.dev/slog"
14-
"cdr.dev/slog/sloggers/sloghuman"
15-
1613
"github.com/coder/code-marketplace/storage"
1714
"github.com/coder/code-marketplace/util"
1815
)
1916

2017
func add() *cobra.Command {
21-
var (
22-
artifactory string
23-
extdir string
24-
repo string
25-
)
26-
18+
addFlags, opts := serverFlags()
2719
cmd := &cobra.Command{
2820
Use: "add <source>",
2921
Short: "Add an extension to the marketplace",
@@ -37,21 +29,7 @@ func add() *cobra.Command {
3729
ctx, cancel := context.WithCancel(cmd.Context())
3830
defer cancel()
3931

40-
verbose, err := cmd.Flags().GetBool("verbose")
41-
if err != nil {
42-
return err
43-
}
44-
logger := slog.Make(sloghuman.Sink(cmd.ErrOrStderr()))
45-
if verbose {
46-
logger = logger.Leveled(slog.LevelDebug)
47-
}
48-
49-
store, err := storage.NewStorage(ctx, &storage.Options{
50-
Artifactory: artifactory,
51-
ExtDir: extdir,
52-
Logger: logger,
53-
Repo: repo,
54-
})
32+
store, err := storage.NewStorage(ctx, opts)
5533
if err != nil {
5634
return err
5735
}
@@ -98,10 +76,7 @@ func add() *cobra.Command {
9876
return nil
9977
},
10078
}
101-
102-
cmd.Flags().StringVar(&extdir, "extensions-dir", "", "The path to extensions.")
103-
cmd.Flags().StringVar(&artifactory, "artifactory", "", "Artifactory server URL.")
104-
cmd.Flags().StringVar(&repo, "repo", "", "Artifactory repository.")
79+
addFlags(cmd)
10580

10681
return cmd
10782
}

cli/remove.go

+4-25
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,15 @@ import (
1010
"github.com/spf13/cobra"
1111
"golang.org/x/xerrors"
1212

13-
"cdr.dev/slog"
14-
"cdr.dev/slog/sloggers/sloghuman"
15-
1613
"github.com/coder/code-marketplace/storage"
1714
"github.com/coder/code-marketplace/util"
1815
)
1916

2017
func remove() *cobra.Command {
2118
var (
22-
all bool
23-
artifactory string
24-
extdir string
25-
repo string
19+
all bool
2620
)
21+
addFlags, opts := serverFlags()
2722

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

40-
verbose, err := cmd.Flags().GetBool("verbose")
41-
if err != nil {
42-
return err
43-
}
44-
logger := slog.Make(sloghuman.Sink(cmd.ErrOrStderr()))
45-
if verbose {
46-
logger = logger.Leveled(slog.LevelDebug)
47-
}
48-
49-
store, err := storage.NewStorage(ctx, &storage.Options{
50-
Artifactory: artifactory,
51-
ExtDir: extdir,
52-
Logger: logger,
53-
Repo: repo,
54-
})
35+
store, err := storage.NewStorage(ctx, opts)
5536
if err != nil {
5637
return err
5738
}
@@ -120,9 +101,7 @@ func remove() *cobra.Command {
120101
}
121102

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

127106
return cmd
128107
}

cli/root.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
package cli
22

33
import (
4-
"github.com/spf13/cobra"
54
"strings"
5+
6+
"github.com/spf13/cobra"
67
)
78

89
func Root() *cobra.Command {
@@ -16,7 +17,7 @@ func Root() *cobra.Command {
1617
}, "\n"),
1718
}
1819

19-
cmd.AddCommand(add(), remove(), server(), version())
20+
cmd.AddCommand(add(), remove(), server(), version(), signature())
2021

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

cli/server.go

+56-26
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,68 @@ import (
1515

1616
"cdr.dev/slog"
1717
"cdr.dev/slog/sloggers/sloghuman"
18+
"github.com/coder/code-marketplace/extensionsign"
1819

1920
"github.com/coder/code-marketplace/api"
2021
"github.com/coder/code-marketplace/database"
2122
"github.com/coder/code-marketplace/storage"
2223
)
2324

25+
func serverFlags() (addFlags func(cmd *cobra.Command), opts *storage.Options) {
26+
opts = &storage.Options{}
27+
var sign bool
28+
return func(cmd *cobra.Command) {
29+
cmd.Flags().StringVar(&opts.ExtDir, "extensions-dir", "", "The path to extensions.")
30+
cmd.Flags().StringVar(&opts.Artifactory, "artifactory", "", "Artifactory server URL.")
31+
cmd.Flags().StringVar(&opts.Repo, "repo", "", "Artifactory repository.")
32+
cmd.Flags().BoolVar(&sign, "sign", false, "Sign extensions.")
33+
_ = cmd.Flags().MarkHidden("sign") // This flag needs to import a key, not just be a bool
34+
35+
if cmd.Use == "server" {
36+
// Server only flags
37+
cmd.Flags().DurationVar(&opts.ListCacheDuration, "list-cache-duration", time.Minute, "The duration of the extension cache.")
38+
}
39+
40+
var before func(cmd *cobra.Command, args []string) error
41+
if cmd.PreRunE != nil {
42+
before = cmd.PreRunE
43+
}
44+
if cmd.PreRun != nil {
45+
beforeNoE := cmd.PreRun
46+
before = func(cmd *cobra.Command, args []string) error {
47+
beforeNoE(cmd, args)
48+
return nil
49+
}
50+
}
51+
52+
cmd.PreRunE = func(cmd *cobra.Command, args []string) error {
53+
opts.Logger = cmdLogger(cmd)
54+
if before != nil {
55+
return before(cmd, args)
56+
}
57+
if sign { // TODO: Remove this for an actual key import
58+
opts.Signer, _ = extensionsign.GenerateKey()
59+
}
60+
return nil
61+
}
62+
}, opts
63+
}
64+
65+
func cmdLogger(cmd *cobra.Command) slog.Logger {
66+
verbose, _ := cmd.Flags().GetBool("verbose")
67+
logger := slog.Make(sloghuman.Sink(cmd.ErrOrStderr()))
68+
if verbose {
69+
logger = logger.Leveled(slog.LevelDebug)
70+
}
71+
return logger
72+
}
73+
2474
func server() *cobra.Command {
2575
var (
26-
address string
27-
artifactory string
28-
extdir string
29-
repo string
30-
listcacheduration time.Duration
31-
maxpagesize int
76+
address string
77+
maxpagesize int
3278
)
79+
addFlags, opts := serverFlags()
3380

3481
cmd := &cobra.Command{
3582
Use: "server",
@@ -41,26 +88,12 @@ func server() *cobra.Command {
4188
RunE: func(cmd *cobra.Command, args []string) error {
4289
ctx, cancel := context.WithCancel(cmd.Context())
4390
defer cancel()
91+
logger := opts.Logger
4492

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

48-
verbose, err := cmd.Flags().GetBool("verbose")
49-
if err != nil {
50-
return err
51-
}
52-
logger := slog.Make(sloghuman.Sink(cmd.ErrOrStderr()))
53-
if verbose {
54-
logger = logger.Leveled(slog.LevelDebug)
55-
}
56-
57-
store, err := storage.NewStorage(ctx, &storage.Options{
58-
Artifactory: artifactory,
59-
ExtDir: extdir,
60-
Logger: logger,
61-
Repo: repo,
62-
ListCacheDuration: listcacheduration,
63-
})
96+
store, err := storage.NewStorage(ctx, opts)
6497
if err != nil {
6598
return err
6699
}
@@ -137,12 +170,9 @@ func server() *cobra.Command {
137170
},
138171
}
139172

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

147177
return cmd
148178
}

cli/signature.go

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
"github.com/spf13/cobra"
8+
"golang.org/x/xerrors"
9+
10+
"github.com/coder/code-marketplace/extensionsign"
11+
)
12+
13+
func signature() *cobra.Command {
14+
cmd := &cobra.Command{
15+
Use: "signature",
16+
Short: "Commands for debugging and working with signatures.",
17+
Hidden: true, // Debugging tools
18+
Aliases: []string{"sig", "sigs", "signatures"},
19+
}
20+
cmd.AddCommand(compareSignatureSigZips())
21+
return cmd
22+
}
23+
24+
func compareSignatureSigZips() *cobra.Command {
25+
cmd := &cobra.Command{
26+
Use: "compare",
27+
Args: cobra.ExactArgs(2),
28+
RunE: func(cmd *cobra.Command, args []string) error {
29+
decode := func(path string) (extensionsign.SignatureManifest, error) {
30+
data, err := os.ReadFile(path)
31+
if err != nil {
32+
return extensionsign.SignatureManifest{}, xerrors.Errorf("read %q: %w", args[0], err)
33+
}
34+
35+
sig, err := extensionsign.ExtractSignatureManifest(data)
36+
if err != nil {
37+
return extensionsign.SignatureManifest{}, xerrors.Errorf("unmarshal %q: %w", path, err)
38+
}
39+
return sig, nil
40+
}
41+
42+
a, err := decode(args[0])
43+
if err != nil {
44+
return err
45+
}
46+
b, err := decode(args[1])
47+
if err != nil {
48+
return err
49+
}
50+
51+
_, _ = fmt.Fprintf(os.Stdout, "Signature A:%s\n", a)
52+
_, _ = fmt.Fprintf(os.Stdout, "Signature B:%s\n", b)
53+
err = a.Equal(b)
54+
if err != nil {
55+
return err
56+
}
57+
58+
_, _ = fmt.Fprintf(os.Stdout, "Signatures are equal\n")
59+
return nil
60+
},
61+
}
62+
return cmd
63+
}

extensionsign/doc.go

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Package extensionsign is a Go implementation of https://github.com/filiptronicek/node-ovsx-sign
2+
package extensionsign

extensionsign/key.go

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package extensionsign
2+
3+
import (
4+
"crypto/ed25519"
5+
"crypto/rand"
6+
)
7+
8+
func GenerateKey() (ed25519.PrivateKey, error) {
9+
_, private, err := ed25519.GenerateKey(rand.Reader)
10+
if err != nil {
11+
return nil, err
12+
}
13+
return private, nil
14+
}

0 commit comments

Comments
 (0)