You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
buildx/vendor/github.com/in-toto/in-toto-golang/in_toto/verifylib.go

1092 lines
36 KiB
Go

/*
Package in_toto implements types and routines to verify a software supply chain
according to the in-toto specification.
See https://github.com/in-toto/docs/blob/master/in-toto-spec.md
*/
package in_toto
import (
"crypto/x509"
"errors"
"fmt"
"io"
"os"
"path"
osPath "path"
"path/filepath"
"reflect"
"regexp"
"strings"
"time"
)
// ErrInspectionRunDirIsSymlink gets thrown if the runDir is a symlink
var ErrInspectionRunDirIsSymlink = errors.New("runDir is a symlink. This is a security risk")
/*
RunInspections iteratively executes the command in the Run field of all
inspections of the passed layout, creating unsigned link metadata that records
all files found in the current working directory as materials (before command
execution) and products (after command execution). A map with inspection names
as keys and Metablocks containing the generated link metadata as values is
returned. The format is:
{
<inspection name> : Metablock,
<inspection name> : Metablock,
...
}
If executing the inspection command fails, or if the executed command has a
non-zero exit code, the first return value is an empty Metablock map and the
second return value is the error.
*/
func RunInspections(layout Layout, runDir string, lineNormalization bool) (map[string]Metablock, error) {
inspectionMetadata := make(map[string]Metablock)
for _, inspection := range layout.Inspect {
paths := []string{"."}
if runDir != "" {
paths = []string{runDir}
}
linkMb, err := InTotoRun(inspection.Name, runDir, paths, paths,
inspection.Run, Key{}, []string{"sha256"}, nil, nil, lineNormalization)
if err != nil {
return nil, err
}
retVal := linkMb.Signed.(Link).ByProducts["return-value"]
if retVal != float64(0) {
return nil, fmt.Errorf("inspection command '%s' of inspection '%s'"+
" returned a non-zero value: %d", inspection.Run, inspection.Name,
retVal)
}
// Dump inspection link to cwd using the short link name format
linkName := fmt.Sprintf(LinkNameFormatShort, inspection.Name)
if err := linkMb.Dump(linkName); err != nil {
fmt.Printf("JSON serialization or writing failed: %s", err)
}
inspectionMetadata[inspection.Name] = linkMb
}
return inspectionMetadata, nil
}
// verifyMatchRule is a helper function to process artifact rules of
// type MATCH. See VerifyArtifacts for more details.
func verifyMatchRule(ruleData map[string]string,
srcArtifacts map[string]interface{}, srcArtifactQueue Set,
itemsMetadata map[string]Metablock) Set {
consumed := NewSet()
// Get destination link metadata
dstLinkMb, exists := itemsMetadata[ruleData["dstName"]]
if !exists {
// Destination link does not exist, rule can't consume any
// artifacts
return consumed
}
// Get artifacts from destination link metadata
var dstArtifacts map[string]interface{}
switch ruleData["dstType"] {
case "materials":
dstArtifacts = dstLinkMb.Signed.(Link).Materials
case "products":
dstArtifacts = dstLinkMb.Signed.(Link).Products
}
// cleanup paths in pattern and artifact maps
if ruleData["pattern"] != "" {
ruleData["pattern"] = path.Clean(ruleData["pattern"])
}
for k := range srcArtifacts {
if path.Clean(k) != k {
srcArtifacts[path.Clean(k)] = srcArtifacts[k]
delete(srcArtifacts, k)
}
}
for k := range dstArtifacts {
if path.Clean(k) != k {
dstArtifacts[path.Clean(k)] = dstArtifacts[k]
delete(dstArtifacts, k)
}
}
// Normalize optional source and destination prefixes, i.e. if
// there is a prefix, then add a trailing slash if not there yet
for _, prefix := range []string{"srcPrefix", "dstPrefix"} {
if ruleData[prefix] != "" {
ruleData[prefix] = path.Clean(ruleData[prefix])
if !strings.HasSuffix(ruleData[prefix], "/") {
ruleData[prefix] += "/"
}
}
}
// Iterate over queue and mark consumed artifacts
for srcPath := range srcArtifactQueue {
// Remove optional source prefix from source artifact path
// Noop if prefix is empty, or artifact does not have it
srcBasePath := strings.TrimPrefix(srcPath, ruleData["srcPrefix"])
// Ignore artifacts not matched by rule pattern
matched, err := match(ruleData["pattern"], srcBasePath)
if err != nil || !matched {
continue
}
// Construct corresponding destination artifact path, i.e.
// an optional destination prefix plus the source base path
dstPath := path.Clean(osPath.Join(ruleData["dstPrefix"], srcBasePath))
// Try to find the corresponding destination artifact
dstArtifact, exists := dstArtifacts[dstPath]
// Ignore artifacts without corresponding destination artifact
if !exists {
continue
}
// Ignore artifact pairs with no matching hashes
if !reflect.DeepEqual(srcArtifacts[srcPath], dstArtifact) {
continue
}
// Only if a source and destination artifact pair was found and
// their hashes are equal, will we mark the source artifact as
// successfully consumed, i.e. it will be removed from the queue
consumed.Add(srcPath)
}
return consumed
}
/*
VerifyArtifacts iteratively applies the material and product rules of the
passed items (step or inspection) to enforce and authorize artifacts (materials
or products) reported by the corresponding link and to guarantee that
artifacts are linked together across links. In the beginning all artifacts are
placed in a queue according to their type. If an artifact gets consumed by a
rule it is removed from the queue. An artifact can only be consumed once in
the course of processing the set of rules in ExpectedMaterials or
ExpectedProducts.
Rules of type MATCH, ALLOW, CREATE, DELETE, MODIFY and DISALLOW are supported.
All rules except for DISALLOW consume queued artifacts on success, and
leave the queue unchanged on failure. Hence, it is left to a terminal
DISALLOW rule to fail overall verification, if artifacts are left in the queue
that should have been consumed by preceding rules.
*/
func VerifyArtifacts(items []interface{},
itemsMetadata map[string]Metablock) error {
// Verify artifact rules for each item in the layout
for _, itemI := range items {
// The layout item (interface) must be a Link or an Inspection we are only
// interested in the name and the expected materials and products
var itemName string
var expectedMaterials [][]string
var expectedProducts [][]string
switch item := itemI.(type) {
case Step:
itemName = item.Name
expectedMaterials = item.ExpectedMaterials
expectedProducts = item.ExpectedProducts
case Inspection:
itemName = item.Name
expectedMaterials = item.ExpectedMaterials
expectedProducts = item.ExpectedProducts
default: // Something wrong
return fmt.Errorf("VerifyArtifacts received an item of invalid type,"+
" elements of passed slice 'items' must be one of 'Step' or"+
" 'Inspection', got: '%s'", reflect.TypeOf(item))
}
// Use the item's name to extract the corresponding link
srcLinkMb, exists := itemsMetadata[itemName]
if !exists {
return fmt.Errorf("VerifyArtifacts could not find metadata"+
" for item '%s', got: '%s'", itemName, itemsMetadata)
}
// Create shortcuts to materials and products (including hashes) reported
// by the item's link, required to verify "match" rules
materials := srcLinkMb.Signed.(Link).Materials
products := srcLinkMb.Signed.(Link).Products
// All other rules only require the material or product paths (without
// hashes). We extract them from the corresponding maps and store them as
// sets for convenience in further processing
materialPaths := NewSet()
for _, p := range InterfaceKeyStrings(materials) {
materialPaths.Add(path.Clean(p))
}
productPaths := NewSet()
for _, p := range InterfaceKeyStrings(products) {
productPaths.Add(path.Clean(p))
}
// For `create`, `delete` and `modify` rules we prepare sets of artifacts
// (without hashes) that were created, deleted or modified in the current
// step or inspection
created := productPaths.Difference(materialPaths)
deleted := materialPaths.Difference(productPaths)
remained := materialPaths.Intersection(productPaths)
modified := NewSet()
for name := range remained {
if !reflect.DeepEqual(materials[name], products[name]) {
modified.Add(name)
}
}
// For each item we have to run rule verification, once per artifact type.
// Here we prepare the corresponding data for each round.
verificationDataList := []map[string]interface{}{
{
"srcType": "materials",
"rules": expectedMaterials,
"artifacts": materials,
"artifactPaths": materialPaths,
},
{
"srcType": "products",
"rules": expectedProducts,
"artifacts": products,
"artifactPaths": productPaths,
},
}
// TODO: Add logging library (see in-toto/in-toto-golang#4)
// fmt.Printf("Verifying %s '%s' ", reflect.TypeOf(itemI), itemName)
// Process all material rules using the corresponding materials and all
// product rules using the corresponding products
for _, verificationData := range verificationDataList {
// TODO: Add logging library (see in-toto/in-toto-golang#4)
// fmt.Printf("%s...\n", verificationData["srcType"])
rules := verificationData["rules"].([][]string)
artifacts := verificationData["artifacts"].(map[string]interface{})
// Use artifacts (without hashes) as base queue. Each rule only operates
// on artifacts in that queue. If a rule consumes an artifact (i.e. can
// be applied successfully), the artifact is removed from the queue. By
// applying a DISALLOW rule eventually, verification may return an error,
// if the rule matches any artifacts in the queue that should have been
// consumed earlier.
queue := verificationData["artifactPaths"].(Set)
// TODO: Add logging library (see in-toto/in-toto-golang#4)
// fmt.Printf("Initial state\nMaterials: %s\nProducts: %s\nQueue: %s\n\n",
// materialPaths.Slice(), productPaths.Slice(), queue.Slice())
// Verify rules sequentially
for _, rule := range rules {
// Parse rule and error out if it is malformed
// NOTE: the rule format should have been validated before
ruleData, err := UnpackRule(rule)
if err != nil {
return err
}
// Apply rule pattern to filter queued artifacts that are up for rule
// specific consumption
filtered := queue.Filter(path.Clean(ruleData["pattern"]))
var consumed Set
switch ruleData["type"] {
case "match":
// Note: here we need to perform more elaborate filtering
consumed = verifyMatchRule(ruleData, artifacts, queue, itemsMetadata)
case "allow":
// Consumes all filtered artifacts
consumed = filtered
case "create":
// Consumes filtered artifacts that were created
consumed = filtered.Intersection(created)
case "delete":
// Consumes filtered artifacts that were deleted
consumed = filtered.Intersection(deleted)
case "modify":
// Consumes filtered artifacts that were modified
consumed = filtered.Intersection(modified)
case "disallow":
// Does not consume but errors out if artifacts were filtered
if len(filtered) > 0 {
return fmt.Errorf("artifact verification failed for %s '%s',"+
" %s %s disallowed by rule %s",
reflect.TypeOf(itemI).Name(), itemName,
verificationData["srcType"], filtered.Slice(), rule)
}
case "require":
// REQUIRE is somewhat of a weird animal that does not use
// patterns bur rather single filenames (for now).
if !queue.Has(ruleData["pattern"]) {
return fmt.Errorf("artifact verification failed for %s in REQUIRE '%s',"+
" because %s is not in %s", verificationData["srcType"],
ruleData["pattern"], ruleData["pattern"], queue.Slice())
}
}
// Update queue by removing consumed artifacts
queue = queue.Difference(consumed)
// TODO: Add logging library (see in-toto/in-toto-golang#4)
// fmt.Printf("Rule: %s\nQueue: %s\n\n", rule, queue.Slice())
}
}
}
return nil
}
/*
ReduceStepsMetadata merges for each step of the passed Layout all the passed
per-functionary links into a single link, asserting that the reported Materials
and Products are equal across links for a given step. This function may be
used at a time during the overall verification, where link threshold's have
been verified and subsequent verification only needs one exemplary link per
step. The function returns a map with one Metablock (link) per step:
{
<step name> : Metablock,
<step name> : Metablock,
...
}
If links corresponding to the same step report different Materials or different
Products, the first return value is an empty Metablock map and the second
return value is the error.
*/
func ReduceStepsMetadata(layout Layout,
stepsMetadata map[string]map[string]Metablock) (map[string]Metablock,
error) {
stepsMetadataReduced := make(map[string]Metablock)
for _, step := range layout.Steps {
linksPerStep, ok := stepsMetadata[step.Name]
// We should never get here, layout verification must fail earlier
if !ok || len(linksPerStep) < 1 {
panic("Could not reduce metadata for step '" + step.Name +
"', no link metadata found.")
}
// Get the first link (could be any link) for the current step, which will
// serve as reference link for below comparisons
var referenceKeyID string
var referenceLinkMb Metablock
for keyID, linkMb := range linksPerStep {
referenceLinkMb = linkMb
referenceKeyID = keyID
break
}
// Only one link, nothing to reduce, take the reference link
if len(linksPerStep) == 1 {
stepsMetadataReduced[step.Name] = referenceLinkMb
// Multiple links, reduce but first check
} else {
// Artifact maps must be equal for each type among all links
// TODO: What should we do if there are more links, than the
// threshold requires, but not all of them are equal? Right now we would
// also error.
for keyID, linkMb := range linksPerStep {
if !reflect.DeepEqual(linkMb.Signed.(Link).Materials,
referenceLinkMb.Signed.(Link).Materials) ||
!reflect.DeepEqual(linkMb.Signed.(Link).Products,
referenceLinkMb.Signed.(Link).Products) {
return nil, fmt.Errorf("link '%s' and '%s' have different"+
" artifacts",
fmt.Sprintf(LinkNameFormat, step.Name, referenceKeyID),
fmt.Sprintf(LinkNameFormat, step.Name, keyID))
}
}
// We haven't errored out, so we can reduce (i.e take the reference link)
stepsMetadataReduced[step.Name] = referenceLinkMb
}
}
return stepsMetadataReduced, nil
}
/*
VerifyStepCommandAlignment (soft) verifies that for each step of the passed
layout the command executed, as per the passed link, matches the expected
command, as per the layout. Soft verification means that, in case a command
does not align, a warning is issued.
*/
func VerifyStepCommandAlignment(layout Layout,
stepsMetadata map[string]map[string]Metablock) {
for _, step := range layout.Steps {
linksPerStep, ok := stepsMetadata[step.Name]
// We should never get here, layout verification must fail earlier
if !ok || len(linksPerStep) < 1 {
panic("Could not verify command alignment for step '" + step.Name +
"', no link metadata found.")
}
for signerKeyID, linkMb := range linksPerStep {
expectedCommandS := strings.Join(step.ExpectedCommand, " ")
executedCommandS := strings.Join(linkMb.Signed.(Link).Command, " ")
if expectedCommandS != executedCommandS {
linkName := fmt.Sprintf(LinkNameFormat, step.Name, signerKeyID)
fmt.Printf("WARNING: Expected command for step '%s' (%s) and command"+
" reported by '%s' (%s) differ.\n",
step.Name, expectedCommandS, linkName, executedCommandS)
}
}
}
}
/*
LoadLayoutCertificates loads the root and intermediate CAs from the layout if in the layout.
This will be used to check signatures that were used to sign links but not configured
in the PubKeys section of the step. No configured CAs means we don't want to allow this.
Returned CertPools will be empty in this case.
*/
func LoadLayoutCertificates(layout Layout, intermediatePems [][]byte) (*x509.CertPool, *x509.CertPool, error) {
rootPool := x509.NewCertPool()
for _, certPem := range layout.RootCas {
ok := rootPool.AppendCertsFromPEM([]byte(certPem.KeyVal.Certificate))
if !ok {
return nil, nil, fmt.Errorf("failed to load root certificates for layout")
}
}
intermediatePool := x509.NewCertPool()
for _, intermediatePem := range layout.IntermediateCas {
ok := intermediatePool.AppendCertsFromPEM([]byte(intermediatePem.KeyVal.Certificate))
if !ok {
return nil, nil, fmt.Errorf("failed to load intermediate certificates for layout")
}
}
for _, intermediatePem := range intermediatePems {
ok := intermediatePool.AppendCertsFromPEM(intermediatePem)
if !ok {
return nil, nil, fmt.Errorf("failed to load provided intermediate certificates")
}
}
return rootPool, intermediatePool, nil
}
/*
VerifyLinkSignatureThesholds verifies that for each step of the passed layout,
there are at least Threshold links, validly signed by different authorized
functionaries. The returned map of link metadata per steps contains only
links with valid signatures from distinct functionaries and has the format:
{
<step name> : {
<key id>: Metablock,
<key id>: Metablock,
...
},
<step name> : {
<key id>: Metablock,
<key id>: Metablock,
...
}
...
}
If for any step of the layout there are not enough links available, the first
return value is an empty map of Metablock maps and the second return value is
the error.
*/
func VerifyLinkSignatureThesholds(layout Layout,
stepsMetadata map[string]map[string]Metablock, rootCertPool, intermediateCertPool *x509.CertPool) (
map[string]map[string]Metablock, error) {
// This will stores links with valid signature from an authorized functionary
// for all steps
stepsMetadataVerified := make(map[string]map[string]Metablock)
// Try to find enough (>= threshold) links each with a valid signature from
// distinct authorized functionaries for each step
for _, step := range layout.Steps {
var stepErr error
// This will store links with valid signature from an authorized
// functionary for the given step
linksPerStepVerified := make(map[string]Metablock)
// Check if there are any links at all for the given step
linksPerStep, ok := stepsMetadata[step.Name]
if !ok || len(linksPerStep) < 1 {
stepErr = fmt.Errorf("no links found")
}
// For each link corresponding to a step, check that the signer key was
// authorized, the layout contains a verification key and the signature
// verification passes. Only good links are stored, to verify thresholds
// below.
isAuthorizedSignature := false
for signerKeyID, linkMb := range linksPerStep {
for _, authorizedKeyID := range step.PubKeys {
if signerKeyID == authorizedKeyID {
if verifierKey, ok := layout.Keys[authorizedKeyID]; ok {
if err := linkMb.VerifySignature(verifierKey); err == nil {
linksPerStepVerified[signerKeyID] = linkMb
isAuthorizedSignature = true
break
}
}
}
}
// If the signer's key wasn't in our step's pubkeys array, check the cert pool to
// see if the key is known to us.
if !isAuthorizedSignature {
sig, err := linkMb.GetSignatureForKeyID(signerKeyID)
if err != nil {
stepErr = err
continue
}
cert, err := sig.GetCertificate()
if err != nil {
stepErr = err
continue
}
// test certificate against the step's constraints to make sure it's a valid functionary
err = step.CheckCertConstraints(cert, layout.RootCAIDs(), rootCertPool, intermediateCertPool)
if err != nil {
stepErr = err
continue
}
err = linkMb.VerifySignature(cert)
if err != nil {
stepErr = err
continue
}
linksPerStepVerified[signerKeyID] = linkMb
}
}
// Store all good links for a step
stepsMetadataVerified[step.Name] = linksPerStepVerified
if len(linksPerStepVerified) < step.Threshold {
linksPerStep := stepsMetadata[step.Name]
return nil, fmt.Errorf("step '%s' requires '%d' link metadata file(s)."+
" '%d' out of '%d' available link(s) have a valid signature from an"+
" authorized signer: %v", step.Name, step.Threshold,
len(linksPerStepVerified), len(linksPerStep), stepErr)
}
}
return stepsMetadataVerified, nil
}
/*
LoadLinksForLayout loads for every Step of the passed Layout a Metablock
containing the corresponding Link. A base path to a directory that contains
the links may be passed using linkDir. Link file names are constructed,
using LinkNameFormat together with the corresponding step name and authorized
functionary key ids. A map of link metadata is returned and has the following
format:
{
<step name> : {
<key id>: Metablock,
<key id>: Metablock,
...
},
<step name> : {
<key id>: Metablock,
<key id>: Metablock,
...
}
...
}
If a link cannot be loaded at a constructed link name or is invalid, it is
ignored. Only a preliminary threshold check is performed, that is, if there
aren't at least Threshold links for any given step, the first return value
is an empty map of Metablock maps and the second return value is the error.
*/
func LoadLinksForLayout(layout Layout, linkDir string) (map[string]map[string]Metablock, error) {
stepsMetadata := make(map[string]map[string]Metablock)
for _, step := range layout.Steps {
linksPerStep := make(map[string]Metablock)
// Since we can verify against certificates belonging to a CA, we need to
// load any possible links
linkFiles, err := filepath.Glob(osPath.Join(linkDir, fmt.Sprintf(LinkGlobFormat, step.Name)))
if err != nil {
return nil, err
}
for _, linkPath := range linkFiles {
var linkMb Metablock
if err := linkMb.Load(linkPath); err != nil {
continue
}
// To get the full key from the metadata's signatures, we have to check
// for one with the same short id...
signerShortKeyID := strings.TrimSuffix(strings.TrimPrefix(filepath.Base(linkPath), step.Name+"."), ".link")
for _, sig := range linkMb.Signatures {
if strings.HasPrefix(sig.KeyID, signerShortKeyID) {
linksPerStep[sig.KeyID] = linkMb
break
}
}
}
if len(linksPerStep) < step.Threshold {
return nil, fmt.Errorf("step '%s' requires '%d' link metadata file(s),"+
" found '%d'", step.Name, step.Threshold, len(linksPerStep))
}
stepsMetadata[step.Name] = linksPerStep
}
return stepsMetadata, nil
}
/*
VerifyLayoutExpiration verifies that the passed Layout has not expired. It
returns an error if the (zulu) date in the Expires field is in the past.
*/
func VerifyLayoutExpiration(layout Layout) error {
expires, err := time.Parse(ISO8601DateSchema, layout.Expires)
if err != nil {
return err
}
// Uses timezone of expires, i.e. UTC
if time.Until(expires) < 0 {
return fmt.Errorf("layout has expired on '%s'", expires)
}
return nil
}
/*
VerifyLayoutSignatures verifies for each key in the passed key map the
corresponding signature of the Layout in the passed Metablock's Signed field.
Signatures and keys are associated by key id. If the key map is empty, or the
Metablock's Signature field does not have a signature for one or more of the
passed keys, or a matching signature is invalid, an error is returned.
*/
func VerifyLayoutSignatures(layoutMb Metablock,
layoutKeys map[string]Key) error {
if len(layoutKeys) < 1 {
return fmt.Errorf("layout verification requires at least one key")
}
for _, key := range layoutKeys {
if err := layoutMb.VerifySignature(key); err != nil {
return err
}
}
return nil
}
/*
GetSummaryLink merges the materials of the first step (as mentioned in the
layout) and the products of the last step and returns a new link. This link
reports the materials and products and summarizes the overall software supply
chain.
NOTE: The assumption is that the steps mentioned in the layout are to be
performed sequentially. So, the first step mentioned in the layout denotes what
comes into the supply chain and the last step denotes what goes out.
*/
func GetSummaryLink(layout Layout, stepsMetadataReduced map[string]Metablock,
stepName string) (Metablock, error) {
var summaryLink Link
var result Metablock
if len(layout.Steps) > 0 {
firstStepLink := stepsMetadataReduced[layout.Steps[0].Name]
lastStepLink := stepsMetadataReduced[layout.Steps[len(layout.Steps)-1].Name]
summaryLink.Materials = firstStepLink.Signed.(Link).Materials
summaryLink.Name = stepName
summaryLink.Type = firstStepLink.Signed.(Link).Type
summaryLink.Products = lastStepLink.Signed.(Link).Products
summaryLink.ByProducts = lastStepLink.Signed.(Link).ByProducts
// Using the last command of the sublayout as the command
// of the summary link can be misleading. Is it necessary to
// include all the commands executed as part of sublayout?
summaryLink.Command = lastStepLink.Signed.(Link).Command
}
result.Signed = summaryLink
return result, nil
}
/*
VerifySublayouts checks if any step in the supply chain is a sublayout, and if
so, recursively resolves it and replaces it with a summary link summarizing the
steps carried out in the sublayout.
*/
func VerifySublayouts(layout Layout,
stepsMetadataVerified map[string]map[string]Metablock,
superLayoutLinkPath string, intermediatePems [][]byte, lineNormalization bool) (map[string]map[string]Metablock, error) {
for stepName, linkData := range stepsMetadataVerified {
for keyID, metadata := range linkData {
if _, ok := metadata.Signed.(Layout); ok {
layoutKeys := make(map[string]Key)
layoutKeys[keyID] = layout.Keys[keyID]
sublayoutLinkDir := fmt.Sprintf(SublayoutLinkDirFormat,
stepName, keyID)
sublayoutLinkPath := filepath.Join(superLayoutLinkPath,
sublayoutLinkDir)
summaryLink, err := InTotoVerify(metadata, layoutKeys,
sublayoutLinkPath, stepName, make(map[string]string), intermediatePems, lineNormalization)
if err != nil {
return nil, err
}
linkData[keyID] = summaryLink
}
}
}
return stepsMetadataVerified, nil
}
// TODO: find a better way than two helper functions for the replacer op
func substituteParamatersInSlice(replacer *strings.Replacer, slice []string) []string {
newSlice := make([]string, 0)
for _, item := range slice {
newSlice = append(newSlice, replacer.Replace(item))
}
return newSlice
}
func substituteParametersInSliceOfSlices(replacer *strings.Replacer,
slice [][]string) [][]string {
newSlice := make([][]string, 0)
for _, item := range slice {
newSlice = append(newSlice, substituteParamatersInSlice(replacer,
item))
}
return newSlice
}
/*
SubstituteParameters performs parameter substitution in steps and inspections
in the following fields:
- Expected Materials and Expected Products of both
- Run of inspections
- Expected Command of steps
The substitution marker is '{}' and the keyword within the braces is replaced
by a value found in the substitution map passed, parameterDictionary. The
layout with parameters substituted is returned to the calling function.
*/
func SubstituteParameters(layout Layout,
parameterDictionary map[string]string) (Layout, error) {
if len(parameterDictionary) == 0 {
return layout, nil
}
parameters := make([]string, 0)
re := regexp.MustCompile("^[a-zA-Z0-9_-]+$")
for parameter, value := range parameterDictionary {
parameterFormatCheck := re.MatchString(parameter)
if !parameterFormatCheck {
return layout, fmt.Errorf("invalid format for parameter")
}
parameters = append(parameters, "{"+parameter+"}")
parameters = append(parameters, value)
}
replacer := strings.NewReplacer(parameters...)
for i := range layout.Steps {
layout.Steps[i].ExpectedMaterials = substituteParametersInSliceOfSlices(
replacer, layout.Steps[i].ExpectedMaterials)
layout.Steps[i].ExpectedProducts = substituteParametersInSliceOfSlices(
replacer, layout.Steps[i].ExpectedProducts)
layout.Steps[i].ExpectedCommand = substituteParamatersInSlice(replacer,
layout.Steps[i].ExpectedCommand)
}
for i := range layout.Inspect {
layout.Inspect[i].ExpectedMaterials =
substituteParametersInSliceOfSlices(replacer,
layout.Inspect[i].ExpectedMaterials)
layout.Inspect[i].ExpectedProducts =
substituteParametersInSliceOfSlices(replacer,
layout.Inspect[i].ExpectedProducts)
layout.Inspect[i].Run = substituteParamatersInSlice(replacer,
layout.Inspect[i].Run)
}
return layout, nil
}
/*
InTotoVerify can be used to verify an entire software supply chain according to
the in-toto specification. It requires the metadata of the root layout, a map
that contains public keys to verify the root layout signatures, a path to a
directory from where it can load link metadata files, which are treated as
signed evidence for the steps defined in the layout, a step name, and a
paramater dictionary used for parameter substitution. The step name only
matters for sublayouts, where it's important to associate the summary of that
step with a unique name. The verification routine is as follows:
1. Verify layout signature(s) using passed key(s)
2. Verify layout expiration date
3. Substitute parameters in layout
4. Load link metadata files for steps of layout
5. Verify signatures and signature thresholds for steps of layout
6. Verify sublayouts recursively
7. Verify command alignment for steps of layout (only warns)
8. Verify artifact rules for steps of layout
9. Execute inspection commands (generates link metadata for each inspection)
10. Verify artifact rules for inspections of layout
InTotoVerify returns a summary link wrapped in a Metablock object and an error
value. If any of the verification routines fail, verification is aborted and
error is returned. In such an instance, the first value remains an empty
Metablock object.
NOTE: Artifact rules of type "create", "modify"
and "delete" are currently not supported.
*/
func InTotoVerify(layoutMb Metablock, layoutKeys map[string]Key,
linkDir string, stepName string, parameterDictionary map[string]string, intermediatePems [][]byte, lineNormalization bool) (
Metablock, error) {
var summaryLink Metablock
var err error
// Verify root signatures
if err := VerifyLayoutSignatures(layoutMb, layoutKeys); err != nil {
return summaryLink, err
}
// Extract the layout from its Metablock container (for further processing)
layout := layoutMb.Signed.(Layout)
// Verify layout expiration
if err := VerifyLayoutExpiration(layout); err != nil {
return summaryLink, err
}
// Substitute parameters in layout
layout, err = SubstituteParameters(layout, parameterDictionary)
if err != nil {
return summaryLink, err
}
rootCertPool, intermediateCertPool, err := LoadLayoutCertificates(layout, intermediatePems)
if err != nil {
return summaryLink, err
}
// Load links for layout
stepsMetadata, err := LoadLinksForLayout(layout, linkDir)
if err != nil {
return summaryLink, err
}
// Verify link signatures
stepsMetadataVerified, err := VerifyLinkSignatureThesholds(layout,
stepsMetadata, rootCertPool, intermediateCertPool)
if err != nil {
return summaryLink, err
}
// Verify and resolve sublayouts
stepsSublayoutVerified, err := VerifySublayouts(layout,
stepsMetadataVerified, linkDir, intermediatePems, lineNormalization)
if err != nil {
return summaryLink, err
}
// Verify command alignment (WARNING only)
VerifyStepCommandAlignment(layout, stepsSublayoutVerified)
// Given that signature thresholds have been checked above and the rest of
// the relevant link properties, i.e. materials and products, have to be
// exactly equal, we can reduce the map of steps metadata. However, we error
// if the relevant properties are not equal among links of a step.
stepsMetadataReduced, err := ReduceStepsMetadata(layout,
stepsSublayoutVerified)
if err != nil {
return summaryLink, err
}
// Verify artifact rules
if err = VerifyArtifacts(layout.stepsAsInterfaceSlice(),
stepsMetadataReduced); err != nil {
return summaryLink, err
}
inspectionMetadata, err := RunInspections(layout, "", lineNormalization)
if err != nil {
return summaryLink, err
}
// Add steps metadata to inspection metadata, because inspection artifact
// rules may also refer to artifacts reported by step links
for k, v := range stepsMetadataReduced {
inspectionMetadata[k] = v
}
if err = VerifyArtifacts(layout.inspectAsInterfaceSlice(),
inspectionMetadata); err != nil {
return summaryLink, err
}
summaryLink, err = GetSummaryLink(layout, stepsMetadataReduced, stepName)
if err != nil {
return summaryLink, err
}
return summaryLink, nil
}
/*
InTotoVerifyWithDirectory provides the same functionality as IntotoVerify, but
adds the possibility to select a local directory from where the inspections are run.
*/
func InTotoVerifyWithDirectory(layoutMb Metablock, layoutKeys map[string]Key,
linkDir string, runDir string, stepName string, parameterDictionary map[string]string, intermediatePems [][]byte, lineNormalization bool) (
Metablock, error) {
var summaryLink Metablock
var err error
// runDir sanity checks
// check if path exists
info, err := os.Stat(runDir)
if err != nil {
return Metablock{}, err
}
// check if runDir is a symlink
if info.Mode()&os.ModeSymlink == os.ModeSymlink {
return Metablock{}, ErrInspectionRunDirIsSymlink
}
// check if runDir is writable and a directory
err = isWritable(runDir)
if err != nil {
return Metablock{}, err
}
// check if runDir is empty (we do not want to overwrite files)
// We abuse File.Readdirnames for this action.
f, err := os.Open(runDir)
if err != nil {
return Metablock{}, err
}
defer f.Close()
// We use Readdirnames(1) for performance reasons, one child node
// is enough to proof that the directory is not empty
_, err = f.Readdirnames(1)
// if io.EOF gets returned as error the directory is empty
if err == io.EOF {
return Metablock{}, err
}
err = f.Close()
if err != nil {
return Metablock{}, err
}
// Verify root signatures
if err := VerifyLayoutSignatures(layoutMb, layoutKeys); err != nil {
return summaryLink, err
}
// Extract the layout from its Metablock container (for further processing)
layout := layoutMb.Signed.(Layout)
// Verify layout expiration
if err := VerifyLayoutExpiration(layout); err != nil {
return summaryLink, err
}
// Substitute parameters in layout
layout, err = SubstituteParameters(layout, parameterDictionary)
if err != nil {
return summaryLink, err
}
rootCertPool, intermediateCertPool, err := LoadLayoutCertificates(layout, intermediatePems)
if err != nil {
return summaryLink, err
}
// Load links for layout
stepsMetadata, err := LoadLinksForLayout(layout, linkDir)
if err != nil {
return summaryLink, err
}
// Verify link signatures
stepsMetadataVerified, err := VerifyLinkSignatureThesholds(layout,
stepsMetadata, rootCertPool, intermediateCertPool)
if err != nil {
return summaryLink, err
}
// Verify and resolve sublayouts
stepsSublayoutVerified, err := VerifySublayouts(layout,
stepsMetadataVerified, linkDir, intermediatePems, lineNormalization)
if err != nil {
return summaryLink, err
}
// Verify command alignment (WARNING only)
VerifyStepCommandAlignment(layout, stepsSublayoutVerified)
// Given that signature thresholds have been checked above and the rest of
// the relevant link properties, i.e. materials and products, have to be
// exactly equal, we can reduce the map of steps metadata. However, we error
// if the relevant properties are not equal among links of a step.
stepsMetadataReduced, err := ReduceStepsMetadata(layout,
stepsSublayoutVerified)
if err != nil {
return summaryLink, err
}
// Verify artifact rules
if err = VerifyArtifacts(layout.stepsAsInterfaceSlice(),
stepsMetadataReduced); err != nil {
return summaryLink, err
}
inspectionMetadata, err := RunInspections(layout, runDir, lineNormalization)
if err != nil {
return summaryLink, err
}
// Add steps metadata to inspection metadata, because inspection artifact
// rules may also refer to artifacts reported by step links
for k, v := range stepsMetadataReduced {
inspectionMetadata[k] = v
}
if err = VerifyArtifacts(layout.inspectAsInterfaceSlice(),
inspectionMetadata); err != nil {
return summaryLink, err
}
summaryLink, err = GetSummaryLink(layout, stepsMetadataReduced, stepName)
if err != nil {
return summaryLink, err
}
return summaryLink, nil
}