mirror of https://github.com/helm/helm.git
Remove unnecessary file i/o operations from signing and verifying
Signed-off-by: Scott Rigby <scott@r6by.com>
This commit is contained in:
parent
9ea35da0d0
commit
e814ff3c38
|
@ -38,7 +38,8 @@ type HTTPInstaller struct {
|
||||||
base
|
base
|
||||||
extractor Extractor
|
extractor Extractor
|
||||||
getter getter.Getter
|
getter getter.Getter
|
||||||
// Provenance data to save after installation
|
// Cached data to avoid duplicate downloads
|
||||||
|
pluginData []byte
|
||||||
provData []byte
|
provData []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,15 +75,18 @@ func NewHTTPInstaller(source string) (*HTTPInstaller, error) {
|
||||||
//
|
//
|
||||||
// Implements Installer.
|
// Implements Installer.
|
||||||
func (i *HTTPInstaller) Install() error {
|
func (i *HTTPInstaller) Install() error {
|
||||||
|
// Ensure plugin data is cached
|
||||||
|
if i.pluginData == nil {
|
||||||
pluginData, err := i.getter.Get(i.Source)
|
pluginData, err := i.getter.Get(i.Source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
i.pluginData = pluginData.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
// Save the original tarball to plugins directory for verification
|
// Save the original tarball to plugins directory for verification
|
||||||
// Extract metadata to get the actual plugin name and version
|
// Extract metadata to get the actual plugin name and version
|
||||||
pluginBytes := pluginData.Bytes()
|
metadata, err := plugin.ExtractTgzPluginMetadata(bytes.NewReader(i.pluginData))
|
||||||
metadata, err := plugin.ExtractPluginMetadataFromReader(bytes.NewReader(pluginBytes))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to extract plugin metadata from tarball: %w", err)
|
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 {
|
if err := os.MkdirAll(filepath.Dir(tarballPath), 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create plugins directory: %w", err)
|
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)
|
return fmt.Errorf("failed to save tarball: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure prov data is cached if available
|
||||||
|
if i.provData == nil {
|
||||||
// Try to download .prov file if it exists
|
// Try to download .prov file if it exists
|
||||||
provURL := i.Source + ".prov"
|
provURL := i.Source + ".prov"
|
||||||
if provData, err := i.getter.Get(provURL); err == nil {
|
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"
|
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)
|
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)
|
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")
|
return strings.HasSuffix(i.Source, ".tgz") || strings.HasSuffix(i.Source, ".tar.gz")
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrepareForVerification downloads the plugin and signature files for verification
|
// GetVerificationData returns cached plugin and provenance data for verification
|
||||||
func (i *HTTPInstaller) PrepareForVerification() (string, func(), error) {
|
func (i *HTTPInstaller) GetVerificationData() (archiveData, provData []byte, filename string, err error) {
|
||||||
if !i.SupportsVerification() {
|
if !i.SupportsVerification() {
|
||||||
return "", nil, fmt.Errorf("verification not supported for this source")
|
return nil, nil, "", fmt.Errorf("verification not supported for this source")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create temporary directory for downloads
|
// Download plugin data once and cache it
|
||||||
tempDir, err := os.MkdirTemp("", "helm-plugin-verify-*")
|
if i.pluginData == nil {
|
||||||
|
data, err := i.getter.Get(i.Source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, fmt.Errorf("failed to create temp directory: %w", err)
|
return nil, nil, "", fmt.Errorf("failed to download plugin: %w", err)
|
||||||
|
}
|
||||||
|
i.pluginData = data.Bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanup := func() {
|
// Download prov data once and cache it if available
|
||||||
os.RemoveAll(tempDir)
|
if i.provData == nil {
|
||||||
}
|
provData, err := i.getter.Get(i.Source + ".prov")
|
||||||
|
|
||||||
// Download plugin tarball
|
|
||||||
pluginFile := filepath.Join(tempDir, filepath.Base(i.Source))
|
|
||||||
|
|
||||||
g, err := getter.All(new(cli.EnvSettings)).ByScheme("http")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cleanup()
|
// If provenance file doesn't exist, set provData to nil
|
||||||
return "", nil, err
|
// The verification logic will handle this gracefully
|
||||||
}
|
i.provData = nil
|
||||||
|
} else {
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
i.provData = provData.Bytes()
|
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 {
|
type Verifier interface {
|
||||||
// SupportsVerification returns true if this installer can verify plugins
|
// SupportsVerification returns true if this installer can verify plugins
|
||||||
SupportsVerification() bool
|
SupportsVerification() bool
|
||||||
// PrepareForVerification downloads necessary files for verification
|
// GetVerificationData returns plugin and provenance data for verification
|
||||||
PrepareForVerification() (pluginPath string, cleanup func(), err error)
|
GetVerificationData() (archiveData, provData []byte, filename string, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Install installs a plugin.
|
// 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)")
|
return nil, fmt.Errorf("--verify is only supported for plugin tarballs (.tgz files)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare for verification (download files if needed)
|
// Get verification data (works for both memory and file-based installers)
|
||||||
pluginPath, cleanup, err := verifier.PrepareForVerification()
|
archiveData, provData, filename, err := verifier.GetVerificationData()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to prepare for verification: %w", err)
|
return nil, fmt.Errorf("failed to get verification data: %w", err)
|
||||||
}
|
|
||||||
if cleanup != nil {
|
|
||||||
defer cleanup()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if provenance file exists
|
// Check if provenance data exists
|
||||||
provFile := pluginPath + ".prov"
|
if len(provData) == 0 {
|
||||||
if _, err := os.Stat(provFile); err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
// No .prov file found - emit warning but continue installation
|
// 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")
|
fmt.Fprintf(os.Stderr, "WARNING: No provenance file found for plugin. Plugin is not signed and cannot be verified.\n")
|
||||||
} else {
|
} else {
|
||||||
// Other error accessing .prov file
|
// Provenance data exists - verify the plugin
|
||||||
return nil, fmt.Errorf("failed to access provenance file: %w", err)
|
verification, err := plugin.VerifyPlugin(archiveData, provData, filename, opts.Keyring)
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Provenance file exists - verify the plugin
|
|
||||||
verification, err := plugin.VerifyPlugin(pluginPath, opts.Keyring)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("plugin verification failed: %w", err)
|
return nil, fmt.Errorf("plugin verification failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,8 @@ type LocalInstaller struct {
|
||||||
base
|
base
|
||||||
isArchive bool
|
isArchive bool
|
||||||
extractor Extractor
|
extractor Extractor
|
||||||
provData []byte // Provenance data to save after installation
|
pluginData []byte // Cached plugin data
|
||||||
|
provData []byte // Cached provenance data
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLocalInstaller creates a new LocalInstaller.
|
// NewLocalInstaller creates a new LocalInstaller.
|
||||||
|
@ -110,7 +111,7 @@ func (i *LocalInstaller) installFromArchive() error {
|
||||||
|
|
||||||
// Copy the original tarball to plugins directory for verification
|
// Copy the original tarball to plugins directory for verification
|
||||||
// Extract metadata to get the actual plugin name and version
|
// 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 {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to extract plugin metadata from tarball: %w", err)
|
return fmt.Errorf("failed to extract plugin metadata from tarball: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -184,21 +185,35 @@ func (i *LocalInstaller) SupportsVerification() bool {
|
||||||
return i.isArchive
|
return i.isArchive
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrepareForVerification returns the local path for verification
|
// GetVerificationData loads plugin and provenance data from local files for verification
|
||||||
func (i *LocalInstaller) PrepareForVerification() (string, func(), error) {
|
func (i *LocalInstaller) GetVerificationData() (archiveData, provData []byte, filename string, err error) {
|
||||||
if !i.SupportsVerification() {
|
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
|
// 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"
|
provFile := i.Source + ".prov"
|
||||||
if provData, err := os.ReadFile(provFile); err == nil {
|
i.provData, err = os.ReadFile(provFile)
|
||||||
// Store the provenance data so we can save it after installation
|
if err != nil {
|
||||||
i.provData = provData
|
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.pluginData, i.provData, filepath.Base(i.Source), nil
|
||||||
return i.Source, nil, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,9 @@ type OCIInstaller struct {
|
||||||
base
|
base
|
||||||
settings *cli.EnvSettings
|
settings *cli.EnvSettings
|
||||||
getter getter.Getter
|
getter getter.Getter
|
||||||
|
// Cached data to avoid duplicate downloads
|
||||||
|
pluginData []byte
|
||||||
|
provData []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewOCIInstaller creates a new OCIInstaller with optional getter options
|
// 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 {
|
func (i *OCIInstaller) Install() error {
|
||||||
slog.Debug("pulling OCI plugin", "source", i.Source)
|
slog.Debug("pulling OCI plugin", "source", i.Source)
|
||||||
|
|
||||||
// Use getter to download the plugin
|
// Ensure plugin data is cached
|
||||||
|
if i.pluginData == nil {
|
||||||
pluginData, err := i.getter.Get(i.Source)
|
pluginData, err := i.getter.Get(i.Source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to pull plugin from %s: %w", i.Source, err)
|
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
|
// 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 {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to extract plugin metadata from tarball: %w", err)
|
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 {
|
if err := os.MkdirAll(filepath.Dir(tarballPath), 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create plugins directory: %w", err)
|
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)
|
return fmt.Errorf("failed to save tarball: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to download and save .prov file alongside the tarball
|
// Ensure prov data is cached if available
|
||||||
|
if i.provData == nil {
|
||||||
|
// Try to download .prov file if it exists
|
||||||
provSource := i.Source + ".prov"
|
provSource := i.Source + ".prov"
|
||||||
if provData, err := i.getter.Get(provSource); err == nil {
|
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"
|
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)
|
slog.Debug("failed to save provenance file", "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is a gzip compressed file
|
// 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")
|
return fmt.Errorf("plugin data is not a gzip compressed archive")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,7 +138,7 @@ func (i *OCIInstaller) Install() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract as gzipped tar
|
// 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)
|
return fmt.Errorf("failed to extract plugin: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -251,55 +261,41 @@ func (i *OCIInstaller) SupportsVerification() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrepareForVerification downloads the plugin tarball and provenance to a temporary directory
|
// GetVerificationData downloads and caches plugin and provenance data from OCI registry for verification
|
||||||
func (i *OCIInstaller) PrepareForVerification() (pluginPath string, cleanup func(), err error) {
|
func (i *OCIInstaller) GetVerificationData() (archiveData, provData []byte, filename string, err error) {
|
||||||
slog.Debug("preparing OCI plugin for verification", "source", i.Source)
|
slog.Debug("getting verification data for OCI plugin", "source", i.Source)
|
||||||
|
|
||||||
// Create temporary directory for verification
|
// Download plugin data once and cache it
|
||||||
tempDir, err := os.MkdirTemp("", "helm-oci-verify-")
|
if i.pluginData == nil {
|
||||||
|
pluginDataBuffer, err := i.getter.Get(i.Source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, fmt.Errorf("failed to create temp directory: %w", err)
|
return nil, nil, "", fmt.Errorf("failed to pull plugin from %s: %w", i.Source, err)
|
||||||
|
}
|
||||||
|
i.pluginData = pluginDataBuffer.Bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanup = func() {
|
// Download prov data once and cache it if available
|
||||||
os.RemoveAll(tempDir)
|
if i.provData == nil {
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract metadata to get the actual plugin name and version
|
|
||||||
pluginBytes := pluginData.Bytes()
|
|
||||||
metadata, err := plugin.ExtractPluginMetadataFromReader(bytes.NewReader(pluginBytes))
|
|
||||||
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"
|
provSource := i.Source + ".prov"
|
||||||
if provData, err := i.getter.Get(provSource); err == nil {
|
// Calling getter.Get again is reasonable because: 1. The OCI registry client already optimizes the underlying network calls
|
||||||
// Save provenance to temp directory
|
// 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
|
||||||
provFile := filepath.Join(tempDir, filename+".prov")
|
provDataBuffer, err := i.getter.Get(provSource)
|
||||||
if err := os.WriteFile(provFile, provData.Bytes(), 0644); err == nil {
|
if err != nil {
|
||||||
slog.Debug("prepared plugin for verification", "plugin", pluginTarball, "provenance", provFile)
|
// 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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Note: We don't fail if .prov file can't be downloaded - the verification logic
|
|
||||||
// in InstallWithOptions will handle missing .prov files appropriately
|
|
||||||
|
|
||||||
slog.Debug("prepared plugin for verification", "plugin", pluginTarball)
|
// Extract metadata to get the filename
|
||||||
return pluginTarball, cleanup, nil
|
metadata, err := plugin.ExtractTgzPluginMetadata(bytes.NewReader(i.pluginData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, "", fmt.Errorf("failed to extract plugin metadata from tarball: %w", err)
|
||||||
|
}
|
||||||
|
filename = fmt.Sprintf("%s-%s.tgz", metadata.Name, metadata.Version)
|
||||||
|
|
||||||
|
slog.Debug("got verification data for OCI plugin", "filename", filename)
|
||||||
|
return i.pluginData, i.provData, filename, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ package plugin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/tar"
|
"archive/tar"
|
||||||
|
"bytes"
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -29,14 +30,14 @@ import (
|
||||||
"helm.sh/helm/v4/pkg/provenance"
|
"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,
|
// It creates a signature that includes the tarball hash and plugin metadata,
|
||||||
// allowing verification of the original tarball later.
|
// allowing verification of the original tarball later.
|
||||||
func SignPlugin(tarballPath string, signer *provenance.Signatory) (string, error) {
|
func SignPlugin(tarballData []byte, filename string, signer *provenance.Signatory) (string, error) {
|
||||||
// Extract plugin metadata from tarball
|
// Extract plugin metadata from tarball data
|
||||||
pluginMeta, err := extractPluginMetadata(tarballPath)
|
pluginMeta, err := ExtractTgzPluginMetadata(bytes.NewReader(tarballData))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to extract plugin metadata: %w", err)
|
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
|
// Use the generic provenance signing function
|
||||||
return signer.ClearSign(tarballPath, metadataBytes)
|
return signer.ClearSign(tarballData, filename, metadataBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// extractPluginMetadata extracts plugin metadata from a tarball
|
// ExtractTgzPluginMetadata extracts plugin metadata from a gzipped tarball reader
|
||||||
func extractPluginMetadata(tarballPath string) (*Metadata, error) {
|
func ExtractTgzPluginMetadata(r io.Reader) (*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) {
|
|
||||||
gzr, err := gzip.NewReader(r)
|
gzr, err := gzip.NewReader(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -69,8 +69,14 @@ runtimeConfig:
|
||||||
t.Fatal(err)
|
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
|
// Sign the plugin tarball
|
||||||
sig, err := SignPlugin(tarballPath, signer)
|
sig, err := SignPlugin(tarballData, filepath.Base(tarballPath), signer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to sign plugin: %v", err)
|
t.Fatalf("failed to sign plugin: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,57 +16,24 @@ limitations under the License.
|
||||||
package plugin
|
package plugin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"helm.sh/helm/v4/pkg/provenance"
|
"helm.sh/helm/v4/pkg/provenance"
|
||||||
)
|
)
|
||||||
|
|
||||||
// VerifyPlugin verifies a plugin tarball against a signature.
|
// VerifyPlugin verifies plugin data against a signature using data in memory.
|
||||||
//
|
func VerifyPlugin(archiveData, provData []byte, filename, keyring string) (*provenance.Verification, error) {
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create signatory from keyring
|
// Create signatory from keyring
|
||||||
sig, err := provenance.NewFromKeyring(keyring, "")
|
sig, err := provenance.NewFromKeyring(keyring, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return verifyPluginTarball(pluginPath, provFile, sig)
|
// Use the new VerifyData method directly
|
||||||
}
|
return sig.Verify(archiveData, provData, filename)
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// isTarball checks if a file has a tarball extension
|
// 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"
|
return filepath.Ext(filename) == ".gz" || filepath.Ext(filename) == ".tgz"
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,6 @@ package plugin
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"helm.sh/helm/v4/pkg/provenance"
|
"helm.sh/helm/v4/pkg/provenance"
|
||||||
|
@ -74,7 +73,13 @@ func TestVerifyPlugin(t *testing.T) {
|
||||||
t.Fatal(err)
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -85,8 +90,19 @@ func TestVerifyPlugin(t *testing.T) {
|
||||||
t.Fatal(err)
|
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
|
// Now verify the plugin
|
||||||
verification, err := VerifyPlugin(tarballPath, testPubFile)
|
verification, err := VerifyPlugin(archiveData, provData, filepath.Base(tarballPath), testPubFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to verify plugin: %v", err)
|
t.Fatalf("Failed to verify plugin: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -146,8 +162,19 @@ InvalidSignatureData
|
||||||
t.Fatal(err)
|
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
|
// Try to verify - should fail
|
||||||
_, err = VerifyPlugin(tarballPath, testPubFile)
|
_, err = VerifyPlugin(archiveData, provData, filepath.Base(tarballPath), testPubFile)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Error("Expected verification to fail with bad signature")
|
t.Error("Expected verification to fail with bad signature")
|
||||||
}
|
}
|
||||||
|
@ -162,40 +189,26 @@ func TestVerifyPluginMissingProvenance(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to verify without .prov file
|
// Read the tarball data
|
||||||
_, err := VerifyPlugin(tarballPath, testPubFile)
|
archiveData, err := os.ReadFile(tarballPath)
|
||||||
if err == nil {
|
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)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a plugin.yaml file
|
// Try to verify with empty provenance data
|
||||||
if err := os.WriteFile(filepath.Join(pluginDir, "plugin.yaml"), []byte(testPluginYAML), 0644); err != nil {
|
_, err = VerifyPlugin(archiveData, nil, filepath.Base(tarballPath), testPubFile)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to verify the directory - should fail
|
|
||||||
_, err := VerifyPlugin(pluginDir, testPubFile)
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Error("Expected directory verification to fail, but it succeeded")
|
t.Error("Expected verification to fail with empty provenance data")
|
||||||
}
|
|
||||||
|
|
||||||
expectedError := "directory verification not supported"
|
|
||||||
if !containsString(err.Error(), expectedError) {
|
|
||||||
t.Errorf("Expected error to contain %q, got %q", expectedError, err.Error())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func containsString(s, substr string) bool {
|
func TestVerifyPluginMalformedData(t *testing.T) {
|
||||||
return len(s) >= len(substr) && (s == substr || len(s) > len(substr) &&
|
// Test with malformed tarball data - should fail
|
||||||
(s[:len(substr)] == substr || s[len(s)-len(substr):] == substr ||
|
malformedData := []byte("not a tarball")
|
||||||
strings.Contains(s, substr)))
|
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"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"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)
|
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
|
// Use the generic provenance signing function
|
||||||
sig, err := signer.ClearSign(filename, metadataBytes)
|
sig, err := signer.ClearSign(archiveData, filepath.Base(filename), metadataBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -142,8 +142,15 @@ func (o *pluginPackageOptions) run(out io.Writer) error {
|
||||||
|
|
||||||
// If signing was requested, sign the tarball
|
// If signing was requested, sign the tarball
|
||||||
if o.sign {
|
if o.sign {
|
||||||
// Sign the plugin tarball (not the source directory)
|
// Read the tarball data
|
||||||
sig, err := plugin.SignPlugin(tarballPath, signer)
|
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 {
|
if err != nil {
|
||||||
os.Remove(tarballPath)
|
os.Remove(tarballPath)
|
||||||
return fmt.Errorf("failed to sign plugin: %w", err)
|
return fmt.Errorf("failed to sign plugin: %w", err)
|
||||||
|
|
|
@ -18,6 +18,8 @@ package cmd
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
@ -65,8 +67,41 @@ func newPluginVerifyCmd(out io.Writer) *cobra.Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *pluginVerifyOptions) run(out io.Writer) error {
|
func (o *pluginVerifyOptions) run(out io.Writer) error {
|
||||||
// Verify the plugin
|
// Verify the plugin path exists
|
||||||
verification, err := plugin.VerifyPlugin(o.pluginPath, o.keyring)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -493,7 +493,18 @@ func VerifyChart(path, provfile, keyring string) (*provenance.Verification, erro
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to load keyring: %w", err)
|
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.
|
// isTar tests whether the given file is a tar file.
|
||||||
|
|
|
@ -23,7 +23,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/crypto/openpgp" //nolint
|
"golang.org/x/crypto/openpgp" //nolint
|
||||||
|
@ -194,29 +193,20 @@ func (s *Signatory) DecryptKey(fn PassphraseFetcher) error {
|
||||||
return s.Entity.PrivateKey.Decrypt(p)
|
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 is the core signing method that works with data in memory.
|
||||||
// This allows both charts and plugins to use the same signing infrastructure.
|
// The Signatory must have a valid Entity.PrivateKey for this to work.
|
||||||
//
|
func (s *Signatory) ClearSign(archiveData []byte, filename string, metadataBytes []byte) (string, error) {
|
||||||
// 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) {
|
|
||||||
if s.Entity == nil {
|
if s.Entity == nil {
|
||||||
return "", errors.New("private key not found")
|
return "", errors.New("private key not found")
|
||||||
} else if s.Entity.PrivateKey == nil {
|
} else if s.Entity.PrivateKey == nil {
|
||||||
return "", errors.New("provided key is not a private key. Try providing a keyring with secret keys")
|
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)
|
out := bytes.NewBuffer(nil)
|
||||||
|
|
||||||
b, err := messageBlock(packagePath, metadataBytes)
|
b, err := messageBlock(archiveData, filename, metadataBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -246,69 +236,47 @@ func (s *Signatory) ClearSign(packagePath string, metadataBytes []byte) (string,
|
||||||
return out.String(), nil
|
return out.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify checks a signature and verifies that it is legit for a package.
|
// Verify checks a signature and verifies that it is legit for package data.
|
||||||
func (s *Signatory) Verify(packagePath, sigpath string) (*Verification, error) {
|
// 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{}
|
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
|
// First verify the signature
|
||||||
sig, err := s.decodeSignature(sigpath)
|
block, _ := clearsign.Decode(provData)
|
||||||
if err != nil {
|
if block == nil {
|
||||||
return ver, fmt.Errorf("failed to decode signature: %w", err)
|
return ver, errors.New("signature block not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
by, err := s.verifySignature(sig)
|
by, err := s.verifySignature(block)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ver, err
|
return ver, err
|
||||||
}
|
}
|
||||||
ver.SignedBy = by
|
ver.SignedBy = by
|
||||||
|
|
||||||
// Second, verify the hash of the tarball.
|
// Second, verify the hash of the data.
|
||||||
sum, err := DigestFile(packagePath)
|
sum, err := Digest(bytes.NewBuffer(archiveData))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ver, err
|
return ver, err
|
||||||
}
|
}
|
||||||
sums, err := parseMessageBlock(sig.Plaintext)
|
sums, err := parseMessageBlock(block.Plaintext)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ver, err
|
return ver, err
|
||||||
}
|
}
|
||||||
|
|
||||||
sum = "sha256:" + sum
|
sum = "sha256:" + sum
|
||||||
basename := filepath.Base(packagePath)
|
if sha, ok := sums.Files[filename]; !ok {
|
||||||
if sha, ok := sums.Files[basename]; !ok {
|
return ver, fmt.Errorf("provenance does not contain a SHA for a file named %q", filename)
|
||||||
return ver, fmt.Errorf("provenance does not contain a SHA for a file named %q", basename)
|
|
||||||
} else if sha != sum {
|
} 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.FileHash = sum
|
||||||
ver.FileName = basename
|
ver.FileName = filename
|
||||||
|
|
||||||
// TODO: when image signing is added, verify that here.
|
// TODO: when image signing is added, verify that here.
|
||||||
|
|
||||||
return ver, nil
|
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.
|
// verifySignature verifies that the given block is validly signed, and returns the signer.
|
||||||
func (s *Signatory) verifySignature(block *clearsign.Block) (*openpgp.Entity, error) {
|
func (s *Signatory) verifySignature(block *clearsign.Block) (*openpgp.Entity, error) {
|
||||||
return openpgp.CheckDetachedSignature(
|
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
|
// messageBlock creates a message block from archive data and pre-marshalled metadata
|
||||||
func messageBlock(packagePath string, metadataBytes []byte) (*bytes.Buffer, error) {
|
func messageBlock(archiveData []byte, filename string, metadataBytes []byte) (*bytes.Buffer, error) {
|
||||||
// Checksum the archive
|
// Checksum the archive data
|
||||||
chash, err := DigestFile(packagePath)
|
chash, err := Digest(bytes.NewBuffer(archiveData))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
base := filepath.Base(packagePath)
|
|
||||||
sums := &SumCollection{
|
sums := &SumCollection{
|
||||||
Files: map[string]string{
|
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) {
|
func TestMessageBlock(t *testing.T) {
|
||||||
metadataBytes := loadChartMetadataForSigning(t, testChartfile)
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -243,7 +249,13 @@ func TestClearSign(t *testing.T) {
|
||||||
|
|
||||||
metadataBytes := loadChartMetadataForSigning(t, testChartfile)
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -276,7 +288,13 @@ func TestClearSignError(t *testing.T) {
|
||||||
|
|
||||||
metadataBytes := loadChartMetadataForSigning(t, testChartfile)
|
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 {
|
if err == nil {
|
||||||
t.Fatal("didn't get an error from ClearSign but expected one")
|
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.
|
|
||||||
|
|
||||||
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)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
by, err := signer.verifySignature(sig2)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := by.Identities[testKeyName]; !ok {
|
|
||||||
t.Errorf("Expected identity %q", testKeyName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVerify(t *testing.T) {
|
func TestVerify(t *testing.T) {
|
||||||
signer, err := NewFromFiles(testKeyfile, testPubfile)
|
signer, err := NewFromFiles(testKeyfile, testPubfile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ver, err := signer.Verify(testChartfile, testSigBlock); err != nil {
|
// Read the chart file data
|
||||||
|
archiveData, err := os.ReadFile(testChartfile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the signature file data
|
||||||
|
sigData, err := os.ReadFile(testSigBlock)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ver, err := signer.Verify(archiveData, sigData, filepath.Base(testChartfile)); err != nil {
|
||||||
t.Errorf("Failed to pass verify. Err: %s", err)
|
t.Errorf("Failed to pass verify. Err: %s", err)
|
||||||
} else if len(ver.FileHash) == 0 {
|
} else if len(ver.FileHash) == 0 {
|
||||||
t.Error("Verification is missing hash.")
|
t.Error("Verification is missing hash.")
|
||||||
|
@ -345,7 +332,13 @@ func TestVerify(t *testing.T) {
|
||||||
t.Errorf("FileName is unexpectedly %q", ver.FileName)
|
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)
|
t.Errorf("Expected %s to fail.", testTamperedSigBlock)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue