diff --git a/.gitignore b/.gitignore index 92996d1..53fd612 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ bin coverage extensions .idea +.run \ No newline at end of file diff --git a/cli/add.go b/cli/add.go index 1bf17d4..6a05dda 100644 --- a/cli/add.go +++ b/cli/add.go @@ -16,6 +16,7 @@ import ( func add() *cobra.Command { addFlags, opts := serverFlags() + cmd := &cobra.Command{ Use: "add ", Short: "Add an extension to the marketplace", diff --git a/cli/server.go b/cli/server.go index 7a63fe0..76f900b 100644 --- a/cli/server.go +++ b/cli/server.go @@ -5,6 +5,7 @@ import ( "errors" "net" "net/http" + "os" "os/signal" "strings" "time" @@ -24,13 +25,14 @@ import ( func serverFlags() (addFlags func(cmd *cobra.Command), opts *storage.Options) { opts = &storage.Options{} - var sign bool + var certificates []string + var signingKeyFile string 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().BoolVar(&sign, "sign", false, "Sign extensions.") - _ = cmd.Flags().MarkHidden("sign") // This flag needs to import a key, not just be a bool + 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.") _ = cmd.Flags().MarkHidden("save-sigs") @@ -56,8 +58,21 @@ func serverFlags() (addFlags func(cmd *cobra.Command), opts *storage.Options) { if before != nil { return before(cmd, args) } - if sign { // TODO: Remove this for an actual key import - opts.Signer, _ = extensionsign.GenerateKey() + if signingKeyFile != "" { // TODO: Remove this for an actual key import + signingKey, err := os.ReadFile(signingKeyFile) + if err != nil { + return xerrors.Errorf("read signing key: %w", err) + } + + signer, err := extensionsign.LoadKey(signingKey) + if err != nil { + return xerrors.Errorf("load signing key: %w", err) + } + opts.Signer = signer + opts.Certificates, err = extensionsign.LoadCertificatesFromDisk(cmd.Context(), opts.Logger, certificates) + if err != nil { + return xerrors.Errorf("load certificates: %w", err) + } } return nil } diff --git a/cli/signature.go b/cli/signature.go index 432a14a..85f3f13 100644 --- a/cli/signature.go +++ b/cli/signature.go @@ -1,12 +1,19 @@ package cli import ( + "context" + "crypto/x509" "fmt" "os" + "os/exec" + "path/filepath" + "strings" + cms "github.com/github/smimesign/ietf-cms" "github.com/spf13/cobra" "golang.org/x/xerrors" + "cdr.dev/slog" "github.com/coder/code-marketplace/extensionsign" ) @@ -17,10 +24,242 @@ func signature() *cobra.Command { Hidden: true, // Debugging tools Aliases: []string{"sig", "sigs", "signatures"}, } - cmd.AddCommand(compareSignatureSigZips()) + + cmd.AddCommand(compareSignatureSigZips(), verifySig()) return cmd } +var ( + localCA = false +) + +func verifySig() *cobra.Command { + cmd := &cobra.Command{ + Use: "verify ", + Short: "Decode & verify a signature archive.", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + logger := cmdLogger(cmd) + ctx := cmd.Context() + extensionVsix := args[0] + msgData, err := os.ReadFile(extensionVsix) + if err != nil { + return xerrors.Errorf("read %q: %w", extensionVsix, err) + } + + p7sFile := args[1] + + logger.Info(ctx, fmt.Sprintf("Decoding %q", p7sFile)) + + data, err := os.ReadFile(p7sFile) + if err != nil { + return xerrors.Errorf("read %q: %w", p7sFile, err) + } + + //msg, err := easyzip.GetZipFileReader(data, extensionVsix) + //if err != nil { + // return xerrors.Errorf("get manifest: %w", err) + //} + //msgData, err := io.ReadAll(msg) + //if err != nil { + // return xerrors.Errorf("read manifest: %w", err) + //} + + signed, err := extensionsign.ExtractP7SSig(data) + if err != nil { + return xerrors.Errorf("extract p7s: %w", err) + } + + fmt.Println("----------------Golang Verify----------------") + valid, err := goVerify(ctx, logger, msgData, signed) + if err != nil { + logger.Error(ctx, "go verify", slog.Error(err)) + } + logger.Info(ctx, fmt.Sprintf("Valid: %t", valid)) + + fmt.Println("----------------OpenSSL Verify CMS----------------") + valid, err = openSSLVerify(ctx, "cms", logger, msgData, signed) + if err != nil { + logger.Error(ctx, "openssl verify", slog.Error(err)) + } + logger.Info(ctx, fmt.Sprintf("Valid: %t", valid)) + + fmt.Println("----------------OpenSSL Verify SMIME----------------") + valid, err = openSSLVerify(ctx, "smime", logger, msgData, signed) + if err != nil { + logger.Error(ctx, "openssl verify", slog.Error(err)) + } + logger.Info(ctx, fmt.Sprintf("Valid: %t", valid)) + + fmt.Println("----------------node-ovsx-sign Verify----------------") + valid, err = openVSXSignVerify(ctx, logger, extensionVsix, p7sFile) + if err != nil { + logger.Error(ctx, "open vsx verify", slog.Error(err)) + } + logger.Info(ctx, fmt.Sprintf("Valid: %t", valid)) + + fmt.Println("----------------vsce-sign Verify----------------") + valid, err = vsceSignVerify(ctx, logger, extensionVsix, p7sFile) + if err != nil { + logger.Error(ctx, "openssl verify", slog.Error(err)) + } + logger.Info(ctx, fmt.Sprintf("Valid: %t", valid)) + + return nil + }, + } + cmd.Flags().BoolVar(&localCA, "local-ca", true, "Use the local CA for verification.") + return cmd +} + +func goVerify(ctx context.Context, logger slog.Logger, message []byte, signature []byte) (bool, error) { + sd, err := cms.ParseSignedData(signature) + if err != nil { + return false, xerrors.Errorf("new signed data: %w", err) + } + + fmt.Println("Detached:", sd.IsDetached()) + certs, err := sd.GetCertificates() + if err != nil { + return false, xerrors.Errorf("get certs: %w", err) + } + fmt.Println("Certificates:", len(certs)) + + sdData, err := sd.GetData() + if err != nil { + return false, xerrors.Errorf("get data: %w", err) + } + fmt.Println("Data:", len(sdData)) + + var verifyErr error + var vcerts [][][]*x509.Certificate + + sys, err := x509.SystemCertPool() + if err != nil { + return false, xerrors.Errorf("system cert pool: %w", err) + } + opts := x509.VerifyOptions{ + Intermediates: sys, + Roots: sys, + } + + if sd.IsDetached() { + vcerts, verifyErr = sd.VerifyDetached(message, opts) + } else { + vcerts, verifyErr = sd.Verify(opts) + } + if verifyErr != nil { + logger.Error(ctx, "verify", slog.Error(verifyErr)) + } + + certChain := dimensions(vcerts) + fmt.Println(certChain) + return verifyErr == nil, nil +} + +func openSSLVerify(ctx context.Context, algo string, logger slog.Logger, message []byte, signature []byte) (bool, error) { + // openssl cms -verify -in message_from_alice_for_bob.msg -inform DER -CAfile ehealth_root_ca.cer | openssl cms -decrypt -inform DER -recip bob_etk_pair.pem | openssl cms -inform DER -cmsout -print + tmpdir := os.TempDir() + tmpdir = filepath.Join(tmpdir, "verify-sigs") + defer os.RemoveAll(tmpdir) + os.MkdirAll(tmpdir, 0755) + msgPath := filepath.Join(tmpdir, ".signature.manifest") + err := os.WriteFile(msgPath, message, 0644) + if err != nil { + return false, xerrors.Errorf("write message: %w", err) + } + + sigPath := filepath.Join(tmpdir, ".signature.p7s") + err = os.WriteFile(sigPath, signature, 0644) + if err != nil { + return false, xerrors.Errorf("write signature: %w", err) + } + + if localCA { + + } + + // smime or cms + cmd := exec.CommandContext(ctx, "openssl", algo, "-verify", + "-in", sigPath, "-content", msgPath, "-inform", "DER", + ) + if localCA { + cmd.Args = append(cmd.Args, "-CAfile", "/home/steven/go/src/github.com/coder/code-marketplace/extensionsign/testdata/cert2.pem") + } + output := &strings.Builder{} + //cmd.Stdout = output + cmd.Stderr = output + err = cmd.Run() + fmt.Println(output.String()) + if err != nil { + return false, xerrors.Errorf("run verify %q: %w", cmd.String(), err) + } + + return cmd.ProcessState.ExitCode() == 0, nil +} + +func openVSXSignVerify(ctx context.Context, logger slog.Logger, vsixPath, sigPath string) (bool, error) { + bin := os.Getenv("OPEN_VSX_SIGN_PATH") + if bin == "" { + return false, xerrors.Errorf("OPEN_VSX_SIGN_PATH not set") + } + + cmd := exec.CommandContext(ctx, bin, "verify", + vsixPath, + sigPath, + "--public-key", "/home/steven/go/src/github.com/coder/code-marketplace/extensionsign/testdata/public.pem", + ) + fmt.Println(cmd.String()) + output := &strings.Builder{} + cmd.Stdout = output + cmd.Stderr = output + err := cmd.Run() + fmt.Println(output.String()) + if err != nil { + return false, xerrors.Errorf("run verify %q: %w", cmd.String(), err) + } + + return cmd.ProcessState.ExitCode() == 0, nil +} + +func vsceSignVerify(ctx context.Context, logger slog.Logger, vsixPath, sigPath string) (bool, error) { + bin := os.Getenv("VSCE_SIGN_PATH") + if bin == "" { + return false, xerrors.Errorf("VSCE_SIGN_PATH not set") + } + + cmd := exec.CommandContext(ctx, bin, "verify", + "--package", vsixPath, + "--signaturearchive", sigPath, + "-v", + ) + fmt.Println(cmd.String()) + output := &strings.Builder{} + cmd.Stdout = output + cmd.Stderr = output + err := cmd.Run() + fmt.Println(output.String()) + if err != nil { + return false, xerrors.Errorf("run verify %q: %w", cmd.String(), err) + } + + return cmd.ProcessState.ExitCode() == 0, nil +} + +func dimensions(chain [][][]*x509.Certificate) string { + var str strings.Builder + for _, top := range chain { + str.WriteString(fmt.Sprintf("Chain, len=%d\n", len(top))) + for _, second := range top { + str.WriteString(fmt.Sprintf(" Certs len=%d\n", len(second))) + for _, cert := range second { + str.WriteString(fmt.Sprintf(" Cert: %s\n", cert.Subject)) + } + } + } + return str.String() +} + func compareSignatureSigZips() *cobra.Command { cmd := &cobra.Command{ Use: "compare", diff --git a/extensionsign/algo.go b/extensionsign/algo.go new file mode 100644 index 0000000..14906f3 --- /dev/null +++ b/extensionsign/algo.go @@ -0,0 +1,82 @@ +package extensionsign + +import ( + "context" + "crypto" + "crypto/x509" + "encoding/pem" + "os" + "os/exec" + "path/filepath" + + cms "github.com/github/smimesign/ietf-cms" + "golang.org/x/xerrors" +) + +var SigningAlgorithm = CMSAlgo + +func CMSAlgo(data []byte, certs []*x509.Certificate, signer crypto.Signer) (result []byte, err error) { + return cms.SignDetached(data, certs, signer) +} + +// openssl smime -sign -signer -inkey -binary -in .signature.manifest -outform der -out openssl.p7s +func OpenSSLSign(data []byte, certs []*x509.Certificate, signer crypto.Signer) (result []byte, err error) { + tmpdir := os.TempDir() + tmpdir = filepath.Join(tmpdir, "sign-sigs") + defer os.RemoveAll(tmpdir) + + err = os.MkdirAll(tmpdir, 0755) + if err != nil { + return nil, xerrors.Errorf("create temp dir: %w", err) + } + + certPath := filepath.Join(tmpdir, "certs.pem") + certFile, err := os.OpenFile(certPath, os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return nil, xerrors.Errorf("open cert file: %w", err) + } + + for _, cert := range certs { + if len(cert.Raw) == 0 { + return nil, xerrors.Errorf("empty certificate") + } + err = pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) + if err != nil { + return nil, err + } + } + + keyPath := "/home/steven/go/src/github.com/coder/code-marketplace/extensionsign/testdata/key2.pem" + //keyFile, err := os.Open(keyPath) + //if err != nil { + // return nil, err + //} + //pem.Encode(keyFile, &pem.Block{Type: "PRIVATE KEY", ) + + msgPath := filepath.Join(tmpdir, ".signature.manifest") + messageFile, err := os.OpenFile(msgPath, os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return nil, err + } + _, err = messageFile.Write(data) + if err != nil { + return nil, xerrors.Errorf("write message: %w", err) + } + + signed := filepath.Join(tmpdir, "openssl.p7s") + cmd := exec.CommandContext(context.Background(), "openssl", "cms", "-sign", + "-signer", certPath, + "-inkey", keyPath, + "-binary", + "-in", msgPath, + "-outform", "der", + "-out", signed, + ) + + err = cmd.Run() + if err != nil { + return nil, xerrors.Errorf("run openssl: %w", err) + } + + return os.ReadFile(signed) +} diff --git a/extensionsign/doc.go b/extensionsign/doc.go index b51e216..6265f83 100644 --- a/extensionsign/doc.go +++ b/extensionsign/doc.go @@ -1,2 +1,3 @@ // Package extensionsign is a Go implementation of https://github.com/filiptronicek/node-ovsx-sign +// See https://github.com/eclipse/openvsx/issues/543 package extensionsign diff --git a/extensionsign/key.go b/extensionsign/key.go index 9af9778..723d03f 100644 --- a/extensionsign/key.go +++ b/extensionsign/key.go @@ -1,8 +1,18 @@ package extensionsign import ( + "context" + "crypto" "crypto/ed25519" "crypto/rand" + "crypto/x509" + "encoding/pem" + "fmt" + "os" + + "golang.org/x/xerrors" + + "cdr.dev/slog" ) func GenerateKey() (ed25519.PrivateKey, error) { @@ -12,3 +22,60 @@ func GenerateKey() (ed25519.PrivateKey, error) { } return private, nil } + +// To generate a new self signed certificate using openssl: +// 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" +// 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" + +func LoadCertificatesFromDisk(ctx context.Context, logger slog.Logger, files []string) ([]*x509.Certificate, error) { + var certs []*x509.Certificate + for _, file := range files { + certData, err := os.ReadFile(file) + if err != nil { + return nil, xerrors.Errorf("read cert file %q: %w", file, err) + } + + for { + block, rest := pem.Decode(certData) + + crt, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, xerrors.Errorf("load certificate %q: %w", file, err) + } + logger.Info(ctx, "Loaded certificate", + slog.F("file", file), + slog.F("subject", crt.Subject.CommonName), + ) + certs = append(certs, crt) + + if len(rest) == 0 { + break + } + certData = rest + } + + } + return certs, nil +} + +// LoadKey takes in a PEM encoded secret +func LoadKey(secret []byte) (crypto.Signer, error) { + data, rest := pem.Decode(secret) + if len(rest) > 0 { + return nil, xerrors.Errorf("extra data after PEM block") + } + + sec, err := x509.ParsePKCS8PrivateKey(data.Bytes) + if err != nil { + _, err2 := x509.ParsePKCS1PrivateKey(secret) + fmt.Println(err2) + return nil, err + } + + signer, ok := sec.(crypto.Signer) + if !ok { + return nil, xerrors.Errorf("%T is not a crypto.Signer and is not supported", sec) + } + + return signer, nil +} diff --git a/extensionsign/sigzip.go b/extensionsign/sigzip.go index 5d9f536..8155896 100644 --- a/extensionsign/sigzip.go +++ b/extensionsign/sigzip.go @@ -4,8 +4,9 @@ import ( "archive/zip" "bytes" "crypto" - "crypto/rand" + "crypto/x509" "encoding/json" + "io" "golang.org/x/xerrors" @@ -27,8 +28,18 @@ func ExtractSignatureManifest(zip []byte) (SignatureManifest, error) { return manifest, nil } +func ExtractP7SSig(zip []byte) ([]byte, error) { + r, err := easyzip.GetZipFileReader(zip, ".signature.p7s") + if err != nil { + return nil, xerrors.Errorf("get p7s: %w", err) + } + + defer r.Close() + return io.ReadAll(r) +} + // SignAndZipManifest signs a manifest and zips it up -func SignAndZipManifest(secret crypto.Signer, vsixData []byte, manifest json.RawMessage) ([]byte, error) { +func SignAndZipManifest(certs []*x509.Certificate, secret crypto.Signer, vsixData []byte, manifest json.RawMessage) ([]byte, error) { var buf bytes.Buffer w := zip.NewWriter(&buf) @@ -42,24 +53,17 @@ func SignAndZipManifest(secret crypto.Signer, vsixData []byte, manifest json.Raw return nil, xerrors.Errorf("write manifest: %w", err) } - // Empty file - _, err = w.Create(".signature.p7s") + p7sFile, err := w.Create(".signature.p7s") if err != nil { return nil, xerrors.Errorf("create empty p7s signature: %w", err) } - // Actual sig - sigFile, err := w.Create(".signature.sig") - if err != nil { - return nil, xerrors.Errorf("create signature: %w", err) - } - - signature, err := secret.Sign(rand.Reader, vsixData, crypto.Hash(0)) + signature, err := SigningAlgorithm(vsixData, certs, secret) if err != nil { return nil, xerrors.Errorf("sign: %w", err) } - _, err = sigFile.Write(signature) + _, err = p7sFile.Write(signature) if err != nil { return nil, xerrors.Errorf("write signature: %w", err) } diff --git a/extensionsign/verify/examples/README.md b/extensionsign/verify/examples/README.md new file mode 100644 index 0000000..e482ca6 --- /dev/null +++ b/extensionsign/verify/examples/README.md @@ -0,0 +1,2 @@ +https://ms-python.gallerycdn.vsassets.io/extensions/ms-python/python/2024.23.2024121003/1733845882640/Microsoft.VisualStudio.Code.Manifest +https://ms-python.gallerycdn.vsassets.io/extensions/ms-python/python/2024.23.2024121003/1733845882640/Microsoft.VisualStudio.Services.VsixSignature diff --git a/extensionsign/verify/examples/extension.vsix b/extensionsign/verify/examples/extension.vsix new file mode 100644 index 0000000..a28965d Binary files /dev/null and b/extensionsign/verify/examples/extension.vsix differ diff --git a/extensionsign/verify/examples/signature.p7s b/extensionsign/verify/examples/signature.p7s new file mode 100644 index 0000000..f2a52f7 Binary files /dev/null and b/extensionsign/verify/examples/signature.p7s differ diff --git a/extensionsign/verify/main.js b/extensionsign/verify/main.js new file mode 100644 index 0000000..7c02797 --- /dev/null +++ b/extensionsign/verify/main.js @@ -0,0 +1,12 @@ + +import { verify } from "@vscode/vsce-sign"; + + +if(process.argv.length < 2) { + console.log("Usage: node main.js ") + process.exit(1) +} + +verify(process.argv[0], process.argv[1], "true").then((x) => { + console.log(x) +}) diff --git a/extensionsign/verify/verify.go b/extensionsign/verify/verify.go new file mode 100644 index 0000000..8998949 --- /dev/null +++ b/extensionsign/verify/verify.go @@ -0,0 +1,77 @@ +package verify + +import ( + "archive/tar" + "compress/gzip" + "context" + "encoding/json" + "fmt" + "net/http" + + "golang.org/x/xerrors" +) + +// curl https://registry.npmjs.org/@vscode/vsce-sign | jq '.versions[."dist-tags".latest].dist.tarball' + +type NPMPackage struct { + DistTags map[string]string `json:"dist-tags"` + Versions map[string]struct { + Dist struct { + Tarball string `json:"tarball"` + } + } +} + +// 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 +// +// ./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 +// ./node_modules/@vscode/vsce-sign/bin/vsce-sign verify --package ./examples/Microsoft.VisualStudio.Services.VSIXPackage --signaturearchive ./examples/Microsoft.VisualStudio.Services.VsixSignature -v +func DownloadVsceSign(ctx context.Context) error { + cli := http.DefaultClient + req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://registry.npmjs.org/@vscode/vsce-sign", nil) + if err != nil { + return err + } + + resp, err := cli.Do(req) + if err != nil { + return err + } + if resp.StatusCode != http.StatusOK { + return xerrors.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var pkg NPMPackage + err = json.NewDecoder(resp.Body).Decode(&pkg) + if err != nil { + return xerrors.Errorf("decode package: %w", err) + } + + // If this panics, sorry + tarURL := pkg.Versions[pkg.DistTags["latest"]].Dist.Tarball + req, err = http.NewRequestWithContext(ctx, http.MethodGet, tarURL, nil) + if err != nil { + return xerrors.Errorf("create tar request: %w", err) + } + + resp, err = cli.Do(req) + if err != nil { + return xerrors.Errorf("do tar request: %w", err) + } + + gzReader, err := gzip.NewReader(resp.Body) + if err != nil { + return xerrors.Errorf("create gzip reader: %w", err) + } + + r := tar.NewReader(gzReader) + for { + hdr, err := r.Next() + if err != nil { + return err + } + fmt.Println(hdr.Name) + } + + return nil +} diff --git a/go.mod b/go.mod index 52220e5..967e5be 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.22.8 require ( cdr.dev/slog v1.6.1 + github.com/github/smimesign v0.2.0 github.com/go-chi/chi/v5 v5.1.0 github.com/go-chi/cors v1.2.1 github.com/go-chi/httprate v0.14.1 diff --git a/go.sum b/go.sum index f19c00c..3e51a6f 100644 --- a/go.sum +++ b/go.sum @@ -11,13 +11,17 @@ cloud.google.com/go/longrunning v0.5.1 h1:Fr7TXftcqTudoyRJa113hyaqlGdiBQkp0Gq7tE cloud.google.com/go/longrunning v0.5.1/go.mod h1:spvimkwdz6SPWKEt/XBij79E9fiTkHSQl/fRUUQJYJc= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/github/smimesign v0.2.0 h1:Hho4YcX5N1I9XNqhq0fNx0Sts8MhLonHd+HRXVGNjvk= +github.com/github/smimesign v0.2.0/go.mod h1:iZiiwNT4HbtGRVqCQu7uJPEZCuEE5sfSSttcnePkDl4= github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= @@ -49,6 +53,8 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/pborman/getopt v0.0.0-20180811024354-2b5b3bfb099b/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -62,6 +68,8 @@ github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -74,6 +82,7 @@ go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs= go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= @@ -81,6 +90,7 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -93,6 +103,7 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -118,6 +129,7 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ= diff --git a/storage/signature.go b/storage/signature.go index ce917bc..453981c 100644 --- a/storage/signature.go +++ b/storage/signature.go @@ -3,6 +3,7 @@ package storage import ( "context" "crypto" + "crypto/x509" "encoding/json" "io" "io/fs" @@ -13,7 +14,6 @@ import ( "golang.org/x/xerrors" "cdr.dev/slog" - "github.com/coder/code-marketplace/extensionsign" ) @@ -32,19 +32,38 @@ func SignatureZipFilename(manifest *VSIXManifest) string { type Signature struct { // Signer if provided, will be used to sign extensions. If not provided, // no extensions will be signed. - Signer crypto.Signer - Logger slog.Logger + Signer crypto.Signer + Certificates []*x509.Certificate + Logger slog.Logger // SaveSigZips is a flag that will save the signed extension to disk. // This is useful for debugging, but the server will never use this file. saveSigZips bool Storage } -func NewSignatureStorage(logger slog.Logger, signer crypto.Signer, s Storage) *Signature { - return &Signature{ - Signer: signer, - Storage: s, +func NewSignatureStorage(logger slog.Logger, signer crypto.Signer, certs []*x509.Certificate, s Storage) (*Signature, error) { + // TODO: We should check if the certs include the public key from the signer. If they do not, + // A cert is probably missing. + st := &Signature{ + Logger: logger, + Signer: signer, + Certificates: certs, + Storage: s, + } + if !st.SigningEnabled() { + return st, nil + } + + // Attempt to sign something to ensure certs/keys are correct and supported + _, err := extensionsign.SigningAlgorithm([]byte("testing configuration"), certs, signer) + if err != nil { + return nil, xerrors.Errorf("extension signer: %w", err) + } + + if st.SigningEnabled() { + logger.Info(context.Background(), "signing of extensions is enabled") } + return st, nil } func (s *Signature) SaveSigZips() { @@ -145,6 +164,7 @@ func (s *Signature) Open(ctx context.Context, fp string) (fs.File, error) { // If this file is missing, it means the extension was added before // signatures were handled by the marketplace. // TODO: Generate the sig manifest payload and insert it? + s.Logger.Error(ctx, "signature manifest not found", slog.Error(err)) return nil, xerrors.Errorf("open signature manifest: %w", err) } defer manifest.Close() @@ -171,6 +191,7 @@ func (s *Signature) Open(ctx context.Context, fp string) (fs.File, error) { // TODO: Fetch the VSIX payload from the storage signed, err := s.SigZip(ctx, vsixData, manifestData) if err != nil { + s.Logger.Error(ctx, "signing manifest", slog.Error(err)) return nil, xerrors.Errorf("sign and zip manifest: %w", err) } @@ -183,7 +204,7 @@ func (s *Signature) Open(ctx context.Context, fp string) (fs.File, error) { } func (s *Signature) SigZip(ctx context.Context, vsix []byte, sigManifest []byte) ([]byte, error) { - signed, err := extensionsign.SignAndZipManifest(s.Signer, vsix, sigManifest) + signed, err := extensionsign.SignAndZipManifest(s.Certificates, s.Signer, vsix, sigManifest) if err != nil { s.Logger.Error(ctx, "signing manifest", slog.Error(err)) return nil, xerrors.Errorf("sign and zip manifest: %w", err) diff --git a/storage/signature_test.go b/storage/signature_test.go index 452bea1..c845f8a 100644 --- a/storage/signature_test.go +++ b/storage/signature_test.go @@ -2,8 +2,11 @@ package storage_test import ( "crypto" + "crypto/x509" "testing" + "github.com/stretchr/testify/require" + "cdr.dev/slog" "github.com/coder/code-marketplace/extensionsign" "github.com/coder/code-marketplace/storage" @@ -28,8 +31,10 @@ func signed(signer bool, factory func(t *testing.T) testStorage) func(t *testing exp = expectSignature } + sst, err := storage.NewSignatureStorage(slog.Make(), key, []*x509.Certificate{}, st.storage) + require.NoError(t, err) return testStorage{ - storage: storage.NewSignatureStorage(slog.Make(), key, st.storage), + storage: sst, write: st.write, exists: st.exists, expectedManifest: exp, diff --git a/storage/storage.go b/storage/storage.go index ad5ed13..d089c3f 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -3,6 +3,7 @@ package storage import ( "context" "crypto" + "crypto/x509" "encoding/json" "encoding/xml" "fmt" @@ -128,13 +129,17 @@ type VSIXAsset struct { } type Options struct { - Signer crypto.Signer Artifactory string ExtDir string Repo string - SaveSigZips bool Logger slog.Logger ListCacheDuration time.Duration + + // Signed and Certificate are used to sign extensions. + // The signer should have a corresponding certificate in the Certificates. + Signer crypto.Signer + Certificates []*x509.Certificate + SaveSigZips bool } type extension struct { @@ -294,7 +299,11 @@ func NewStorage(ctx context.Context, options *Options) (Storage, error) { return nil, err } - signingStorage := NewSignatureStorage(options.Logger, options.Signer, store) + signingStorage, err := NewSignatureStorage(options.Logger, options.Signer, options.Certificates, store) + if err != nil { + return nil, err + } + if options.SaveSigZips { signingStorage.SaveSigZips() }