Support podman image trust command

Display the trust policy of the host system. The trust policy is stored in the /etc/containers/policy.json file and defines a scope of registries or repositories.

Signed-off-by: Qi Wang <qiwan@redhat.com>
This commit is contained in:
Qi Wang
2018-11-29 09:55:15 -05:00
parent 68414c5ee3
commit 31edf47285
8 changed files with 725 additions and 0 deletions

View File

@ -19,6 +19,7 @@ var (
rmImageCommand, rmImageCommand,
saveCommand, saveCommand,
tagCommand, tagCommand,
trustCommand,
} }
imageDescription = "Manage images" imageDescription = "Manage images"

293
cmd/podman/trust.go Normal file
View File

@ -0,0 +1,293 @@
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"sort"
"github.com/containers/image/types"
"github.com/containers/libpod/cmd/podman/formats"
"github.com/containers/libpod/cmd/podman/libpodruntime"
"github.com/containers/libpod/libpod/image"
"github.com/containers/libpod/pkg/trust"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var (
setTrustFlags = []cli.Flag{
cli.StringFlag{
Name: "type, t",
Usage: "Trust type, accept values: signedBy(default), accept, reject.",
Value: "signedBy",
},
cli.StringSliceFlag{
Name: "pubkeysfile, f",
Usage: `Path of installed public key(s) to trust for TARGET.
Absolute path to keys is added to policy.json. May
used multiple times to define multiple public keys.
File(s) must exist before using this command.`,
},
cli.StringFlag{
Name: "policypath",
Hidden: true,
},
}
showTrustFlags = []cli.Flag{
cli.BoolFlag{
Name: "raw",
Usage: "Output raw policy file",
},
cli.BoolFlag{
Name: "json, j",
Usage: "Output as json",
},
cli.StringFlag{
Name: "policypath",
Hidden: true,
},
cli.StringFlag{
Name: "registrypath",
Hidden: true,
},
}
setTrustDescription = "Set default trust policy or add a new trust policy for a registry"
setTrustCommand = cli.Command{
Name: "set",
Usage: "Set default trust policy or a new trust policy for a registry",
Description: setTrustDescription,
Flags: sortFlags(setTrustFlags),
ArgsUsage: "default | REGISTRY[/REPOSITORY]",
Action: setTrustCmd,
OnUsageError: usageErrorHandler,
}
showTrustDescription = "Display trust policy for the system"
showTrustCommand = cli.Command{
Name: "show",
Usage: "Display trust policy for the system",
Description: showTrustDescription,
Flags: sortFlags(showTrustFlags),
Action: showTrustCmd,
ArgsUsage: "",
UseShortOptionHandling: true,
OnUsageError: usageErrorHandler,
}
trustSubCommands = []cli.Command{
setTrustCommand,
showTrustCommand,
}
trustDescription = fmt.Sprintf(`Manages the trust policy of the host system. (%s)
Trust policy describes a registry scope that must be signed by public keys.`, getDefaultPolicyPath())
trustCommand = cli.Command{
Name: "trust",
Usage: "Manage container image trust policy",
Description: trustDescription,
ArgsUsage: "{set,show} ...",
Subcommands: trustSubCommands,
OnUsageError: usageErrorHandler,
}
)
func showTrustCmd(c *cli.Context) error {
runtime, err := libpodruntime.GetRuntime(c)
if err != nil {
return errors.Wrapf(err, "could not create runtime")
}
var (
policyPath string
systemRegistriesDirPath string
)
if c.IsSet("policypath") {
policyPath = c.String("policypath")
} else {
policyPath = trust.DefaultPolicyPath(runtime.SystemContext())
}
policyContent, err := ioutil.ReadFile(policyPath)
if err != nil {
return errors.Wrapf(err, "unable to read %s", policyPath)
}
if c.IsSet("registrypath") {
systemRegistriesDirPath = c.String("registrypath")
} else {
systemRegistriesDirPath = trust.RegistriesDirPath(runtime.SystemContext())
}
if c.Bool("raw") {
_, err := os.Stdout.Write(policyContent)
if err != nil {
return errors.Wrap(err, "could not read trust policies")
}
return nil
}
var policyContentStruct trust.PolicyContent
if err := json.Unmarshal(policyContent, &policyContentStruct); err != nil {
return errors.Errorf("could not read trust policies")
}
policyJSON, err := trust.GetPolicyJSON(policyContentStruct, systemRegistriesDirPath)
if err != nil {
return errors.Wrapf(err, "error reading registry config file")
}
if c.Bool("json") {
var outjson interface{}
outjson = policyJSON
out := formats.JSONStruct{Output: outjson}
return formats.Writer(out).Out()
}
sortedRepos := sortPolicyJSONKey(policyJSON)
type policydefault struct {
Repo string
Trusttype string
GPGid string
Sigstore string
}
var policyoutput []policydefault
for _, repo := range sortedRepos {
repoval := policyJSON[repo]
var defaultstruct policydefault
defaultstruct.Repo = repo
if repoval["type"] != nil {
defaultstruct.Trusttype = trustTypeDescription(repoval["type"].(string))
}
if repoval["keys"] != nil && len(repoval["keys"].([]string)) > 0 {
defaultstruct.GPGid = trust.GetGPGId(repoval["keys"].([]string))
}
if repoval["sigstore"] != nil {
defaultstruct.Sigstore = repoval["sigstore"].(string)
}
policyoutput = append(policyoutput, defaultstruct)
}
var output []interface{}
for _, ele := range policyoutput {
output = append(output, interface{}(ele))
}
out := formats.StdoutTemplateArray{Output: output, Template: "{{.Repo}}\t{{.Trusttype}}\t{{.GPGid}}\t{{.Sigstore}}"}
return formats.Writer(out).Out()
}
func setTrustCmd(c *cli.Context) error {
runtime, err := libpodruntime.GetRuntime(c)
if err != nil {
return errors.Wrapf(err, "could not create runtime")
}
args := c.Args()
if len(args) != 1 {
return errors.Errorf("default or a registry name must be specified")
}
valid, err := image.IsValidImageURI(args[0])
if err != nil || !valid {
return errors.Wrapf(err, "invalid image uri %s", args[0])
}
trusttype := c.String("type")
if !isValidTrustType(trusttype) {
return errors.Errorf("invalid choice: %s (choose from 'accept', 'reject', 'signedBy')", trusttype)
}
if trusttype == "accept" {
trusttype = "insecureAcceptAnything"
}
pubkeysfile := c.StringSlice("pubkeysfile")
if len(pubkeysfile) == 0 && trusttype == "signedBy" {
return errors.Errorf("At least one public key must be defined for type 'signedBy'")
}
var policyPath string
if c.IsSet("policypath") {
policyPath = c.String("policypath")
} else {
policyPath = trust.DefaultPolicyPath(runtime.SystemContext())
}
var policyContentStruct trust.PolicyContent
_, err = os.Stat(policyPath)
if !os.IsNotExist(err) {
policyContent, err := ioutil.ReadFile(policyPath)
if err != nil {
return errors.Wrapf(err, "unable to read %s", policyPath)
}
if err := json.Unmarshal(policyContent, &policyContentStruct); err != nil {
return errors.Errorf("could not read trust policies")
}
}
var newReposContent []trust.RepoContent
if len(pubkeysfile) != 0 {
for _, filepath := range pubkeysfile {
newReposContent = append(newReposContent, trust.RepoContent{Type: trusttype, KeyType: "GPGKeys", KeyPath: filepath})
}
} else {
newReposContent = append(newReposContent, trust.RepoContent{Type: trusttype})
}
if args[0] == "default" {
policyContentStruct.Default = newReposContent
} else {
exists := false
for transport, transportval := range policyContentStruct.Transports {
_, exists = transportval[args[0]]
if exists {
policyContentStruct.Transports[transport][args[0]] = newReposContent
break
}
}
if !exists {
if policyContentStruct.Transports == nil {
policyContentStruct.Transports = make(map[string]trust.RepoMap)
}
if policyContentStruct.Transports["docker"] == nil {
policyContentStruct.Transports["docker"] = make(map[string][]trust.RepoContent)
}
policyContentStruct.Transports["docker"][args[0]] = append(policyContentStruct.Transports["docker"][args[0]], newReposContent...)
}
}
data, err := json.MarshalIndent(policyContentStruct, "", " ")
if err != nil {
return errors.Wrapf(err, "error setting trust policy")
}
err = ioutil.WriteFile(policyPath, data, 0644)
if err != nil {
return errors.Wrapf(err, "error setting trust policy")
}
return nil
}
var typeDescription = map[string]string{"insecureAcceptAnything": "accept", "signedBy": "signed", "reject": "reject"}
func trustTypeDescription(trustType string) string {
trustDescription, exist := typeDescription[trustType]
if !exist {
logrus.Warnf("invalid trust type %s", trustType)
}
return trustDescription
}
func sortPolicyJSONKey(m map[string]map[string]interface{}) []string {
keys := make([]string, len(m))
i := 0
for k := range m {
keys[i] = k
i++
}
sort.Strings(keys)
return keys
}
func isValidTrustType(t string) bool {
if t == "accept" || t == "insecureAcceptAnything" || t == "reject" || t == "signedBy" {
return true
}
return false
}
func getDefaultPolicyPath() string {
return trust.DefaultPolicyPath(&types.SystemContext{})
}

