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