Skip to content
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

Support package signatures #760

Merged
merged 7 commits into from
Oct 25, 2021
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* Update APM Go Agent to 1.14.0. [#759](https://github.com/elastic/package-registry/pull/759)
* Update Gorilla to 1.8.0 [#759](https://github.com/elastic/package-registry/pull/759)
* Support package signatures [#760](https://github.com/elastic/package-registry/pull/760)

### Deprecated

Expand Down
2 changes: 2 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ func mustLoadRouter(config *Config, indexer Indexer) *mux.Router {

func getRouter(config *Config, indexer Indexer) (*mux.Router, error) {
artifactsHandler := artifactsHandler(indexer, config.CacheTimeCatchAll)
signaturesHandler := signaturesHandler(indexer, config.CacheTimeCatchAll)
faviconHandleFunc, err := faviconHandler(config.CacheTimeCatchAll)
if err != nil {
return nil, err
Expand All @@ -248,6 +249,7 @@ func getRouter(config *Config, indexer Indexer) (*mux.Router, error) {
router.HandleFunc("/health", healthHandler)
router.HandleFunc("/favicon.ico", faviconHandleFunc)
router.HandleFunc(artifactsRouterPath, artifactsHandler)
router.HandleFunc(signaturesRouterPath, signaturesHandler)
router.HandleFunc(packageIndexRouterPath, packageIndexHandler)
router.HandleFunc(staticRouterPath, staticHandler)
router.Use(loggingMiddleware)
Expand Down
25 changes: 25 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,31 @@ func TestArtifacts(t *testing.T) {
}
}

func TestSignatures(t *testing.T) {
indexer := packages.NewZipFileSystemIndexer("./testdata/local-storage")

err := indexer.Init(context.Background())
require.NoError(t, err)

signaturesHandler := signaturesHandler(indexer, testCacheTime)

tests := []struct {
endpoint string
path string
file string
handler func(w http.ResponseWriter, r *http.Request)
}{
{"/epr/example/example-1.0.1.zip.sig", signaturesRouterPath, "example-1.0.1.zip.sig", signaturesHandler},
{"/epr/example/example-0.0.1.zip.sig", signaturesRouterPath, "missing-signature.txt", signaturesHandler},
}

for _, test := range tests {
t.Run(test.endpoint, func(t *testing.T) {
runEndpoint(t, test.endpoint, test.path, test.file, test.handler)
})
}
}

func TestStatics(t *testing.T) {
packagesBasePaths := []string{"./testdata/package"}
indexer := packages.NewFileSystemIndexer(packagesBasePaths...)
Expand Down
4 changes: 4 additions & 0 deletions packages/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,7 @@ func ServeFile(w http.ResponseWriter, r *http.Request, p *Package, name string)

http.ServeContent(w, r, name, stat.ModTime(), f)
}

func ServeSignature(w http.ResponseWriter, r *http.Request, p *Package) {
http.ServeFile(w, r, p.BasePath+".sig")
}
18 changes: 18 additions & 0 deletions packages/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ type BasePackage struct {
Conditions *Conditions `config:"conditions,omitempty" json:"conditions,omitempty" yaml:"conditions,omitempty"`
Owner *Owner `config:"owner,omitempty" json:"owner,omitempty" yaml:"owner,omitempty"`
Categories []string `config:"categories,omitempty" json:"categories,omitempty" yaml:"categories,omitempty"`
SignaturePath string `config:"signature_path,omitempty" json:"signature_path,omitempty" yaml:"signature_path,omitempty"`
}

// BasePolicyTemplate is used for the package policy templates in the /search endpoint
Expand Down Expand Up @@ -313,6 +314,12 @@ func NewPackage(basePath string, fsBuilder FileSystemBuilder) (*Package, error)
if err != nil {
return nil, errors.Wrapf(err, "loading package data streams failed (path '%s')", p.BasePath)
}

// Read path for package signature
p.SignaturePath, err = p.GetSignaturePath()
if err != nil {
return nil, errors.Wrapf(err, "can't process the package signature")
}
return p, nil
}

Expand Down Expand Up @@ -584,3 +591,14 @@ func (p *Package) GetDownloadPath() string {
func (p *Package) GetUrlPath() string {
return path.Join(packagePathPrefix, p.Name, p.Version)
}

func (p *Package) GetSignaturePath() (string, error) {
_, err := os.Stat(p.BasePath + ".sig")
if err != nil && errors.Is(err, os.ErrNotExist) {
return "", nil
}
if err != nil {
return "", errors.Wrap(err, "can't stat signature file")
}
return p.GetDownloadPath() + ".sig", nil
}
59 changes: 59 additions & 0 deletions signatures.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.

package main

import (
"log"
"net/http"
"time"

"github.com/Masterminds/semver/v3"
"github.com/gorilla/mux"
"github.com/pkg/errors"

"github.com/elastic/package-registry/packages"
)

const signaturesRouterPath = "/epr/{packageName}/{packageName:[a-z0-9_]+}-{packageVersion}.zip.sig"

var errSignatureFileNotFound = errors.New("signature file not found")

func signaturesHandler(indexer Indexer, cacheTime time.Duration) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
packageName, ok := vars["packageName"]
if !ok {
badRequest(w, "missing package name")
return
}

packageVersion, ok := vars["packageVersion"]
if !ok {
badRequest(w, "missing package version")
return
}

_, err := semver.StrictNewVersion(packageVersion)
if err != nil {
badRequest(w, "invalid package version")
return
}

opts := packages.NameVersionFilter(packageName, packageVersion)
packageList, err := indexer.Get(r.Context(), &opts)
if err != nil {
log.Printf("getting package path failed: %v", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
if len(packageList) == 0 {
notFoundError(w, errSignatureFileNotFound)
return
}

cacheHeaders(w, cacheTime)
packages.ServeSignature(w, r, packageList[0])
}
}
1 change: 1 addition & 0 deletions testdata/generated/example-1.0.1.zip.sig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
e16ddaf4f91df524b27bf4f2e4b1ac09
1 change: 1 addition & 0 deletions testdata/generated/missing-signature.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
signature file not found
1 change: 1 addition & 0 deletions testdata/generated/package-zip.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"crm",
"azure"
],
"signature_path": "/epr/example/example-1.0.1.zip.sig",
"format_version": "1.0.0",
"readme": "/package/example/1.0.1/docs/README.md",
"license": "basic",
Expand Down
1 change: 1 addition & 0 deletions testdata/local-storage/example-1.0.1.zip.sig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
e16ddaf4f91df524b27bf4f2e4b1ac09
Copy link
Member

Choose a reason for hiding this comment

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

Is this the final format of the signature? What kind of hash is this one?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

md5(elastic)

It's just a mock as the EPR doesn't enforce any hash form, it will depend on the internal logic on the CI side, unless we want to document and define it also here.

Copy link
Member

Choose a reason for hiding this comment

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

Yep, no need to enter into details here, but I am wondering about the more convenient form to distribute different signatures (also related to my other question about providing a download path). For example gpg signatures are quite longer and usually distributed as files. Would we still want to include them in the package index?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this is a similar problem we had with template vs template_path in data stream's manifest and we ended up with template_path as these files are long.

Definitely a signature_path would be more human readable than JSON index with signature blobs.

I will adjust the implementation then.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed, used signature_path.