View File

@ -0,0 +1,81 @@
% podman-image-trust "1"
# NAME
podman\-trust - Manage container image trust policy
# SYNOPSIS
**podman image trust set|show**
[**-h**|**--help**]
[**-j**|**--json**]
[**--raw**]
[**-f**|**--pubkeysfile** KEY1 [**f**|**--pubkeysfile** KEY2,...]]
[**-t**|**--type** signedBy|accept|reject]
REGISTRY[/REPOSITORY]
# DESCRIPTION
Manages the trust policy of the host system. Trust policy describes
a registry scope (registry and/or repository) that must be signed by public keys. Trust
is defined in **/etc/containers/policy.json**. Trust is enforced when a user attempts to pull
an image from a registry.
Trust scope is evaluated by most specific to least specific. In other words, policy may
be defined for an entire registry, but refined for a particular repository in that
registry. See below for examples.
Trust **type** provides a way to whitelist ("accept") or blacklist
("reject") registries.
Trust may be updated using the command **podman image trust set** for an existing trust scope.
# OPTIONS
**-h** **--help**
Print usage statement.
**-f** **--pubkeysfile**
A path to an exported public key on the local system. Key paths
will be referenced in policy.json. Any path may be used but path
**/etc/pki/containers** is recommended. Option may be used multiple times to
require an image be sigend by multiple keys. One of **--pubkeys** or
**--pubkeysfile** is required for **signedBy** type.
**-t** **--type**
The trust type for this policy entry. Accepted values:
**signedBy** (default): Require signatures with corresponding list of
public keys
**accept**: do not require any signatures for this
registry scope
**reject**: do not accept images for this registry scope
# show OPTIONS
**--raw**
Output trust policy file as raw JSON
**-j** **--json**
Output trust as JSON for machine parsing
# EXAMPLES
Accept all unsigned images from a registry
podman image trust set --type accept docker.io
Modify default trust policy
podman image trust set -t reject default
Display system trust policy
podman image trust show
Display trust policy file
podman image trust show --raw
Display trust as JSON
podman image trust show --json
# HISTORY
December 2018, originally compiled by Qi Wang (qiwan at redhat dot com)

