Skip to content

Commit 58e01d2

Browse files
committed
chore: implement signature storage, still need to load a key
1 parent cd09533 commit 58e01d2

File tree

7 files changed

+141
-50
lines changed

7 files changed

+141
-50
lines changed

api/api.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package api
22

33
import (
4+
"context"
45
"encoding/json"
6+
"io/fs"
57
"net/http"
68
"os"
79
"strconv"
@@ -112,7 +114,13 @@ func New(options *Options) *API {
112114
r.Post("/api/extensionquery", api.extensionQuery)
113115

114116
// Endpoint for getting an extension's files or the extension zip.
115-
r.Mount("/files", http.StripPrefix("/files", options.Storage.FileServer()))
117+
r.Mount("/files", http.StripPrefix("/files", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
118+
// Convert the storage into a fs.FS, including the request context
119+
http.FileServerFS(&contextFs{
120+
ctx: r.Context(),
121+
open: options.Storage.Open,
122+
}).ServeHTTP(rw, r)
123+
})))
116124

117125
// VS Code can use the files in the response to get file paths but it will
118126
// sometimes ignore that and use requests to /assets with hardcoded types to
@@ -256,3 +264,12 @@ func (api *API) assetRedirect(rw http.ResponseWriter, r *http.Request) {
256264

257265
http.Redirect(rw, r, url, http.StatusMovedPermanently)
258266
}
267+
268+
type contextFs struct {
269+
ctx context.Context
270+
open func(ctx context.Context, name string) (fs.File, error)
271+
}
272+
273+
func (c *contextFs) Open(name string) (fs.File, error) {
274+
return c.open(c.ctx, name)
275+
}

extensionsign/sigmanifest.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ package extensionsign
22

