Skip to content

Commit

Permalink
Add signedIdentity type:matchRepoDigestOrExact, make it the default
Browse files Browse the repository at this point in the history
This is the new default: tag references require a signature with a
matching repo:tag, digest references require a signature with a matching
repo (and any tag [or digest]), with the digest itself still being
validated in image.UnparsedImage, independently of signature processing.

Users can still opt into strict checking by specifying matchExact
in signedIdentity.

Also update most tests to use matchExactOrSignedDigest, to match
the default.

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
  • Loading branch information
mtrmac committed Nov 8, 2016
1 parent 99d9941 commit 322058e
Show file tree
Hide file tree
Showing 8 changed files with 268 additions and 55 deletions.
38 changes: 11 additions & 27 deletions docs/policy.json.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,11 +156,19 @@ Exactly one of `keyPath` and `keyData` must be present, containing a GPG keyring
The `signedIdentity` field, a JSON object, specifies what image identity the signature claims about the image.
One of the following alternatives are supported:

- The identity in the signature must exactly match the image identity:
- The identity in the signature must exactly match the image identity. Note that with this, referencing an image by digest (with a signature claiming a _repository_`:`_tag_ identity) will fail.

```json
{"type":"matchExact"}
```
- If the image identity carries a tag, the identity in the signature must exactly match;
if the image identity uses a digest reference, the identity in the signature must be in the same repository as the image identity (using any tag).

(Note that with images identified using digest references, the digest from the reference is validated even before signature verification starts.)

```json
{"type":"matchRepoDigestOrExact"}
```
- The identity in the signature must be in the same repository as the image identity. This is useful e.g. to pull an image using the `:latest` tag when the image is signed with a tag specifing an exact image version.

```json
Expand All @@ -185,9 +193,9 @@ One of the following alternatives are supported:
}
```

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

*Note*: `matchExact` and `matchRepository` can be only used if a Docker-like image identity is
*Note*: `matchExact`, `matchRepoDigestOrExact` and `matchRepository` can be only used if a Docker-like image identity is
provided by the transport. In particular, the `dir:` and `oci:` transports can be only
used with `exactReference` or `exactRepository`.

