Skip to content

Commit

Permalink
Merge pull request #1075 from mtrmac/remapIdentity
Browse files Browse the repository at this point in the history
Add a signedIdentity choice "type": "remapIdentity"
  • Loading branch information
mtrmac committed Dec 4, 2020
2 parents b6d3133 + 9dd2c4d commit 7589fa9
Show file tree
Hide file tree
Showing 7 changed files with 597 additions and 51 deletions.
39 changes: 39 additions & 0 deletions docs/containers-policy.json.5.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,30 @@ One of the following alternatives are supported:
"dockerRepository": docker_repository_value
}
```
- Prefix remapping:

If the image identity matches the specified prefix, that prefix is replaced by the specified “signed prefix”
(otherwise it is used as unchanged and no remapping takes place);
matching then follows the `matchRepoDigestOrExact` semantics documented above
(i.e. if the image identity carries a tag, the identity in the signature must exactly match,
if it uses a digest reference, the repository must match).

The `prefix` and `signedPrefix` values can be either host[:port] values
(matching exactly the same host[:port], string),
repository namespaces, or repositories (i.e. they must not contain tags/digests),
and match as prefixes *of the fully expanded form*.
For example, `docker.io/library/busybox` (*not* `busybox`) to specify that single repository,
or `docker.io/library` (not an empty string) to specify the parent namespace of `docker.io/library/busybox`==`busybox`).

The `prefix` value is usually the same as the scope containing the parent `signedBy` requirement.

```js
{
"type": "remapIdentity",
"prefix": prefix,
"signedPrefix": prefix,
}
```

If the `signedIdentity` field is missing, it is treated as `matchRepoDigestOrExact`.

Expand Down Expand Up @@ -260,6 +284,21 @@ selectively allow individual transports and scopes as desired.
"keyType": "GPGKeys",
"keyPath": "/path/to/reviewer-pubkey.gpg"
}
],
/* A way to mirror many repositories from a single vendor */
"private-mirror:5000/vendor-mirror": [
{ /* Require the image to be signed by the original vendor, using the vendor's repository location.
For example, private-mirror:5000/vendor-mirror/productA/image1:latest needs to be signed as
vendor.example/productA/image1:latest . */
"type": "signedBy",
"keyType": "GPGKeys",
"keyPath": "/path/to/vendor-pubkey.gpg",
"signedIdentity": {
"type": "remapIdentity",
"prefix": "private-mirror:5000/vendor-mirror",
"signedPrefix": "vendor.example.com",
}
}
]
}
}
Expand Down
12 changes: 12 additions & 0 deletions signature/fixtures/policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,18 @@
}
}
],
"private-mirror:5000/vendor-mirror": [
{
"type": "signedBy",
"keyType": "GPGKeys",
"keyPath": "/keys/vendor-gpg-keyring",
"signedIdentity": {
"type": "remapIdentity",
"prefix": "private-mirror:5000/vendor-mirror",
"signedPrefix": "vendor.example.com"
}
}
],
"bogus/key-data-example": [
{
"type": "signedBy",
Expand Down
76 changes: 76 additions & 0 deletions signature/policy_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"io/ioutil"
"os"
"path/filepath"
"regexp"

"github.com/containers/image/v5/docker/reference"
"github.com/containers/image/v5/transports"
Expand Down Expand Up @@ -507,6 +508,8 @@ func newPolicyReferenceMatchFromJSON(data []byte) (PolicyReferenceMatch, error)
res = &prmExactReference{}
case prmTypeExactRepository:
res = &prmExactRepository{}
case prmTypeRemapIdentity:
res = &prmRemapIdentity{}
default:
return nil, InvalidPolicyFormatError(fmt.Sprintf("Unknown policy reference match type \"%s\"", typeField.Type))
}
Expand Down Expand Up @@ -693,3 +696,76 @@ func (prm *prmExactRepository) UnmarshalJSON(data []byte) error {
*prm = *res
return nil
}

// Private objects for validateIdentityRemappingPrefix
var (
// remapIdentityDomainRegexp matches exactly a reference domain (name[:port])
remapIdentityDomainRegexp = regexp.MustCompile("^" + reference.DomainRegexp.String() + "$")
// remapIdentityDomainPrefixRegexp matches a reference that starts with a domain;
// we need this because reference.NameRegexp accepts short names with docker.io implied.
remapIdentityDomainPrefixRegexp = regexp.MustCompile("^" + reference.DomainRegexp.String() + "/")
// remapIdentityNameRegexp matches exactly a reference.Named name (possibly unnormalized)
remapIdentityNameRegexp = regexp.MustCompile("^" + reference.NameRegexp.String() + "$")
)

// validateIdentityRemappingPrefix returns an InvalidPolicyFormatError if s is detected to be invalid
// for the Prefix or SignedPrefix values of prmRemapIdentity.
// Note that it may not recognize _all_ invalid values.
func validateIdentityRemappingPrefix(s string) error {
if remapIdentityDomainRegexp.MatchString(s) ||
(remapIdentityNameRegexp.MatchString(s) && remapIdentityDomainPrefixRegexp.MatchString(s)) {
// FIXME? This does not reject "shortname" nor "ns/shortname", because docker/reference
// does not provide an API for the short vs. long name logic.
// It will either not match, or fail in the ParseNamed call of
// prmRemapIdentity.remapReferencePrefix when trying to use such a prefix.
return nil
}
return InvalidPolicyFormatError(fmt.Sprintf("prefix %q is not valid", s))
}

// newPRMRemapIdentity is NewPRMRemapIdentity, except it returns the private type.
func newPRMRemapIdentity(prefix, signedPrefix string) (*prmRemapIdentity, error) {
if err := validateIdentityRemappingPrefix(prefix); err != nil {
return nil, err
}
if err := validateIdentityRemappingPrefix(signedPrefix); err != nil {
return nil, err
}
return &prmRemapIdentity{
prmCommon: prmCommon{Type: prmTypeRemapIdentity},
Prefix: prefix,
SignedPrefix: signedPrefix,
}, nil
}

// NewPRMRemapIdentity returns a new "remapIdentity" PolicyRepositoryMatch.
func NewPRMRemapIdentity(prefix, signedPrefix string) (PolicyReferenceMatch, error) {
return newPRMRemapIdentity(prefix, signedPrefix)
}

// Compile-time check that prmRemapIdentity implements json.Unmarshaler.
var _ json.Unmarshaler = (*prmRemapIdentity)(nil)

// UnmarshalJSON implements the json.Unmarshaler interface.
func (prm *prmRemapIdentity) UnmarshalJSON(data []byte) error {
*prm = prmRemapIdentity{}
var tmp prmRemapIdentity
if err := paranoidUnmarshalJSONObjectExactFields(data, map[string]interface{}{
"type": &tmp.Type,
"prefix": &tmp.Prefix,
"signedPrefix": &tmp.SignedPrefix,
}); err != nil {
return err
}

if tmp.Type != prmTypeRemapIdentity {
return InvalidPolicyFormatError(fmt.Sprintf("Unexpected policy requirement type \"%s\"", tmp.Type))
}

res, err := newPRMRemapIdentity(tmp.Prefix, tmp.SignedPrefix)
if err != nil {
return err
}
*prm = *res
return nil
}
119 changes: 115 additions & 4 deletions signature/policy_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ var policyFixtureContents = &Policy{
"/keys/RH-key-signing-key-gpg-keyring",
NewPRMMatchRepoDigestOrExact()),
},
"private-mirror:5000/vendor-mirror": {
xNewPRSignedByKeyPath(SBKeyTypeGPGKeys,
"/keys/vendor-gpg-keyring",
xNewPRMRemapIdentity("private-mirror:5000/vendor-mirror", "vendor.example.com")),
},
"bogus/key-data-example": {
xNewPRSignedByKeyData(SBKeyTypeSignedByGPGKeys,
[]byte("nonsense"),
Expand Down Expand Up @@ -250,7 +255,7 @@ func testInvalidJSONInput(t *testing.T, dest json.Unmarshaler) {
assert.Error(t, err)
}

// addExtraJSONMember adds adds an additional member "$name": $extra,
// addExtraJSONMember adds an additional member "$name": $extra,
// possibly with a duplicate name, to encoded.
// Errors, if any, are reported through t.
func addExtraJSONMember(t *testing.T, encoded []byte, name string, extra interface{}) []byte {
Expand All @@ -260,7 +265,13 @@ func addExtraJSONMember(t *testing.T, encoded []byte, name string, extra interfa
require.True(t, bytes.HasSuffix(encoded, []byte("}")))
preservedLen := len(encoded) - 1

return bytes.Join([][]byte{encoded[:preservedLen], []byte(`,"`), []byte(name), []byte(`":`), extraJSON, []byte("}")}, nil)
res := bytes.Join([][]byte{encoded[:preservedLen], []byte(`,"`), []byte(name), []byte(`":`), extraJSON, []byte("}")}, nil)
// Verify that the result is valid JSON, as a sanity check that we are actually triggering
// the “duplicate member” case in the caller.
var raw map[string]interface{}
err = json.Unmarshal(res, &raw)
require.NoError(t, err)
return res
}

// policyJSONUnmarshallerTests formalizes the repeated structure of the JSON unmasrhaller
Expand Down Expand Up @@ -1104,7 +1115,7 @@ func TestPRMExactReferenceUnmarshalJSON(t *testing.T) {
// Invalid "dockerReference" field
func(v mSI) { v["dockerReference"] = 1 },
},
duplicateFields: []string{"type", "baseLayerIdentity"},
duplicateFields: []string{"type", "dockerReference"},
}.run(t)
}

Expand Down Expand Up @@ -1160,6 +1171,106 @@ func TestPRMExactRepositoryUnmarshalJSON(t *testing.T) {
// Invalid "dockerRepository" field
func(v mSI) { v["dockerRepository"] = 1 },
},
duplicateFields: []string{"type", "baseLayerIdentity"},
duplicateFields: []string{"type", "dockerRepository"},
}.run(t)
}

func TestValidateIdentityRemappingPrefix(t *testing.T) {
for _, s := range []string{
"localhost",
"example.com",
"example.com:80",
"example.com/repo",
"example.com/ns1/ns2/ns3/repo.with.dots-dashes_underscores",
"example.com:80/ns1/ns2/ns3/repo.with.dots-dashes_underscores",
// NOTE: These values are invalid, do not actually work, and may be rejected by this function
// and in NewPRMRemapIdentity in the future.
"shortname",
"ns/shortname",
} {
err := validateIdentityRemappingPrefix(s)
assert.NoError(t, err, s)
}

for _, s := range []string{
"",
"repo_with_underscores", // Not a valid DNS name, at least per docker/reference
"example.com/",
"example.com/UPPERCASEISINVALID",
"example.com/repo/",
"example.com/repo:tag",
"example.com/repo@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"example.com/repo:tag@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
} {
err := validateIdentityRemappingPrefix(s)
assert.Error(t, err, s)
}
}

// xNewPRMRemapIdentity is like NewPRMRemapIdentity, except it must not fail.
func xNewPRMRemapIdentity(prefix, signedPrefix string) PolicyReferenceMatch {
pr, err := NewPRMRemapIdentity(prefix, signedPrefix)
if err != nil {
panic("xNewPRMRemapIdentity failed")
}
return pr
}

func TestNewPRMRemapIdentity(t *testing.T) {
const testPrefix = "example.com/docker-library"
const testSignedPrefix = "docker.io/library"

// Success
_prm, err := NewPRMRemapIdentity(testPrefix, testSignedPrefix)
require.NoError(t, err)
prm, ok := _prm.(*prmRemapIdentity)
require.True(t, ok)
assert.Equal(t, &prmRemapIdentity{
prmCommon: prmCommon{prmTypeRemapIdentity},
Prefix: testPrefix,
SignedPrefix: testSignedPrefix,
}, prm)

// Invalid prefix
_, err = NewPRMRemapIdentity("", testSignedPrefix)
assert.Error(t, err)
_, err = NewPRMRemapIdentity("example.com/UPPERCASEISINVALID", testSignedPrefix)
assert.Error(t, err)
// Invalid signedPrefix
_, err = NewPRMRemapIdentity(testPrefix, "")
assert.Error(t, err)
_, err = NewPRMRemapIdentity(testPrefix, "example.com/UPPERCASEISINVALID")
assert.Error(t, err)
}

func TestPRMRemapIdentityUnmarshalJSON(t *testing.T) {
policyJSONUmarshallerTests{
newDest: func() json.Unmarshaler { return &prmRemapIdentity{} },
newValidObject: func() (interface{}, error) {
return NewPRMRemapIdentity("example.com/docker-library", "docker.io/library")
},
otherJSONParser: func(validJSON []byte) (interface{}, error) {
return newPolicyReferenceMatchFromJSON(validJSON)
},
breakFns: []func(mSI){
// The "type" field is missing
func(v mSI) { delete(v, "type") },
// Wrong "type" field
func(v mSI) { v["type"] = 1 },
func(v mSI) { v["type"] = "this is invalid" },
// Extra top-level sub-object
func(v mSI) { v["unexpected"] = 1 },
// The "prefix" field is missing
func(v mSI) { delete(v, "prefix") },
// Invalid "prefix" field
func(v mSI) { v["prefix"] = 1 },
func(v mSI) { v["prefix"] = "this is invalid" },
// The "signedPrefix" field is missing
func(v mSI) { delete(v, "signedPrefix") },
// Invalid "signedPrefix" field
func(v mSI) { v["signedPrefix"] = 1 },
func(v mSI) { v["signedPrefix"] = "this is invalid" },
},
duplicateFields: []string{"type", "prefix", "signedPrefix"},
}.run(t)
}

0 comments on commit 7589fa9

Please sign in to comment.