Remove unnecessary file i/o operations from signing and verifying

Signed-off-by: Scott Rigby <scott@r6by.com>
pull/31196/head
Scott Rigby 1 week ago
parent 9ea35da0d0
commit e814ff3c38
No known key found for this signature in database
GPG Key ID: C7C6FBB5B91C1155

@ -38,8 +38,9 @@ type HTTPInstaller struct {
base
extractor Extractor
getter getter.Getter
// Provenance data to save after installation
provData []byte
// Cached data to avoid duplicate downloads
pluginData []byte
provData []byte
}
// NewHTTPInstaller creates a new HttpInstaller.
@ -74,15 +75,18 @@ func NewHTTPInstaller(source string) (*HTTPInstaller, error) {
//
// Implements Installer.
func (i *HTTPInstaller) Install() error {
pluginData, err := i.getter.Get(i.Source)
if err != nil {
return err
// Ensure plugin data is cached
if i.pluginData == nil {
pluginData, err := i.getter.Get(i.Source)
if err != nil {
return err
}
i.pluginData = pluginData.Bytes()
}
// Save the original tarball to plugins directory for verification
// Extract metadata to get the actual plugin name and version
pluginBytes := pluginData.Bytes()
metadata, err := plugin.ExtractPluginMetadataFromReader(bytes.NewReader(pluginBytes))
metadata, err := plugin.ExtractTgzPluginMetadata(bytes.NewReader(i.pluginData))
if err != nil {
return fmt.Errorf("failed to extract plugin metadata from tarball: %w", err)
}
@ -91,20 +95,28 @@ func (i *HTTPInstaller) Install() error {
if err := os.MkdirAll(filepath.Dir(tarballPath), 0755); err != nil {
return fmt.Errorf("failed to create plugins directory: %w", err)
}
if err := os.WriteFile(tarballPath, pluginBytes, 0644); err != nil {
if err := os.WriteFile(tarballPath, i.pluginData, 0644); err != nil {
return fmt.Errorf("failed to save tarball: %w", err)
}
// Try to download .prov file if it exists
provURL := i.Source + ".prov"
if provData, err := i.getter.Get(provURL); err == nil {
// Ensure prov data is cached if available
if i.provData == nil {
// Try to download .prov file if it exists
provURL := i.Source + ".prov"
if provData, err := i.getter.Get(provURL); err == nil {
i.provData = provData.Bytes()
}
}
// Save prov file if we have the data
if i.provData != nil {
provPath := tarballPath + ".prov"
if err := os.WriteFile(provPath, provData.Bytes(), 0644); err != nil {
if err := os.WriteFile(provPath, i.provData, 0644); err != nil {
slog.Debug("failed to save provenance file", "error", err)
}
}
if err := i.extractor.Extract(pluginData, i.CacheDir); err != nil {
if err := i.extractor.Extract(bytes.NewBuffer(i.pluginData), i.CacheDir); err != nil {
return fmt.Errorf("extracting files from archive: %w", err)
}
@ -148,51 +160,32 @@ func (i *HTTPInstaller) SupportsVerification() bool {
return strings.HasSuffix(i.Source, ".tgz") || strings.HasSuffix(i.Source, ".tar.gz")
}
// PrepareForVerification downloads the plugin and signature files for verification
func (i *HTTPInstaller) PrepareForVerification() (string, func(), error) {
// GetVerificationData returns cached plugin and provenance data for verification
func (i *HTTPInstaller) GetVerificationData() (archiveData, provData []byte, filename string, err error) {
if !i.SupportsVerification() {
return "", nil, fmt.Errorf("verification not supported for this source")
}
// Create temporary directory for downloads
tempDir, err := os.MkdirTemp("", "helm-plugin-verify-*")
if err != nil {
return "", nil, fmt.Errorf("failed to create temp directory: %w", err)
return nil, nil, "", fmt.Errorf("verification not supported for this source")
}
cleanup := func() {
os.RemoveAll(tempDir)
}
// Download plugin tarball
pluginFile := filepath.Join(tempDir, filepath.Base(i.Source))
g, err := getter.All(new(cli.EnvSettings)).ByScheme("http")
if err != nil {
cleanup()
return "", nil, err
}
data, err := g.Get(i.Source, getter.WithURL(i.Source))
if err != nil {
cleanup()
return "", nil, fmt.Errorf("failed to download plugin: %w", err)
}
if err := os.WriteFile(pluginFile, data.Bytes(), 0644); err != nil {
cleanup()
return "", nil, fmt.Errorf("failed to write plugin file: %w", err)
// Download plugin data once and cache it
if i.pluginData == nil {
data, err := i.getter.Get(i.Source)
if err != nil {
return nil, nil, "", fmt.Errorf("failed to download plugin: %w", err)
}
i.pluginData = data.Bytes()
}
// Try to download signature file - don't fail if it doesn't exist
if provData, err := g.Get(i.Source+".prov", getter.WithURL(i.Source+".prov")); err == nil {
if err := os.WriteFile(pluginFile+".prov", provData.Bytes(), 0644); err == nil {
// Store the provenance data so we can save it after installation
// Download prov data once and cache it if available
if i.provData == nil {
provData, err := i.getter.Get(i.Source + ".prov")
if err != nil {
// If provenance file doesn't exist, set provData to nil
// The verification logic will handle this gracefully
i.provData = nil
} else {
i.provData = provData.Bytes()
}
}
// Note: We don't fail if .prov file can't be downloaded - the verification logic
// in InstallWithOptions will handle missing .prov files appropriately
return pluginFile, cleanup, nil
return i.pluginData, i.provData, filepath.Base(i.Source), nil
}

@ -55,8 +55,8 @@ type Installer interface {
type Verifier interface {
// SupportsVerification returns true if this installer can verify plugins
SupportsVerification() bool
// PrepareForVerification downloads necessary files for verification
PrepareForVerification() (pluginPath string, cleanup func(), err error)
// GetVerificationData returns plugin and provenance data for verification
GetVerificationData() (archiveData, provData []byte, filename string, err error)
}
// Install installs a plugin.
@ -91,28 +91,19 @@ func InstallWithOptions(i Installer, opts Options) (*VerificationResult, error)
return nil, fmt.Errorf("--verify is only supported for plugin tarballs (.tgz files)")
}
// Prepare for verification (download files if needed)
pluginPath, cleanup, err := verifier.PrepareForVerification()
// Get verification data (works for both memory and file-based installers)
archiveData, provData, filename, err := verifier.GetVerificationData()
if err != nil {
return nil, fmt.Errorf("failed to prepare for verification: %w", err)
}
if cleanup != nil {
defer cleanup()
return nil, fmt.Errorf("failed to get verification data: %w", err)
}
// Check if provenance file exists
provFile := pluginPath + ".prov"
if _, err := os.Stat(provFile); err != nil {
if os.IsNotExist(err) {
// No .prov file found - emit warning but continue installation
fmt.Fprintf(os.Stderr, "WARNING: No provenance file found for plugin. Plugin is not signed and cannot be verified.\n")
} else {
// Other error accessing .prov file
return nil, fmt.Errorf("failed to access provenance file: %w", err)
}
// Check if provenance data exists
if len(provData) == 0 {
// No .prov file found - emit warning but continue installation
fmt.Fprintf(os.Stderr, "WARNING: No provenance file found for plugin. Plugin is not signed and cannot be verified.\n")
} else {
// Provenance file exists - verify the plugin
verification, err := plugin.VerifyPlugin(pluginPath, opts.Keyring)
// Provenance data exists - verify the plugin
verification, err := plugin.VerifyPlugin(archiveData, provData, filename, opts.Keyring)
if err != nil {
return nil, fmt.Errorf("plugin verification failed: %w", err)
}

@ -35,9 +35,10 @@ var ErrPluginNotAFolder = errors.New("expected plugin to be a folder")
// LocalInstaller installs plugins from the filesystem.
type LocalInstaller struct {
base
isArchive bool
extractor Extractor
provData []byte // Provenance data to save after installation
isArchive bool
extractor Extractor
pluginData []byte // Cached plugin data
provData []byte // Cached provenance data
}
// NewLocalInstaller creates a new LocalInstaller.
@ -110,7 +111,7 @@ func (i *LocalInstaller) installFromArchive() error {
// Copy the original tarball to plugins directory for verification
// Extract metadata to get the actual plugin name and version
metadata, err := plugin.ExtractPluginMetadataFromReader(bytes.NewReader(data))
metadata, err := plugin.ExtractTgzPluginMetadata(bytes.NewReader(data))
if err != nil {
return fmt.Errorf("failed to extract plugin metadata from tarball: %w", err)
}
@ -184,21 +185,35 @@ func (i *LocalInstaller) SupportsVerification() bool {
return i.isArchive
}
// PrepareForVerification returns the local path for verification
func (i *LocalInstaller) PrepareForVerification() (string, func(), error) {
// GetVerificationData loads plugin and provenance data from local files for verification
func (i *LocalInstaller) GetVerificationData() (archiveData, provData []byte, filename string, err error) {
if !i.SupportsVerification() {
return "", nil, fmt.Errorf("verification not supported for directories")
return nil, nil, "", fmt.Errorf("verification not supported for directories")
}
// For local files, try to read the .prov file if it exists
provFile := i.Source + ".prov"
if provData, err := os.ReadFile(provFile); err == nil {
// Store the provenance data so we can save it after installation
i.provData = provData
// Read and cache the plugin archive file
if i.pluginData == nil {
i.pluginData, err = os.ReadFile(i.Source)
if err != nil {
return nil, nil, "", fmt.Errorf("failed to read plugin file: %w", err)
}
}
// Read and cache the provenance file if it exists
if i.provData == nil {
provFile := i.Source + ".prov"
i.provData, err = os.ReadFile(provFile)
if err != nil {
if os.IsNotExist(err) {
// If provenance file doesn't exist, set provData to nil
// The verification logic will handle this gracefully
i.provData = nil
} else {
// If file exists but can't be read (permissions, etc), return error
return nil, nil, "", fmt.Errorf("failed to access provenance file %s: %w", provFile, err)
}
}
}
// Note: We don't fail if .prov file doesn't exist - the verification logic
// in InstallWithOptions will handle missing .prov files appropriately
// Return the source path directly, no cleanup needed
return i.Source, nil, nil
return i.pluginData, i.provData, filepath.Base(i.Source), nil
}

@ -44,6 +44,9 @@ type OCIInstaller struct {
base
settings *cli.EnvSettings
getter getter.Getter
// Cached data to avoid duplicate downloads
pluginData []byte
provData []byte
}
// NewOCIInstaller creates a new OCIInstaller with optional getter options
@ -83,18 +86,17 @@ func NewOCIInstaller(source string, options ...getter.Option) (*OCIInstaller, er
func (i *OCIInstaller) Install() error {
slog.Debug("pulling OCI plugin", "source", i.Source)
// Use getter to download the plugin
pluginData, err := i.getter.Get(i.Source)
if err != nil {
return fmt.Errorf("failed to pull plugin from %s: %w", i.Source, err)
// Ensure plugin data is cached
if i.pluginData == nil {
pluginData, err := i.getter.Get(i.Source)
if err != nil {
return fmt.Errorf("failed to pull plugin from %s: %w", i.Source, err)
}
i.pluginData = pluginData.Bytes()
}
// Save the original tarball to plugins directory for verification
// For OCI plugins, extract version from plugin.yaml inside the tarball
pluginBytes := pluginData.Bytes()
// Extract metadata to get the actual plugin name and version
metadata, err := plugin.ExtractPluginMetadataFromReader(bytes.NewReader(pluginBytes))
metadata, err := plugin.ExtractTgzPluginMetadata(bytes.NewReader(i.pluginData))
if err != nil {
return fmt.Errorf("failed to extract plugin metadata from tarball: %w", err)
}
@ -104,21 +106,29 @@ func (i *OCIInstaller) Install() error {
if err := os.MkdirAll(filepath.Dir(tarballPath), 0755); err != nil {
return fmt.Errorf("failed to create plugins directory: %w", err)
}
if err := os.WriteFile(tarballPath, pluginBytes, 0644); err != nil {
if err := os.WriteFile(tarballPath, i.pluginData, 0644); err != nil {
return fmt.Errorf("failed to save tarball: %w", err)
}
// Try to download and save .prov file alongside the tarball
provSource := i.Source + ".prov"
if provData, err := i.getter.Get(provSource); err == nil {
// Ensure prov data is cached if available
if i.provData == nil {
// Try to download .prov file if it exists
provSource := i.Source + ".prov"
if provData, err := i.getter.Get(provSource); err == nil {
i.provData = provData.Bytes()
}
}
// Save prov file if we have the data
if i.provData != nil {
provPath := tarballPath + ".prov"
if err := os.WriteFile(provPath, provData.Bytes(), 0644); err != nil {
if err := os.WriteFile(provPath, i.provData, 0644); err != nil {
slog.Debug("failed to save provenance file", "error", err)
}
}
// Check if this is a gzip compressed file
if len(pluginBytes) < 2 || pluginBytes[0] != 0x1f || pluginBytes[1] != 0x8b {
if len(i.pluginData) < 2 || i.pluginData[0] != 0x1f || i.pluginData[1] != 0x8b {
return fmt.Errorf("plugin data is not a gzip compressed archive")
}
@ -128,7 +138,7 @@ func (i *OCIInstaller) Install() error {
}
// Extract as gzipped tar
if err := extractTarGz(bytes.NewReader(pluginBytes), i.CacheDir); err != nil {
if err := extractTarGz(bytes.NewReader(i.pluginData), i.CacheDir); err != nil {
return fmt.Errorf("failed to extract plugin: %w", err)
}
@ -251,55 +261,41 @@ func (i *OCIInstaller) SupportsVerification() bool {
return true
}
// PrepareForVerification downloads the plugin tarball and provenance to a temporary directory
func (i *OCIInstaller) PrepareForVerification() (pluginPath string, cleanup func(), err error) {
slog.Debug("preparing OCI plugin for verification", "source", i.Source)
// GetVerificationData downloads and caches plugin and provenance data from OCI registry for verification
func (i *OCIInstaller) GetVerificationData() (archiveData, provData []byte, filename string, err error) {
slog.Debug("getting verification data for OCI plugin", "source", i.Source)
// Create temporary directory for verification
tempDir, err := os.MkdirTemp("", "helm-oci-verify-")
if err != nil {
return "", nil, fmt.Errorf("failed to create temp directory: %w", err)
}
cleanup = func() {
os.RemoveAll(tempDir)
// Download plugin data once and cache it
if i.pluginData == nil {
pluginDataBuffer, err := i.getter.Get(i.Source)
if err != nil {
return nil, nil, "", fmt.Errorf("failed to pull plugin from %s: %w", i.Source, err)
}
i.pluginData = pluginDataBuffer.Bytes()
}
// Download the plugin tarball
pluginData, err := i.getter.Get(i.Source)
if err != nil {
cleanup()
return "", nil, fmt.Errorf("failed to pull plugin from %s: %w", i.Source, err)
// Download prov data once and cache it if available
if i.provData == nil {
provSource := i.Source + ".prov"
// Calling getter.Get again is reasonable because: 1. The OCI registry client already optimizes the underlying network calls
// 2. Both calls use the same underlying manifest and memory store 3. The second .prov call is very fast since the data is already pulled
provDataBuffer, err := i.getter.Get(provSource)
if err != nil {
// If provenance file doesn't exist, set provData to nil
// The verification logic will handle this gracefully
i.provData = nil
} else {
i.provData = provDataBuffer.Bytes()
}
}
// Extract metadata to get the actual plugin name and version
pluginBytes := pluginData.Bytes()
metadata, err := plugin.ExtractPluginMetadataFromReader(bytes.NewReader(pluginBytes))
// Extract metadata to get the filename
metadata, err := plugin.ExtractTgzPluginMetadata(bytes.NewReader(i.pluginData))
if err != nil {
cleanup()
return "", nil, fmt.Errorf("failed to extract plugin metadata from tarball: %w", err)
}
filename := fmt.Sprintf("%s-%s.tgz", metadata.Name, metadata.Version)
// Save plugin tarball to temp directory
pluginTarball := filepath.Join(tempDir, filename)
if err := os.WriteFile(pluginTarball, pluginBytes, 0644); err != nil {
cleanup()
return "", nil, fmt.Errorf("failed to save plugin tarball: %w", err)
}
// Try to download the provenance file - don't fail if it doesn't exist
provSource := i.Source + ".prov"
if provData, err := i.getter.Get(provSource); err == nil {
// Save provenance to temp directory
provFile := filepath.Join(tempDir, filename+".prov")
if err := os.WriteFile(provFile, provData.Bytes(), 0644); err == nil {
slog.Debug("prepared plugin for verification", "plugin", pluginTarball, "provenance", provFile)
}
return nil, nil, "", fmt.Errorf("failed to extract plugin metadata from tarball: %w", err)
}
// Note: We don't fail if .prov file can't be downloaded - the verification logic
// in InstallWithOptions will handle missing .prov files appropriately
filename = fmt.Sprintf("%s-%s.tgz", metadata.Name, metadata.Version)
slog.Debug("prepared plugin for verification", "plugin", pluginTarball)
return pluginTarball, cleanup, nil
slog.Debug("got verification data for OCI plugin", "filename", filename)
return i.pluginData, i.provData, filename, nil
}

@ -17,6 +17,7 @@ package plugin
import (
"archive/tar"
"bytes"
"compress/gzip"
"errors"
"fmt"
@ -29,14 +30,14 @@ import (
"helm.sh/helm/v4/pkg/provenance"
)
// SignPlugin signs a plugin using the SHA256 hash of the tarball.
// SignPlugin signs a plugin using the SHA256 hash of the tarball data.
//
// This is used when packaging and signing a plugin from a tarball file.
// This is used when packaging and signing a plugin from tarball data.
// It creates a signature that includes the tarball hash and plugin metadata,
// allowing verification of the original tarball later.
func SignPlugin(tarballPath string, signer *provenance.Signatory) (string, error) {
// Extract plugin metadata from tarball
pluginMeta, err := extractPluginMetadata(tarballPath)
func SignPlugin(tarballData []byte, filename string, signer *provenance.Signatory) (string, error) {
// Extract plugin metadata from tarball data
pluginMeta, err := ExtractTgzPluginMetadata(bytes.NewReader(tarballData))
if err != nil {
return "", fmt.Errorf("failed to extract plugin metadata: %w", err)
}
@ -48,22 +49,11 @@ func SignPlugin(tarballPath string, signer *provenance.Signatory) (string, error
}
// Use the generic provenance signing function
return signer.ClearSign(tarballPath, metadataBytes)
return signer.ClearSign(tarballData, filename, metadataBytes)
}
// extractPluginMetadata extracts plugin metadata from a tarball
func extractPluginMetadata(tarballPath string) (*Metadata, error) {
f, err := os.Open(tarballPath)
if err != nil {
return nil, err
}
defer f.Close()
return ExtractPluginMetadataFromReader(f)
}
// ExtractPluginMetadataFromReader extracts plugin metadata from a tarball reader
func ExtractPluginMetadataFromReader(r io.Reader) (*Metadata, error) {
// ExtractTgzPluginMetadata extracts plugin metadata from a gzipped tarball reader
func ExtractTgzPluginMetadata(r io.Reader) (*Metadata, error) {
gzr, err := gzip.NewReader(r)
if err != nil {
return nil, err

@ -69,8 +69,14 @@ runtimeConfig:
t.Fatal(err)
}
// Read the tarball data
tarballData, err := os.ReadFile(tarballPath)
if err != nil {
t.Fatalf("failed to read tarball: %v", err)
}
// Sign the plugin tarball
sig, err := SignPlugin(tarballPath, signer)
sig, err := SignPlugin(tarballData, filepath.Base(tarballPath), signer)
if err != nil {
t.Fatalf("failed to sign plugin: %v", err)
}

@ -16,57 +16,24 @@ limitations under the License.
package plugin
import (
"errors"
"fmt"
"os"
"path/filepath"
"helm.sh/helm/v4/pkg/provenance"
)
// VerifyPlugin verifies a plugin tarball against a signature.
//
// This function verifies that a plugin tarball has a valid provenance file
// and that the provenance file is signed by a trusted entity.
func VerifyPlugin(pluginPath, keyring string) (*provenance.Verification, error) {
// Verify the plugin path exists
fi, err := os.Stat(pluginPath)
if err != nil {
return nil, err
}
// Only support tarball verification
if fi.IsDir() {
return nil, errors.New("directory verification not supported - only plugin tarballs can be verified")
}
// Verify it's a tarball
if !isTarball(pluginPath) {
return nil, errors.New("plugin file must be a gzipped tarball (.tar.gz or .tgz)")
}
// Look for provenance file
provFile := pluginPath + ".prov"
if _, err := os.Stat(provFile); err != nil {
return nil, fmt.Errorf("could not find provenance file %s: %w", provFile, err)
}
// VerifyPlugin verifies plugin data against a signature using data in memory.
func VerifyPlugin(archiveData, provData []byte, filename, keyring string) (*provenance.Verification, error) {
// Create signatory from keyring
sig, err := provenance.NewFromKeyring(keyring, "")
if err != nil {
return nil, err
}
return verifyPluginTarball(pluginPath, provFile, sig)
}
// verifyPluginTarball verifies a plugin tarball against its signature
func verifyPluginTarball(pluginPath, provPath string, sig *provenance.Signatory) (*provenance.Verification, error) {
// Reuse chart verification logic from pkg/provenance
return sig.Verify(pluginPath, provPath)
// Use the new VerifyData method directly
return sig.Verify(archiveData, provData, filename)
}
// isTarball checks if a file has a tarball extension
func isTarball(filename string) bool {
func IsTarball(filename string) bool {
return filepath.Ext(filename) == ".gz" || filepath.Ext(filename) == ".tgz"
}

@ -18,7 +18,6 @@ package plugin
import (
"os"
"path/filepath"
"strings"
"testing"
"helm.sh/helm/v4/pkg/provenance"
@ -74,7 +73,13 @@ func TestVerifyPlugin(t *testing.T) {
t.Fatal(err)
}
sig, err := SignPlugin(tarballPath, signer)
// Read the tarball data
tarballData, err := os.ReadFile(tarballPath)
if err != nil {
t.Fatal(err)
}
sig, err := SignPlugin(tarballData, filepath.Base(tarballPath), signer)
if err != nil {
t.Fatal(err)
}
@ -85,8 +90,19 @@ func TestVerifyPlugin(t *testing.T) {
t.Fatal(err)
}
// Read the files for verification
archiveData, err := os.ReadFile(tarballPath)
if err != nil {
t.Fatal(err)
}
provData, err := os.ReadFile(provFile)
if err != nil {
t.Fatal(err)
}
// Now verify the plugin
verification, err := VerifyPlugin(tarballPath, testPubFile)
verification, err := VerifyPlugin(archiveData, provData, filepath.Base(tarballPath), testPubFile)
if err != nil {
t.Fatalf("Failed to verify plugin: %v", err)
}
@ -146,8 +162,19 @@ InvalidSignatureData
t.Fatal(err)
}
// Read the files
archiveData, err := os.ReadFile(tarballPath)
if err != nil {
t.Fatal(err)
}
provData, err := os.ReadFile(provFile)
if err != nil {
t.Fatal(err)
}
// Try to verify - should fail
_, err = VerifyPlugin(tarballPath, testPubFile)
_, err = VerifyPlugin(archiveData, provData, filepath.Base(tarballPath), testPubFile)
if err == nil {
t.Error("Expected verification to fail with bad signature")
}
@ -162,40 +189,26 @@ func TestVerifyPluginMissingProvenance(t *testing.T) {
t.Fatal(err)
}
// Try to verify without .prov file
_, err := VerifyPlugin(tarballPath, testPubFile)
if err == nil {
t.Error("Expected verification to fail without provenance file")
}
}
func TestVerifyPluginDirectory(t *testing.T) {
// Create a test plugin directory
tempDir := t.TempDir()
pluginDir := filepath.Join(tempDir, "test-plugin")
if err := os.MkdirAll(pluginDir, 0755); err != nil {
t.Fatal(err)
}
// Create a plugin.yaml file
if err := os.WriteFile(filepath.Join(pluginDir, "plugin.yaml"), []byte(testPluginYAML), 0644); err != nil {
// Read the tarball data
archiveData, err := os.ReadFile(tarballPath)
if err != nil {
t.Fatal(err)
}
// Attempt to verify the directory - should fail
_, err := VerifyPlugin(pluginDir, testPubFile)
// Try to verify with empty provenance data
_, err = VerifyPlugin(archiveData, nil, filepath.Base(tarballPath), testPubFile)
if err == nil {
t.Error("Expected directory verification to fail, but it succeeded")
}
expectedError := "directory verification not supported"
if !containsString(err.Error(), expectedError) {
t.Errorf("Expected error to contain %q, got %q", expectedError, err.Error())
t.Error("Expected verification to fail with empty provenance data")
}
}
func containsString(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(s) > len(substr) &&
(s[:len(substr)] == substr || s[len(s)-len(substr):] == substr ||
strings.Contains(s, substr)))
func TestVerifyPluginMalformedData(t *testing.T) {
// Test with malformed tarball data - should fail
malformedData := []byte("not a tarball")
provData := []byte("fake provenance")
_, err := VerifyPlugin(malformedData, provData, "malformed.tar.gz", testPubFile)
if err == nil {
t.Error("Expected malformed data verification to fail, but it succeeded")
}
}

@ -21,6 +21,7 @@ import (
"errors"
"fmt"
"os"
"path/filepath"
"syscall"
"github.com/Masterminds/semver/v3"
@ -156,8 +157,14 @@ func (p *Package) Clearsign(filename string) error {
return fmt.Errorf("failed to marshal chart metadata: %w", err)
}
// Read the chart archive file
archiveData, err := os.ReadFile(filename)
if err != nil {
return fmt.Errorf("failed to read chart archive: %w", err)
}
// Use the generic provenance signing function
sig, err := signer.ClearSign(filename, metadataBytes)
sig, err := signer.ClearSign(archiveData, filepath.Base(filename), metadataBytes)
if err != nil {
return err
}

@ -142,8 +142,15 @@ func (o *pluginPackageOptions) run(out io.Writer) error {
// If signing was requested, sign the tarball
if o.sign {
// Sign the plugin tarball (not the source directory)
sig, err := plugin.SignPlugin(tarballPath, signer)
// Read the tarball data
tarballData, err := os.ReadFile(tarballPath)
if err != nil {
os.Remove(tarballPath)
return fmt.Errorf("failed to read tarball for signing: %w", err)
}
// Sign the plugin tarball data
sig, err := plugin.SignPlugin(tarballData, filepath.Base(tarballPath), signer)
if err != nil {
os.Remove(tarballPath)
return fmt.Errorf("failed to sign plugin: %w", err)

@ -18,6 +18,8 @@ package cmd
import (
"fmt"
"io"
"os"
"path/filepath"
"github.com/spf13/cobra"
@ -65,8 +67,41 @@ func newPluginVerifyCmd(out io.Writer) *cobra.Command {
}
func (o *pluginVerifyOptions) run(out io.Writer) error {
// Verify the plugin
verification, err := plugin.VerifyPlugin(o.pluginPath, o.keyring)
// Verify the plugin path exists
fi, err := os.Stat(o.pluginPath)
if err != nil {
return err
}
// Only support tarball verification
if fi.IsDir() {
return fmt.Errorf("directory verification not supported - only plugin tarballs can be verified")
}
// Verify it's a tarball
if !plugin.IsTarball(o.pluginPath) {
return fmt.Errorf("plugin file must be a gzipped tarball (.tar.gz or .tgz)")
}
// Look for provenance file
provFile := o.pluginPath + ".prov"
if _, err := os.Stat(provFile); err != nil {
return fmt.Errorf("could not find provenance file %s: %w", provFile, err)
}
// Read the files
archiveData, err := os.ReadFile(o.pluginPath)
if err != nil {
return fmt.Errorf("failed to read plugin file: %w", err)
}
provData, err := os.ReadFile(provFile)
if err != nil {
return fmt.Errorf("failed to read provenance file: %w", err)
}
// Verify the plugin using data
verification, err := plugin.VerifyPlugin(archiveData, provData, filepath.Base(o.pluginPath), o.keyring)
if err != nil {
return err
}

@ -493,7 +493,18 @@ func VerifyChart(path, provfile, keyring string) (*provenance.Verification, erro
if err != nil {
return nil, fmt.Errorf("failed to load keyring: %w", err)
}
return sig.Verify(path, provfile)
// Read archive and provenance files
archiveData, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read chart archive: %w", err)
}
provData, err := os.ReadFile(provfile)
if err != nil {
return nil, fmt.Errorf("failed to read provenance file: %w", err)
}
return sig.Verify(archiveData, provData, filepath.Base(path))
}
// isTar tests whether the given file is a tar file.

@ -23,7 +23,6 @@ import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"golang.org/x/crypto/openpgp" //nolint
@ -194,29 +193,20 @@ func (s *Signatory) DecryptKey(fn PassphraseFetcher) error {
return s.Entity.PrivateKey.Decrypt(p)
}
// ClearSign signs a package with the given key and pre-marshalled metadata.
// ClearSign signs package data with the given key and pre-marshalled metadata.
//
// This takes the path to a package archive file, a key, and marshalled metadata bytes.
// This allows both charts and plugins to use the same signing infrastructure.
//
// The Signatory must have a valid Entity.PrivateKey for this to work. If it does
// not, an error will be returned.
func (s *Signatory) ClearSign(packagePath string, metadataBytes []byte) (string, error) {
// This is the core signing method that works with data in memory.
// The Signatory must have a valid Entity.PrivateKey for this to work.
func (s *Signatory) ClearSign(archiveData []byte, filename string, metadataBytes []byte) (string, error) {
if s.Entity == nil {
return "", errors.New("private key not found")
} else if s.Entity.PrivateKey == nil {
return "", errors.New("provided key is not a private key. Try providing a keyring with secret keys")
}
if fi, err := os.Stat(packagePath); err != nil {
return "", err
} else if fi.IsDir() {
return "", errors.New("cannot sign a directory")
}
out := bytes.NewBuffer(nil)
b, err := messageBlock(packagePath, metadataBytes)
b, err := messageBlock(archiveData, filename, metadataBytes)
if err != nil {
return "", err
}
@ -246,69 +236,47 @@ func (s *Signatory) ClearSign(packagePath string, metadataBytes []byte) (string,
return out.String(), nil
}
// Verify checks a signature and verifies that it is legit for a package.
func (s *Signatory) Verify(packagePath, sigpath string) (*Verification, error) {
// Verify checks a signature and verifies that it is legit for package data.
// This is the core verification method that works with data in memory.
func (s *Signatory) Verify(archiveData, provData []byte, filename string) (*Verification, error) {
ver := &Verification{}
for _, fname := range []string{packagePath, sigpath} {
if fi, err := os.Stat(fname); err != nil {
return ver, err
} else if fi.IsDir() {
return ver, fmt.Errorf("%s cannot be a directory", fname)
}
}
// First verify the signature
sig, err := s.decodeSignature(sigpath)
if err != nil {
return ver, fmt.Errorf("failed to decode signature: %w", err)
block, _ := clearsign.Decode(provData)
if block == nil {
return ver, errors.New("signature block not found")
}
by, err := s.verifySignature(sig)
by, err := s.verifySignature(block)
if err != nil {
return ver, err
}
ver.SignedBy = by
// Second, verify the hash of the tarball.
sum, err := DigestFile(packagePath)
// Second, verify the hash of the data.
sum, err := Digest(bytes.NewBuffer(archiveData))
if err != nil {
return ver, err
}
sums, err := parseMessageBlock(sig.Plaintext)
sums, err := parseMessageBlock(block.Plaintext)
if err != nil {
return ver, err
}
sum = "sha256:" + sum
basename := filepath.Base(packagePath)
if sha, ok := sums.Files[basename]; !ok {
return ver, fmt.Errorf("provenance does not contain a SHA for a file named %q", basename)
if sha, ok := sums.Files[filename]; !ok {
return ver, fmt.Errorf("provenance does not contain a SHA for a file named %q", filename)
} else if sha != sum {
return ver, fmt.Errorf("sha256 sum does not match for %s: %q != %q", basename, sha, sum)
return ver, fmt.Errorf("sha256 sum does not match for %s: %q != %q", filename, sha, sum)
}
ver.FileHash = sum
ver.FileName = basename
ver.FileName = filename
// TODO: when image signing is added, verify that here.
return ver, nil
}
func (s *Signatory) decodeSignature(filename string) (*clearsign.Block, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
block, _ := clearsign.Decode(data)
if block == nil {
// There was no sig in the file.
return nil, errors.New("signature block not found")
}
return block, nil
}
// verifySignature verifies that the given block is validly signed, and returns the signer.
func (s *Signatory) verifySignature(block *clearsign.Block) (*openpgp.Entity, error) {
return openpgp.CheckDetachedSignature(
@ -318,18 +286,17 @@ func (s *Signatory) verifySignature(block *clearsign.Block) (*openpgp.Entity, er
)
}
// messageBlock creates a message block from a package path and pre-marshalled metadata
func messageBlock(packagePath string, metadataBytes []byte) (*bytes.Buffer, error) {
// Checksum the archive
chash, err := DigestFile(packagePath)
// messageBlock creates a message block from archive data and pre-marshalled metadata
func messageBlock(archiveData []byte, filename string, metadataBytes []byte) (*bytes.Buffer, error) {
// Checksum the archive data
chash, err := Digest(bytes.NewBuffer(archiveData))
if err != nil {
return nil, err
}
base := filepath.Base(packagePath)
sums := &SumCollection{
Files: map[string]string{
base: "sha256:" + chash,
filename: "sha256:" + chash,
},
}

@ -98,7 +98,13 @@ func loadChartMetadataForSigning(t *testing.T, chartPath string) []byte {
func TestMessageBlock(t *testing.T) {
metadataBytes := loadChartMetadataForSigning(t, testChartfile)
out, err := messageBlock(testChartfile, metadataBytes)
// Read the chart file data
archiveData, err := os.ReadFile(testChartfile)
if err != nil {
t.Fatal(err)
}
out, err := messageBlock(archiveData, filepath.Base(testChartfile), metadataBytes)
if err != nil {
t.Fatal(err)
}
@ -243,7 +249,13 @@ func TestClearSign(t *testing.T) {
metadataBytes := loadChartMetadataForSigning(t, testChartfile)
sig, err := signer.ClearSign(testChartfile, metadataBytes)
// Read the chart file data
archiveData, err := os.ReadFile(testChartfile)
if err != nil {
t.Fatal(err)
}
sig, err := signer.ClearSign(archiveData, filepath.Base(testChartfile), metadataBytes)
if err != nil {
t.Fatal(err)
}
@ -276,7 +288,13 @@ func TestClearSignError(t *testing.T) {
metadataBytes := loadChartMetadataForSigning(t, testChartfile)
sig, err := signer.ClearSign(testChartfile, metadataBytes)
// Read the chart file data
archiveData, err := os.ReadFile(testChartfile)
if err != nil {
t.Fatal(err)
}
sig, err := signer.ClearSign(archiveData, filepath.Base(testChartfile), metadataBytes)
if err == nil {
t.Fatal("didn't get an error from ClearSign but expected one")
}
@ -286,56 +304,25 @@ func TestClearSignError(t *testing.T) {
}
}
func TestDecodeSignature(t *testing.T) {
// Unlike other tests, this does a round-trip test, ensuring that a signature
// generated by the library can also be verified by the library.
func TestVerify(t *testing.T) {
signer, err := NewFromFiles(testKeyfile, testPubfile)
if err != nil {
t.Fatal(err)
}
metadataBytes := loadChartMetadataForSigning(t, testChartfile)
sig, err := signer.ClearSign(testChartfile, metadataBytes)
if err != nil {
t.Fatal(err)
}
f, err := os.CreateTemp(t.TempDir(), "helm-test-sig-")
if err != nil {
t.Fatal(err)
}
tname := f.Name()
defer func() {
os.Remove(tname)
}()
f.WriteString(sig)
f.Close()
sig2, err := signer.decodeSignature(tname)
// Read the chart file data
archiveData, err := os.ReadFile(testChartfile)
if err != nil {
t.Fatal(err)
}
by, err := signer.verifySignature(sig2)
// Read the signature file data
sigData, err := os.ReadFile(testSigBlock)
if err != nil {
t.Fatal(err)
}
if _, ok := by.Identities[testKeyName]; !ok {
t.Errorf("Expected identity %q", testKeyName)
}
}
func TestVerify(t *testing.T) {
signer, err := NewFromFiles(testKeyfile, testPubfile)
if err != nil {
t.Fatal(err)
}
if ver, err := signer.Verify(testChartfile, testSigBlock); err != nil {
if ver, err := signer.Verify(archiveData, sigData, filepath.Base(testChartfile)); err != nil {
t.Errorf("Failed to pass verify. Err: %s", err)
} else if len(ver.FileHash) == 0 {
t.Error("Verification is missing hash.")
@ -345,7 +332,13 @@ func TestVerify(t *testing.T) {
t.Errorf("FileName is unexpectedly %q", ver.FileName)
}
if _, err = signer.Verify(testChartfile, testTamperedSigBlock); err == nil {
// Read the tampered signature file data
tamperedSigData, err := os.ReadFile(testTamperedSigBlock)
if err != nil {
t.Fatal(err)
}
if _, err = signer.Verify(archiveData, tamperedSigData, filepath.Base(testChartfile)); err == nil {
t.Errorf("Expected %s to fail.", testTamperedSigBlock)
}

Loading…
Cancel
Save