github.com/stefanmcshane/helm@v0.0.0-20221213002717-88a4a2c6e77d/pkg/provenance/sign_test.go (about)

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