mirror of https://github.com/helm/helm.git
				
				
				
			
		
			
				
	
	
		
			179 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			179 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			Go
		
	
	
	
| /*
 | |
| Copyright The Helm Authors.
 | |
| Licensed under the Apache License, Version 2.0 (the "License");
 | |
| you may not use this file except in compliance with the License.
 | |
| You may obtain a copy of the License at
 | |
| 
 | |
| http://www.apache.org/licenses/LICENSE-2.0
 | |
| 
 | |
| Unless required by applicable law or agreed to in writing, software
 | |
| distributed under the License is distributed on an "AS IS" BASIS,
 | |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| See the License for the specific language governing permissions and
 | |
| limitations under the License.
 | |
| */
 | |
| 
 | |
| package plugin
 | |
| 
 | |
| import (
 | |
| 	"crypto/sha256"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"os"
 | |
| 	"path/filepath"
 | |
| 	"strings"
 | |
| 
 | |
| 	"golang.org/x/crypto/openpgp/clearsign" //nolint
 | |
| 
 | |
| 	"helm.sh/helm/v4/pkg/helmpath"
 | |
| )
 | |
| 
 | |
| // SigningInfo contains information about a plugin's signing status
 | |
| type SigningInfo struct {
 | |
| 	// Status can be:
 | |
| 	// - "local dev": Plugin is a symlink (development mode)
 | |
| 	// - "unsigned": No provenance file found
 | |
| 	// - "invalid provenance": Provenance file is malformed
 | |
| 	// - "mismatched provenance": Provenance file does not match the installed tarball
 | |
| 	// - "signed": Valid signature exists for the installed tarball
 | |
| 	Status   string
 | |
| 	IsSigned bool // True if plugin has a valid signature (even if not verified against keyring)
 | |
| }
 | |
| 
 | |
| // GetPluginSigningInfo returns signing information for an installed plugin
 | |
| func GetPluginSigningInfo(metadata Metadata) (*SigningInfo, error) {
 | |
| 	pluginName := metadata.Name
 | |
| 	pluginDir := helmpath.DataPath("plugins", pluginName)
 | |
| 
 | |
| 	// Check if plugin directory exists
 | |
| 	fi, err := os.Lstat(pluginDir)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("plugin %s not found: %w", pluginName, err)
 | |
| 	}
 | |
| 
 | |
| 	// Check if it's a symlink (local development)
 | |
| 	if fi.Mode()&os.ModeSymlink != 0 {
 | |
| 		return &SigningInfo{
 | |
| 			Status:   "local dev",
 | |
| 			IsSigned: false,
 | |
| 		}, nil
 | |
| 	}
 | |
| 
 | |
| 	// Find the exact tarball file for this plugin
 | |
| 	pluginsDir := helmpath.DataPath("plugins")
 | |
| 	tarballPath := filepath.Join(pluginsDir, fmt.Sprintf("%s-%s.tgz", metadata.Name, metadata.Version))
 | |
| 	if _, err := os.Stat(tarballPath); err != nil {
 | |
| 		return &SigningInfo{
 | |
| 			Status:   "unsigned",
 | |
| 			IsSigned: false,
 | |
| 		}, nil
 | |
| 	}
 | |
| 
 | |
| 	// Check for .prov file associated with the tarball
 | |
| 	provFile := tarballPath + ".prov"
 | |
| 	provData, err := os.ReadFile(provFile)
 | |
| 	if err != nil {
 | |
| 		if os.IsNotExist(err) {
 | |
| 			return &SigningInfo{
 | |
| 				Status:   "unsigned",
 | |
| 				IsSigned: false,
 | |
| 			}, nil
 | |
| 		}
 | |
| 		return nil, fmt.Errorf("failed to read provenance file: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// Parse the provenance file to check validity
 | |
| 	block, _ := clearsign.Decode(provData)
 | |
| 	if block == nil {
 | |
| 		return &SigningInfo{
 | |
| 			Status:   "invalid provenance",
 | |
| 			IsSigned: false,
 | |
| 		}, nil
 | |
| 	}
 | |
| 
 | |
| 	// Check if provenance matches the actual tarball
 | |
| 	blockContent := string(block.Plaintext)
 | |
| 	if !validateProvenanceHash(blockContent, tarballPath) {
 | |
| 		return &SigningInfo{
 | |
| 			Status:   "mismatched provenance",
 | |
| 			IsSigned: false,
 | |
| 		}, nil
 | |
| 	}
 | |
| 
 | |
| 	// We have a provenance file that is valid for this plugin
 | |
| 	// Without a keyring, we can't verify the signature, but we know:
 | |
| 	// 1. A .prov file exists
 | |
| 	// 2. It's a valid clearsigned document (cryptographically signed)
 | |
| 	// 3. The provenance contains valid checksums
 | |
| 	return &SigningInfo{
 | |
| 		Status:   "signed",
 | |
| 		IsSigned: true,
 | |
| 	}, nil
 | |
| }
 | |
| 
 | |
| func validateProvenanceHash(blockContent string, tarballPath string) bool {
 | |
| 	// Parse provenance to get the expected hash
 | |
| 	_, sums, err := parsePluginMessageBlock([]byte(blockContent))
 | |
| 	if err != nil {
 | |
| 		return false
 | |
| 	}
 | |
| 
 | |
| 	// Must have file checksums
 | |
| 	if len(sums.Files) == 0 {
 | |
| 		return false
 | |
| 	}
 | |
| 
 | |
| 	// Calculate actual hash of the tarball
 | |
| 	actualHash, err := calculateFileHash(tarballPath)
 | |
| 	if err != nil {
 | |
| 		return false
 | |
| 	}
 | |
| 
 | |
| 	// Check if the actual hash matches the expected hash in the provenance
 | |
| 	for filename, expectedHash := range sums.Files {
 | |
| 		if strings.Contains(filename, filepath.Base(tarballPath)) && expectedHash == actualHash {
 | |
| 			return true
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| // calculateFileHash calculates the SHA256 hash of a file
 | |
| func calculateFileHash(filePath string) (string, error) {
 | |
| 	file, err := os.Open(filePath)
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 	defer file.Close()
 | |
| 
 | |
| 	hasher := sha256.New()
 | |
| 	if _, err := io.Copy(hasher, file); err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 
 | |
| 	return fmt.Sprintf("sha256:%x", hasher.Sum(nil)), nil
 | |
| }
 | |
| 
 | |
| // GetSigningInfoForPlugins returns signing info for multiple plugins
 | |
| func GetSigningInfoForPlugins(plugins []Plugin) map[string]*SigningInfo {
 | |
| 	result := make(map[string]*SigningInfo)
 | |
| 
 | |
| 	for _, p := range plugins {
 | |
| 		m := p.Metadata()
 | |
| 
 | |
| 		info, err := GetPluginSigningInfo(m)
 | |
| 		if err != nil {
 | |
| 			// If there's an error, treat as unsigned
 | |
| 			result[m.Name] = &SigningInfo{
 | |
| 				Status:   "unknown",
 | |
| 				IsSigned: false,
 | |
| 			}
 | |
| 		} else {
 | |
| 			result[m.Name] = info
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return result
 | |
| }
 |