github.com/apptainer/singularity@v3.1.1+incompatible/internal/pkg/syecl/syecl.go (about)

     1  // Copyright (c) 2018, Sylabs Inc. All rights reserved.
     2  // This software is licensed under a 3-clause BSD license. Please consult the
     3  // LICENSE.md file distributed with the sources of this project regarding your
     4  // rights to use or distribute this software.
     5  
     6  // Package syecl implements the loading and management of the container
     7  // execution control list feature. This code uses the TOML config file standard
     8  // to extract the structured configuration for activating or disabling the list
     9  // and for the implementation of the execution groups.
    10  package syecl
    11  
    12  import (
    13  	"encoding/hex"
    14  	"fmt"
    15  	"io/ioutil"
    16  	"os"
    17  	"path/filepath"
    18  
    19  	toml "github.com/pelletier/go-toml"
    20  	"github.com/sylabs/singularity/pkg/signing"
    21  )
    22  
    23  // EclConfig describes the structure of an execution control list configuration file
    24  type EclConfig struct {
    25  	Activated  bool        `toml:"activated"` // toggle the activation of the ECL rules
    26  	ExecGroups []execgroup `toml:"execgroup"` // Slice of all execution groups
    27  }
    28  
    29  // execgroup describes an execution group, the main unit of configuration:
    30  //	TagName: a descriptive identifier
    31  //	ListMode: whether the execgroup follows a whitelist, whitestrict or blacklist model
    32  //		whitelist: one or more KeyFP's present and verified,
    33  //		whitestrict: all KeyFP's present and verified,
    34  //		blacklist: none of the KeyFP should be present
    35  //	DirPath: containers must be stored in this directory path
    36  //	KeyFPs: list of Key Fingerprints of entities to verify
    37  type execgroup struct {
    38  	TagName  string   `toml:"tagname"`
    39  	ListMode string   `toml:"mode"`
    40  	DirPath  string   `toml:"dirpath"`
    41  	KeyFPs   []string `toml:"keyfp"`
    42  }
    43  
    44  // LoadConfig opens an ECL config file and unmarshals it into structures
    45  func LoadConfig(confPath string) (ecl EclConfig, err error) {
    46  	// read in the ECL config file
    47  	b, err := ioutil.ReadFile(confPath)
    48  	if err != nil {
    49  		return
    50  	}
    51  
    52  	// Unmarshal config file
    53  	err = toml.Unmarshal(b, &ecl)
    54  	return
    55  }
    56  
    57  // PutConfig takes the content of an EclConfig struct and Marshals it to file
    58  func PutConfig(ecl EclConfig, confPath string) (err error) {
    59  	data, err := toml.Marshal(ecl)
    60  	if err != nil {
    61  		return
    62  	}
    63  
    64  	return ioutil.WriteFile(confPath, data, 0600)
    65  }
    66  
    67  // ValidateConfig makes sure paths from configs are fully resolved and that
    68  // values from an execgroup are logically correct.
    69  func (ecl *EclConfig) ValidateConfig() (err error) {
    70  	m := map[string]bool{}
    71  
    72  	for _, v := range ecl.ExecGroups {
    73  		if m[v.DirPath] {
    74  			return fmt.Errorf("a specific dirpath can only appear in one execgroup: %s", v.DirPath)
    75  		}
    76  		m[v.DirPath] = true
    77  
    78  		// if we allow containers everywhere, don't test dirpath constraint
    79  		if v.DirPath != "" {
    80  			path, err := filepath.EvalSymlinks(v.DirPath)
    81  			if err != nil {
    82  				return err
    83  			}
    84  			abs, err := filepath.Abs(path)
    85  			if err != nil {
    86  				return err
    87  			}
    88  			if v.DirPath != abs {
    89  				return fmt.Errorf("all execgroup dirpath`s should be fully cleaned with symlinks resolved")
    90  			}
    91  		}
    92  		if v.ListMode != "whitelist" && v.ListMode != "whitestrict" && v.ListMode != "blacklist" {
    93  			return fmt.Errorf("the mode field can only be either: whitelist, whitestrict, blacklist")
    94  		}
    95  		for _, k := range v.KeyFPs {
    96  			decoded, err := hex.DecodeString(k)
    97  			if err != nil || len(decoded) != 20 {
    98  				return fmt.Errorf("expecting a 40 chars hex fingerprint string")
    99  			}
   100  		}
   101  	}
   102  	return
   103  }
   104  
   105  // checkWhiteList evaluates authorization by requiring at least 1 entity
   106  func checkWhiteList(fp *os.File, egroup *execgroup) (ok bool, err error) {
   107  	// get all signing entities fingerprints on the primary partition
   108  	keyfps, err := signing.GetSignEntitiesFp(fp)
   109  	if err != nil {
   110  		return
   111  	}
   112  	// was the primary partition signed by an authorized entity?
   113  	for _, v := range egroup.KeyFPs {
   114  		for _, u := range keyfps {
   115  			if v == u {
   116  				ok = true
   117  			}
   118  		}
   119  	}
   120  	if !ok {
   121  		return false, fmt.Errorf("%s is not signed by required entities", fp.Name())
   122  	}
   123  
   124  	return true, nil
   125  }
   126  
   127  // checkWhiteStrict evaluates authorization by requiring all entities
   128  func checkWhiteStrict(fp *os.File, egroup *execgroup) (ok bool, err error) {
   129  	// get all signing entities fingerprints on the primary partition
   130  	keyfps, err := signing.GetSignEntitiesFp(fp)
   131  	if err != nil {
   132  		return
   133  	}
   134  
   135  	// was the primary partition signed by all authorized entity?
   136  	m := map[string]bool{}
   137  	for _, v := range egroup.KeyFPs {
   138  		m[v] = false
   139  		for _, u := range keyfps {
   140  			if v == u {
   141  				m[v] = true
   142  			}
   143  		}
   144  	}
   145  	for _, v := range m {
   146  		if v != true {
   147  			return false, fmt.Errorf("%s is not signed by required entities", fp.Name())
   148  		}
   149  	}
   150  
   151  	return true, nil
   152  }
   153  
   154  // checkBlackList evaluates authorization by requiring all entities to be absent
   155  func checkBlackList(fp *os.File, egroup *execgroup) (ok bool, err error) {
   156  	// get all signing entities fingerprints on the primary partition
   157  	keyfps, err := signing.GetSignEntitiesFp(fp)
   158  	if err != nil {
   159  		return
   160  	}
   161  	// was the primary partition signed by an authorized entity?
   162  	for _, v := range egroup.KeyFPs {
   163  		for _, u := range keyfps {
   164  			if v == u {
   165  				return false, fmt.Errorf("%s is signed by a forbidden entity", fp.Name())
   166  			}
   167  		}
   168  	}
   169  
   170  	return true, nil
   171  }
   172  
   173  func shouldRun(ecl *EclConfig, fp *os.File) (ok bool, err error) {
   174  	var egroup *execgroup
   175  
   176  	// look what execgroup a container is part of
   177  	for _, v := range ecl.ExecGroups {
   178  		if filepath.Dir(fp.Name()) == v.DirPath {
   179  			egroup = &v
   180  			break
   181  		}
   182  	}
   183  	// go back at it and this time look for an empty dirpath execgroup to fallback into
   184  	if egroup == nil {
   185  		for _, v := range ecl.ExecGroups {
   186  			if v.DirPath == "" {
   187  				egroup = &v
   188  				break
   189  			}
   190  		}
   191  	}
   192  
   193  	if egroup == nil {
   194  		return false, fmt.Errorf("%s not part of any execgroup", fp.Name())
   195  	}
   196  
   197  	switch egroup.ListMode {
   198  	case "whitelist":
   199  		return checkWhiteList(fp, egroup)
   200  	case "whitestrict":
   201  		return checkWhiteStrict(fp, egroup)
   202  	case "blacklist":
   203  		return checkBlackList(fp, egroup)
   204  	}
   205  
   206  	return false, fmt.Errorf("ECL config file invalid")
   207  }
   208  
   209  // ShouldRun determines if a container should run according to its execgroup rules
   210  func (ecl *EclConfig) ShouldRun(cpath string) (ok bool, err error) {
   211  	// look if ECL rules are activated
   212  	if ecl.Activated == false {
   213  		return true, nil
   214  	}
   215  
   216  	fp, err := os.Open(cpath)
   217  	if err != nil {
   218  		return false, err
   219  	}
   220  
   221  	return shouldRun(ecl, fp)
   222  }
   223  
   224  // ShouldRunFp determines if an already opened container should run according to its execgroup rules
   225  func (ecl *EclConfig) ShouldRunFp(fp *os.File) (ok bool, err error) {
   226  	// look if ECL rules are activated
   227  	if ecl.Activated == false {
   228  		return true, nil
   229  	}
   230  
   231  	return shouldRun(ecl, fp)
   232  }