View File

@ -26,6 +26,7 @@ The image command allows you to manage images
| rm | [podman-rm(1)](podman-rmi.1.md) | Removes one or more locally stored images. | | rm | [podman-rm(1)](podman-rmi.1.md) | Removes one or more locally stored images. |
| save | [podman-save(1)](podman-save.1.md) | Save an image to docker-archive or oci. | | save | [podman-save(1)](podman-save.1.md) | Save an image to docker-archive or oci. |
| tag | [podman-tag(1)](podman-tag.1.md) | Add an additional name to a local image. | | tag | [podman-tag(1)](podman-tag.1.md) | Add an additional name to a local image. |
| trust | [podman-image-trust(1)](podman-image-trust.1.md) | Manage container image trust policy.
## SEE ALSO ## SEE ALSO
podman podman

View File

@ -2,6 +2,8 @@ package image
import ( import (
"io" "io"
"net/url"
"regexp"
"strings" "strings"
cp "github.com/containers/image/copy" cp "github.com/containers/image/copy"
@ -117,3 +119,23 @@ func GetAdditionalTags(images []string) ([]reference.NamedTagged, error) {
} }
return allTags, nil return allTags, nil
} }
// IsValidImageURI checks if image name has valid format
func IsValidImageURI(imguri string) (bool, error) {
uri := "http://" + imguri
u, err := url.Parse(uri)
if err != nil {
return false, errors.Wrapf(err, "invalid image uri: %s", imguri)
}
reg := regexp.MustCompile(`^[a-zA-Z0-9-_\.]+\/?:?[0-9]*[a-z0-9-\/:]*$`)
ret := reg.FindAllString(u.Host, -1)
if len(ret) == 0 {
return false, errors.Wrapf(err, "invalid image uri: %s", imguri)
}
reg = regexp.MustCompile(`^[a-z0-9-:\./]*$`)
ret = reg.FindAllString(u.Fragment, -1)
if len(ret) == 0 {
return false, errors.Wrapf(err, "invalid image uri: %s", imguri)
}
return true, nil
}