33
import (
44
"bytes"
5+
"crypto/sha256"
56
"encoding/base64"
67
"errors"
78
"fmt"
89
"io"
910

10-
"github.com/cloudflare/cfssl/scan/crypto/sha256"
1111
"golang.org/x/xerrors"
1212

1313
"github.com/coder/code-marketplace/storage/easyzip"

extensionsign/sigzip.go

Lines changed: 10 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ package extensionsign
33
import (
44
"archive/zip"
55
"bytes"
6-
"crypto/ed25519"
6+
"crypto"
7+
"crypto/rand"
78
"encoding/json"
89

910
"golang.org/x/xerrors"
@@ -26,18 +27,9 @@ func ExtractSignatureManifest(zip []byte) (SignatureManifest, error) {
2627
return manifest, nil
2728
}
2829

29-
func SignAndZipVSIX(key ed25519.PrivateKey, vsix []byte) ([]byte, error) {
30-
manifest, err := GenerateSignatureManifest(vsix)
31-
if err != nil {
32-
return nil, xerrors.Errorf("generate manifest: %w", err)
33-
}
34-
return SignAndZipManifest(key, manifest)
35-
}
36-
37-
// SignAndZip signs a manifest and zips it up
38-
// Should be a PCKS8 key
39-
// TODO: Support other key types
40-
func SignAndZipManifest(key ed25519.PrivateKey, manifest SignatureManifest) ([]byte, error) {
30+
// SignAndZipManifest signs a manifest and zips it up
31+
// Sign
32+
func SignAndZipManifest(secret crypto.Signer, manifest json.RawMessage) ([]byte, error) {
4133
var buf bytes.Buffer
4234
w := zip.NewWriter(&buf)
4335

@@ -46,12 +38,7 @@ func SignAndZipManifest(key ed25519.PrivateKey, manifest SignatureManifest) ([]b
4638
return nil, xerrors.Errorf("create manifest: %w", err)
4739
}
4840

49-
manifestData, err := json.Marshal(manifest)
50-
if err != nil {
51-
return nil, xerrors.Errorf("encode manifest: %w", err)
52-
}
53-
54-
_, err = manFile.Write(manifestData)
41+
_, err = manFile.Write(manifest)
5542
if err != nil {
5643
return nil, xerrors.Errorf("write manifest: %w", err)
5744
}
@@ -68,12 +55,10 @@ func SignAndZipManifest(key ed25519.PrivateKey, manifest SignatureManifest) ([]b
6855
return nil, xerrors.Errorf("create signature: %w", err)
6956
}
7057

71-
signature := ed25519.Sign(key, manifestData)
72-
73-
//signature, err := key.Sign(rand.Reader, manifestData, crypto.SHA512)
74-
//if err != nil {
75-
// return nil, xerrors.Errorf("sign: %w", err)
76-
//}
58+
signature, err := secret.Sign(rand.Reader, manifest, nil)
59+
if err != nil {
60+
return nil, xerrors.Errorf("sign: %w", err)
61+
}
7762

7863
_, err = sigFile.Write(signature)
7964
if err != nil {

storage/artifactory.go

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ import (
77
"errors"
88
"fmt"
99
"io"
10+
"io/fs"
1011
"net/http"
1112
"os"
1213
"path"
14+
"path/filepath"
1315
"sort"
1416
"strings"
1517
"sync"
@@ -276,25 +278,32 @@ func (s *Artifactory) AddExtension(ctx context.Context, manifest *VSIXManifest,
276278
return s.uri + dir, nil
277279
}
278280

279-
func (s *Artifactory) FileServer() http.Handler {
280-
// TODO: Since we only extract a subset of files perhaps if the file does not
281-
// exist we should download the vsix and extract the requested file as a
282-
// fallback. Obviously this seems like quite a bit of overhead so we would
283-
// then emit a warning so we can notice that VS Code has added new asset types
284-
// that we should be extracting to avoid that overhead. Other solutions could
285-
// be implemented though like extracting the VSIX to disk locally and only
286-
// going to Artifactory for the VSIX when it is missing on disk (basically
287-
// using the disk as a cache).
288-
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
289-
reader, code, err := s.read(r.Context(), r.URL.Path)
290-
if err != nil {
291-
http.Error(rw, err.Error(), code)
292-
return
281+
// Open returns a file from Artifactory.
282+
// TODO: Since we only extract a subset of files perhaps if the file does not
283+
// exist we should download the vsix and extract the requested file as a
284+
// fallback. Obviously this seems like quite a bit of overhead so we would
285+
// then emit a warning so we can notice that VS Code has added new asset types
286+
// that we should be extracting to avoid that overhead. Other solutions could
287+
// be implemented though like extracting the VSIX to disk locally and only
288+
// going to Artifactory for the VSIX when it is missing on disk (basically
289+
// using the disk as a cache).
290+
func (s *Artifactory) Open(ctx context.Context, fp string) (fs.File, error) {
291+
resp, code, err := s.request(ctx, http.MethodGet, path.Join(s.repo, fp), nil)
292+
if err != nil {
293+
switch code {
294+
case http.StatusNotFound:
295+
return nil, fs.ErrNotExist
296+
case http.StatusForbidden:
297+
return nil, fs.ErrPermission
298+
default:
299+
return nil, err
293300
}
294-
defer reader.Close()
295-
rw.WriteHeader(http.StatusOK)
296-
_, _ = io.Copy(rw, reader)
297-
})
301+
}
302+
303+
return artifactoryFile{
304+
Response: resp,
305+
path: fp,
306+
}, nil
298307
}
299308

300309
func (s *Artifactory) Manifest(ctx context.Context, publisher, name string, version Version) (*VSIXManifest, error) {
@@ -443,3 +452,30 @@ func (s *Artifactory) Versions(ctx context.Context, publisher, name string) ([]V
443452
sort.Sort(ByVersion(versions))
444453
return versions, nil
445454
}
455+
456+
type contextFs struct {
457+
ctx context.Context
458+
open func(ctx context.Context, name string) (fs.File, error)
459+
}
460+
461+
func (c *contextFs) Open(name string) (fs.File, error) {
462+
return c.open(c.ctx, name)
463+
}
464+
465+
var _ fs.File = (*artifactoryFile)(nil)
466+
var _ fs.FileInfo = (*artifactoryFile)(nil)
467+
468+
type artifactoryFile struct {
469+
*http.Response
470+
path string
471+
}
472+
473+
func (a artifactoryFile) Name() string { return filepath.Base(a.path) }
474+
func (a artifactoryFile) Size() int64 { return a.Response.ContentLength }
475+
func (a artifactoryFile) Mode() fs.FileMode { return fs.FileMode(0) } // ?
476+
func (a artifactoryFile) ModTime() time.Time { return time.Now() }
477+
func (a artifactoryFile) IsDir() bool { return false }
478+
func (a artifactoryFile) Sys() any { return nil }
479+
func (a artifactoryFile) Stat() (fs.FileInfo, error) { return a, nil }
480+
func (a artifactoryFile) Read(i []byte) (int, error) { return a.Response.Body.Read(i) }
481+
func (a artifactoryFile) Close() error { return a.Response.Body.Close() }

storage/local.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"io"
7+
"io/fs"
78
"net/http"
89
"os"
910
"path/filepath"
@@ -141,8 +142,8 @@ func (s *Local) AddExtension(ctx context.Context, manifest *VSIXManifest, vsix [
141142
return dir, nil
142143
}
143144

144-
func (s *Local) FileServer() http.Handler {
145-
return http.FileServer(http.Dir(s.extdir))
145+
func (s *Local) Open(_ context.Context, fp string) (fs.File, error) {
146+
return http.Dir(s.extdir).Open(fp)
146147
}
147148

148149
func (s *Local) Manifest(ctx context.Context, publisher, name string, version Version) (*VSIXManifest, error) {

storage/signature.go

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
package storage
22

33
import (
4+
"bytes"
45
"context"
56
"encoding/json"
7+
"io"
8+
"io/fs"
9+
"path/filepath"
10+
"time"
611

712
"golang.org/x/xerrors"
813

@@ -11,6 +16,11 @@ import (
1116

1217
var _ Storage = (*Signature)(nil)
1318

19+
const (
20+
sigzipFilename = "extension.sigzip"
21+
sigManifestName = ".signature.manifest"
22+
)
23+
1424
type Signature struct {
1525
// SignDesignExtensions is a flag that determines if the signature should
1626
// include the extension payloads.
@@ -40,7 +50,7 @@ func (s *Signature) AddExtension(ctx context.Context, manifest *VSIXManifest, vs
4050
}
4151

4252
return s.Storage.AddExtension(ctx, manifest, vsix, append(extra, File{
43-
RelativePath: ".signature.manifest",
53+
RelativePath: sigManifestName,
4454
Content: data,
4555
})...)
4656
}
@@ -54,10 +64,50 @@ func (s *Signature) Manifest(ctx context.Context, publisher, name string, versio
5464
if s.signExtensions {
5565
manifest.Assets.Asset = append(manifest.Assets.Asset, VSIXAsset{
5666
Type: VSIXSignatureType,
57-
Path: "extension.sigzip",
67+
Path: sigzipFilename,
5868
Addressable: "true",
5969
})
6070
}
6171

6272
return manifest, nil
6373
}
74+
75+
func (s *Signature) Open(ctx context.Context, fp string) (fs.File, error) {
76+
if s.signExtensions && filepath.Base(fp) == sigzipFilename {
77+
// hijack this request, sign the sig manifest
78+
manifest, err := s.Storage.Open(ctx, sigManifestName)
79+
if err != nil {
80+
return nil, err
81+
}
82+
defer manifest.Close()
83+
84+
key, _ := extensionsign.GenerateKey()
85+
manifestData, err := io.ReadAll(manifest)
86+
if err != nil {
87+
return nil, xerrors.Errorf("read signature manifest: %w", err)
88+
}
89+
90+
signed, err := extensionsign.SignAndZipManifest(key, manifestData)
91+
if err != nil {
92+
return nil, xerrors.Errorf("sign and zip manifest: %w", err)
93+
}
94+
return memFile{data: bytes.NewBuffer(signed), name: sigzipFilename}, nil
95+
}
96+
97+
return s.Storage.Open(ctx, fp)
98+
}
99+
100+
type memFile struct {
101+
data *bytes.Buffer
102+
name string
103+
}
104+
105+
func (a memFile) Name() string { return a.name }
106+
func (a memFile) Size() int64 { return int64(a.data.Len()) }
107+
func (a memFile) Mode() fs.FileMode { return fs.FileMode(0) } // ?
108+
func (a memFile) ModTime() time.Time { return time.Now() }
109+
func (a memFile) IsDir() bool { return false }
110+
func (a memFile) Sys() any { return nil }
111+
func (a memFile) Stat() (fs.FileInfo, error) { return a, nil }
112+
func (a memFile) Read(i []byte) (int, error) { return a.data.Read(i) }
113+
func (a memFile) Close() error { return nil }

storage/storage.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/xml"
77
"fmt"
88
"io"
9+
"io/fs"
910
"net/http"
1011
"os"
1112
"regexp"
@@ -212,7 +213,8 @@ type Storage interface {
212213
AddExtension(ctx context.Context, manifest *VSIXManifest, vsix []byte, extra ...File) (string, error)
213214
// FileServer provides a handler for fetching extension repository files from
214215
// a client.
215-
FileServer() http.Handler
216+
//FileServer() fs.FS
217+
Open(ctx context.Context, name string) (fs.File, error)
216218
// Manifest returns the manifest bytes for the provided extension. The
217219
// extension asset itself (the VSIX) will be included on the manifest even if
218220
// it does not exist on the manifest on disk.

0 commit comments

Comments
 (0)