Merge pull request #15466 from mtrmac/image-trust-sigstore
podman image trust overhaul, incl. sigstore
This commit is contained in:
commit
bb7ae54ef7
|
@ -53,7 +53,7 @@ File(s) must exist before using this command`)
|
|||
}
|
||||
|
||||
func setTrust(cmd *cobra.Command, args []string) error {
|
||||
validTrustTypes := []string{"accept", "insecureAcceptAnything", "reject", "signedBy"}
|
||||
validTrustTypes := []string{"accept", "insecureAcceptAnything", "reject", "signedBy", "sigstoreSigned"}
|
||||
|
||||
valid, err := isValidImageURI(args[0])
|
||||
if err != nil || !valid {
|
||||
|
@ -61,7 +61,7 @@ func setTrust(cmd *cobra.Command, args []string) error {
|
|||
}
|
||||
|
||||
if !util.StringInSlice(setOptions.Type, validTrustTypes) {
|
||||
return fmt.Errorf("invalid choice: %s (choose from 'accept', 'reject', 'signedBy')", setOptions.Type)
|
||||
return fmt.Errorf("invalid choice: %s (choose from 'accept', 'reject', 'signedBy', 'sigstoreSigned')", setOptions.Type)
|
||||
}
|
||||
return registry.ImageEngine().SetTrust(registry.Context(), args, setOptions)
|
||||
}
|
||||
|
|
|
@ -32,7 +32,8 @@ Trust **type** provides a way to:
|
|||
|
||||
Allowlist ("accept") or
|
||||
Denylist ("reject") registries or
|
||||
Require signature (“signedBy”).
|
||||
Require a simple signing signature (“signedBy”),
|
||||
Require a sigstore signature ("sigstoreSigned").
|
||||
|
||||
Trust may be updated using the command **podman image trust set** for an existing trust scope.
|
||||
|
||||
|
@ -45,12 +46,14 @@ Trust may be updated using the command **podman image trust set** for an existin
|
|||
#### **--pubkeysfile**, **-f**=*KEY1*
|
||||
A path to an exported public key on the local system. Key paths
|
||||
will be referenced in policy.json. Any path to a file may be used but locating the file in **/etc/pki/containers** is recommended. Options may be used multiple times to
|
||||
require an image be signed by multiple keys. The **--pubkeysfile** option is required for the **signedBy** type.
|
||||
require an image be signed by multiple keys. The **--pubkeysfile** option is required for the **signedBy** and **sigstoreSigned** types.
|
||||
|
||||
#### **--type**, **-t**=*value*
|
||||
The trust type for this policy entry.
|
||||
Accepted values:
|
||||
**signedBy** (default): Require signatures with corresponding list of
|
||||
**signedBy** (default): Require simple signing signatures with corresponding list of
|
||||
public keys
|
||||
**sigstoreSigned**: Require sigstore signatures with corresponding list of
|
||||
public keys
|
||||
**accept**: do not require any signatures for this
|
||||
registry scope
|
||||
|
|
|
@ -2,16 +2,11 @@ package abi
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/containers/podman/v4/pkg/domain/entities"
|
||||
"github.com/containers/podman/v4/pkg/trust"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (ir *ImageEngine) ShowTrust(ctx context.Context, args []string, options entities.ShowTrustOptions) (*entities.ShowTrustReport, error) {
|
||||
|
@ -34,11 +29,7 @@ func (ir *ImageEngine) ShowTrust(ctx context.Context, args []string, options ent
|
|||
if len(options.RegistryPath) > 0 {
|
||||
report.SystemRegistriesDirPath = options.RegistryPath
|
||||
}
|
||||
policyContentStruct, err := trust.GetPolicy(policyPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read trust policies: %w", err)
|
||||
}
|
||||
report.Policies, err = getPolicyShowOutput(policyContentStruct, report.SystemRegistriesDirPath)
|
||||
report.Policies, err = trust.PolicyDescription(policyPath, report.SystemRegistriesDirPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not show trust policies: %w", err)
|
||||
}
|
||||
|
@ -46,133 +37,19 @@ func (ir *ImageEngine) ShowTrust(ctx context.Context, args []string, options ent
|
|||
}
|
||||
|
||||
func (ir *ImageEngine) SetTrust(ctx context.Context, args []string, options entities.SetTrustOptions) error {
|
||||
var (
|
||||
policyContentStruct trust.PolicyContent
|
||||
newReposContent []trust.RepoContent
|
||||
)
|
||||
trustType := options.Type
|
||||
if trustType == "accept" {
|
||||
trustType = "insecureAcceptAnything"
|
||||
}
|
||||
|
||||
pubkeysfile := options.PubKeysFile
|
||||
if len(pubkeysfile) == 0 && trustType == "signedBy" {
|
||||
return errors.New("at least one public key must be defined for type 'signedBy'")
|
||||
if len(args) != 1 {
|
||||
return fmt.Errorf("SetTrust called with unexpected %d args", len(args))
|
||||
}
|
||||
scope := args[0]
|
||||
|
||||
policyPath := trust.DefaultPolicyPath(ir.Libpod.SystemContext())
|
||||
if len(options.PolicyPath) > 0 {
|
||||
policyPath = options.PolicyPath
|
||||
}
|
||||
_, 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 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 {
|
||||
if len(policyContentStruct.Default) == 0 {
|
||||
return errors.New("default trust policy must be set")
|
||||
}
|
||||
registryExists := false
|
||||
for transport, transportval := range policyContentStruct.Transports {
|
||||
_, registryExists = transportval[args[0]]
|
||||
if registryExists {
|
||||
policyContentStruct.Transports[transport][args[0]] = newReposContent
|
||||
break
|
||||
}
|
||||
}
|
||||
if !registryExists {
|
||||
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 fmt.Errorf("error setting trust policy: %w", err)
|
||||
}
|
||||
return ioutil.WriteFile(policyPath, data, 0644)
|
||||
}
|
||||
|
||||
func getPolicyShowOutput(policyContentStruct trust.PolicyContent, systemRegistriesDirPath string) ([]*trust.Policy, error) {
|
||||
var output []*trust.Policy
|
||||
|
||||
registryConfigs, err := trust.LoadAndMergeConfig(systemRegistriesDirPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(policyContentStruct.Default) > 0 {
|
||||
defaultPolicyStruct := trust.Policy{
|
||||
Transport: "all",
|
||||
Name: "* (default)",
|
||||
RepoName: "default",
|
||||
Type: trustTypeDescription(policyContentStruct.Default[0].Type),
|
||||
}
|
||||
output = append(output, &defaultPolicyStruct)
|
||||
}
|
||||
for transport, transval := range policyContentStruct.Transports {
|
||||
if transport == "docker" {
|
||||
transport = "repository"
|
||||
}
|
||||
|
||||
for repo, repoval := range transval {
|
||||
tempTrustShowOutput := trust.Policy{
|
||||
Name: repo,
|
||||
RepoName: repo,
|
||||
Transport: transport,
|
||||
Type: trustTypeDescription(repoval[0].Type),
|
||||
}
|
||||
// TODO - keyarr is not used and I don't know its intent; commenting out for now for someone to fix later
|
||||
// keyarr := []string{}
|
||||
uids := []string{}
|
||||
for _, repoele := range repoval {
|
||||
if len(repoele.KeyPath) > 0 {
|
||||
// keyarr = append(keyarr, repoele.KeyPath)
|
||||
uids = append(uids, trust.GetGPGIdFromKeyPath(repoele.KeyPath)...)
|
||||
}
|
||||
if len(repoele.KeyData) > 0 {
|
||||
// keyarr = append(keyarr, string(repoele.KeyData))
|
||||
uids = append(uids, trust.GetGPGIdFromKeyData(repoele.KeyData)...)
|
||||
}
|
||||
}
|
||||
tempTrustShowOutput.GPGId = strings.Join(uids, ", ")
|
||||
|
||||
registryNamespace := trust.HaveMatchRegistry(repo, registryConfigs)
|
||||
if registryNamespace != nil {
|
||||
tempTrustShowOutput.SignatureStore = registryNamespace.SigStore
|
||||
}
|
||||
output = append(output, &tempTrustShowOutput)
|
||||
}
|
||||
}
|
||||
return output, 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
|
||||
return trust.AddPolicyEntries(policyPath, trust.AddPolicyEntriesInput{
|
||||
Scope: scope,
|
||||
Type: options.Type,
|
||||
PubKeyFiles: options.PubKeysFile,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
package trust
|
||||
|
||||
// Policy describes a basic trust policy configuration
|
||||
type Policy struct {
|
||||
Transport string `json:"transport"`
|
||||
Name string `json:"name,omitempty"`
|
||||
RepoName string `json:"repo_name,omitempty"`
|
||||
Keys []string `json:"keys,omitempty"`
|
||||
SignatureStore string `json:"sigstore,omitempty"`
|
||||
Type string `json:"type"`
|
||||
GPGId string `json:"gpg_id,omitempty"`
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -0,0 +1,196 @@
|
|||
package trust
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/containers/image/v5/signature"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAddPolicyEntries(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
policyPath := filepath.Join(tempDir, "policy.json")
|
||||
|
||||
minimalPolicy := &signature.Policy{
|
||||
Default: []signature.PolicyRequirement{
|
||||
signature.NewPRInsecureAcceptAnything(),
|
||||
},
|
||||
}
|
||||
minimalPolicyJSON, err := json.Marshal(minimalPolicy)
|
||||
require.NoError(t, err)
|
||||
err = os.WriteFile(policyPath, minimalPolicyJSON, 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Invalid input:
|
||||
for _, invalid := range []AddPolicyEntriesInput{
|
||||
{
|
||||
Scope: "default",
|
||||
Type: "accept",
|
||||
PubKeyFiles: []string{"/does-not-make-sense"},
|
||||
},
|
||||
{
|
||||
Scope: "default",
|
||||
Type: "insecureAcceptAnything",
|
||||
PubKeyFiles: []string{"/does-not-make-sense"},
|
||||
},
|
||||
{
|
||||
Scope: "default",
|
||||
Type: "reject",
|
||||
PubKeyFiles: []string{"/does-not-make-sense"},
|
||||
},
|
||||
{
|
||||
Scope: "default",
|
||||
Type: "signedBy",
|
||||
PubKeyFiles: []string{}, // A key is missing
|
||||
},
|
||||
{
|
||||
Scope: "default",
|
||||
Type: "sigstoreSigned",
|
||||
PubKeyFiles: []string{}, // A key is missing
|
||||
},
|
||||
{
|
||||
Scope: "default",
|
||||
Type: "this-is-unknown",
|
||||
PubKeyFiles: []string{},
|
||||
},
|
||||
} {
|
||||
err := AddPolicyEntries(policyPath, invalid)
|
||||
assert.Error(t, err, "%#v", invalid)
|
||||
}
|
||||
|
||||
err = AddPolicyEntries(policyPath, AddPolicyEntriesInput{
|
||||
Scope: "default",
|
||||
Type: "reject",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
err = AddPolicyEntries(policyPath, AddPolicyEntriesInput{
|
||||
Scope: "quay.io/accepted",
|
||||
Type: "accept",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
err = AddPolicyEntries(policyPath, AddPolicyEntriesInput{
|
||||
Scope: "quay.io/multi-signed",
|
||||
Type: "signedBy",
|
||||
PubKeyFiles: []string{"/1.pub", "/2.pub"},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
err = AddPolicyEntries(policyPath, AddPolicyEntriesInput{
|
||||
Scope: "quay.io/sigstore-signed",
|
||||
Type: "sigstoreSigned",
|
||||
PubKeyFiles: []string{"/1.pub", "/2.pub"},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test that the outcome is consumable, and compare it with the expected values.
|
||||
parsedPolicy, err := signature.NewPolicyFromFile(policyPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, &signature.Policy{
|
||||
Default: signature.PolicyRequirements{
|
||||
signature.NewPRReject(),
|
||||
},
|
||||
Transports: map[string]signature.PolicyTransportScopes{
|
||||
"docker": {
|
||||
"quay.io/accepted": {
|
||||
signature.NewPRInsecureAcceptAnything(),
|
||||
},
|
||||
"quay.io/multi-signed": {
|
||||
xNewPRSignedByKeyPath(t, "/1.pub", signature.NewPRMMatchRepoDigestOrExact()),
|
||||
xNewPRSignedByKeyPath(t, "/2.pub", signature.NewPRMMatchRepoDigestOrExact()),
|
||||
},
|
||||
"quay.io/sigstore-signed": {
|
||||
xNewPRSigstoreSignedKeyPath(t, "/1.pub", signature.NewPRMMatchRepoDigestOrExact()),
|
||||
xNewPRSigstoreSignedKeyPath(t, "/2.pub", signature.NewPRMMatchRepoDigestOrExact()),
|
||||
},
|
||||
},
|
||||
},
|
||||
}, parsedPolicy)
|
||||
|
||||
// Test that completely unknown JSON is preserved
|
||||
jsonWithUnknownData := `{
|
||||
"default": [
|
||||
{
|
||||
"type": "this is unknown",
|
||||
"unknown field": "should be preserved"
|
||||
}
|
||||
],
|
||||
"transports":
|
||||
{
|
||||
"docker-daemon":
|
||||
{
|
||||
"": [{
|
||||
"type":"this is unknown 2",
|
||||
"unknown field 2": "should be preserved 2"
|
||||
}]
|
||||
}
|
||||
}
|
||||
}`
|
||||
err = os.WriteFile(policyPath, []byte(jsonWithUnknownData), 0600)
|
||||
require.NoError(t, err)
|
||||
err = AddPolicyEntries(policyPath, AddPolicyEntriesInput{
|
||||
Scope: "quay.io/innocuous",
|
||||
Type: "signedBy",
|
||||
PubKeyFiles: []string{"/1.pub"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
updatedJSONWithUnknownData, err := os.ReadFile(policyPath)
|
||||
require.NoError(t, err)
|
||||
// Decode updatedJSONWithUnknownData so that this test does not depend on details of the encoding.
|
||||
// To reduce noise in the constants below:
|
||||
type a = []interface{}
|
||||
type m = map[string]interface{}
|
||||
var parsedUpdatedJSON m
|
||||
err = json.Unmarshal(updatedJSONWithUnknownData, &parsedUpdatedJSON)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, m{
|
||||
"default": a{
|
||||
m{
|
||||
"type": "this is unknown",
|
||||
"unknown field": "should be preserved",
|
||||
},
|
||||
},
|
||||
"transports": m{
|
||||
"docker-daemon": m{
|
||||
"": a{
|
||||
m{
|
||||
"type": "this is unknown 2",
|
||||
"unknown field 2": "should be preserved 2",
|
||||
},
|
||||
},
|
||||
},
|
||||
"docker": m{
|
||||
"quay.io/innocuous": a{
|
||||
m{
|
||||
"type": "signedBy",
|
||||
"keyType": "GPGKeys",
|
||||
"keyPath": "/1.pub",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, parsedUpdatedJSON)
|
||||
}
|
||||
|
||||
// xNewPRSignedByKeyPath is a wrapper for NewPRSignedByKeyPath which must not fail.
|
||||
func xNewPRSignedByKeyPath(t *testing.T, keyPath string, signedIdentity signature.PolicyReferenceMatch) signature.PolicyRequirement {
|
||||
pr, err := signature.NewPRSignedByKeyPath(signature.SBKeyTypeGPGKeys, keyPath, signedIdentity)
|
||||
require.NoError(t, err)
|
||||
return pr
|
||||
}
|
||||
|
||||
// xNewPRSignedByKeyPaths is a wrapper for NewPRSignedByKeyPaths which must not fail.
|
||||
func xNewPRSignedByKeyPaths(t *testing.T, keyPaths []string, signedIdentity signature.PolicyReferenceMatch) signature.PolicyRequirement {
|
||||
pr, err := signature.NewPRSignedByKeyPaths(signature.SBKeyTypeGPGKeys, keyPaths, signedIdentity)
|
||||
require.NoError(t, err)
|
||||
return pr
|
||||
}
|
||||
|
||||
// xNewPRSigstoreSignedKeyPath is a wrapper for NewPRSigstoreSignedKeyPath which must not fail.
|
||||
func xNewPRSigstoreSignedKeyPath(t *testing.T, keyPath string, signedIdentity signature.PolicyReferenceMatch) signature.PolicyRequirement {
|
||||
pr, err := signature.NewPRSigstoreSignedKeyPath(keyPath, signedIdentity)
|
||||
require.NoError(t, err)
|
||||
return pr
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
package trust
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/containers/image/v5/types"
|
||||
"github.com/docker/docker/pkg/homedir"
|
||||
"github.com/ghodss/yaml"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
Lookaside string `json:"lookaside"` // For reading, and if LookasideStaging is not present, for writing.
|
||||
LookasideStaging string `json:"lookaside-staging"` // For writing only.
|
||||
SigStore string `json:"sigstore"` // For reading, and if SigStoreStaging is not present, for writing.
|
||||
SigStoreStaging string `json:"sigstore-staging"` // For writing only.
|
||||
}
|
||||
|
||||
// systemRegistriesDirPath is the path to registries.d.
|
||||
const systemRegistriesDirPath = "/etc/containers/registries.d"
|
||||
|
||||
// userRegistriesDir is the path to the per user registries.d.
|
||||
var userRegistriesDir = filepath.FromSlash(".config/containers/registries.d")
|
||||
|
||||
// RegistriesDirPath returns a path to registries.d
|
||||
func RegistriesDirPath(sys *types.SystemContext) string {
|
||||
if sys != nil && sys.RegistriesDirPath != "" {
|
||||
return sys.RegistriesDirPath
|
||||
}
|
||||
userRegistriesDirPath := filepath.Join(homedir.Get(), userRegistriesDir)
|
||||
if _, err := os.Stat(userRegistriesDirPath); err == nil {
|
||||
return userRegistriesDirPath
|
||||
}
|
||||
if sys != nil && sys.RootForImplicitAbsolutePaths != "" {
|
||||
return filepath.Join(sys.RootForImplicitAbsolutePaths, systemRegistriesDirPath)
|
||||
}
|
||||
|
||||
return systemRegistriesDirPath
|
||||
}
|
||||
|
||||
// loadAndMergeConfig loads registries.d 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, fmt.Errorf("error parsing %s: %w", configPath, err)
|
||||
}
|
||||
if config.DefaultDocker != nil {
|
||||
if mergedConfig.DefaultDocker != nil {
|
||||
return nil, fmt.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, fmt.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
|
||||
}
|
||||
|
||||
// registriesDConfigurationForScope returns registries.d configuration for the provided scope.
|
||||
// scope can be "" to return only the global default configuration entry.
|
||||
func registriesDConfigurationForScope(registryConfigs *registryConfiguration, scope string) *registryNamespace {
|
||||
searchScope := scope
|
||||
if searchScope != "" {
|
||||
if !strings.Contains(searchScope, "/") {
|
||||
val, exists := registryConfigs.Docker[searchScope]
|
||||
if exists {
|
||||
return &val
|
||||
}
|
||||
}
|
||||
for range strings.Split(scope, "/") {
|
||||
val, exists := registryConfigs.Docker[searchScope]
|
||||
if exists {
|
||||
return &val
|
||||
}
|
||||
if strings.Contains(searchScope, "/") {
|
||||
searchScope = searchScope[:strings.LastIndex(searchScope, "/")]
|
||||
}
|
||||
}
|
||||
}
|
||||
return registryConfigs.DefaultDocker
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
# This is a default registries.d configuration file. You may
|
||||
# add to this file or create additional files in registries.d/.
|
||||
#
|
||||
# lookaside: indicates a location that is read and write
|
||||
# lookaside-staging: indicates a location that is only for write
|
||||
#
|
||||
# lookaside and lookaside-staging take a value of the following:
|
||||
# lookaside: {schema}://location
|
||||
#
|
||||
# For reading signatures, schema may be http, https, or file.
|
||||
# For writing signatures, schema may only be file.
|
||||
|
||||
# This is the default signature write location for docker registries.
|
||||
default-docker:
|
||||
# lookaside: file:///var/lib/containers/sigstore
|
||||
lookaside-staging: file:///var/lib/containers/sigstore
|
||||
|
||||
# The 'docker' indicator here is the start of the configuration
|
||||
# for docker registries.
|
||||
#
|
||||
# docker:
|
||||
#
|
||||
# privateregistry.com:
|
||||
# lookaside: http://privateregistry.com/sigstore/
|
||||
# lookaside-staging: /mnt/nfs/privateregistry/sigstore
|
|
@ -0,0 +1,3 @@
|
|||
docker:
|
||||
quay.io/multi-signed:
|
||||
lookaside: https://quay.example.com/sigstore
|
|
@ -0,0 +1,5 @@
|
|||
docker:
|
||||
registry.redhat.io:
|
||||
sigstore: https://registry.redhat.io/containers/sigstore
|
||||
registry.access.redhat.com:
|
||||
sigstore: https://registry.redhat.io/containers/sigstore
|
|
@ -1,243 +1,127 @@
|
|||
package trust
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/containers/image/v5/types"
|
||||
"github.com/docker/docker/pkg/homedir"
|
||||
"github.com/ghodss/yaml"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// PolicyContent struct for policy.json file
|
||||
type PolicyContent struct {
|
||||
Default []RepoContent `json:"default"`
|
||||
Transports TransportsContent `json:"transports,omitempty"`
|
||||
// Policy describes a basic trust policy configuration
|
||||
type Policy struct {
|
||||
Transport string `json:"transport"`
|
||||
Name string `json:"name,omitempty"`
|
||||
RepoName string `json:"repo_name,omitempty"`
|
||||
Keys []string `json:"keys,omitempty"`
|
||||
SignatureStore string `json:"sigstore,omitempty"`
|
||||
Type string `json:"type"`
|
||||
GPGId string `json:"gpg_id,omitempty"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
// PolicyDescription returns an user-focused description of the policy in policyPath and registries.d data from registriesDirPath.
|
||||
func PolicyDescription(policyPath, registriesDirPath string) ([]*Policy, error) {
|
||||
return policyDescriptionWithGPGIDReader(policyPath, registriesDirPath, getGPGIdFromKeyPath)
|
||||
}
|
||||
|
||||
// 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.
|
||||
}
|
||||
|
||||
// ShowOutput keep the fields for image trust show command
|
||||
type ShowOutput struct {
|
||||
Repo string
|
||||
Trusttype string
|
||||
GPGid string
|
||||
Sigstore string
|
||||
}
|
||||
|
||||
// systemRegistriesDirPath is the path to registries.d.
|
||||
const systemRegistriesDirPath = "/etc/containers/registries.d"
|
||||
|
||||
// userRegistriesDir is the path to the per user registries.d.
|
||||
var userRegistriesDir = filepath.FromSlash(".config/containers/registries.d")
|
||||
|
||||
// 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 {
|
||||
if sys != nil && sys.RegistriesDirPath != "" {
|
||||
return sys.RegistriesDirPath
|
||||
}
|
||||
userRegistriesDirPath := filepath.Join(homedir.Get(), userRegistriesDir)
|
||||
if _, err := os.Stat(userRegistriesDirPath); err == nil {
|
||||
return userRegistriesDirPath
|
||||
}
|
||||
if sys != nil && 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)
|
||||
// policyDescriptionWithGPGIDReader is PolicyDescription with a gpgIDReader parameter. It exists only to make testing easier.
|
||||
func policyDescriptionWithGPGIDReader(policyPath, registriesDirPath string, idReader gpgIDReader) ([]*Policy, error) {
|
||||
policyContentStruct, err := getPolicy(policyPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return &mergedConfig, nil
|
||||
}
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("could not read trust policies: %w", err)
|
||||
}
|
||||
configNames, err := dir.Readdirnames(0)
|
||||
res, err := getPolicyShowOutput(policyContentStruct, registriesDirPath, idReader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not show trust policies: %w", err)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func getPolicyShowOutput(policyContentStruct policyContent, systemRegistriesDirPath string, idReader gpgIDReader) ([]*Policy, error) {
|
||||
var output []*Policy
|
||||
|
||||
registryConfigs, err := loadAndMergeConfig(systemRegistriesDirPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, configName := range configNames {
|
||||
if !strings.HasSuffix(configName, ".yaml") {
|
||||
continue
|
||||
|
||||
if len(policyContentStruct.Default) > 0 {
|
||||
template := Policy{
|
||||
Transport: "all",
|
||||
Name: "* (default)",
|
||||
RepoName: "default",
|
||||
}
|
||||
configPath := filepath.Join(dirPath, configName)
|
||||
configBytes, err := ioutil.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
output = append(output, descriptionsOfPolicyRequirements(policyContentStruct.Default, template, registryConfigs, "", idReader)...)
|
||||
}
|
||||
// FIXME: This should use x/exp/maps.Keys after we update to Go 1.18.
|
||||
transports := []string{}
|
||||
for t := range policyContentStruct.Transports {
|
||||
transports = append(transports, t)
|
||||
}
|
||||
sort.Strings(transports)
|
||||
for _, transport := range transports {
|
||||
transval := policyContentStruct.Transports[transport]
|
||||
if transport == "docker" {
|
||||
transport = "repository"
|
||||
}
|
||||
var config RegistryConfiguration
|
||||
err = yaml.Unmarshal(configBytes, &config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing %s: %w", configPath, err)
|
||||
|
||||
// FIXME: This should use x/exp/maps.Keys after we update to Go 1.18.
|
||||
scopes := []string{}
|
||||
for s := range transval {
|
||||
scopes = append(scopes, s)
|
||||
}
|
||||
if config.DefaultDocker != nil {
|
||||
if mergedConfig.DefaultDocker != nil {
|
||||
return nil, fmt.Errorf(`error parsing signature storage configuration: "default-docker" defined both in "%s" and "%s"`,
|
||||
dockerDefaultMergedFrom, configPath)
|
||||
sort.Strings(scopes)
|
||||
for _, repo := range scopes {
|
||||
repoval := transval[repo]
|
||||
template := Policy{
|
||||
Transport: transport,
|
||||
Name: repo,
|
||||
RepoName: repo,
|
||||
}
|
||||
mergedConfig.DefaultDocker = config.DefaultDocker
|
||||
dockerDefaultMergedFrom = configPath
|
||||
output = append(output, descriptionsOfPolicyRequirements(repoval, template, registryConfigs, repo, idReader)...)
|
||||
}
|
||||
for nsName, nsConfig := range config.Docker { // includes config.Docker == nil
|
||||
if _, ok := mergedConfig.Docker[nsName]; ok {
|
||||
return nil, fmt.Errorf(`error parsing signature storage configuration: "docker" namespace "%s" defined both in "%s" and "%s"`,
|
||||
nsName, nsMergedFrom[nsName], configPath)
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// descriptionsOfPolicyRequirements turns reqs into user-readable policy entries, with Transport/Name/Reponame coming from template, potentially looking up scope (which may be "") in registryConfigs.
|
||||
func descriptionsOfPolicyRequirements(reqs []repoContent, template Policy, registryConfigs *registryConfiguration, scope string, idReader gpgIDReader) []*Policy {
|
||||
res := []*Policy{}
|
||||
|
||||
var lookasidePath string
|
||||
registryNamespace := registriesDConfigurationForScope(registryConfigs, scope)
|
||||
if registryNamespace != nil {
|
||||
if registryNamespace.Lookaside != "" {
|
||||
lookasidePath = registryNamespace.Lookaside
|
||||
} else { // incl. registryNamespace.SigStore == ""
|
||||
lookasidePath = registryNamespace.SigStore
|
||||
}
|
||||
}
|
||||
|
||||
for _, repoele := range reqs {
|
||||
entry := template
|
||||
entry.Type = trustTypeDescription(repoele.Type)
|
||||
|
||||
var gpgIDString string
|
||||
switch repoele.Type {
|
||||
case "signedBy":
|
||||
uids := []string{}
|
||||
if len(repoele.KeyPath) > 0 {
|
||||
uids = append(uids, idReader(repoele.KeyPath)...)
|
||||
}
|
||||
mergedConfig.Docker[nsName] = nsConfig
|
||||
nsMergedFrom[nsName] = configPath
|
||||
}
|
||||
}
|
||||
return &mergedConfig, nil
|
||||
}
|
||||
|
||||
// HaveMatchRegistry checks if trust settings for the registry have been configured 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 registryConfigs.DefaultDocker
|
||||
}
|
||||
|
||||
// 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 return user keyring from key 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 return user keyring from keydata
|
||||
func GetGPGIdFromKeyData(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 GetGPGIdFromKeyPath(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
|
||||
for _, path := range repoele.KeyPaths {
|
||||
uids = append(uids, idReader(path)...)
|
||||
}
|
||||
parseduid := uid
|
||||
if strings.Contains(uid, "<") && strings.Contains(uid, ">") {
|
||||
parseduid = strings.SplitN(strings.SplitAfterN(uid, "<", 2)[1], ">", 2)[0]
|
||||
if len(repoele.KeyData) > 0 {
|
||||
uids = append(uids, getGPGIdFromKeyData(idReader, repoele.KeyData)...)
|
||||
}
|
||||
parseduids = append(parseduids, parseduid)
|
||||
}
|
||||
}
|
||||
return parseduids
|
||||
}
|
||||
gpgIDString = strings.Join(uids, ", ")
|
||||
|
||||
// GetPolicy parse policy.json into PolicyContent struct
|
||||
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)
|
||||
case "sigstoreSigned":
|
||||
gpgIDString = "N/A" // We could potentially return key fingerprints here, but they would not be _GPG_ fingerprints.
|
||||
}
|
||||
entry.GPGId = gpgIDString
|
||||
entry.SignatureStore = lookasidePath // We do this even for sigstoreSigned and things like type: reject, to show that the sigstore is being read.
|
||||
res = append(res, &entry)
|
||||
}
|
||||
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
|
||||
|
||||
return res
|
||||
}
|
||||
|
|
|
@ -0,0 +1,376 @@
|
|||
package trust
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/containers/image/v5/signature"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPolicyDescription(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
policyPath := filepath.Join(tempDir, "policy.json")
|
||||
|
||||
// Override getGPGIdFromKeyPath because we don't want to bother with (and spend the unit-test time on) generating valid GPG keys, and running the real GPG binary.
|
||||
// Instead of reading the files at all, just expect file names like /id1,id2,...,idN.pub
|
||||
idReader := func(keyPath string) []string {
|
||||
require.True(t, strings.HasPrefix(keyPath, "/"))
|
||||
require.True(t, strings.HasSuffix(keyPath, ".pub"))
|
||||
return strings.Split(keyPath[1:len(keyPath)-4], ",")
|
||||
}
|
||||
|
||||
for _, c := range []struct {
|
||||
policy *signature.Policy
|
||||
expected []*Policy
|
||||
}{
|
||||
{
|
||||
&signature.Policy{
|
||||
Default: signature.PolicyRequirements{
|
||||
signature.NewPRReject(),
|
||||
},
|
||||
Transports: map[string]signature.PolicyTransportScopes{
|
||||
"docker": {
|
||||
"quay.io/accepted": {
|
||||
signature.NewPRInsecureAcceptAnything(),
|
||||
},
|
||||
"registry.redhat.io": {
|
||||
xNewPRSignedByKeyPath(t, "/redhat.pub", signature.NewPRMMatchRepoDigestOrExact()),
|
||||
},
|
||||
"registry.access.redhat.com": {
|
||||
xNewPRSignedByKeyPaths(t, []string{"/redhat.pub", "/redhat-beta.pub"}, signature.NewPRMMatchRepoDigestOrExact()),
|
||||
},
|
||||
"quay.io/multi-signed": {
|
||||
xNewPRSignedByKeyPath(t, "/1.pub", signature.NewPRMMatchRepoDigestOrExact()),
|
||||
xNewPRSignedByKeyPath(t, "/2,3.pub", signature.NewPRMMatchRepoDigestOrExact()),
|
||||
},
|
||||
"quay.io/sigstore-signed": {
|
||||
xNewPRSigstoreSignedKeyPath(t, "/1.pub", signature.NewPRMMatchRepoDigestOrExact()),
|
||||
xNewPRSigstoreSignedKeyPath(t, "/2.pub", signature.NewPRMMatchRepoDigestOrExact()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[]*Policy{
|
||||
{
|
||||
Transport: "all",
|
||||
Name: "* (default)",
|
||||
RepoName: "default",
|
||||
Type: "reject",
|
||||
},
|
||||
{
|
||||
Transport: "repository",
|
||||
Name: "quay.io/accepted",
|
||||
RepoName: "quay.io/accepted",
|
||||
Type: "accept",
|
||||
},
|
||||
{
|
||||
Transport: "repository",
|
||||
Name: "quay.io/multi-signed",
|
||||
RepoName: "quay.io/multi-signed",
|
||||
Type: "signed",
|
||||
SignatureStore: "https://quay.example.com/sigstore",
|
||||
GPGId: "1",
|
||||
},
|
||||
{
|
||||
Transport: "repository",
|
||||
Name: "quay.io/multi-signed",
|
||||
RepoName: "quay.io/multi-signed",
|
||||
Type: "signed",
|
||||
SignatureStore: "https://quay.example.com/sigstore",
|
||||
GPGId: "2, 3",
|
||||
},
|
||||
{
|
||||
Transport: "repository",
|
||||
Name: "quay.io/sigstore-signed",
|
||||
RepoName: "quay.io/sigstore-signed",
|
||||
Type: "sigstoreSigned",
|
||||
SignatureStore: "",
|
||||
GPGId: "N/A",
|
||||
},
|
||||
{
|
||||
Transport: "repository",
|
||||
Name: "quay.io/sigstore-signed",
|
||||
RepoName: "quay.io/sigstore-signed",
|
||||
Type: "sigstoreSigned",
|
||||
SignatureStore: "",
|
||||
GPGId: "N/A",
|
||||
},
|
||||
{
|
||||
Transport: "repository",
|
||||
Name: "registry.access.redhat.com",
|
||||
RepoName: "registry.access.redhat.com",
|
||||
Type: "signed",
|
||||
SignatureStore: "https://registry.redhat.io/containers/sigstore",
|
||||
GPGId: "redhat, redhat-beta",
|
||||
}, {
|
||||
Transport: "repository",
|
||||
Name: "registry.redhat.io",
|
||||
RepoName: "registry.redhat.io",
|
||||
Type: "signed",
|
||||
SignatureStore: "https://registry.redhat.io/containers/sigstore",
|
||||
GPGId: "redhat",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
&signature.Policy{
|
||||
Default: signature.PolicyRequirements{
|
||||
xNewPRSignedByKeyPath(t, "/1.pub", signature.NewPRMMatchRepoDigestOrExact()),
|
||||
xNewPRSignedByKeyPath(t, "/2,3.pub", signature.NewPRMMatchRepoDigestOrExact()),
|
||||
},
|
||||
},
|
||||
[]*Policy{
|
||||
{
|
||||
Transport: "all",
|
||||
Name: "* (default)",
|
||||
RepoName: "default",
|
||||
Type: "signed",
|
||||
SignatureStore: "",
|
||||
GPGId: "1",
|
||||
},
|
||||
{
|
||||
Transport: "all",
|
||||
Name: "* (default)",
|
||||
RepoName: "default",
|
||||
Type: "signed",
|
||||
SignatureStore: "",
|
||||
GPGId: "2, 3",
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
policyJSON, err := json.Marshal(c.policy)
|
||||
require.NoError(t, err)
|
||||
err = os.WriteFile(policyPath, policyJSON, 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := policyDescriptionWithGPGIDReader(policyPath, "./testdata", idReader)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, c.expected, res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDescriptionsOfPolicyRequirements(t *testing.T) {
|
||||
// Override getGPGIdFromKeyPath because we don't want to bother with (and spend the unit-test time on) generating valid GPG keys, and running the real GPG binary.
|
||||
// Instead of reading the files at all, just expect file names like /id1,id2,...,idN.pub
|
||||
idReader := func(keyPath string) []string {
|
||||
require.True(t, strings.HasPrefix(keyPath, "/"))
|
||||
require.True(t, strings.HasSuffix(keyPath, ".pub"))
|
||||
return strings.Split(keyPath[1:len(keyPath)-4], ",")
|
||||
}
|
||||
|
||||
template := Policy{
|
||||
Transport: "transport",
|
||||
Name: "name",
|
||||
RepoName: "repoName",
|
||||
}
|
||||
registryConfigs, err := loadAndMergeConfig("./testdata")
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, c := range []struct {
|
||||
scope string
|
||||
reqs signature.PolicyRequirements
|
||||
expected []*Policy
|
||||
}{
|
||||
{
|
||||
"",
|
||||
signature.PolicyRequirements{
|
||||
signature.NewPRReject(),
|
||||
},
|
||||
[]*Policy{
|
||||
{
|
||||
Transport: "transport",
|
||||
Name: "name",
|
||||
RepoName: "repoName",
|
||||
Type: "reject",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"quay.io/accepted",
|
||||
signature.PolicyRequirements{
|
||||
signature.NewPRInsecureAcceptAnything(),
|
||||
},
|
||||
[]*Policy{
|
||||
{
|
||||
Transport: "transport",
|
||||
Name: "name",
|
||||
RepoName: "repoName",
|
||||
Type: "accept",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"registry.redhat.io",
|
||||
signature.PolicyRequirements{
|
||||
xNewPRSignedByKeyPath(t, "/redhat.pub", signature.NewPRMMatchRepoDigestOrExact()),
|
||||
},
|
||||
[]*Policy{
|
||||
{
|
||||
Transport: "transport",
|
||||
Name: "name",
|
||||
RepoName: "repoName",
|
||||
Type: "signed",
|
||||
SignatureStore: "https://registry.redhat.io/containers/sigstore",
|
||||
GPGId: "redhat",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"registry.access.redhat.com",
|
||||
signature.PolicyRequirements{
|
||||
xNewPRSignedByKeyPaths(t, []string{"/redhat.pub", "/redhat-beta.pub"}, signature.NewPRMMatchRepoDigestOrExact()),
|
||||
},
|
||||
[]*Policy{
|
||||
{
|
||||
Transport: "transport",
|
||||
Name: "name",
|
||||
RepoName: "repoName",
|
||||
Type: "signed",
|
||||
SignatureStore: "https://registry.redhat.io/containers/sigstore",
|
||||
GPGId: "redhat, redhat-beta",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"quay.io/multi-signed",
|
||||
signature.PolicyRequirements{
|
||||
xNewPRSignedByKeyPath(t, "/1.pub", signature.NewPRMMatchRepoDigestOrExact()),
|
||||
xNewPRSignedByKeyPath(t, "/2,3.pub", signature.NewPRMMatchRepoDigestOrExact()),
|
||||
},
|
||||
[]*Policy{
|
||||
{
|
||||
Transport: "transport",
|
||||
Name: "name",
|
||||
RepoName: "repoName",
|
||||
Type: "signed",
|
||||
SignatureStore: "https://quay.example.com/sigstore",
|
||||
GPGId: "1",
|
||||
},
|
||||
{
|
||||
Transport: "transport",
|
||||
Name: "name",
|
||||
RepoName: "repoName",
|
||||
Type: "signed",
|
||||
SignatureStore: "https://quay.example.com/sigstore",
|
||||
GPGId: "2, 3",
|
||||
},
|
||||
},
|
||||
}, {
|
||||
"quay.io/sigstore-signed",
|
||||
signature.PolicyRequirements{
|
||||
xNewPRSigstoreSignedKeyPath(t, "/1.pub", signature.NewPRMMatchRepoDigestOrExact()),
|
||||
xNewPRSigstoreSignedKeyPath(t, "/2.pub", signature.NewPRMMatchRepoDigestOrExact()),
|
||||
},
|
||||
[]*Policy{
|
||||
{
|
||||
Transport: "transport",
|
||||
Name: "name",
|
||||
RepoName: "repoName",
|
||||
Type: "sigstoreSigned",
|
||||
SignatureStore: "",
|
||||
GPGId: "N/A",
|
||||
},
|
||||
{
|
||||
Transport: "transport",
|
||||
Name: "name",
|
||||
RepoName: "repoName",
|
||||
Type: "sigstoreSigned",
|
||||
SignatureStore: "",
|
||||
GPGId: "N/A",
|
||||
},
|
||||
},
|
||||
},
|
||||
{ // Multiple kinds of requirements are represented individually.
|
||||
"registry.redhat.io",
|
||||
signature.PolicyRequirements{
|
||||
signature.NewPRReject(),
|
||||
signature.NewPRInsecureAcceptAnything(),
|
||||
xNewPRSignedByKeyPath(t, "/redhat.pub", signature.NewPRMMatchRepoDigestOrExact()),
|
||||
xNewPRSignedByKeyPaths(t, []string{"/redhat.pub", "/redhat-beta.pub"}, signature.NewPRMMatchRepoDigestOrExact()),
|
||||
xNewPRSignedByKeyPath(t, "/1.pub", signature.NewPRMMatchRepoDigestOrExact()),
|
||||
xNewPRSignedByKeyPath(t, "/2,3.pub", signature.NewPRMMatchRepoDigestOrExact()),
|
||||
xNewPRSigstoreSignedKeyPath(t, "/1.pub", signature.NewPRMMatchRepoDigestOrExact()),
|
||||
xNewPRSigstoreSignedKeyPath(t, "/2.pub", signature.NewPRMMatchRepoDigestOrExact()),
|
||||
},
|
||||
[]*Policy{
|
||||
{
|
||||
Transport: "transport",
|
||||
Name: "name",
|
||||
RepoName: "repoName",
|
||||
SignatureStore: "https://registry.redhat.io/containers/sigstore",
|
||||
Type: "reject",
|
||||
},
|
||||
{
|
||||
Transport: "transport",
|
||||
Name: "name",
|
||||
RepoName: "repoName",
|
||||
SignatureStore: "https://registry.redhat.io/containers/sigstore",
|
||||
Type: "accept",
|
||||
},
|
||||
{
|
||||
Transport: "transport",
|
||||
Name: "name",
|
||||
RepoName: "repoName",
|
||||
Type: "signed",
|
||||
SignatureStore: "https://registry.redhat.io/containers/sigstore",
|
||||
GPGId: "redhat",
|
||||
},
|
||||
{
|
||||
Transport: "transport",
|
||||
Name: "name",
|
||||
RepoName: "repoName",
|
||||
Type: "signed",
|
||||
SignatureStore: "https://registry.redhat.io/containers/sigstore",
|
||||
GPGId: "redhat, redhat-beta",
|
||||
},
|
||||
{
|
||||
Transport: "transport",
|
||||
Name: "name",
|
||||
RepoName: "repoName",
|
||||
Type: "signed",
|
||||
SignatureStore: "https://registry.redhat.io/containers/sigstore",
|
||||
GPGId: "1",
|
||||
},
|
||||
{
|
||||
Transport: "transport",
|
||||
Name: "name",
|
||||
RepoName: "repoName",
|
||||
Type: "signed",
|
||||
SignatureStore: "https://registry.redhat.io/containers/sigstore",
|
||||
GPGId: "2, 3",
|
||||
},
|
||||
{
|
||||
Transport: "transport",
|
||||
Name: "name",
|
||||
RepoName: "repoName",
|
||||
Type: "sigstoreSigned",
|
||||
SignatureStore: "https://registry.redhat.io/containers/sigstore",
|
||||
GPGId: "N/A",
|
||||
},
|
||||
{
|
||||
Transport: "transport",
|
||||
Name: "name",
|
||||
RepoName: "repoName",
|
||||
Type: "sigstoreSigned",
|
||||
SignatureStore: "https://registry.redhat.io/containers/sigstore",
|
||||
GPGId: "N/A",
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
reqsJSON, err := json.Marshal(c.reqs)
|
||||
require.NoError(t, err)
|
||||
var parsedRegs []repoContent
|
||||
err = json.Unmarshal(reqsJSON, &parsedRegs)
|
||||
require.NoError(t, err)
|
||||
|
||||
res := descriptionsOfPolicyRequirements(parsedRegs, template, registryConfigs, c.scope, idReader)
|
||||
assert.Equal(t, c.expected, res)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue