aboutsummaryrefslogtreecommitdiff
path: root/pkg/trust/policy.go
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/trust/policy.go')
-rw-r--r--pkg/trust/policy.go248
1 files changed, 248 insertions, 0 deletions
diff --git a/pkg/trust/policy.go b/pkg/trust/policy.go
new file mode 100644
index 000000000..326fe17af
--- /dev/null
+++ b/pkg/trust/policy.go
@@ -0,0 +1,248 @@
+package trust
+
+import (
+ "bufio"
+ "bytes"
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+
+ "github.com/containers/image/v5/types"
+ "github.com/sirupsen/logrus"
+)
+
+// policyContent is the overall structure of a policy.json file (= c/image/v5/signature.Policy)
+type policyContent struct {
+ Default []repoContent `json:"default"`
+ Transports transportsContent `json:"transports,omitempty"`
+}
+
+// transportsContent contains policies for individual transports (= c/image/v5/signature.Policy.Transports)
+type transportsContent map[string]repoMap
+
+// repoMap maps a scope name to requirements that apply to that scope (= c/image/v5/signature.PolicyTransportScopes)
+type repoMap map[string][]repoContent
+
+// repoContent is a single policy requirement (one of possibly several for a scope), representing all of the individual alternatives in a single merged struct
+// (= c/image/v5/signature.{PolicyRequirement,pr*})
+type repoContent struct {
+ Type string `json:"type"`
+ KeyType string `json:"keyType,omitempty"`
+ KeyPath string `json:"keyPath,omitempty"`
+ KeyPaths []string `json:"keyPaths,omitempty"`
+ KeyData string `json:"keyData,omitempty"`
+ SignedIdentity json.RawMessage `json:"signedIdentity,omitempty"`
+}
+
+// genericPolicyContent is the overall structure of a policy.json file (= c/image/v5/signature.Policy), using generic data for individual requirements.
+type genericPolicyContent struct {
+ Default json.RawMessage `json:"default"`
+ Transports genericTransportsContent `json:"transports,omitempty"`
+}
+
+// genericTransportsContent contains policies for individual transports (= c/image/v5/signature.Policy.Transports), using generic data for individual requirements.
+type genericTransportsContent map[string]genericRepoMap
+
+// genericRepoMap maps a scope name to requirements that apply to that scope (= c/image/v5/signature.PolicyTransportScopes)
+type genericRepoMap map[string]json.RawMessage
+
+// DefaultPolicyPath returns a path to the default policy of the system.
+func DefaultPolicyPath(sys *types.SystemContext) string {
+ systemDefaultPolicyPath := "/etc/containers/policy.json"
+ if sys != nil {
+ if sys.SignaturePolicyPath != "" {
+ return sys.SignaturePolicyPath
+ }
+ if sys.RootForImplicitAbsolutePaths != "" {
+ return filepath.Join(sys.RootForImplicitAbsolutePaths, systemDefaultPolicyPath)
+ }
+ }
+ return systemDefaultPolicyPath
+}
+
+// gpgIDReader returns GPG key IDs of keys stored at the provided path.
+// It exists only for tests, production code should always use getGPGIdFromKeyPath.
+type gpgIDReader func(string) []string
+
+// createTmpFile creates a temp file under dir and writes the content into it
+func createTmpFile(dir, pattern string, content []byte) (string, error) {
+ tmpfile, err := ioutil.TempFile(dir, pattern)
+ if err != nil {
+ return "", err
+ }
+ defer tmpfile.Close()
+
+ if _, err := tmpfile.Write(content); err != nil {
+ return "", err
+ }
+ return tmpfile.Name(), nil
+}
+
+// getGPGIdFromKeyPath returns GPG key IDs of keys stored at the provided path.
+func getGPGIdFromKeyPath(path string) []string {
+ cmd := exec.Command("gpg2", "--with-colons", path)
+ results, err := cmd.Output()
+ if err != nil {
+ logrus.Errorf("Getting key identity: %s", err)
+ return nil
+ }
+ return parseUids(results)
+}
+
+// getGPGIdFromKeyData returns GPG key IDs of keys in the provided keyring.
+func getGPGIdFromKeyData(idReader gpgIDReader, key string) []string {
+ decodeKey, err := base64.StdEncoding.DecodeString(key)
+ if err != nil {
+ logrus.Errorf("%s, error decoding key data", err)
+ return nil
+ }
+ tmpfileName, err := createTmpFile("", "", decodeKey)
+ if err != nil {
+ logrus.Errorf("Creating key date temp file %s", err)
+ }
+ defer os.Remove(tmpfileName)
+ return idReader(tmpfileName)
+}
+
+func parseUids(colonDelimitKeys []byte) []string {
+ var parseduids []string
+ scanner := bufio.NewScanner(bytes.NewReader(colonDelimitKeys))
+ for scanner.Scan() {
+ line := scanner.Text()
+ if strings.HasPrefix(line, "uid:") || strings.HasPrefix(line, "pub:") {
+ uid := strings.Split(line, ":")[9]
+ if uid == "" {
+ continue
+ }
+ parseduid := uid
+ if strings.Contains(uid, "<") && strings.Contains(uid, ">") {
+ parseduid = strings.SplitN(strings.SplitAfterN(uid, "<", 2)[1], ">", 2)[0]
+ }
+ parseduids = append(parseduids, parseduid)
+ }
+ }
+ return parseduids
+}
+
+// getPolicy parses policy.json into policyContent.
+func getPolicy(policyPath string) (policyContent, error) {
+ var policyContentStruct policyContent
+ policyContent, err := ioutil.ReadFile(policyPath)
+ if err != nil {
+ return policyContentStruct, fmt.Errorf("unable to read policy file: %w", err)
+ }
+ if err := json.Unmarshal(policyContent, &policyContentStruct); err != nil {
+ return policyContentStruct, fmt.Errorf("could not parse trust policies from %s: %w", policyPath, err)
+ }
+ return policyContentStruct, nil
+}
+
+var typeDescription = map[string]string{"insecureAcceptAnything": "accept", "signedBy": "signed", "sigstoreSigned": "sigstoreSigned", "reject": "reject"}
+
+func trustTypeDescription(trustType string) string {
+ trustDescription, exist := typeDescription[trustType]
+ if !exist {
+ logrus.Warnf("Invalid trust type %s", trustType)
+ }
+ return trustDescription
+}
+
+// AddPolicyEntriesInput collects some parameters to AddPolicyEntries,
+// primarily so that the callers use named values instead of just strings in a sequence.
+type AddPolicyEntriesInput struct {
+ Scope string // "default" or a docker/atomic scope name
+ Type string
+ PubKeyFiles []string // For signature enforcement types, paths to public keys files (where the image needs to be signed by at least one key from _each_ of the files). File format depends on Type.
+}
+
+// AddPolicyEntries adds one or more policy entries necessary to implement AddPolicyEntriesInput.
+func AddPolicyEntries(policyPath string, input AddPolicyEntriesInput) error {
+ var (
+ policyContentStruct genericPolicyContent
+ newReposContent []repoContent
+ )
+ trustType := input.Type
+ if trustType == "accept" {
+ trustType = "insecureAcceptAnything"
+ }
+ pubkeysfile := input.PubKeyFiles
+
+ // The error messages in validation failures use input.Type instead of trustType to match the user’s input.
+ switch trustType {
+ case "insecureAcceptAnything", "reject":
+ if len(pubkeysfile) != 0 {
+ return fmt.Errorf("%d public keys unexpectedly provided for trust type %v", len(pubkeysfile), input.Type)
+ }
+ newReposContent = append(newReposContent, repoContent{Type: trustType})
+
+ case "signedBy":
+ if len(pubkeysfile) == 0 {
+ return errors.New("at least one public key must be defined for type 'signedBy'")
+ }
+ for _, filepath := range pubkeysfile {
+ newReposContent = append(newReposContent, repoContent{Type: trustType, KeyType: "GPGKeys", KeyPath: filepath})
+ }
+
+ case "sigstoreSigned":
+ if len(pubkeysfile) == 0 {
+ return errors.New("at least one public key must be defined for type 'sigstoreSigned'")
+ }
+ for _, filepath := range pubkeysfile {
+ newReposContent = append(newReposContent, repoContent{Type: trustType, KeyPath: filepath})
+ }
+
+ default:
+ return fmt.Errorf("unknown trust type %q", input.Type)
+ }
+ newReposJSON, err := json.Marshal(newReposContent)
+ if err != nil {
+ return err
+ }
+
+ _, err = os.Stat(policyPath)
+ if !os.IsNotExist(err) {
+ policyContent, err := ioutil.ReadFile(policyPath)
+ if err != nil {
+ return err
+ }
+ if err := json.Unmarshal(policyContent, &policyContentStruct); err != nil {
+ return errors.New("could not read trust policies")
+ }
+ }
+ if input.Scope == "default" {
+ policyContentStruct.Default = json.RawMessage(newReposJSON)
+ } else {
+ if len(policyContentStruct.Default) == 0 {
+ return errors.New("default trust policy must be set")
+ }
+ registryExists := false
+ for transport, transportval := range policyContentStruct.Transports {
+ _, registryExists = transportval[input.Scope]
+ if registryExists {
+ policyContentStruct.Transports[transport][input.Scope] = json.RawMessage(newReposJSON)
+ break
+ }
+ }
+ if !registryExists {
+ if policyContentStruct.Transports == nil {
+ policyContentStruct.Transports = make(map[string]genericRepoMap)
+ }
+ if policyContentStruct.Transports["docker"] == nil {
+ policyContentStruct.Transports["docker"] = make(map[string]json.RawMessage)
+ }
+ policyContentStruct.Transports["docker"][input.Scope] = json.RawMessage(newReposJSON)
+ }
+ }
+
+ data, err := json.MarshalIndent(policyContentStruct, "", " ")
+ if err != nil {
+ return fmt.Errorf("error setting trust policy: %w", err)
+ }
+ return ioutil.WriteFile(policyPath, data, 0644)
+}