github.com/danielqsj/helm@v2.0.0-alpha.4.0.20160908204436-976e0ba5199b+incompatible/pkg/provenance/sign.go (about) 1 /* 2 Copyright 2016 The Kubernetes Authors All rights reserved. 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 } 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, fmt.Errorf("more than one key contain the id %q", id) 144 } 145 s.Entity = candidate 146 return s, nil 147 } 148 149 // ClearSign signs a chart with the given key. 150 // 151 // This takes the path to a chart archive file and a key, and it returns a clear signature. 152 // 153 // The Signatory must have a valid Entity.PrivateKey for this to work. If it does 154 // not, an error will be returned. 155 func (s *Signatory) ClearSign(chartpath string) (string, error) { 156 if s.Entity.PrivateKey == nil { 157 return "", errors.New("private key not found") 158 } 159 160 if fi, err := os.Stat(chartpath); err != nil { 161 return "", err 162 } else if fi.IsDir() { 163 return "", errors.New("cannot sign a directory") 164 } 165 166 out := bytes.NewBuffer(nil) 167 168 b, err := messageBlock(chartpath) 169 if err != nil { 170 return "", nil 171 } 172 173 // Sign the buffer 174 w, err := clearsign.Encode(out, s.Entity.PrivateKey, &defaultPGPConfig) 175 if err != nil { 176 return "", err 177 } 178 _, err = io.Copy(w, b) 179 w.Close() 180 return out.String(), err 181 } 182 183 // Verify checks a signature and verifies that it is legit for a chart. 184 func (s *Signatory) Verify(chartpath, sigpath string) (*Verification, error) { 185 ver := &Verification{} 186 for _, fname := range []string{chartpath, sigpath} { 187 if fi, err := os.Stat(fname); err != nil { 188 return ver, err 189 } else if fi.IsDir() { 190 return ver, fmt.Errorf("%s cannot be a directory", fname) 191 } 192 } 193 194 // First verify the signature 195 sig, err := s.decodeSignature(sigpath) 196 if err != nil { 197 return ver, fmt.Errorf("failed to decode signature: %s", err) 198 } 199 200 by, err := s.verifySignature(sig) 201 if err != nil { 202 return ver, err 203 } 204 ver.SignedBy = by 205 206 // Second, verify the hash of the tarball. 207 sum, err := sumArchive(chartpath) 208 if err != nil { 209 return ver, err 210 } 211 _, sums, err := parseMessageBlock(sig.Plaintext) 212 if err != nil { 213 return ver, err 214 } 215 216 sum = "sha256:" + sum 217 basename := filepath.Base(chartpath) 218 if sha, ok := sums.Files[basename]; !ok { 219 return ver, fmt.Errorf("provenance does not contain a SHA for a file named %q", basename) 220 } else if sha != sum { 221 return ver, fmt.Errorf("sha256 sum does not match for %s: %q != %q", basename, sha, sum) 222 } 223 ver.FileHash = sum 224 225 // TODO: when image signing is added, verify that here. 226 227 return ver, nil 228 } 229 230 func (s *Signatory) decodeSignature(filename string) (*clearsign.Block, error) { 231 data, err := ioutil.ReadFile(filename) 232 if err != nil { 233 return nil, err 234 } 235 236 block, _ := clearsign.Decode(data) 237 if block == nil { 238 // There was no sig in the file. 239 return nil, errors.New("signature block not found") 240 } 241 242 return block, nil 243 } 244 245 // verifySignature verifies that the given block is validly signed, and returns the signer. 246 func (s *Signatory) verifySignature(block *clearsign.Block) (*openpgp.Entity, error) { 247 return openpgp.CheckDetachedSignature( 248 s.KeyRing, 249 bytes.NewBuffer(block.Bytes), 250 block.ArmoredSignature.Body, 251 ) 252 } 253 254 func messageBlock(chartpath string) (*bytes.Buffer, error) { 255 var b *bytes.Buffer 256 // Checksum the archive 257 chash, err := sumArchive(chartpath) 258 if err != nil { 259 return b, err 260 } 261 262 base := filepath.Base(chartpath) 263 sums := &SumCollection{ 264 Files: map[string]string{ 265 base: "sha256:" + chash, 266 }, 267 } 268 269 // Load the archive into memory. 270 chart, err := chartutil.LoadFile(chartpath) 271 if err != nil { 272 return b, err 273 } 274 275 // Buffer a hash + checksums YAML file 276 data, err := yaml.Marshal(chart.Metadata) 277 if err != nil { 278 return b, err 279 } 280 281 // FIXME: YAML uses ---\n as a file start indicator, but this is not legal in a PGP 282 // clearsign block. So we use ...\n, which is the YAML document end marker. 283 // http://yaml.org/spec/1.2/spec.html#id2800168 284 b = bytes.NewBuffer(data) 285 b.WriteString("\n...\n") 286 287 data, err = yaml.Marshal(sums) 288 if err != nil { 289 return b, err 290 } 291 b.Write(data) 292 293 return b, nil 294 } 295 296 // parseMessageBlock 297 func parseMessageBlock(data []byte) (*hapi.Metadata, *SumCollection, error) { 298 // This sucks. 299 parts := bytes.Split(data, []byte("\n...\n")) 300 if len(parts) < 2 { 301 return nil, nil, errors.New("message block must have at least two parts") 302 } 303 304 md := &hapi.Metadata{} 305 sc := &SumCollection{} 306 307 if err := yaml.Unmarshal(parts[0], md); err != nil { 308 return md, sc, err 309 } 310 err := yaml.Unmarshal(parts[1], sc) 311 return md, sc, err 312 } 313 314 // loadKey loads a GPG key found at a particular path. 315 func loadKey(keypath string) (*openpgp.Entity, error) { 316 f, err := os.Open(keypath) 317 if err != nil { 318 return nil, err 319 } 320 defer f.Close() 321 322 pr := packet.NewReader(f) 323 return openpgp.ReadEntity(pr) 324 } 325 326 func loadKeyRing(ringpath string) (openpgp.EntityList, error) { 327 f, err := os.Open(ringpath) 328 if err != nil { 329 return nil, err 330 } 331 defer f.Close() 332 return openpgp.ReadKeyRing(f) 333 } 334 335 // sumArchive calculates a SHA256 hash (like Docker) for a given file. 336 // 337 // It takes the path to the archive file, and returns a string representation of 338 // the SHA256 sum. 339 // 340 // The intended use of this function is to generate a sum of a chart TGZ file. 341 func sumArchive(filename string) (string, error) { 342 f, err := os.Open(filename) 343 if err != nil { 344 return "", err 345 } 346 defer f.Close() 347 348 hash := crypto.SHA256.New() 349 io.Copy(hash, f) 350 return hex.EncodeToString(hash.Sum(nil)), nil 351 }