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 }