Expand Down Expand Up @@ -257,27 +265,3 @@ selectively allow individual transports and scopes as desired.
"default": [{"type": "insecureAcceptAnything"}]
}
```

### A _temporary_ work-around to allow accessing any image by digest

Usually, identities in signatures use the _repository_`:`_tag_ format,
which is not matched when pulling a specific image using a digest.
To allow such operations, a policy may set `signedIdentity` to `matchRepository`, similar
to the following fragment:

```json
"hostname:5000/allow/pull/by/tag": [
{
"type": "signedBy",
"keyType": "GPGKeys",
"keyPath": "/path/to/some.gpg"
"signedIdentity": {
"type": "matchRepository"
}
}
]
```

*Warning*: This completely turns off tag matching for the signature check in question,
allowing also pulls by tag to accept signatures for any other tag.
A more granular solution for this situation will be provided in the future ( https://github.com/containers/image/issues/99 ).
5 changes: 4 additions & 1 deletion signature/fixtures/policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,10 @@
{
"type": "signedBy",
"keyType": "signedByGPGKeys",
"keyPath": "/keys/RH-key-signing-key-gpg-keyring"
"keyPath": "/keys/RH-key-signing-key-gpg-keyring",
"signedIdentity": {
"type": "matchRepoDigestOrExact"
}
}
],
"bogus/key-data-example": [
Expand Down
41 changes: 39 additions & 2 deletions signature/policy_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ func (pr *prSignedBy) UnmarshalJSON(data []byte) error {
return InvalidPolicyFormatError(fmt.Sprintf("Unexpected policy requirement type \"%s\"", tmp.Type))
}
if signedIdentity == nil {
tmp.SignedIdentity = NewPRMMatchExact()
tmp.SignedIdentity = NewPRMMatchRepoDigestOrExact()
} else {
si, err := newPolicyReferenceMatchFromJSON(signedIdentity)
if err != nil {
Expand Down Expand Up @@ -501,7 +501,7 @@ func (pr *prSignedBaseLayer) UnmarshalJSON(data []byte) error {
return nil
}

// newPolicyRequirementFromJSON parses JSON data into a PolicyReferenceMatch implementation.
// newPolicyReferenceMatchFromJSON parses JSON data into a PolicyReferenceMatch implementation.
func newPolicyReferenceMatchFromJSON(data []byte) (PolicyReferenceMatch, error) {
var typeField prmCommon
if err := json.Unmarshal(data, &typeField); err != nil {
Expand All @@ -511,6 +511,8 @@ func newPolicyReferenceMatchFromJSON(data []byte) (PolicyReferenceMatch, error)
switch typeField.Type {
case prmTypeMatchExact:
res = &prmMatchExact{}
case prmTypeMatchRepoDigestOrExact:
res = &prmMatchRepoDigestOrExact{}
case prmTypeMatchRepository:
res = &prmMatchRepository{}
case prmTypeExactReference:
Expand Down Expand Up @@ -561,6 +563,41 @@ func (prm *prmMatchExact) UnmarshalJSON(data []byte) error {
return nil
}

// newPRMMatchRepoDigestOrExact is NewPRMMatchRepoDigestOrExact, except it resturns the private type.
func newPRMMatchRepoDigestOrExact() *prmMatchRepoDigestOrExact {
return &prmMatchRepoDigestOrExact{prmCommon{Type: prmTypeMatchRepoDigestOrExact}}
}

// NewPRMMatchRepoDigestOrExact returns a new "matchRepoDigestOrExact" PolicyReferenceMatch.
func NewPRMMatchRepoDigestOrExact() PolicyReferenceMatch {
return newPRMMatchRepoDigestOrExact()
}

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

// UnmarshalJSON implements the json.Unmarshaler interface.
func (prm *prmMatchRepoDigestOrExact) UnmarshalJSON(data []byte) error {
*prm = prmMatchRepoDigestOrExact{}
var tmp prmMatchRepoDigestOrExact
if err := paranoidUnmarshalJSONObject(data, func(key string) interface{} {
switch key {
case "type":
return &tmp.Type
default:
return nil
}
}); err != nil {
return err
}

if tmp.Type != prmTypeMatchRepoDigestOrExact {
return InvalidPolicyFormatError(fmt.Sprintf("Unexpected policy requirement type \"%s\"", tmp.Type))
}
*prm = *newPRMMatchRepoDigestOrExact()
return nil
}

// newPRMMatchRepository is NewPRMMatchRepository, except it resturns the private type.
func newPRMMatchRepository() *prmMatchRepository {
return &prmMatchRepository{prmCommon{Type: prmTypeMatchRepository}}
Expand Down
108 changes: 91 additions & 17 deletions signature/policy_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ var policyFixtureContents = &Policy{
"example.com/production": {
xNewPRSignedByKeyPath(SBKeyTypeGPGKeys,
"/keys/employee-gpg-keyring",
NewPRMMatchExact()),
NewPRMMatchRepoDigestOrExact()),
},
"example.com/hardened": {
xNewPRSignedByKeyPath(SBKeyTypeGPGKeys,
Expand All @@ -45,17 +45,17 @@ var policyFixtureContents = &Policy{
NewPRMMatchRepository()),
xNewPRSignedByKeyPath(SBKeyTypeSignedByX509CAs,
"/keys/public-key-signing-ca-file",
NewPRMMatchExact()),
NewPRMMatchRepoDigestOrExact()),
},
"registry.access.redhat.com": {
xNewPRSignedByKeyPath(SBKeyTypeSignedByGPGKeys,
"/keys/RH-key-signing-key-gpg-keyring",
NewPRMMatchExact()),
NewPRMMatchRepoDigestOrExact()),
},
"bogus/key-data-example": {
xNewPRSignedByKeyData(SBKeyTypeSignedByGPGKeys,
[]byte("nonsense"),
NewPRMMatchExact()),
NewPRMMatchRepoDigestOrExact()),
},
"bogus/signed-identity-example": {
xNewPRSignedBaseLayer(xNewPRMExactReference("registry.access.redhat.com/rhel7/rhel:latest")),
Expand Down Expand Up @@ -228,12 +228,12 @@ func TestPolicyUnmarshalJSON(t *testing.T) {
// Start with a valid JSON.
validPolicy := Policy{
Default: []PolicyRequirement{
xNewPRSignedByKeyData(SBKeyTypeGPGKeys, []byte("abc"), NewPRMMatchExact()),
xNewPRSignedByKeyData(SBKeyTypeGPGKeys, []byte("abc"), NewPRMMatchRepoDigestOrExact()),
},
Transports: map[string]PolicyTransportScopes{
"docker": {
"docker.io/library/busybox": []PolicyRequirement{
xNewPRSignedByKeyData(SBKeyTypeGPGKeys, []byte("def"), NewPRMMatchExact()),
xNewPRSignedByKeyData(SBKeyTypeGPGKeys, []byte("def"), NewPRMMatchRepoDigestOrExact()),
},
"registry.access.redhat.com": []PolicyRequirement{
xNewPRSignedByKeyData(SBKeyTypeSignedByGPGKeys, []byte("RH"), NewPRMMatchRepository()),
Expand Down Expand Up @@ -310,7 +310,7 @@ func TestPolicyTransportScopesUnmarshalJSON(t *testing.T) {
// Start with a valid JSON.
validPTS := PolicyTransportScopes{
"": []PolicyRequirement{
xNewPRSignedByKeyData(SBKeyTypeGPGKeys, []byte("global"), NewPRMMatchExact()),
xNewPRSignedByKeyData(SBKeyTypeGPGKeys, []byte("global"), NewPRMMatchRepoDigestOrExact()),
},
}
validJSON, err := json.Marshal(validPTS)
Expand Down Expand Up @@ -355,13 +355,13 @@ func TestPolicyTransportScopesWithTransportUnmarshalJSON(t *testing.T) {
// Start with a valid JSON.
validPTS := PolicyTransportScopes{
"docker.io/library/busybox": []PolicyRequirement{
xNewPRSignedByKeyData(SBKeyTypeGPGKeys, []byte("def"), NewPRMMatchExact()),
xNewPRSignedByKeyData(SBKeyTypeGPGKeys, []byte("def"), NewPRMMatchRepoDigestOrExact()),
},
"registry.access.redhat.com": []PolicyRequirement{
xNewPRSignedByKeyData(SBKeyTypeSignedByGPGKeys, []byte("RH"), NewPRMMatchRepository()),
},
"": []PolicyRequirement{
xNewPRSignedByKeyData(SBKeyTypeGPGKeys, []byte("global"), NewPRMMatchExact()),
xNewPRSignedByKeyData(SBKeyTypeGPGKeys, []byte("global"), NewPRMMatchRepoDigestOrExact()),
},
}
validJSON, err := json.Marshal(validPTS)
Expand Down Expand Up @@ -440,7 +440,7 @@ func TestPolicyRequirementsUnmarshalJSON(t *testing.T) {

// Start with a valid JSON.
validReqs := PolicyRequirements{
xNewPRSignedByKeyData(SBKeyTypeGPGKeys, []byte("def"), NewPRMMatchExact()),
xNewPRSignedByKeyData(SBKeyTypeGPGKeys, []byte("def"), NewPRMMatchRepoDigestOrExact()),
xNewPRSignedByKeyData(SBKeyTypeSignedByGPGKeys, []byte("RH"), NewPRMMatchRepository()),
}
validJSON, err := json.Marshal(validReqs)
Expand Down Expand Up @@ -632,7 +632,7 @@ func TestPRRejectUnmarshalJSON(t *testing.T) {
func TestNewPRSignedBy(t *testing.T) {
const testPath = "/foo/bar"
testData := []byte("abc")
testIdentity := NewPRMMatchExact()
testIdentity := NewPRMMatchRepoDigestOrExact()

// Success
pr, err := newPRSignedBy(SBKeyTypeGPGKeys, testPath, nil, testIdentity)
Expand Down Expand Up @@ -671,7 +671,7 @@ func TestNewPRSignedBy(t *testing.T) {

func TestNewPRSignedByKeyPath(t *testing.T) {
const testPath = "/foo/bar"
_pr, err := NewPRSignedByKeyPath(SBKeyTypeGPGKeys, testPath, NewPRMMatchExact())
_pr, err := NewPRSignedByKeyPath(SBKeyTypeGPGKeys, testPath, NewPRMMatchRepoDigestOrExact())
require.NoError(t, err)
pr, ok := _pr.(*prSignedBy)
require.True(t, ok)
Expand All @@ -681,7 +681,7 @@ func TestNewPRSignedByKeyPath(t *testing.T) {

func TestNewPRSignedByKeyData(t *testing.T) {
testData := []byte("abc")
_pr, err := NewPRSignedByKeyData(SBKeyTypeGPGKeys, testData, NewPRMMatchExact())
_pr, err := NewPRSignedByKeyData(SBKeyTypeGPGKeys, testData, NewPRMMatchRepoDigestOrExact())
require.NoError(t, err)
pr, ok := _pr.(*prSignedBy)
require.True(t, ok)
Expand Down Expand Up @@ -710,7 +710,7 @@ func TestPRSignedByUnmarshalJSON(t *testing.T) {
testInvalidJSONInput(t, &pr)

// Start with a valid JSON.
validPR, err := NewPRSignedByKeyData(SBKeyTypeGPGKeys, []byte("abc"), NewPRMMatchExact())
validPR, err := NewPRSignedByKeyData(SBKeyTypeGPGKeys, []byte("abc"), NewPRMMatchRepoDigestOrExact())
require.NoError(t, err)
validJSON, err := json.Marshal(validPR)
require.NoError(t, err)
Expand All @@ -722,7 +722,7 @@ func TestPRSignedByUnmarshalJSON(t *testing.T) {
assert.Equal(t, validPR, &pr)

// Success with KeyPath
kpPR, err := NewPRSignedByKeyPath(SBKeyTypeGPGKeys, "/foo/bar", NewPRMMatchExact())
kpPR, err := NewPRSignedByKeyPath(SBKeyTypeGPGKeys, "/foo/bar", NewPRMMatchRepoDigestOrExact())
require.NoError(t, err)
testJSON, err := json.Marshal(kpPR)
require.NoError(t, err)
Expand Down Expand Up @@ -782,7 +782,7 @@ func TestPRSignedByUnmarshalJSON(t *testing.T) {
assert.Error(t, err)
}
// Handle "keyPath", which is not in validJSON, specially
pathPR, err := NewPRSignedByKeyPath(SBKeyTypeGPGKeys, "/foo/bar", NewPRMMatchExact())
pathPR, err := NewPRSignedByKeyPath(SBKeyTypeGPGKeys, "/foo/bar", NewPRMMatchRepoDigestOrExact())
require.NoError(t, err)
testJSON, err = json.Marshal(pathPR)
require.NoError(t, err)
Expand All @@ -801,6 +801,18 @@ func TestPRSignedByUnmarshalJSON(t *testing.T) {
require.NoError(t, err)
}

// Various ways to set signedIdentity to the default value
signedIdentityDefaultFns := []func(mSI){
// Set signedIdentity to the default explicitly
func(v mSI) { v["signedIdentity"] = NewPRMMatchRepoDigestOrExact() },
// Delete the signedIdentity field
func(v mSI) { delete(v, "signedIdentity") },
}
for _, fn := range signedIdentityDefaultFns {
err = tryUnmarshalModifiedSignedBy(t, &pr, validJSON, fn)
require.NoError(t, err)
assert.Equal(t, NewPRMMatchRepoDigestOrExact(), pr.SignedIdentity)
}
}

func TestSBKeyTypeIsValid(t *testing.T) {
Expand Down Expand Up @@ -945,7 +957,7 @@ func TestPRSignedBaseLayerUnmarshalJSON(t *testing.T) {

func TestNewPolicyReferenceMatchFromJSON(t *testing.T) {
// Sample success. Others tested in the individual PolicyReferenceMatch.UnmarshalJSON implementations.
validPRM := NewPRMMatchExact()
validPRM := NewPRMMatchRepoDigestOrExact()
validJSON, err := json.Marshal(validPRM)
require.NoError(t, err)
prm, err := newPolicyReferenceMatchFromJSON(validJSON)
Expand Down Expand Up @@ -1036,6 +1048,68 @@ func TestPRMMatchExactUnmarshalJSON(t *testing.T) {
}
}

func TestNewPRMMatchRepoDigestOrExact(t *testing.T) {
_prm := NewPRMMatchRepoDigestOrExact()
prm, ok := _prm.(*prmMatchRepoDigestOrExact)
require.True(t, ok)
assert.Equal(t, &prmMatchRepoDigestOrExact{prmCommon{prmTypeMatchRepoDigestOrExact}}, prm)
}

func TestPRMMatchRepoDigestOrExactUnmarshalJSON(t *testing.T) {
var prm prmMatchRepoDigestOrExact

testInvalidJSONInput(t, &prm)

// Start with a valid JSON.
validPR := NewPRMMatchRepoDigestOrExact()
validJSON, err := json.Marshal(validPR)
require.NoError(t, err)

// Success
prm = prmMatchRepoDigestOrExact{}
err = json.Unmarshal(validJSON, &prm)
require.NoError(t, err)
assert.Equal(t, validPR, &prm)

// newPolicyReferenceMatchFromJSON recognizes this type
_pr, err := newPolicyReferenceMatchFromJSON(validJSON)
require.NoError(t, err)
assert.Equal(t, validPR, _pr)

for _, invalid := range []mSI{
// Missing "type" field
{},
// Wrong "type" field
{"type": 1},
{"type": "this is invalid"},
// Extra fields
{
"type": string(prmTypeMatchRepoDigestOrExact),
"unknown": "foo",
},
} {
testJSON, err := json.Marshal(invalid)
require.NoError(t, err)

prm = prmMatchRepoDigestOrExact{}
err = json.Unmarshal(testJSON, &prm)
assert.Error(t, err, string(testJSON))
}

// Duplicated fields
for _, field := range []string{"type"} {
var tmp mSI
err := json.Unmarshal(validJSON, &tmp)
require.NoError(t, err)

testJSON := addExtraJSONMember(t, validJSON, field, tmp[field])

prm = prmMatchRepoDigestOrExact{}
err = json.Unmarshal(testJSON, &prm)
assert.Error(t, err)
}
}

func TestNewPRMMatchRepository(t *testing.T) {
_prm := NewPRMMatchRepository()
prm, ok := _prm.(*prmMatchRepository)
Expand Down
2 changes: 1 addition & 1 deletion signature/policy_eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func (ref pcImageReferenceMock) DeleteImage(ctx *types.SystemContext) error {

func TestPolicyContextRequirementsForImageRef(t *testing.T) {
ktGPG := SBKeyTypeGPGKeys
prm := NewPRMMatchExact()
prm := NewPRMMatchRepoDigestOrExact()

policy := &Policy{
Default: PolicyRequirements{NewPRReject()},
Expand Down
Loading

0 comments on commit 322058e

Please sign in to comment.