...

Source file src/helm.sh/helm/v3/pkg/provenance/sign_test.go

Documentation: helm.sh/helm/v3/pkg/provenance

     1  /*
     2  Copyright The Helm Authors.
     3  Licensed under the Apache License, Version 2.0 (the "License");
     4  you may not use this file except in compliance with the License.
     5  You may obtain a copy of the License at
     6  
     7  http://www.apache.org/licenses/LICENSE-2.0
     8  
     9  Unless required by applicable law or agreed to in writing, software
    10  distributed under the License is distributed on an "AS IS" BASIS,
    11  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  See the License for the specific language governing permissions and
    13  limitations under the License.
    14  */
    15  
    16  package provenance
    17  
    18  import (
    19  	"crypto"
    20  	"fmt"
    21  	"io"
    22  	"os"
    23  	"path/filepath"
    24  	"strings"
    25  	"testing"
    26  
    27  	pgperrors "golang.org/x/crypto/openpgp/errors" //nolint
    28  )
    29  
    30  const (
    31  	// testKeyFile is the secret key.
    32  	// Generating keys should be done with `gpg --gen-key`. The current key
    33  	// was generated to match Go's defaults (RSA/RSA 2048). It has no pass
    34  	// phrase. Use `gpg --export-secret-keys helm-test` to export the secret.
    35  	testKeyfile = "testdata/helm-test-key.secret"
    36  
    37  	// testPasswordKeyFile is a keyfile with a password.
    38  	testPasswordKeyfile = "testdata/helm-password-key.secret"
    39  
    40  	// testPubfile is the public key file.
    41  	// Use `gpg --export helm-test` to export the public key.
    42  	testPubfile = "testdata/helm-test-key.pub"
    43  
    44  	// Generated name for the PGP key in testKeyFile.
    45  	testKeyName = `Helm Testing (This key should only be used for testing. DO NOT TRUST.) <helm-testing@helm.sh>`
    46  
    47  	testPasswordKeyName = `password key (fake) <fake@helm.sh>`
    48  
    49  	testChartfile = "testdata/hashtest-1.2.3.tgz"
    50  
    51  	// testSigBlock points to a signature generated by an external tool.
    52  	// This file was generated with GnuPG:
    53  	// gpg --clearsign -u helm-test --openpgp testdata/msgblock.yaml
    54  	testSigBlock = "testdata/msgblock.yaml.asc"
    55  
    56  	// testTamperedSigBlock is a tampered copy of msgblock.yaml.asc
    57  	testTamperedSigBlock = "testdata/msgblock.yaml.tampered"
    58  
    59  	// testSumfile points to a SHA256 sum generated by an external tool.
    60  	// We always want to validate against an external tool's representation to
    61  	// verify that we haven't done something stupid. This file was generated
    62  	// with shasum.
    63  	// shasum -a 256 hashtest-1.2.3.tgz > testdata/hashtest.sha256
    64  	testSumfile = "testdata/hashtest.sha256"
    65  )
    66  
    67  // testMessageBlock represents the expected message block for the testdata/hashtest chart.
    68  const testMessageBlock = `apiVersion: v1
    69  description: Test chart versioning
    70  name: hashtest
    71  version: 1.2.3
    72  
    73  ...
    74  files:
    75    hashtest-1.2.3.tgz: sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888
    76  `
    77  
    78  func TestMessageBlock(t *testing.T) {
    79  	out, err := messageBlock(testChartfile)
    80  	if err != nil {
    81  		t.Fatal(err)
    82  	}
    83  	got := out.String()
    84  
    85  	if got != testMessageBlock {
    86  		t.Errorf("Expected:\n%q\nGot\n%q\n", testMessageBlock, got)
    87  	}
    88  }
    89  
    90  func TestParseMessageBlock(t *testing.T) {
    91  	md, sc, err := parseMessageBlock([]byte(testMessageBlock))
    92  	if err != nil {
    93  		t.Fatal(err)
    94  	}
    95  
    96  	if md.Name != "hashtest" {
    97  		t.Errorf("Expected name %q, got %q", "hashtest", md.Name)
    98  	}
    99  
   100  	if lsc := len(sc.Files); lsc != 1 {
   101  		t.Errorf("Expected 1 file, got %d", lsc)
   102  	}
   103  
   104  	if hash, ok := sc.Files["hashtest-1.2.3.tgz"]; !ok {
   105  		t.Errorf("hashtest file not found in Files")
   106  	} else if hash != "sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888" {
   107  		t.Errorf("Unexpected hash: %q", hash)
   108  	}
   109  }
   110  
   111  func TestLoadKey(t *testing.T) {
   112  	k, err := loadKey(testKeyfile)
   113  	if err != nil {
   114  		t.Fatal(err)
   115  	}
   116  
   117  	if _, ok := k.Identities[testKeyName]; !ok {
   118  		t.Errorf("Expected to load a key for user %q", testKeyName)
   119  	}
   120  }
   121  
   122  func TestLoadKeyRing(t *testing.T) {
   123  	k, err := loadKeyRing(testPubfile)
   124  	if err != nil {
   125  		t.Fatal(err)
   126  	}
   127  
   128  	if len(k) > 1 {
   129  		t.Errorf("Expected 1, got %d", len(k))
   130  	}
   131  
   132  	for _, e := range k {
   133  		if ii, ok := e.Identities[testKeyName]; !ok {
   134  			t.Errorf("Expected %s in %v", testKeyName, ii)
   135  		}
   136  	}
   137  }
   138  
   139  func TestDigest(t *testing.T) {
   140  	f, err := os.Open(testChartfile)
   141  	if err != nil {
   142  		t.Fatal(err)
   143  	}
   144  	defer f.Close()
   145  
   146  	hash, err := Digest(f)
   147  	if err != nil {
   148  		t.Fatal(err)
   149  	}
   150  
   151  	sig, err := readSumFile(testSumfile)
   152  	if err != nil {
   153  		t.Fatal(err)
   154  	}
   155  
   156  	if !strings.Contains(sig, hash) {
   157  		t.Errorf("Expected %s to be in %s", hash, sig)
   158  	}
   159  }
   160  
   161  func TestNewFromFiles(t *testing.T) {
   162  	s, err := NewFromFiles(testKeyfile, testPubfile)
   163  	if err != nil {
   164  		t.Fatal(err)
   165  	}
   166  
   167  	if _, ok := s.Entity.Identities[testKeyName]; !ok {
   168  		t.Errorf("Expected to load a key for user %q", testKeyName)
   169  	}
   170  }
   171  
   172  func TestDigestFile(t *testing.T) {
   173  	hash, err := DigestFile(testChartfile)
   174  	if err != nil {
   175  		t.Fatal(err)
   176  	}
   177  
   178  	sig, err := readSumFile(testSumfile)
   179  	if err != nil {
   180  		t.Fatal(err)
   181  	}
   182  
   183  	if !strings.Contains(sig, hash) {
   184  		t.Errorf("Expected %s to be in %s", hash, sig)
   185  	}
   186  }
   187  
   188  func TestDecryptKey(t *testing.T) {
   189  	k, err := NewFromKeyring(testPasswordKeyfile, testPasswordKeyName)
   190  	if err != nil {
   191  		t.Fatal(err)
   192  	}
   193  
   194  	if !k.Entity.PrivateKey.Encrypted {
   195  		t.Fatal("Key is not encrypted")
   196  	}
   197  
   198  	// We give this a simple callback that returns the password.
   199  	if err := k.DecryptKey(func(_ string) ([]byte, error) {
   200  		return []byte("secret"), nil
   201  	}); err != nil {
   202  		t.Fatal(err)
   203  	}
   204  
   205  	// Re-read the key (since we already unlocked it)
   206  	k, err = NewFromKeyring(testPasswordKeyfile, testPasswordKeyName)
   207  	if err != nil {
   208  		t.Fatal(err)
   209  	}
   210  	// Now we give it a bogus password.
   211  	if err := k.DecryptKey(func(_ string) ([]byte, error) {
   212  		return []byte("secrets_and_lies"), nil
   213  	}); err == nil {
   214  		t.Fatal("Expected an error when giving a bogus passphrase")
   215  	}
   216  }
   217  
   218  func TestClearSign(t *testing.T) {
   219  	signer, err := NewFromFiles(testKeyfile, testPubfile)
   220  	if err != nil {
   221  		t.Fatal(err)
   222  	}
   223  
   224  	sig, err := signer.ClearSign(testChartfile)
   225  	if err != nil {
   226  		t.Fatal(err)
   227  	}
   228  	t.Logf("Sig:\n%s", sig)
   229  
   230  	if !strings.Contains(sig, testMessageBlock) {
   231  		t.Errorf("expected message block to be in sig: %s", sig)
   232  	}
   233  }
   234  
   235  // failSigner always fails to sign and returns an error
   236  type failSigner struct{}
   237  
   238  func (s failSigner) Public() crypto.PublicKey {
   239  	return nil
   240  }
   241  
   242  func (s failSigner) Sign(_ io.Reader, _ []byte, _ crypto.SignerOpts) ([]byte, error) {
   243  	return nil, fmt.Errorf("always fails")
   244  }
   245  
   246  func TestClearSignError(t *testing.T) {
   247  	signer, err := NewFromFiles(testKeyfile, testPubfile)
   248  	if err != nil {
   249  		t.Fatal(err)
   250  	}
   251  
   252  	// ensure that signing always fails
   253  	signer.Entity.PrivateKey.PrivateKey = failSigner{}
   254  
   255  	sig, err := signer.ClearSign(testChartfile)
   256  	if err == nil {
   257  		t.Fatal("didn't get an error from ClearSign but expected one")
   258  	}
   259  
   260  	if sig != "" {
   261  		t.Fatalf("expected an empty signature after failed ClearSign but got %q", sig)
   262  	}
   263  }
   264  
   265  func TestDecodeSignature(t *testing.T) {
   266  	// Unlike other tests, this does a round-trip test, ensuring that a signature
   267  	// generated by the library can also be verified by the library.
   268  
   269  	signer, err := NewFromFiles(testKeyfile, testPubfile)
   270  	if err != nil {
   271  		t.Fatal(err)
   272  	}
   273  
   274  	sig, err := signer.ClearSign(testChartfile)
   275  	if err != nil {
   276  		t.Fatal(err)
   277  	}
   278  
   279  	f, err := os.CreateTemp("", "helm-test-sig-")
   280  	if err != nil {
   281  		t.Fatal(err)
   282  	}
   283  
   284  	tname := f.Name()
   285  	defer func() {
   286  		os.Remove(tname)
   287  	}()
   288  	f.WriteString(sig)
   289  	f.Close()
   290  
   291  	sig2, err := signer.decodeSignature(tname)
   292  	if err != nil {
   293  		t.Fatal(err)
   294  	}
   295  
   296  	by, err := signer.verifySignature(sig2)
   297  	if err != nil {
   298  		t.Fatal(err)
   299  	}
   300  
   301  	if _, ok := by.Identities[testKeyName]; !ok {
   302  		t.Errorf("Expected identity %q", testKeyName)
   303  	}
   304  }
   305  
   306  func TestVerify(t *testing.T) {
   307  	signer, err := NewFromFiles(testKeyfile, testPubfile)
   308  	if err != nil {
   309  		t.Fatal(err)
   310  	}
   311  
   312  	if ver, err := signer.Verify(testChartfile, testSigBlock); err != nil {
   313  		t.Errorf("Failed to pass verify. Err: %s", err)
   314  	} else if len(ver.FileHash) == 0 {
   315  		t.Error("Verification is missing hash.")
   316  	} else if ver.SignedBy == nil {
   317  		t.Error("No SignedBy field")
   318  	} else if ver.FileName != filepath.Base(testChartfile) {
   319  		t.Errorf("FileName is unexpectedly %q", ver.FileName)
   320  	}
   321  
   322  	if _, err = signer.Verify(testChartfile, testTamperedSigBlock); err == nil {
   323  		t.Errorf("Expected %s to fail.", testTamperedSigBlock)
   324  	}
   325  
   326  	switch err.(type) {
   327  	case pgperrors.SignatureError:
   328  		t.Logf("Tampered sig block error: %s (%T)", err, err)
   329  	default:
   330  		t.Errorf("Expected invalid signature error, got %q (%T)", err, err)
   331  	}
   332  }
   333  
   334  // readSumFile reads a file containing a sum generated by the UNIX shasum tool.
   335  func readSumFile(sumfile string) (string, error) {
   336  	data, err := os.ReadFile(sumfile)
   337  	if err != nil {
   338  		return "", err
   339  	}
   340  
   341  	sig := string(data)
   342  	parts := strings.SplitN(sig, " ", 2)
   343  	return parts[0], nil
   344  }
   345  

View as plain text