View File

@ -877,3 +877,8 @@ func (r *Runtime) generateName() (string, error) {
func (r *Runtime) ImageRuntime() *image.Runtime { func (r *Runtime) ImageRuntime() *image.Runtime {
return r.imageRuntime return r.imageRuntime
} }
// SystemContext returns the imagecontext
func (r *Runtime) SystemContext() *types.SystemContext {
return r.imageContext
}

250
pkg/trust/trust.go Normal file
View File

@ -0,0 +1,250 @@
package trust
import (
"bufio"
"encoding/base64"
"encoding/json"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"unsafe"
"github.com/containers/image/types"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
yaml "gopkg.in/yaml.v2"
)
// PolicyContent struct for policy.json file
type PolicyContent struct {
Default []RepoContent `json:"default"`
Transports TransportsContent `json:"transports"`
}
// RepoContent struct used under each repo
type RepoContent struct {
Type string `json:"type"`
KeyType string `json:"keyType,omitempty"`
KeyPath string `json:"keyPath,omitempty"`
KeyData string `json:"keyData,omitempty"`
SignedIdentity json.RawMessage `json:"signedIdentity,omitempty"`
}
// RepoMap map repo name to policycontent for each repo
type RepoMap map[string][]RepoContent
// TransportsContent struct for content under "transports"
type TransportsContent map[string]RepoMap
// RegistryConfiguration is one of the files in registriesDirPath configuring lookaside locations, or the result of merging them all.
// NOTE: Keep this in sync with docs/registries.d.md!
type RegistryConfiguration struct {
DefaultDocker *RegistryNamespace `json:"default-docker"`
// The key is a namespace, using fully-expanded Docker reference format or parent namespaces (per dockerReference.PolicyConfiguration*),
Docker map[string]RegistryNamespace `json:"docker"`
}
// RegistryNamespace defines lookaside locations for a single namespace.
type RegistryNamespace struct {
SigStore string `json:"sigstore"` // For reading, and if SigStoreStaging is not present, for writing.
SigStoreStaging string `json:"sigstore-staging"` // For writing only.
}
// 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
}
// RegistriesDirPath returns a path to registries.d
func RegistriesDirPath(sys *types.SystemContext) string {
systemRegistriesDirPath := "/etc/containers/registries.d"
if sys != nil {
if sys.RegistriesDirPath != "" {
return sys.RegistriesDirPath
}
if sys.RootForImplicitAbsolutePaths != "" {
return filepath.Join(sys.RootForImplicitAbsolutePaths, systemRegistriesDirPath)
}
}
return systemRegistriesDirPath
}
// LoadAndMergeConfig loads configuration files in dirPath
func LoadAndMergeConfig(dirPath string) (*RegistryConfiguration, error) {
mergedConfig := RegistryConfiguration{Docker: map[string]RegistryNamespace{}}
dockerDefaultMergedFrom := ""
nsMergedFrom := map[string]string{}
dir, err := os.Open(dirPath)
if err != nil {
if os.IsNotExist(err) {
return &mergedConfig, nil
}
return nil, err
}
configNames, err := dir.Readdirnames(0)
if err != nil {
return nil, err
}
for _, configName := range configNames {
if !strings.HasSuffix(configName, ".yaml") {
continue
}
configPath := filepath.Join(dirPath, configName)
configBytes, err := ioutil.ReadFile(configPath)
if err != nil {
return nil, err
}
var config RegistryConfiguration
err = yaml.Unmarshal(configBytes, &config)
if err != nil {
return nil, errors.Wrapf(err, "Error parsing %s", configPath)
}
if config.DefaultDocker != nil {
if mergedConfig.DefaultDocker != nil {
return nil, errors.Errorf(`Error parsing signature storage configuration: "default-docker" defined both in "%s" and "%s"`,
dockerDefaultMergedFrom, configPath)
}
mergedConfig.DefaultDocker = config.DefaultDocker
dockerDefaultMergedFrom = configPath
}
for nsName, nsConfig := range config.Docker { // includes config.Docker == nil
if _, ok := mergedConfig.Docker[nsName]; ok {
return nil, errors.Errorf(`Error parsing signature storage configuration: "docker" namespace "%s" defined both in "%s" and "%s"`,
nsName, nsMergedFrom[nsName], configPath)
}
mergedConfig.Docker[nsName] = nsConfig
nsMergedFrom[nsName] = configPath
}
}
return &mergedConfig, nil
}
// HaveMatchRegistry checks if trust settings for the registry have been configed in yaml file
func HaveMatchRegistry(key string, registryConfigs *RegistryConfiguration) *RegistryNamespace {
searchKey := key
if !strings.Contains(searchKey, "/") {
val, exists := registryConfigs.Docker[searchKey]
if exists {
return &val
}
}
for range strings.Split(key, "/") {
val, exists := registryConfigs.Docker[searchKey]
if exists {
return &val
}
if strings.Contains(searchKey, "/") {
searchKey = searchKey[:strings.LastIndex(searchKey, "/")]
}
}
return nil
}
// 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
}
// GetGPGId return GPG identity, either bracketed <email> or ID string
// comma separated if more than one key
func GetGPGId(keys []string) string {
for _, k := range keys {
if _, err := os.Stat(k); err != nil {
decodeKey, err := base64.StdEncoding.DecodeString(k)
if err != nil {
logrus.Warnf("error decoding key data")
continue
}
tmpfileName, err := CreateTmpFile("/run/", "", decodeKey)
if err != nil {
logrus.Warnf("error creating key date temp file %s", err)
}
defer os.Remove(tmpfileName)
k = tmpfileName
}
cmd := exec.Command("gpg2", "--with-colons", k)
results, err := cmd.Output()
if err != nil {
logrus.Warnf("error get key identity: %s", err)
continue
}
resultsStr := *(*string)(unsafe.Pointer(&results))
scanner := bufio.NewScanner(strings.NewReader(resultsStr))
var parseduids []string
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 strings.Join(parseduids, ",")
}
return ""
}
// GetPolicyJSON return the struct to show policy.json in json format
func GetPolicyJSON(policyContentStruct PolicyContent, systemRegistriesDirPath string) (map[string]map[string]interface{}, error) {
registryConfigs, err := LoadAndMergeConfig(systemRegistriesDirPath)
if err != nil {
return nil, err
}
policyJSON := make(map[string]map[string]interface{})
if len(policyContentStruct.Default) > 0 {
policyJSON["* (default)"] = make(map[string]interface{})
policyJSON["* (default)"]["type"] = policyContentStruct.Default[0].Type
}
for transname, transval := range policyContentStruct.Transports {
for repo, repoval := range transval {
policyJSON[repo] = make(map[string]interface{})
policyJSON[repo]["type"] = repoval[0].Type
policyJSON[repo]["transport"] = transname
for _, repoele := range repoval {
keyarr := []string{}
if len(repoele.KeyPath) > 0 {
keyarr = append(keyarr, repoele.KeyPath)
}
if len(repoele.KeyData) > 0 {
keyarr = append(keyarr, string(repoele.KeyData))
}
policyJSON[repo]["keys"] = keyarr
}
policyJSON[repo]["sigstore"] = ""
registryNamespace := HaveMatchRegistry(repo, registryConfigs)
if registryNamespace != nil {
policyJSON[repo]["sigstore"] = registryNamespace.SigStore
}
}
}
return policyJSON, nil
}

