Skip to content

Commit 59376d2

Browse files
committed
chore: more work towards supporting p7s
1 parent dd76177 commit 59376d2

14 files changed

+353
-30
lines changed

cli/add.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616

1717
func add() *cobra.Command {
1818
addFlags, opts := serverFlags()
19+
1920
cmd := &cobra.Command{
2021
Use: "add <source>",
2122
Short: "Add an extension to the marketplace",

cli/server.go

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"net"
77
"net/http"
8+
"os"
89
"os/signal"
910
"strings"
1011
"time"
@@ -24,14 +25,18 @@ import (
2425

2526
func serverFlags() (addFlags func(cmd *cobra.Command), opts *storage.Options) {
2627
opts = &storage.Options{}
27-
var sign bool
28+
var certificates []string
29+
var signingKeyFile string
2830
return func(cmd *cobra.Command) {
2931
cmd.Flags().StringVar(&opts.ExtDir, "extensions-dir", "", "The path to extensions.")
3032
cmd.Flags().StringVar(&opts.Artifactory, "artifactory", "", "Artifactory server URL.")
3133
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-
34+
cmd.Flags().DurationVar(&opts.ListCacheDuration, "list-cache-duration", time.Minute, "The duration of the extension cache.")
35+
cmd.Flags().StringArrayVar(&certificates, "certs", []string{}, "The path to certificates that match the signing key.")
36+
cmd.Flags().StringVar(&signingKeyFile, "key", "", "The path to signing key file in PEM format.")
37+
cmd.Flags().BoolVar(&opts.SaveSigZips, "save-sigs", false, "Save signed extensions to disk for debugging.")
38+
_ = cmd.Flags().MarkHidden("save-sigs")
39+
3540
if cmd.Use == "server" {
3641
// Server only flags
3742
cmd.Flags().DurationVar(&opts.ListCacheDuration, "list-cache-duration", time.Minute, "The duration of the extension cache.")
@@ -54,8 +59,21 @@ func serverFlags() (addFlags func(cmd *cobra.Command), opts *storage.Options) {
5459
if before != nil {
5560
return before(cmd, args)
5661
}
57-
if sign { // TODO: Remove this for an actual key import
58-
opts.Signer, _ = extensionsign.GenerateKey()
62+
if signingKeyFile != "" { // TODO: Remove this for an actual key import
63+
signingKey, err := os.ReadFile(signingKeyFile)
64+
if err != nil {
65+
return xerrors.Errorf("read signing key: %w", err)
66+
}
67+
68+
signer, err := extensionsign.LoadKey(signingKey)
69+
if err != nil {
70+
return xerrors.Errorf("load signing key: %w", err)
71+
}
72+
opts.Signer = signer
73+
opts.Certificates, err = extensionsign.LoadCertificatesFromDisk(cmd.Context(), opts.Logger, certificates)
74+
if err != nil {
75+
return xerrors.Errorf("load certificates: %w", err)
76+
}
5977
}
6078
return nil
6179
}

cli/signature.go

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
package cli
22

33
import (
4+
"crypto/x509"
45
"fmt"
56
"os"
67

8+
cms "github.com/github/smimesign/ietf-cms"
79
"github.com/spf13/cobra"
810
"golang.org/x/xerrors"
911

1012
"github.com/coder/code-marketplace/extensionsign"
13+
"github.com/coder/code-marketplace/extensionsign/verify"
1114
)
1215

1316
func signature() *cobra.Command {
@@ -17,7 +20,63 @@ func signature() *cobra.Command {
1720
Hidden: true, // Debugging tools
1821
Aliases: []string{"sig", "sigs", "signatures"},
1922
}
20-
cmd.AddCommand(compareSignatureSigZips())
23+
cmd.AddCommand(compareSignatureSigZips(), verifyCmd(), decodeSigCmd())
24+
return cmd
25+
}
26+
27+
func decodeSigCmd() *cobra.Command {
28+
cmd := &cobra.Command{
29+
Use: "decode",
30+
Args: cobra.ExactArgs(1),
31+
RunE: func(cmd *cobra.Command, args []string) error {
32+
data, err := os.ReadFile(args[0])
33+
if err != nil {
34+
return xerrors.Errorf("read %q: %w", args[0], err)
35+
}
36+
37+
signed, err := extensionsign.ExtractP7SSig(data)
38+
if err != nil {
39+
return xerrors.Errorf("extract p7s: %w", err)
40+
}
41+
42+
sd, err := cms.ParseSignedData(signed)
43+
if err != nil {
44+
return xerrors.Errorf("new signed data: %w", err)
45+
}
46+
47+
fmt.Println("Detached:", sd.IsDetached())
48+
certs, err := sd.GetCertificates()
49+
if err != nil {
50+
return xerrors.Errorf("get certs: %w", err)
51+
}
52+
fmt.Println("Certificates:", len(certs))
53+
54+
sdData, err := sd.GetData()
55+
if err != nil {
56+
return xerrors.Errorf("get data: %w", err)
57+
}
58+
fmt.Println("Data:", len(sdData))
59+
60+
vcerts, err := sd.Verify(x509.VerifyOptions{})
61+
if err != nil {
62+
return xerrors.Errorf("verify: %w", err)
63+
}
64+
var _ = vcerts
65+
66+
return nil
67+
},
68+
}
69+
return cmd
70+
}
71+
72+
func verifyCmd() *cobra.Command {
73+
cmd := &cobra.Command{
74+
Use: "verify",
75+
RunE: func(cmd *cobra.Command, args []string) error {
76+
ctx := cmd.Context()
77+
return verify.DownloadVsceSign(ctx)
78+
},
79+
}
2180
return cmd
2281
}
2382

extensionsign/doc.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
// Package extensionsign is a Go implementation of https://github.com/filiptronicek/node-ovsx-sign
2+
// See https://github.com/eclipse/openvsx/issues/543
23
package extensionsign

extensionsign/key.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
11
package extensionsign
22

33
import (
4+
"context"
5+
"crypto"
46
"crypto/ed25519"
57
"crypto/rand"
8+
"crypto/x509"
9+
"encoding/pem"
10+
"fmt"
11+
"os"
12+
13+
"golang.org/x/xerrors"
14+
15+
"cdr.dev/slog"
616
)
717

818
func GenerateKey() (ed25519.PrivateKey, error) {
@@ -12,3 +22,60 @@ func GenerateKey() (ed25519.PrivateKey, error) {
1222
}
1323
return private, nil
1424
}
25+
26+
// To generate a new self signed certificate using openssl:
27+
// openssl req -x509 -newkey Ed25519 -keyout key.pem -out cert.pem -sha256 -days 3650 -nodes -subj "/C=XX/ST=StateName/L=CityName/O=CompanyName/OU=CompanySectionName/CN=CommonNameOrHostname"
28+
// openssl req -x509 -newkey rsa:4096 -keyout key2.pem -out cert2.pem -sha256 -days 3650 -nodes -subj "/C=XX/ST=StateName/L=CityName/O=CompanyName/OU=CompanySectionName/CN=CommonNameOrHostname"
29+
30+
func LoadCertificatesFromDisk(ctx context.Context, logger slog.Logger, files []string) ([]*x509.Certificate, error) {
31+
var certs []*x509.Certificate
32+
for _, file := range files {
33+
certData, err := os.ReadFile(file)
34+
if err != nil {
35+
return nil, xerrors.Errorf("read cert file %q: %w", file, err)
36+
}
37+
38+
for {
39+
block, rest := pem.Decode(certData)
40+
41+
crt, err := x509.ParseCertificate(block.Bytes)
42+
if err != nil {
43+
return nil, xerrors.Errorf("load certificate %q: %w", file, err)
44+
}
45+
logger.Info(ctx, "Loaded certificate",
46+
slog.F("file", file),
47+
slog.F("subject", crt.Subject.CommonName),
48+
)
49+
certs = append(certs, crt)
50+
51+
if len(rest) == 0 {
52+
break
53+
}
54+
certData = rest
55+
}
56+
57+
}
58+
return certs, nil
59+
}
60+
61+
// LoadKey takes in a PEM encoded secret
62+
func LoadKey(secret []byte) (crypto.Signer, error) {
63+
data, rest := pem.Decode(secret)
64+
if len(rest) > 0 {
65+
return nil, xerrors.Errorf("extra data after PEM block")
66+
}
67+
68+
sec, err := x509.ParsePKCS8PrivateKey(data.Bytes)
69+
if err != nil {
70+
_, err2 := x509.ParsePKCS1PrivateKey(secret)
71+
fmt.Println(err2)
72+
return nil, err
73+
}
74+
75+
signer, ok := sec.(crypto.Signer)
76+
if !ok {
77+
return nil, xerrors.Errorf("%T is not a crypto.Signer and is not supported", sec)
78+
}
79+
80+
return signer, nil
81+
}

extensionsign/sigzip.go

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import (
44
"archive/zip"
55
"bytes"
66
"crypto"
7-
"crypto/rand"
7+
"crypto/x509"
88
"encoding/json"
9+
"io"
910

11+
cms "github.com/github/smimesign/ietf-cms"
1012
"golang.org/x/xerrors"
1113

1214
"github.com/coder/code-marketplace/storage/easyzip"
@@ -27,8 +29,18 @@ func ExtractSignatureManifest(zip []byte) (SignatureManifest, error) {
2729
return manifest, nil
2830
}
2931

32+
func ExtractP7SSig(zip []byte) ([]byte, error) {
33+
r, err := easyzip.GetZipFileReader(zip, ".signature.p7s")
34+
if err != nil {
35+
return nil, xerrors.Errorf("get p7s: %w", err)
36+
}
37+
38+
defer r.Close()
39+
return io.ReadAll(r)
40+
}
41+
3042
// SignAndZipManifest signs a manifest and zips it up
31-
func SignAndZipManifest(secret crypto.Signer, manifest json.RawMessage) ([]byte, error) {
43+
func SignAndZipManifest(certs []*x509.Certificate, secret crypto.Signer, manifest json.RawMessage) ([]byte, error) {
3244
var buf bytes.Buffer
3345
w := zip.NewWriter(&buf)
3446

@@ -43,23 +55,18 @@ func SignAndZipManifest(secret crypto.Signer, manifest json.RawMessage) ([]byte,
4355
}
4456

4557
// Empty file
46-
_, err = w.Create(".signature.p7s")
58+
p7sFile, err := w.Create(".signature.p7s")
4759
if err != nil {
4860
return nil, xerrors.Errorf("create empty p7s signature: %w", err)
4961
}
5062

51-
// Actual sig
52-
sigFile, err := w.Create(".signature.sig")
53-
if err != nil {
54-
return nil, xerrors.Errorf("create signature: %w", err)
55-
}
56-
57-
signature, err := secret.Sign(rand.Reader, manifest, crypto.Hash(0))
63+
// Signature file
64+
signature, err := cms.SignDetached(manifest, certs, secret)
5865
if err != nil {
5966
return nil, xerrors.Errorf("sign: %w", err)
6067
}
6168

62-
_, err = sigFile.Write(signature)
69+
_, err = p7sFile.Write(signature)
6370
if err != nil {
6471
return nil, xerrors.Errorf("write signature: %w", err)
6572
}
Binary file not shown.
Binary file not shown.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
https://ms-python.gallerycdn.vsassets.io/extensions/ms-python/python/2024.23.2024121003/1733845882640/Microsoft.VisualStudio.Code.Manifest
2+
https://ms-python.gallerycdn.vsassets.io/extensions/ms-python/python/2024.23.2024121003/1733845882640/Microsoft.VisualStudio.Services.VsixSignature

extensionsign/verify/main.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
2+
import { verify } from "@vscode/vsce-sign";
3+
4+
5+
if(process.argv.length < 2) {
6+
console.log("Usage: node main.js <extension.vsix> <extension.sigzip>")
7+
process.exit(1)
8+
}
9+
10+
verify(process.argv[0], process.argv[1], "true").then((x) => {
11+
console.log(x)
12+
})

extensionsign/verify/verify.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package verify
2+
3+
import (
4+
"archive/tar"
5+
"compress/gzip"
6+
"context"
7+
"encoding/json"
8+
"fmt"
9+
"net/http"
10+
11+
"golang.org/x/xerrors"
12+
)
13+
14+
// curl https://registry.npmjs.org/@vscode/vsce-sign | jq '.versions[."dist-tags".latest].dist.tarball'
15+
16+
type NPMPackage struct {
17+
DistTags map[string]string `json:"dist-tags"`
18+
Versions map[string]struct {
19+
Dist struct {
20+
Tarball string `json:"tarball"`
21+
}
22+
}
23+
}
24+
25+
// go run /home/steven/go/src/github.com/coder/code-marketplace/cmd/marketplace/main.go add --extensions-dir ./extensions -v --key=./extensionsign/testdata/key2.pem --certs=./extensionsign/testdata/cert2.pem --save-sigs https://github.com/VSCodeVim/Vim/releases/download/v1.24.1/vim-1.24.1.vsix
26+
//
27+
// ./node_modules/@vscode/vsce-sign/bin/vsce-sign verify -v --package ../../extensions/vscodevim/vim/1.24.1/vscodevim.vim-1.24.1.vsix --signaturearchive ../../extensions/vscodevim/vim/1.24.1/signature.p7s
28+
// ./node_modules/@vscode/vsce-sign/bin/vsce-sign verify --package ./examples/Microsoft.VisualStudio.Services.VSIXPackage --signaturearchive ./examples/Microsoft.VisualStudio.Services.VsixSignature -v
29+
func DownloadVsceSign(ctx context.Context) error {
30+
cli := http.DefaultClient
31+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://registry.npmjs.org/@vscode/vsce-sign", nil)
32+
if err != nil {
33+
return err
34+
}
35+
36+
resp, err := cli.Do(req)
37+
if err != nil {
38+
return err
39+
}
40+
if resp.StatusCode != http.StatusOK {
41+
return xerrors.Errorf("unexpected status code: %d", resp.StatusCode)
42+
}
43+
44+
var pkg NPMPackage
45+
err = json.NewDecoder(resp.Body).Decode(&pkg)
46+
if err != nil {
47+
return xerrors.Errorf("decode package: %w", err)
48+
}
49+
50+
// If this panics, sorry
51+
tarURL := pkg.Versions[pkg.DistTags["latest"]].Dist.Tarball
52+
req, err = http.NewRequestWithContext(ctx, http.MethodGet, tarURL, nil)
53+
if err != nil {
54+
return xerrors.Errorf("create tar request: %w", err)
55+
}
56+
57+
resp, err = cli.Do(req)
58+
if err != nil {
59+
return xerrors.Errorf("do tar request: %w", err)
60+
}
61+
62+
gzReader, err := gzip.NewReader(resp.Body)
63+
if err != nil {
64+
return xerrors.Errorf("create gzip reader: %w", err)
65+
}
66+
67+
r := tar.NewReader(gzReader)
68+
for {
69+
hdr, err := r.Next()
70+
if err != nil {
71+
return err
72+
}
73+
fmt.Println(hdr.Name)
74+
}
75+
76+
return nil
77+
}

0 commit comments

Comments
 (0)