github.com/in-toto/in-toto-golang@v0.9.1-0.20240517212500-990269f763cf/in_toto/verifylib.go (about) 1 /* 2 Package in_toto implements types and routines to verify a software supply chain 3 according to the in-toto specification. 4 See https://github.com/in-toto/docs/blob/master/in-toto-spec.md 5 */ 6 package in_toto 7 8 import ( 9 "crypto/x509" 10 "errors" 11 "fmt" 12 "io" 13 "os" 14 "path" 15 "path/filepath" 16 "reflect" 17 "regexp" 18 "strings" 19 "time" 20 ) 21 22 // ErrInspectionRunDirIsSymlink gets thrown if the runDir is a symlink 23 var ErrInspectionRunDirIsSymlink = errors.New("runDir is a symlink. This is a security risk") 24 25 var ErrNotLayout = errors.New("verification workflow passed a non-layout") 26 27 /* 28 RunInspections iteratively executes the command in the Run field of all 29 inspections of the passed layout, creating unsigned link metadata that records 30 all files found in the current working directory as materials (before command 31 execution) and products (after command execution). A map with inspection names 32 as keys and Metablocks containing the generated link metadata as values is 33 returned. The format is: 34 35 { 36 <inspection name> : Metablock, 37 <inspection name> : Metablock, 38 ... 39 } 40 41 If executing the inspection command fails, or if the executed command has a 42 non-zero exit code, the first return value is an empty Metablock map and the 43 second return value is the error. 44 */ 45 func RunInspections(layout Layout, runDir string, lineNormalization bool, useDSSE bool) (map[string]Metadata, error) { 46 inspectionMetadata := make(map[string]Metadata) 47 48 for _, inspection := range layout.Inspect { 49 50 paths := []string{"."} 51 if runDir != "" { 52 paths = []string{runDir} 53 } 54 55 linkEnv, err := InTotoRun(inspection.Name, runDir, paths, paths, 56 inspection.Run, Key{}, []string{"sha256"}, nil, nil, lineNormalization, false, useDSSE) 57 58 if err != nil { 59 return nil, err 60 } 61 62 retVal := linkEnv.GetPayload().(Link).ByProducts["return-value"] 63 if retVal != float64(0) { 64 return nil, fmt.Errorf("inspection command '%s' of inspection '%s'"+ 65 " returned a non-zero value: %d", inspection.Run, inspection.Name, 66 retVal) 67 } 68 69 // Dump inspection link to cwd using the short link name format 70 linkName := fmt.Sprintf(LinkNameFormatShort, inspection.Name) 71 if err := linkEnv.Dump(linkName); err != nil { 72 fmt.Printf("JSON serialization or writing failed: %s", err) 73 } 74 75 inspectionMetadata[inspection.Name] = linkEnv 76 } 77 return inspectionMetadata, nil 78 } 79 80 // verifyMatchRule is a helper function to process artifact rules of 81 // type MATCH. See VerifyArtifacts for more details. 82 func verifyMatchRule(ruleData map[string]string, 83 srcArtifacts map[string]HashObj, srcArtifactQueue Set, 84 itemsMetadata map[string]Metadata) Set { 85 consumed := NewSet() 86 // Get destination link metadata 87 dstLinkEnv, exists := itemsMetadata[ruleData["dstName"]] 88 if !exists { 89 // Destination link does not exist, rule can't consume any 90 // artifacts 91 return consumed 92 } 93 94 // Get artifacts from destination link metadata 95 var dstArtifacts map[string]HashObj 96 switch ruleData["dstType"] { 97 case "materials": 98 dstArtifacts = dstLinkEnv.GetPayload().(Link).Materials 99 case "products": 100 dstArtifacts = dstLinkEnv.GetPayload().(Link).Products 101 } 102 103 // cleanup paths in pattern and artifact maps 104 if ruleData["pattern"] != "" { 105 ruleData["pattern"] = path.Clean(ruleData["pattern"]) 106 } 107 for k := range srcArtifacts { 108 if path.Clean(k) != k { 109 srcArtifacts[path.Clean(k)] = srcArtifacts[k] 110 delete(srcArtifacts, k) 111 } 112 } 113 for k := range dstArtifacts { 114 if path.Clean(k) != k { 115 dstArtifacts[path.Clean(k)] = dstArtifacts[k] 116 delete(dstArtifacts, k) 117 } 118 } 119 120 // Normalize optional source and destination prefixes, i.e. if 121 // there is a prefix, then add a trailing slash if not there yet 122 for _, prefix := range []string{"srcPrefix", "dstPrefix"} { 123 if ruleData[prefix] != "" { 124 ruleData[prefix] = path.Clean(ruleData[prefix]) 125 if !strings.HasSuffix(ruleData[prefix], "/") { 126 ruleData[prefix] += "/" 127 } 128 } 129 } 130 // Iterate over queue and mark consumed artifacts 131 for srcPath := range srcArtifactQueue { 132 // Remove optional source prefix from source artifact path 133 // Noop if prefix is empty, or artifact does not have it 134 srcBasePath := strings.TrimPrefix(srcPath, ruleData["srcPrefix"]) 135 136 // Ignore artifacts not matched by rule pattern 137 matched, err := match(ruleData["pattern"], srcBasePath) 138 if err != nil || !matched { 139 continue 140 } 141 142 // Construct corresponding destination artifact path, i.e. 143 // an optional destination prefix plus the source base path 144 dstPath := path.Clean(path.Join(ruleData["dstPrefix"], srcBasePath)) 145 146 // Try to find the corresponding destination artifact 147 dstArtifact, exists := dstArtifacts[dstPath] 148 // Ignore artifacts without corresponding destination artifact 149 if !exists { 150 continue 151 } 152 153 // Ignore artifact pairs with no matching hashes 154 if !reflect.DeepEqual(srcArtifacts[srcPath], dstArtifact) { 155 continue 156 } 157 158 // Only if a source and destination artifact pair was found and 159 // their hashes are equal, will we mark the source artifact as 160 // successfully consumed, i.e. it will be removed from the queue 161 consumed.Add(srcPath) 162 } 163 return consumed 164 } 165 166 /* 167 VerifyArtifacts iteratively applies the material and product rules of the 168 passed items (step or inspection) to enforce and authorize artifacts (materials 169 or products) reported by the corresponding link and to guarantee that 170 artifacts are linked together across links. In the beginning all artifacts are 171 placed in a queue according to their type. If an artifact gets consumed by a 172 rule it is removed from the queue. An artifact can only be consumed once in 173 the course of processing the set of rules in ExpectedMaterials or 174 ExpectedProducts. 175 176 Rules of type MATCH, ALLOW, CREATE, DELETE, MODIFY and DISALLOW are supported. 177 178 All rules except for DISALLOW consume queued artifacts on success, and 179 leave the queue unchanged on failure. Hence, it is left to a terminal 180 DISALLOW rule to fail overall verification, if artifacts are left in the queue 181 that should have been consumed by preceding rules. 182 */ 183 func VerifyArtifacts(items []interface{}, 184 itemsMetadata map[string]Metadata) error { 185 // Verify artifact rules for each item in the layout 186 for _, itemI := range items { 187 // The layout item (interface) must be a Link or an Inspection we are only 188 // interested in the name and the expected materials and products 189 var itemName string 190 var expectedMaterials [][]string 191 var expectedProducts [][]string 192 193 switch item := itemI.(type) { 194 case Step: 195 itemName = item.Name 196 expectedMaterials = item.ExpectedMaterials 197 expectedProducts = item.ExpectedProducts 198 199 case Inspection: 200 itemName = item.Name 201 expectedMaterials = item.ExpectedMaterials 202 expectedProducts = item.ExpectedProducts 203 204 default: // Something wrong 205 return fmt.Errorf("VerifyArtifacts received an item of invalid type,"+ 206 " elements of passed slice 'items' must be one of 'Step' or"+ 207 " 'Inspection', got: '%s'", reflect.TypeOf(item)) 208 } 209 210 // Use the item's name to extract the corresponding link 211 srcLinkEnv, exists := itemsMetadata[itemName] 212 if !exists { 213 return fmt.Errorf("VerifyArtifacts could not find metadata"+ 214 " for item '%s', got: '%s'", itemName, itemsMetadata) 215 } 216 217 // Create shortcuts to materials and products (including hashes) reported 218 // by the item's link, required to verify "match" rules 219 materials := srcLinkEnv.GetPayload().(Link).Materials 220 products := srcLinkEnv.GetPayload().(Link).Products 221 222 // All other rules only require the material or product paths (without 223 // hashes). We extract them from the corresponding maps and store them as 224 // sets for convenience in further processing 225 materialPaths := NewSet() 226 for _, p := range artifactsDictKeyStrings(materials) { 227 materialPaths.Add(path.Clean(p)) 228 } 229 productPaths := NewSet() 230 for _, p := range artifactsDictKeyStrings(products) { 231 productPaths.Add(path.Clean(p)) 232 } 233 234 // For `create`, `delete` and `modify` rules we prepare sets of artifacts 235 // (without hashes) that were created, deleted or modified in the current 236 // step or inspection 237 created := productPaths.Difference(materialPaths) 238 deleted := materialPaths.Difference(productPaths) 239 remained := materialPaths.Intersection(productPaths) 240 modified := NewSet() 241 for name := range remained { 242 if !reflect.DeepEqual(materials[name], products[name]) { 243 modified.Add(name) 244 } 245 } 246 247 // For each item we have to run rule verification, once per artifact type. 248 // Here we prepare the corresponding data for each round. 249 verificationDataList := []map[string]interface{}{ 250 { 251 "srcType": "materials", 252 "rules": expectedMaterials, 253 "artifacts": materials, 254 "artifactPaths": materialPaths, 255 }, 256 { 257 "srcType": "products", 258 "rules": expectedProducts, 259 "artifacts": products, 260 "artifactPaths": productPaths, 261 }, 262 } 263 // TODO: Add logging library (see in-toto/in-toto-golang#4) 264 // fmt.Printf("Verifying %s '%s' ", reflect.TypeOf(itemI), itemName) 265 266 // Process all material rules using the corresponding materials and all 267 // product rules using the corresponding products 268 for _, verificationData := range verificationDataList { 269 // TODO: Add logging library (see in-toto/in-toto-golang#4) 270 // fmt.Printf("%s...\n", verificationData["srcType"]) 271 272 rules := verificationData["rules"].([][]string) 273 artifacts := verificationData["artifacts"].(map[string]HashObj) 274 275 // Use artifacts (without hashes) as base queue. Each rule only operates 276 // on artifacts in that queue. If a rule consumes an artifact (i.e. can 277 // be applied successfully), the artifact is removed from the queue. By 278 // applying a DISALLOW rule eventually, verification may return an error, 279 // if the rule matches any artifacts in the queue that should have been 280 // consumed earlier. 281 queue := verificationData["artifactPaths"].(Set) 282 283 // TODO: Add logging library (see in-toto/in-toto-golang#4) 284 // fmt.Printf("Initial state\nMaterials: %s\nProducts: %s\nQueue: %s\n\n", 285 // materialPaths.Slice(), productPaths.Slice(), queue.Slice()) 286 287 // Verify rules sequentially 288 for _, rule := range rules { 289 // Parse rule and error out if it is malformed 290 // NOTE: the rule format should have been validated before 291 ruleData, err := UnpackRule(rule) 292 if err != nil { 293 return err 294 } 295 296 // Apply rule pattern to filter queued artifacts that are up for rule 297 // specific consumption 298 filtered := queue.Filter(path.Clean(ruleData["pattern"])) 299 300 var consumed Set 301 switch ruleData["type"] { 302 case "match": 303 // Note: here we need to perform more elaborate filtering 304 consumed = verifyMatchRule(ruleData, artifacts, queue, itemsMetadata) 305 306 case "allow": 307 // Consumes all filtered artifacts 308 consumed = filtered 309 310 case "create": 311 // Consumes filtered artifacts that were created 312 consumed = filtered.Intersection(created) 313 314 case "delete": 315 // Consumes filtered artifacts that were deleted 316 consumed = filtered.Intersection(deleted) 317 318 case "modify": 319 // Consumes filtered artifacts that were modified 320 consumed = filtered.Intersection(modified) 321 322 case "disallow": 323 // Does not consume but errors out if artifacts were filtered 324 if len(filtered) > 0 { 325 return fmt.Errorf("artifact verification failed for %s '%s',"+ 326 " %s %s disallowed by rule %s", 327 reflect.TypeOf(itemI).Name(), itemName, 328 verificationData["srcType"], filtered.Slice(), rule) 329 } 330 case "require": 331 // REQUIRE is somewhat of a weird animal that does not use 332 // patterns bur rather single filenames (for now). 333 if !queue.Has(ruleData["pattern"]) { 334 return fmt.Errorf("artifact verification failed for %s in REQUIRE '%s',"+ 335 " because %s is not in %s", verificationData["srcType"], 336 ruleData["pattern"], ruleData["pattern"], queue.Slice()) 337 } 338 } 339 // Update queue by removing consumed artifacts 340 queue = queue.Difference(consumed) 341 // TODO: Add logging library (see in-toto/in-toto-golang#4) 342 // fmt.Printf("Rule: %s\nQueue: %s\n\n", rule, queue.Slice()) 343 } 344 } 345 } 346 return nil 347 } 348 349 /* 350 ReduceStepsMetadata merges for each step of the passed Layout all the passed 351 per-functionary links into a single link, asserting that the reported Materials 352 and Products are equal across links for a given step. This function may be 353 used at a time during the overall verification, where link threshold's have 354 been verified and subsequent verification only needs one exemplary link per 355 step. The function returns a map with one Metablock (link) per step: 356 357 { 358 <step name> : Metablock, 359 <step name> : Metablock, 360 ... 361 } 362 363 If links corresponding to the same step report different Materials or different 364 Products, the first return value is an empty Metablock map and the second 365 return value is the error. 366 */ 367 func ReduceStepsMetadata(layout Layout, 368 stepsMetadata map[string]map[string]Metadata) (map[string]Metadata, 369 error) { 370 stepsMetadataReduced := make(map[string]Metadata) 371 372 for _, step := range layout.Steps { 373 linksPerStep, ok := stepsMetadata[step.Name] 374 // We should never get here, layout verification must fail earlier 375 if !ok || len(linksPerStep) < 1 { 376 panic("Could not reduce metadata for step '" + step.Name + 377 "', no link metadata found.") 378 } 379 380 // Get the first link (could be any link) for the current step, which will 381 // serve as reference link for below comparisons 382 var referenceKeyID string 383 var referenceLinkEnv Metadata 384 for keyID, linkEnv := range linksPerStep { 385 referenceLinkEnv = linkEnv 386 referenceKeyID = keyID 387 break 388 } 389 390 // Only one link, nothing to reduce, take the reference link 391 if len(linksPerStep) == 1 { 392 stepsMetadataReduced[step.Name] = referenceLinkEnv 393 394 // Multiple links, reduce but first check 395 } else { 396 // Artifact maps must be equal for each type among all links 397 // TODO: What should we do if there are more links, than the 398 // threshold requires, but not all of them are equal? Right now we would 399 // also error. 400 for keyID, linkEnv := range linksPerStep { 401 if !reflect.DeepEqual(linkEnv.GetPayload().(Link).Materials, 402 referenceLinkEnv.GetPayload().(Link).Materials) || 403 !reflect.DeepEqual(linkEnv.GetPayload().(Link).Products, 404 referenceLinkEnv.GetPayload().(Link).Products) { 405 return nil, fmt.Errorf("link '%s' and '%s' have different"+ 406 " artifacts", 407 fmt.Sprintf(LinkNameFormat, step.Name, referenceKeyID), 408 fmt.Sprintf(LinkNameFormat, step.Name, keyID)) 409 } 410 } 411 // We haven't errored out, so we can reduce (i.e take the reference link) 412 stepsMetadataReduced[step.Name] = referenceLinkEnv 413 } 414 } 415 return stepsMetadataReduced, nil 416 } 417 418 /* 419 VerifyStepCommandAlignment (soft) verifies that for each step of the passed 420 layout the command executed, as per the passed link, matches the expected 421 command, as per the layout. Soft verification means that, in case a command 422 does not align, a warning is issued. 423 */ 424 func VerifyStepCommandAlignment(layout Layout, 425 stepsMetadata map[string]map[string]Metadata) { 426 for _, step := range layout.Steps { 427 linksPerStep, ok := stepsMetadata[step.Name] 428 // We should never get here, layout verification must fail earlier 429 if !ok || len(linksPerStep) < 1 { 430 panic("Could not verify command alignment for step '" + step.Name + 431 "', no link metadata found.") 432 } 433 434 for signerKeyID, linkEnv := range linksPerStep { 435 expectedCommandS := strings.Join(step.ExpectedCommand, " ") 436 executedCommandS := strings.Join(linkEnv.GetPayload().(Link).Command, " ") 437 438 if expectedCommandS != executedCommandS { 439 linkName := fmt.Sprintf(LinkNameFormat, step.Name, signerKeyID) 440 fmt.Printf("WARNING: Expected command for step '%s' (%s) and command"+ 441 " reported by '%s' (%s) differ.\n", 442 step.Name, expectedCommandS, linkName, executedCommandS) 443 } 444 } 445 } 446 } 447 448 /* 449 LoadLayoutCertificates loads the root and intermediate CAs from the layout if in the layout. 450 This will be used to check signatures that were used to sign links but not configured 451 in the PubKeys section of the step. No configured CAs means we don't want to allow this. 452 Returned CertPools will be empty in this case. 453 */ 454 func LoadLayoutCertificates(layout Layout, intermediatePems [][]byte) (*x509.CertPool, *x509.CertPool, error) { 455 rootPool := x509.NewCertPool() 456 for _, certPem := range layout.RootCas { 457 ok := rootPool.AppendCertsFromPEM([]byte(certPem.KeyVal.Certificate)) 458 if !ok { 459 return nil, nil, fmt.Errorf("failed to load root certificates for layout") 460 } 461 } 462 463 intermediatePool := x509.NewCertPool() 464 for _, intermediatePem := range layout.IntermediateCas { 465 ok := intermediatePool.AppendCertsFromPEM([]byte(intermediatePem.KeyVal.Certificate)) 466 if !ok { 467 return nil, nil, fmt.Errorf("failed to load intermediate certificates for layout") 468 } 469 } 470 471 for _, intermediatePem := range intermediatePems { 472 ok := intermediatePool.AppendCertsFromPEM(intermediatePem) 473 if !ok { 474 return nil, nil, fmt.Errorf("failed to load provided intermediate certificates") 475 } 476 } 477 478 return rootPool, intermediatePool, nil 479 } 480 481 /* 482 VerifyLinkSignatureThesholds verifies that for each step of the passed layout, 483 there are at least Threshold links, validly signed by different authorized 484 functionaries. The returned map of link metadata per steps contains only 485 links with valid signatures from distinct functionaries and has the format: 486 487 { 488 <step name> : { 489 <key id>: Metablock, 490 <key id>: Metablock, 491 ... 492 }, 493 <step name> : { 494 <key id>: Metablock, 495 <key id>: Metablock, 496 ... 497 } 498 ... 499 } 500 501 If for any step of the layout there are not enough links available, the first 502 return value is an empty map of Metablock maps and the second return value is 503 the error. 504 */ 505 func VerifyLinkSignatureThesholds(layout Layout, 506 stepsMetadata map[string]map[string]Metadata, rootCertPool, intermediateCertPool *x509.CertPool) ( 507 map[string]map[string]Metadata, error) { 508 // This will stores links with valid signature from an authorized functionary 509 // for all steps 510 stepsMetadataVerified := make(map[string]map[string]Metadata) 511 512 // Try to find enough (>= threshold) links each with a valid signature from 513 // distinct authorized functionaries for each step 514 for _, step := range layout.Steps { 515 var stepErr error 516 517 // This will store links with valid signature from an authorized 518 // functionary for the given step 519 linksPerStepVerified := make(map[string]Metadata) 520 521 // Check if there are any links at all for the given step 522 linksPerStep, ok := stepsMetadata[step.Name] 523 if !ok || len(linksPerStep) < 1 { 524 stepErr = fmt.Errorf("no links found") 525 } 526 527 // For each link corresponding to a step, check that the signer key was 528 // authorized, the layout contains a verification key and the signature 529 // verification passes. Only good links are stored, to verify thresholds 530 // below. 531 isAuthorizedSignature := false 532 for signerKeyID, linkEnv := range linksPerStep { 533 for _, authorizedKeyID := range step.PubKeys { 534 if signerKeyID == authorizedKeyID { 535 if verifierKey, ok := layout.Keys[authorizedKeyID]; ok { 536 if err := linkEnv.VerifySignature(verifierKey); err == nil { 537 linksPerStepVerified[signerKeyID] = linkEnv 538 isAuthorizedSignature = true 539 break 540 } 541 } 542 } 543 } 544 545 // If the signer's key wasn't in our step's pubkeys array, check the cert pool to 546 // see if the key is known to us. 547 if !isAuthorizedSignature { 548 sig, err := linkEnv.GetSignatureForKeyID(signerKeyID) 549 if err != nil { 550 stepErr = err 551 continue 552 } 553 554 cert, err := sig.GetCertificate() 555 if err != nil { 556 stepErr = err 557 continue 558 } 559 560 // test certificate against the step's constraints to make sure it's a valid functionary 561 err = step.CheckCertConstraints(cert, layout.RootCAIDs(), rootCertPool, intermediateCertPool) 562 if err != nil { 563 stepErr = err 564 continue 565 } 566 567 err = linkEnv.VerifySignature(cert) 568 if err != nil { 569 stepErr = err 570 continue 571 } 572 573 linksPerStepVerified[signerKeyID] = linkEnv 574 } 575 } 576 577 // Store all good links for a step 578 stepsMetadataVerified[step.Name] = linksPerStepVerified 579 580 if len(linksPerStepVerified) < step.Threshold { 581 linksPerStep := stepsMetadata[step.Name] 582 return nil, fmt.Errorf("step '%s' requires '%d' link metadata file(s)."+ 583 " '%d' out of '%d' available link(s) have a valid signature from an"+ 584 " authorized signer: %v", step.Name, step.Threshold, 585 len(linksPerStepVerified), len(linksPerStep), stepErr) 586 } 587 } 588 return stepsMetadataVerified, nil 589 } 590 591 /* 592 LoadLinksForLayout loads for every Step of the passed Layout a Metablock 593 containing the corresponding Link. A base path to a directory that contains 594 the links may be passed using linkDir. Link file names are constructed, 595 using LinkNameFormat together with the corresponding step name and authorized 596 functionary key ids. A map of link metadata is returned and has the following 597 format: 598 599 { 600 <step name> : { 601 <key id>: Metablock, 602 <key id>: Metablock, 603 ... 604 }, 605 <step name> : { 606 <key id>: Metablock, 607 <key id>: Metablock, 608 ... 609 } 610 ... 611 } 612 613 If a link cannot be loaded at a constructed link name or is invalid, it is 614 ignored. Only a preliminary threshold check is performed, that is, if there 615 aren't at least Threshold links for any given step, the first return value 616 is an empty map of Metablock maps and the second return value is the error. 617 */ 618 func LoadLinksForLayout(layout Layout, linkDir string) (map[string]map[string]Metadata, error) { 619 stepsMetadata := make(map[string]map[string]Metadata) 620 621 for _, step := range layout.Steps { 622 linksPerStep := make(map[string]Metadata) 623 // Since we can verify against certificates belonging to a CA, we need to 624 // load any possible links 625 linkFiles, err := filepath.Glob(path.Join(linkDir, fmt.Sprintf(LinkGlobFormat, step.Name))) 626 if err != nil { 627 return nil, err 628 } 629 630 for _, linkPath := range linkFiles { 631 linkEnv, err := LoadMetadata(linkPath) 632 if err != nil { 633 continue 634 } 635 636 // To get the full key from the metadata's signatures, we have to check 637 // for one with the same short id... 638 signerShortKeyID := strings.TrimSuffix(strings.TrimPrefix(filepath.Base(linkPath), step.Name+"."), ".link") 639 for _, sig := range linkEnv.Sigs() { 640 if strings.HasPrefix(sig.KeyID, signerShortKeyID) { 641 linksPerStep[sig.KeyID] = linkEnv 642 break 643 } 644 } 645 } 646 647 if len(linksPerStep) < step.Threshold { 648 return nil, fmt.Errorf("step '%s' requires '%d' link metadata file(s),"+ 649 " found '%d'", step.Name, step.Threshold, len(linksPerStep)) 650 } 651 652 stepsMetadata[step.Name] = linksPerStep 653 } 654 655 return stepsMetadata, nil 656 } 657 658 /* 659 VerifyLayoutExpiration verifies that the passed Layout has not expired. It 660 returns an error if the (zulu) date in the Expires field is in the past. 661 */ 662 func VerifyLayoutExpiration(layout Layout) error { 663 expires, err := time.Parse(ISO8601DateSchema, layout.Expires) 664 if err != nil { 665 return err 666 } 667 // Uses timezone of expires, i.e. UTC 668 if time.Until(expires) < 0 { 669 return fmt.Errorf("layout has expired on '%s'", expires) 670 } 671 return nil 672 } 673 674 /* 675 VerifyLayoutSignatures verifies for each key in the passed key map the 676 corresponding signature of the Layout in the passed Metablock's Signed field. 677 Signatures and keys are associated by key id. If the key map is empty, or the 678 Metablock's Signature field does not have a signature for one or more of the 679 passed keys, or a matching signature is invalid, an error is returned. 680 */ 681 func VerifyLayoutSignatures(layoutEnv Metadata, 682 layoutKeys map[string]Key) error { 683 if len(layoutKeys) < 1 { 684 return fmt.Errorf("layout verification requires at least one key") 685 } 686 687 for _, key := range layoutKeys { 688 if err := layoutEnv.VerifySignature(key); err != nil { 689 return err 690 } 691 } 692 return nil 693 } 694 695 /* 696 GetSummaryLink merges the materials of the first step (as mentioned in the 697 layout) and the products of the last step and returns a new link. This link 698 reports the materials and products and summarizes the overall software supply 699 chain. 700 NOTE: The assumption is that the steps mentioned in the layout are to be 701 performed sequentially. So, the first step mentioned in the layout denotes what 702 comes into the supply chain and the last step denotes what goes out. 703 */ 704 func GetSummaryLink(layout Layout, stepsMetadataReduced map[string]Metadata, 705 stepName string, useDSSE bool) (Metadata, error) { 706 var summaryLink Link 707 if len(layout.Steps) > 0 { 708 firstStepLink := stepsMetadataReduced[layout.Steps[0].Name] 709 lastStepLink := stepsMetadataReduced[layout.Steps[len(layout.Steps)-1].Name] 710 711 summaryLink.Materials = firstStepLink.GetPayload().(Link).Materials 712 summaryLink.Name = stepName 713 summaryLink.Type = firstStepLink.GetPayload().(Link).Type 714 715 summaryLink.Products = lastStepLink.GetPayload().(Link).Products 716 summaryLink.ByProducts = lastStepLink.GetPayload().(Link).ByProducts 717 // Using the last command of the sublayout as the command 718 // of the summary link can be misleading. Is it necessary to 719 // include all the commands executed as part of sublayout? 720 summaryLink.Command = lastStepLink.GetPayload().(Link).Command 721 } 722 723 if useDSSE { 724 env := &Envelope{} 725 if err := env.SetPayload(summaryLink); err != nil { 726 return nil, err 727 } 728 729 return env, nil 730 } 731 732 return &Metablock{Signed: summaryLink}, nil 733 } 734 735 /* 736 VerifySublayouts checks if any step in the supply chain is a sublayout, and if 737 so, recursively resolves it and replaces it with a summary link summarizing the 738 steps carried out in the sublayout. 739 */ 740 func VerifySublayouts(layout Layout, 741 stepsMetadataVerified map[string]map[string]Metadata, 742 superLayoutLinkPath string, intermediatePems [][]byte, lineNormalization bool) (map[string]map[string]Metadata, error) { 743 for stepName, linkData := range stepsMetadataVerified { 744 for keyID, metadata := range linkData { 745 if _, ok := metadata.GetPayload().(Layout); ok { 746 layoutKeys := make(map[string]Key) 747 layoutKeys[keyID] = layout.Keys[keyID] 748 749 sublayoutLinkDir := fmt.Sprintf(SublayoutLinkDirFormat, 750 stepName, keyID) 751 sublayoutLinkPath := filepath.Join(superLayoutLinkPath, 752 sublayoutLinkDir) 753 summaryLink, err := InTotoVerify(metadata, layoutKeys, 754 sublayoutLinkPath, stepName, make(map[string]string), intermediatePems, lineNormalization) 755 if err != nil { 756 return nil, err 757 } 758 linkData[keyID] = summaryLink 759 } 760 761 } 762 } 763 return stepsMetadataVerified, nil 764 } 765 766 // TODO: find a better way than two helper functions for the replacer op 767 768 func substituteParamatersInSlice(replacer *strings.Replacer, slice []string) []string { 769 newSlice := make([]string, 0) 770 for _, item := range slice { 771 newSlice = append(newSlice, replacer.Replace(item)) 772 } 773 return newSlice 774 } 775 776 func substituteParametersInSliceOfSlices(replacer *strings.Replacer, 777 slice [][]string) [][]string { 778 newSlice := make([][]string, 0) 779 for _, item := range slice { 780 newSlice = append(newSlice, substituteParamatersInSlice(replacer, 781 item)) 782 } 783 return newSlice 784 } 785 786 /* 787 SubstituteParameters performs parameter substitution in steps and inspections 788 in the following fields: 789 - Expected Materials and Expected Products of both 790 - Run of inspections 791 - Expected Command of steps 792 The substitution marker is '{}' and the keyword within the braces is replaced 793 by a value found in the substitution map passed, parameterDictionary. The 794 layout with parameters substituted is returned to the calling function. 795 */ 796 func SubstituteParameters(layout Layout, 797 parameterDictionary map[string]string) (Layout, error) { 798 799 if len(parameterDictionary) == 0 { 800 return layout, nil 801 } 802 803 parameters := make([]string, 0) 804 805 re := regexp.MustCompile("^[a-zA-Z0-9_-]+$") 806 807 for parameter, value := range parameterDictionary { 808 parameterFormatCheck := re.MatchString(parameter) 809 if !parameterFormatCheck { 810 return layout, fmt.Errorf("invalid format for parameter") 811 } 812 813 parameters = append(parameters, "{"+parameter+"}") 814 parameters = append(parameters, value) 815 } 816 817 replacer := strings.NewReplacer(parameters...) 818 819 for i := range layout.Steps { 820 layout.Steps[i].ExpectedMaterials = substituteParametersInSliceOfSlices( 821 replacer, layout.Steps[i].ExpectedMaterials) 822 layout.Steps[i].ExpectedProducts = substituteParametersInSliceOfSlices( 823 replacer, layout.Steps[i].ExpectedProducts) 824 layout.Steps[i].ExpectedCommand = substituteParamatersInSlice(replacer, 825 layout.Steps[i].ExpectedCommand) 826 } 827 828 for i := range layout.Inspect { 829 layout.Inspect[i].ExpectedMaterials = 830 substituteParametersInSliceOfSlices(replacer, 831 layout.Inspect[i].ExpectedMaterials) 832 layout.Inspect[i].ExpectedProducts = 833 substituteParametersInSliceOfSlices(replacer, 834 layout.Inspect[i].ExpectedProducts) 835 layout.Inspect[i].Run = substituteParamatersInSlice(replacer, 836 layout.Inspect[i].Run) 837 } 838 839 return layout, nil 840 } 841 842 /* 843 InTotoVerify can be used to verify an entire software supply chain according to 844 the in-toto specification. It requires the metadata of the root layout, a map 845 that contains public keys to verify the root layout signatures, a path to a 846 directory from where it can load link metadata files, which are treated as 847 signed evidence for the steps defined in the layout, a step name, and a 848 paramater dictionary used for parameter substitution. The step name only 849 matters for sublayouts, where it's important to associate the summary of that 850 step with a unique name. The verification routine is as follows: 851 852 1. Verify layout signature(s) using passed key(s) 853 2. Verify layout expiration date 854 3. Substitute parameters in layout 855 4. Load link metadata files for steps of layout 856 5. Verify signatures and signature thresholds for steps of layout 857 6. Verify sublayouts recursively 858 7. Verify command alignment for steps of layout (only warns) 859 8. Verify artifact rules for steps of layout 860 9. Execute inspection commands (generates link metadata for each inspection) 861 10. Verify artifact rules for inspections of layout 862 863 InTotoVerify returns a summary link wrapped in a Metablock object and an error 864 value. If any of the verification routines fail, verification is aborted and 865 error is returned. In such an instance, the first value remains an empty 866 Metablock object. 867 868 NOTE: Artifact rules of type "create", "modify" 869 and "delete" are currently not supported. 870 */ 871 func InTotoVerify(layoutEnv Metadata, layoutKeys map[string]Key, 872 linkDir string, stepName string, parameterDictionary map[string]string, intermediatePems [][]byte, lineNormalization bool) ( 873 Metadata, error) { 874 875 // Verify root signatures 876 if err := VerifyLayoutSignatures(layoutEnv, layoutKeys); err != nil { 877 return nil, err 878 } 879 880 useDSSE := false 881 if _, ok := layoutEnv.(*Envelope); ok { 882 useDSSE = true 883 } 884 885 // Extract the layout from its Metadata container (for further processing) 886 layout, ok := layoutEnv.GetPayload().(Layout) 887 if !ok { 888 return nil, ErrNotLayout 889 } 890 891 // Verify layout expiration 892 if err := VerifyLayoutExpiration(layout); err != nil { 893 return nil, err 894 } 895 896 // Substitute parameters in layout 897 layout, err := SubstituteParameters(layout, parameterDictionary) 898 if err != nil { 899 return nil, err 900 } 901 902 rootCertPool, intermediateCertPool, err := LoadLayoutCertificates(layout, intermediatePems) 903 if err != nil { 904 return nil, err 905 } 906 907 // Load links for layout 908 stepsMetadata, err := LoadLinksForLayout(layout, linkDir) 909 if err != nil { 910 return nil, err 911 } 912 913 // Verify link signatures 914 stepsMetadataVerified, err := VerifyLinkSignatureThesholds(layout, 915 stepsMetadata, rootCertPool, intermediateCertPool) 916 if err != nil { 917 return nil, err 918 } 919 920 // Verify and resolve sublayouts 921 stepsSublayoutVerified, err := VerifySublayouts(layout, 922 stepsMetadataVerified, linkDir, intermediatePems, lineNormalization) 923 if err != nil { 924 return nil, err 925 } 926 927 // Verify command alignment (WARNING only) 928 VerifyStepCommandAlignment(layout, stepsSublayoutVerified) 929 930 // Given that signature thresholds have been checked above and the rest of 931 // the relevant link properties, i.e. materials and products, have to be 932 // exactly equal, we can reduce the map of steps metadata. However, we error 933 // if the relevant properties are not equal among links of a step. 934 stepsMetadataReduced, err := ReduceStepsMetadata(layout, 935 stepsSublayoutVerified) 936 if err != nil { 937 return nil, err 938 } 939 940 // Verify artifact rules 941 if err = VerifyArtifacts(layout.stepsAsInterfaceSlice(), 942 stepsMetadataReduced); err != nil { 943 return nil, err 944 } 945 946 inspectionMetadata, err := RunInspections(layout, "", lineNormalization, useDSSE) 947 if err != nil { 948 return nil, err 949 } 950 951 // Add steps metadata to inspection metadata, because inspection artifact 952 // rules may also refer to artifacts reported by step links 953 for k, v := range stepsMetadataReduced { 954 inspectionMetadata[k] = v 955 } 956 957 if err = VerifyArtifacts(layout.inspectAsInterfaceSlice(), 958 inspectionMetadata); err != nil { 959 return nil, err 960 } 961 962 summaryLink, err := GetSummaryLink(layout, stepsMetadataReduced, stepName, useDSSE) 963 if err != nil { 964 return nil, err 965 } 966 967 return summaryLink, nil 968 } 969 970 /* 971 InTotoVerifyWithDirectory provides the same functionality as InTotoVerify, but 972 adds the possibility to select a local directory from where the inspections are run. 973 */ 974 func InTotoVerifyWithDirectory(layoutEnv Metadata, layoutKeys map[string]Key, 975 linkDir string, runDir string, stepName string, parameterDictionary map[string]string, intermediatePems [][]byte, lineNormalization bool) ( 976 Metadata, error) { 977 978 // runDir sanity checks 979 // check if path exists 980 info, err := os.Stat(runDir) 981 if err != nil { 982 return nil, err 983 } 984 985 // check if runDir is a symlink 986 if info.Mode()&os.ModeSymlink == os.ModeSymlink { 987 return nil, ErrInspectionRunDirIsSymlink 988 } 989 990 // check if runDir is writable and a directory 991 err = isWritable(runDir) 992 if err != nil { 993 return nil, err 994 } 995 996 // check if runDir is empty (we do not want to overwrite files) 997 // We abuse File.Readdirnames for this action. 998 f, err := os.Open(runDir) 999 if err != nil { 1000 return nil, err 1001 } 1002 defer f.Close() 1003 // We use Readdirnames(1) for performance reasons, one child node 1004 // is enough to proof that the directory is not empty 1005 _, err = f.Readdirnames(1) 1006 // if io.EOF gets returned as error the directory is empty 1007 if err == io.EOF { 1008 return nil, err 1009 } 1010 err = f.Close() 1011 if err != nil { 1012 return nil, err 1013 } 1014 1015 // Verify root signatures 1016 if err := VerifyLayoutSignatures(layoutEnv, layoutKeys); err != nil { 1017 return nil, err 1018 } 1019 1020 useDSSE := false 1021 if _, ok := layoutEnv.(*Envelope); ok { 1022 useDSSE = true 1023 } 1024 1025 // Extract the layout from its Metadata container (for further processing) 1026 layout, ok := layoutEnv.GetPayload().(Layout) 1027 if !ok { 1028 return nil, ErrNotLayout 1029 } 1030 1031 // Verify layout expiration 1032 if err := VerifyLayoutExpiration(layout); err != nil { 1033 return nil, err 1034 } 1035 1036 // Substitute parameters in layout 1037 layout, err = SubstituteParameters(layout, parameterDictionary) 1038 if err != nil { 1039 return nil, err 1040 } 1041 1042 rootCertPool, intermediateCertPool, err := LoadLayoutCertificates(layout, intermediatePems) 1043 if err != nil { 1044 return nil, err 1045 } 1046 1047 // Load links for layout 1048 stepsMetadata, err := LoadLinksForLayout(layout, linkDir) 1049 if err != nil { 1050 return nil, err 1051 } 1052 1053 // Verify link signatures 1054 stepsMetadataVerified, err := VerifyLinkSignatureThesholds(layout, 1055 stepsMetadata, rootCertPool, intermediateCertPool) 1056 if err != nil { 1057 return nil, err 1058 } 1059 1060 // Verify and resolve sublayouts 1061 stepsSublayoutVerified, err := VerifySublayouts(layout, 1062 stepsMetadataVerified, linkDir, intermediatePems, lineNormalization) 1063 if err != nil { 1064 return nil, err 1065 } 1066 1067 // Verify command alignment (WARNING only) 1068 VerifyStepCommandAlignment(layout, stepsSublayoutVerified) 1069 1070 // Given that signature thresholds have been checked above and the rest of 1071 // the relevant link properties, i.e. materials and products, have to be 1072 // exactly equal, we can reduce the map of steps metadata. However, we error 1073 // if the relevant properties are not equal among links of a step. 1074 stepsMetadataReduced, err := ReduceStepsMetadata(layout, 1075 stepsSublayoutVerified) 1076 if err != nil { 1077 return nil, err 1078 } 1079 1080 // Verify artifact rules 1081 if err = VerifyArtifacts(layout.stepsAsInterfaceSlice(), 1082 stepsMetadataReduced); err != nil { 1083 return nil, err 1084 } 1085 1086 inspectionMetadata, err := RunInspections(layout, runDir, lineNormalization, useDSSE) 1087 if err != nil { 1088 return nil, err 1089 } 1090 1091 // Add steps metadata to inspection metadata, because inspection artifact 1092 // rules may also refer to artifacts reported by step links 1093 for k, v := range stepsMetadataReduced { 1094 inspectionMetadata[k] = v 1095 } 1096 1097 if err = VerifyArtifacts(layout.inspectAsInterfaceSlice(), 1098 inspectionMetadata); err != nil { 1099 return nil, err 1100 } 1101 1102 summaryLink, err := GetSummaryLink(layout, stepsMetadataReduced, stepName, useDSSE) 1103 if err != nil { 1104 return nil, err 1105 } 1106 1107 return summaryLink, nil 1108 }