github.com/stefanmcshane/helm@v0.0.0-20221213002717-88a4a2c6e77d/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" //nolint 30 "golang.org/x/crypto/openpgp/clearsign" //nolint 31 "golang.org/x/crypto/openpgp/packet" //nolint 32 "sigs.k8s.io/yaml" 33 34 hapi "github.com/stefanmcshane/helm/pkg/chart" 35 "github.com/stefanmcshane/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. Try providing a keyring with secret keys") 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. Try providing a keyring with secret keys") 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 "", err 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 228 _, err = io.Copy(w, b) 229 230 if err != nil { 231 // NB: We intentionally don't call `w.Close()` here! `w.Close()` is the method which 232 // actually does the PGP signing, and therefore is the part which uses the private key. 233 // In other words, if we call Close here, there's a risk that there's an attempt to use the 234 // private key to sign garbage data (since we know that io.Copy failed, `w` won't contain 235 // anything useful). 236 return "", errors.Wrap(err, "failed to write to clearsign encoder") 237 } 238 239 err = w.Close() 240 if err != nil { 241 return "", errors.Wrap(err, "failed to either sign or armor message block") 242 } 243 244 return out.String(), nil 245 } 246 247 // Verify checks a signature and verifies that it is legit for a chart. 248 func (s *Signatory) Verify(chartpath, sigpath string) (*Verification, error) { 249 ver := &Verification{} 250 for _, fname := range []string{chartpath, sigpath} { 251 if fi, err := os.Stat(fname); err != nil { 252 return ver, err 253 } else if fi.IsDir() { 254 return ver, errors.Errorf("%s cannot be a directory", fname) 255 } 256 } 257 258 // First verify the signature 259 sig, err := s.decodeSignature(sigpath) 260 if err != nil { 261 return ver, errors.Wrap(err, "failed to decode signature") 262 } 263 264 by, err := s.verifySignature(sig) 265 if err != nil { 266 return ver, err 267 } 268 ver.SignedBy = by 269 270 // Second, verify the hash of the tarball. 271 sum, err := DigestFile(chartpath) 272 if err != nil { 273 return ver, err 274 } 275 _, sums, err := parseMessageBlock(sig.Plaintext) 276 if err != nil { 277 return ver, err 278 } 279 280 sum = "sha256:" + sum 281 basename := filepath.Base(chartpath) 282 if sha, ok := sums.Files[basename]; !ok { 283 return ver, errors.Errorf("provenance does not contain a SHA for a file named %q", basename) 284 } else if sha != sum { 285 return ver, errors.Errorf("sha256 sum does not match for %s: %q != %q", basename, sha, sum) 286 } 287 ver.FileHash = sum 288 ver.FileName = basename 289 290 // TODO: when image signing is added, verify that here. 291 292 return ver, nil 293 } 294 295 func (s *Signatory) decodeSignature(filename string) (*clearsign.Block, error) { 296 data, err := ioutil.ReadFile(filename) 297 if err != nil { 298 return nil, err 299 } 300 301 block, _ := clearsign.Decode(data) 302 if block == nil { 303 // There was no sig in the file. 304 return nil, errors.New("signature block not found") 305 } 306 307 return block, nil 308 } 309 310 // verifySignature verifies that the given block is validly signed, and returns the signer. 311 func (s *Signatory) verifySignature(block *clearsign.Block) (*openpgp.Entity, error) { 312 return openpgp.CheckDetachedSignature( 313 s.KeyRing, 314 bytes.NewBuffer(block.Bytes), 315 block.ArmoredSignature.Body, 316 ) 317 } 318 319 func messageBlock(chartpath string) (*bytes.Buffer, error) { 320 var b *bytes.Buffer 321 // Checksum the archive 322 chash, err := DigestFile(chartpath) 323 if err != nil { 324 return b, err 325 } 326 327 base := filepath.Base(chartpath) 328 sums := &SumCollection{ 329 Files: map[string]string{ 330 base: "sha256:" + chash, 331 }, 332 } 333 334 // Load the archive into memory. 335 chart, err := loader.LoadFile(chartpath) 336 if err != nil { 337 return b, err 338 } 339 340 // Buffer a hash + checksums YAML file 341 data, err := yaml.Marshal(chart.Metadata) 342 if err != nil { 343 return b, err 344 } 345 346 // FIXME: YAML uses ---\n as a file start indicator, but this is not legal in a PGP 347 // clearsign block. So we use ...\n, which is the YAML document end marker. 348 // http://yaml.org/spec/1.2/spec.html#id2800168 349 b = bytes.NewBuffer(data) 350 b.WriteString("\n...\n") 351 352 data, err = yaml.Marshal(sums) 353 if err != nil { 354 return b, err 355 } 356 b.Write(data) 357 358 return b, nil 359 } 360 361 // parseMessageBlock 362 func parseMessageBlock(data []byte) (*hapi.Metadata, *SumCollection, error) { 363 // This sucks. 364 parts := bytes.Split(data, []byte("\n...\n")) 365 if len(parts) < 2 { 366 return nil, nil, errors.New("message block must have at least two parts") 367 } 368 369 md := &hapi.Metadata{} 370 sc := &SumCollection{} 371 372 if err := yaml.Unmarshal(parts[0], md); err != nil { 373 return md, sc, err 374 } 375 err := yaml.Unmarshal(parts[1], sc) 376 return md, sc, err 377 } 378 379 // loadKey loads a GPG key found at a particular path. 380 func loadKey(keypath string) (*openpgp.Entity, error) { 381 f, err := os.Open(keypath) 382 if err != nil { 383 return nil, err 384 } 385 defer f.Close() 386 387 pr := packet.NewReader(f) 388 return openpgp.ReadEntity(pr) 389 } 390 391 func loadKeyRing(ringpath string) (openpgp.EntityList, error) { 392 f, err := os.Open(ringpath) 393 if err != nil { 394 return nil, err 395 } 396 defer f.Close() 397 return openpgp.ReadKeyRing(f) 398 } 399 400 // DigestFile calculates a SHA256 hash (like Docker) for a given file. 401 // 402 // It takes the path to the archive file, and returns a string representation of 403 // the SHA256 sum. 404 // 405 // The intended use of this function is to generate a sum of a chart TGZ file. 406 func DigestFile(filename string) (string, error) { 407 f, err := os.Open(filename) 408 if err != nil { 409 return "", err 410 } 411 defer f.Close() 412 return Digest(f) 413 } 414 415 // Digest hashes a reader and returns a SHA256 digest. 416 // 417 // Helm uses SHA256 as its default hash for all non-cryptographic applications. 418 func Digest(in io.Reader) (string, error) { 419 hash := crypto.SHA256.New() 420 if _, err := io.Copy(hash, in); err != nil { 421 return "", nil 422 } 423 return hex.EncodeToString(hash.Sum(nil)), nil 424 }