Skip to content
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
31 changes: 30 additions & 1 deletion pkg/pm/installer/installer.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

"wpm/pkg/archive"
"wpm/pkg/pm/registry"
"wpm/pkg/pm/signatures"
"wpm/pkg/pm/wpmjson/types"

"github.com/pkg/errors"
Expand All @@ -27,6 +28,7 @@ type Installer struct {
tmpDir string
client registry.Client
extractSem chan struct{}
keysJson signatures.KeysJson
}

func New(contentDir string, concurrency int, client registry.Client) *Installer {
Expand All @@ -47,6 +49,13 @@ func New(contentDir string, concurrency int, client registry.Client) *Installer
}

func (i *Installer) InstallAll(ctx context.Context, plan []Action, progressFn func(Action)) error {
keys, err := i.client.GetKeysJson(ctx)
if err != nil {
return errors.Wrap(err, "failed to fetch public keys for signature verification")
}

i.keysJson = keys

g, ctx := errgroup.WithContext(ctx)
g.SetLimit(i.concurrency)

Expand All @@ -65,7 +74,7 @@ func (i *Installer) InstallAll(ctx context.Context, plan []Action, progressFn fu
})
}

err := g.Wait()
err = g.Wait()
os.RemoveAll(i.tmpDir)
return err
}
Expand All @@ -87,6 +96,26 @@ func (i *Installer) Install(ctx context.Context, action Action) error {
}

func (i *Installer) installOrUpdate(ctx context.Context, action Action, targetDir string) error {
manifest, err := i.client.GetPackageManifest(ctx, action.Name, action.Version, true)
if err != nil {
return errors.Wrapf(err, "failed to fetch manifest for %s@%s", action.Name, action.Version)
}

sigs := manifest.Dist.Signatures
if len(sigs) == 0 {
return errors.Errorf("no signatures found for package %s@%s", action.Name, action.Version)
}

err = signatures.Verify(
i.keysJson,
sigs[0].KeyID,
sigs[0].Sig,
fmt.Appendf(nil, "%s:%s:%s", action.Name, action.Version, action.Digest),
)
if err != nil {
return errors.Wrapf(err, "signature verification failed for package %s@%s", action.Name, action.Version)
}

resp, err := i.client.DownloadTarball(ctx, action.Resolved)
if err != nil {
return errors.Wrapf(err, "failed to download %s", action.Resolved)
Expand Down
17 changes: 17 additions & 0 deletions pkg/pm/registry/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"net/http"

"wpm/pkg/api"
"wpm/pkg/pm/signatures"
"wpm/pkg/pm/wpmjson/manifest"
)

Expand All @@ -25,6 +26,7 @@ type client struct {
// registry
type Client interface {
Whoami(ctx context.Context, token string) (string, error)
GetKeysJson(ctx context.Context) (signatures.KeysJson, error)
DownloadTarball(ctx context.Context, url string) (io.ReadCloser, error)
PutPackage(ctx context.Context, data *manifest.Package, tarball io.Reader) error
GetPackageManifest(ctx context.Context, packageName, versionOrTag string, force bool) (*manifest.Package, error)
Expand Down Expand Up @@ -122,3 +124,18 @@ func (c *client) Whoami(ctx context.Context, token string) (string, error) {

return response, nil
}

// GetKeysJson retrieves the public keys from the registry
func (c *client) GetKeysJson(ctx context.Context) (signatures.KeysJson, error) {
var keys signatures.KeysJson

err := c.restClient.Get(
"/keys.json",
&keys,
)
if err != nil {
return nil, err
}

return keys, nil
}
81 changes: 81 additions & 0 deletions pkg/pm/signatures/signatures.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package signatures

import (
"crypto/ecdsa"
"crypto/sha256"
"crypto/x509"
"encoding/asn1"
"encoding/base64"
"fmt"
"math/big"

"github.com/pkg/errors"
)

const signingAlgorithm = "ECDSA_SHA_256"

type sig struct {
R, S *big.Int
}

type keyJson struct {
Expires string `json:"expires"`
Type string `json:"type"`
KeyID string `json:"keyid"`
PubKey string `json:"pubkey"`
}

type KeysJson []keyJson

// Verify verifies a Base64 encoded ASN.1 DER signature against a message using a PEM encoded Public Key.
func Verify(keys KeysJson, keyId string, signatureBase64 string, originalMessage []byte) error {
var rawPublicKeyBase64, keyType string
for _, key := range keys {
if key.KeyID == keyId {
keyType = key.Type
rawPublicKeyBase64 = key.PubKey
break
}
}

if rawPublicKeyBase64 == "" {
return fmt.Errorf("public key with KeyID %s not found", keyId)
}

if keyType != signingAlgorithm {
return fmt.Errorf("unsupported signing algorithm: %s", keyType)
}

keyBytes, err := base64.StdEncoding.DecodeString(rawPublicKeyBase64)
if err != nil {
return errors.Wrap(err, "failed to decode base64 public key")
}

genericPublicKey, err := x509.ParsePKIXPublicKey(keyBytes)
if err != nil {
return fmt.Errorf("failed to parse PKIX public key: %v", err)
}

publicKey, ok := genericPublicKey.(*ecdsa.PublicKey)
if !ok {
return errors.New("public key is not of type ECDSA")
}

sigBytes, err := base64.StdEncoding.DecodeString(signatureBase64)
if err != nil {
return fmt.Errorf("failed to decode base64 signature: %v", err)
}

var sig sig
if _, err := asn1.Unmarshal(sigBytes, &sig); err != nil {
return fmt.Errorf("failed to unmarshal ASN.1 signature: %v", err)
}

hash := sha256.Sum256(originalMessage)
valid := ecdsa.Verify(publicKey, hash[:], sig.R, sig.S)
if !valid {
return errors.New("signature verification failed: invalid signature")
}

return nil
}
14 changes: 10 additions & 4 deletions pkg/pm/wpmjson/manifest/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@ package manifest

import "wpm/pkg/pm/wpmjson/types"

type Signature struct {
KeyID string `json:"keyid"`
Sig string `json:"sig"`
}

// Dist struct to define the distribution metadata
type Dist struct {
Digest string `json:"digest"`
TotalFiles int64 `json:"totalFiles"`
PackedSize int64 `json:"packedSize"`
UnpackedSize int64 `json:"unpackedSize"`
Digest string `json:"digest"`
Signatures []Signature `json:"signatures"`
TotalFiles int64 `json:"totalFiles"`
PackedSize int64 `json:"packedSize"`
UnpackedSize int64 `json:"unpackedSize"`
}

// Package struct to define the package manifest in registry
Expand Down