github.com/sri09kanth/helm@v3.0.0-beta.3+incompatible/pkg/provenance/sign.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 "bytes" 20 "crypto" 21 "encoding/hex" 22 "io" 23 "io/ioutil" 24 "os" 25 "path/filepath" 26 "strings" 27 28 "github.com/pkg/errors" 29 "golang.org/x/crypto/openpgp" 30 "golang.org/x/crypto/openpgp/clearsign" 31 "golang.org/x/crypto/openpgp/packet" 32 "sigs.k8s.io/yaml" 33 34 hapi "helm.sh/helm/pkg/chart" 35 "helm.sh/helm/pkg/chart/loader" 36 ) 37 38 var defaultPGPConfig = packet.Config{ 39 DefaultHash: crypto.SHA512, 40 } 41 42 // SumCollection represents a collection of file and image checksums. 43 // 44 // Files are of the form: 45 // FILENAME: "sha256:SUM" 46 // Images are of the form: 47 // "IMAGE:TAG": "sha256:SUM" 48 // Docker optionally supports sha512, and if this is the case, the hash marker 49 // will be 'sha512' instead of 'sha256'. 50 type SumCollection struct { 51 Files map[string]string `json:"files"` 52 Images map[string]string `json:"images,omitempty"` 53 } 54 55 // Verification contains information about a verification operation. 56 type Verification struct { 57 // SignedBy contains the entity that signed a chart. 58 SignedBy *openpgp.Entity 59 // FileHash is the hash, prepended with the scheme, for the file that was verified. 60 FileHash string 61 // FileName is the name of the file that FileHash verifies. 62 FileName string 63 } 64 65 // Signatory signs things. 66 // 67 // Signatories can be constructed from a PGP private key file using NewFromFiles 68 // or they can be constructed manually by setting the Entity to a valid 69 // PGP entity. 70 // 71 // The same Signatory can be used to sign or validate multiple charts. 72 type Signatory struct { 73 // The signatory for this instance of Helm. This is used for signing. 74 Entity *openpgp.Entity 75 // The keyring for this instance of Helm. This is used for verification. 76 KeyRing openpgp.EntityList 77 } 78 79 // NewFromFiles constructs a new Signatory from the PGP key in the given filename. 80 // 81 // This will emit an error if it cannot find a valid GPG keyfile (entity) at the 82 // given location. 83 // 84 // Note that the keyfile may have just a public key, just a private key, or 85 // both. The Signatory methods may have different requirements of the keys. For 86 // example, ClearSign must have a valid `openpgp.Entity.PrivateKey` before it 87 // can sign something. 88 func NewFromFiles(keyfile, keyringfile string) (*Signatory, error) { 89 e, err := loadKey(keyfile) 90 if err != nil { 91 return nil, err 92 } 93 94 ring, err := loadKeyRing(keyringfile) 95 if err != nil { 96 return nil, err 97 } 98 99 return &Signatory{ 100 Entity: e, 101 KeyRing: ring, 102 }, nil 103 } 104 105 // NewFromKeyring reads a keyring file and creates a Signatory. 106 // 107 // If id is not the empty string, this will also try to find an Entity in the 108 // keyring whose name matches, and set that as the signing entity. It will return 109 // an error if the id is not empty and also not found. 110 func NewFromKeyring(keyringfile, id string) (*Signatory, error) { 111 ring, err := loadKeyRing(keyringfile) 112 if err != nil { 113 return nil, err 114 } 115 116 s := &Signatory{KeyRing: ring} 117 118 // If the ID is empty, we can return now. 119 if id == "" { 120 return s, nil 121 } 122 123 // We're gonna go all GnuPG on this and look for a string that _contains_. If 124 // two or more keys contain the string and none are a direct match, we error 125 // out. 126 var candidate *openpgp.Entity 127 vague := false 128 for _, e := range ring { 129 for n := range e.Identities { 130 if n == id { 131 s.Entity = e 132 return s, nil 133 } 134 if strings.Contains(n, id) { 135 if candidate != nil { 136 vague = true 137 } 138 candidate = e 139 } 140 } 141 } 142 if vague { 143 return s, errors.Errorf("more than one key contain the id %q", id) 144 } 145 146 s.Entity = candidate 147 return s, nil 148 } 149 150 // PassphraseFetcher returns a passphrase for decrypting keys. 151 // 152 // This is used as a callback to read a passphrase from some other location. The 153 // given name is the Name field on the key, typically of the form: 154 // 155 // USER_NAME (COMMENT) <EMAIL> 156 type PassphraseFetcher func(name string) ([]byte, error) 157 158 // DecryptKey decrypts a private key in the Signatory. 159 // 160 // If the key is not encrypted, this will return without error. 161 // 162 // If the key does not exist, this will return an error. 163 // 164 // If the key exists, but cannot be unlocked with the passphrase returned by 165 // the PassphraseFetcher, this will return an error. 166 // 167 // If the key is successfully unlocked, it will return nil. 168 func (s *Signatory) DecryptKey(fn PassphraseFetcher) error { 169 if s.Entity == nil { 170 return errors.New("private key not found") 171 } else if s.Entity.PrivateKey == nil { 172 return errors.New("provided key is not a private key") 173 } 174 175 // Nothing else to do if key is not encrypted. 176 if !s.Entity.PrivateKey.Encrypted { 177 return nil 178 } 179 180 fname := "Unknown" 181 for i := range s.Entity.Identities { 182 if i != "" { 183 fname = i 184 break 185 } 186 } 187 188 p, err := fn(fname) 189 if err != nil { 190 return err 191 } 192 193 return s.Entity.PrivateKey.Decrypt(p) 194 } 195 196 // ClearSign signs a chart with the given key. 197 // 198 // This takes the path to a chart archive file and a key, and it returns a clear signature. 199 // 200 // The Signatory must have a valid Entity.PrivateKey for this to work. If it does 201 // not, an error will be returned. 202 func (s *Signatory) ClearSign(chartpath string) (string, error) { 203 if s.Entity == nil { 204 return "", errors.New("private key not found") 205 } else if s.Entity.PrivateKey == nil { 206 return "", errors.New("provided key is not a private key") 207 } 208 209 if fi, err := os.Stat(chartpath); err != nil { 210 return "", err 211 } else if fi.IsDir() { 212 return "", errors.New("cannot sign a directory") 213 } 214 215 out := bytes.NewBuffer(nil) 216 217 b, err := messageBlock(chartpath) 218 if err != nil { 219 return "", nil 220 } 221 222 // Sign the buffer 223 w, err := clearsign.Encode(out, s.Entity.PrivateKey, &defaultPGPConfig) 224 if err != nil { 225 return "", err 226 } 227 _, err = io.Copy(w, b) 228 w.Close() 229 return out.String(), err 230 } 231 232 // Verify checks a signature and verifies that it is legit for a chart. 233 func (s *Signatory) Verify(chartpath, sigpath string) (*Verification, error) { 234 ver := &Verification{} 235 for _, fname := range []string{chartpath, sigpath} { 236 if fi, err := os.Stat(fname); err != nil { 237 return ver, err 238 } else if fi.IsDir() { 239 return ver, errors.Errorf("%s cannot be a directory", fname) 240 } 241 } 242 243 // First verify the signature 244 sig, err := s.decodeSignature(sigpath) 245 if err != nil { 246 return ver, errors.Wrap(err, "failed to decode signature") 247 } 248 249 by, err := s.verifySignature(sig) 250 if err != nil { 251 return ver, err 252 } 253 ver.SignedBy = by 254 255 // Second, verify the hash of the tarball. 256 sum, err := DigestFile(chartpath) 257 if err != nil { 258 return ver, err 259 } 260 _, sums, err := parseMessageBlock(sig.Plaintext) 261 if err != nil { 262 return ver, err 263 } 264 265 sum = "sha256:" + sum 266 basename := filepath.Base(chartpath) 267 if sha, ok := sums.Files[basename]; !ok { 268 return ver, errors.Errorf("provenance does not contain a SHA for a file named %q", basename) 269 } else if sha != sum { 270 return ver, errors.Errorf("sha256 sum does not match for %s: %q != %q", basename, sha, sum) 271 } 272 ver.FileHash = sum 273 ver.FileName = basename 274 275 // TODO: when image signing is added, verify that here. 276 277 return ver, nil 278 } 279 280 func (s *Signatory) decodeSignature(filename string) (*clearsign.Block, error) { 281 data, err := ioutil.ReadFile(filename) 282 if err != nil { 283 return nil, err 284 } 285 286 block, _ := clearsign.Decode(data) 287 if block == nil { 288 // There was no sig in the file. 289 return nil, errors.New("signature block not found") 290 } 291 292 return block, nil 293 } 294 295 // verifySignature verifies that the given block is validly signed, and returns the signer. 296 func (s *Signatory) verifySignature(block *clearsign.Block) (*openpgp.Entity, error) { 297 return openpgp.CheckDetachedSignature( 298 s.KeyRing, 299 bytes.NewBuffer(block.Bytes), 300 block.ArmoredSignature.Body, 301 ) 302 } 303 304 func messageBlock(chartpath string) (*bytes.Buffer, error) { 305 var b *bytes.Buffer 306 // Checksum the archive 307 chash, err := DigestFile(chartpath) 308 if err != nil { 309 return b, err 310 } 311 312 base := filepath.Base(chartpath) 313 sums := &SumCollection{ 314 Files: map[string]string{ 315 base: "sha256:" + chash, 316 }, 317 } 318 319 // Load the archive into memory. 320 chart, err := loader.LoadFile(chartpath) 321 if err != nil { 322 return b, err 323 } 324 325 // Buffer a hash + checksums YAML file 326 data, err := yaml.Marshal(chart.Metadata) 327 if err != nil { 328 return b, err 329 } 330 331 // FIXME: YAML uses ---\n as a file start indicator, but this is not legal in a PGP 332 // clearsign block. So we use ...\n, which is the YAML document end marker. 333 // http://yaml.org/spec/1.2/spec.html#id2800168 334 b = bytes.NewBuffer(data) 335 b.WriteString("\n...\n") 336 337 data, err = yaml.Marshal(sums) 338 if err != nil { 339 return b, err 340 } 341 b.Write(data) 342 343 return b, nil 344 } 345 346 // parseMessageBlock 347 func parseMessageBlock(data []byte) (*hapi.Metadata, *SumCollection, error) { 348 // This sucks. 349 parts := bytes.Split(data, []byte("\n...\n")) 350 if len(parts) < 2 { 351 return nil, nil, errors.New("message block must have at least two parts") 352 } 353 354 md := &hapi.Metadata{} 355 sc := &SumCollection{} 356 357 if err := yaml.Unmarshal(parts[0], md); err != nil { 358 return md, sc, err 359 } 360 err := yaml.Unmarshal(parts[1], sc) 361 return md, sc, err 362 } 363 364 // loadKey loads a GPG key found at a particular path. 365 func loadKey(keypath string) (*openpgp.Entity, error) { 366 f, err := os.Open(keypath) 367 if err != nil { 368 return nil, err 369 } 370 defer f.Close() 371 372 pr := packet.NewReader(f) 373 return openpgp.ReadEntity(pr) 374 } 375 376 func loadKeyRing(ringpath string) (openpgp.EntityList, error) { 377 f, err := os.Open(ringpath) 378 if err != nil { 379 return nil, err 380 } 381 defer f.Close() 382 return openpgp.ReadKeyRing(f) 383 } 384 385 // DigestFile calculates a SHA256 hash (like Docker) for a given file. 386 // 387 // It takes the path to the archive file, and returns a string representation of 388 // the SHA256 sum. 389 // 390 // The intended use of this function is to generate a sum of a chart TGZ file. 391 func DigestFile(filename string) (string, error) { 392 f, err := os.Open(filename) 393 if err != nil { 394 return "", err 395 } 396 defer f.Close() 397 return Digest(f) 398 } 399 400 // Digest hashes a reader and returns a SHA256 digest. 401 // 402 // Helm uses SHA256 as its default hash for all non-cryptographic applications. 403 func Digest(in io.Reader) (string, error) { 404 hash := crypto.SHA256.New() 405 io.Copy(hash, in) 406 return hex.EncodeToString(hash.Sum(nil)), nil 407 }