72
test/e2e/trust_test.go Normal file
View File

@ -0,0 +1,72 @@
package integration
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
. "github.com/containers/libpod/test/utils"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("Podman trust", func() {
var (
tempdir string
err error
podmanTest *PodmanTestIntegration
)
BeforeEach(func() {
tempdir, err = CreateTempDirInTempDir()
if err != nil {
os.Exit(1)
}
podmanTest = PodmanTestCreate(tempdir)
podmanTest.RestoreAllArtifacts()
})
AfterEach(func() {
podmanTest.Cleanup()
f := CurrentGinkgoTestDescription()
timedResult := fmt.Sprintf("Test: %s completed in %f seconds", f.TestText, f.Duration.Seconds())
GinkgoWriter.Write([]byte(timedResult))
})
It("podman image trust show", func() {
path, err := os.Getwd()
if err != nil {
os.Exit(1)
}
session := podmanTest.Podman([]string{"image", "trust", "show", "--registrypath", filepath.Dir(path), "--policypath", filepath.Join(filepath.Dir(path), "policy.json")})
session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Equal(0))
outArray := session.OutputToStringArray()
Expect(len(outArray)).To(Equal(3))
Expect(outArray[0]).Should(ContainSubstring("accept"))
Expect(outArray[1]).Should(ContainSubstring("reject"))
Expect(outArray[2]).Should(ContainSubstring("signed"))
})
It("podman image trust set", func() {
path, err := os.Getwd()
if err != nil {
os.Exit(1)
}
session := podmanTest.Podman([]string{"image", "trust", "set", "--policypath", filepath.Join(filepath.Dir(path), "trust_set_test.json"), "-t", "accept", "default"})
session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Equal(0))
var teststruct map[string][]map[string]string
policyContent, err := ioutil.ReadFile(filepath.Join(filepath.Dir(path), "trust_set_test.json"))
if err != nil {
os.Exit(1)
}
err = json.Unmarshal(policyContent, &teststruct)
if err != nil {
os.Exit(1)
}
Expect(teststruct["default"][0]["type"]).To(Equal("insecureAcceptAnything"))
})
})