diff --git a/docs/policy.json.md b/docs/policy.json.md index 9e705a6138..e8fa12aeb0 100644 --- a/docs/policy.json.md +++ b/docs/policy.json.md @@ -146,13 +146,25 @@ This requirement requires an image to be signed with an expected identity, or ac "keyType": "GPGKeys", /* The only currently supported value */ "keyPath": "/path/to/local/keyring/file", "keyData": "base64-encoded-keyring-data", - "signedIdentity": identity_requirement + "signedIdentity": identity_requirement, + "referencesByDigest": refs_by_digest_requirement } ``` Exactly one of `keyPath` and `keyData` must be present, containing a GPG keyring of one or more public keys. Only signatures made by these keys are accepted. +The `referencesByDigest` field controls whether an reference with a digest should be taken into consideration when checking signatures or be rejected altogheter. +One of the following alternatives are supported: + +- References by digest are allowed (default). In this case, the digest should match the one from the signature for the verification to be successful. + ```json + {"referencesByDigest: allow"} + ``` +- References by digest are rejected and no signature verification is performed. + ```json + {"referencesByDigest: reject"} + ``` The `signedIdentity` field, a JSON object, specifies what image identity the signature claims about the image. One of the following alternatives are supported: @@ -257,27 +269,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 ). diff --git a/signature/docker.go b/signature/docker.go index 9f8ad61994..dcaa46866c 100644 --- a/signature/docker.go +++ b/signature/docker.go @@ -35,7 +35,7 @@ func VerifyDockerManifestSignature(unverifiedSignature, unverifiedManifest []byt } return nil }, - validateSignedDockerReference: func(signedDockerReference string) error { + validateSignedDockerReference: func(signedDockerReference, digest string) error { if signedDockerReference != expectedDockerReference { return InvalidSignatureError{msg: fmt.Sprintf("Docker reference %s does not match %s", signedDockerReference, expectedDockerReference)} diff --git a/signature/policy_config.go b/signature/policy_config.go index d5e20489bd..9ae714b07d 100644 --- a/signature/policy_config.go +++ b/signature/policy_config.go @@ -313,7 +313,7 @@ func (pr *prReject) UnmarshalJSON(data []byte) error { } // newPRSignedBy returns a new prSignedBy if parameters are valid. -func newPRSignedBy(keyType sbKeyType, keyPath string, keyData []byte, signedIdentity PolicyReferenceMatch) (*prSignedBy, error) { +func newPRSignedBy(keyType sbKeyType, keyPath string, keyData []byte, signedIdentity PolicyReferenceMatch, referencesByDigest sbUntaggedReferenceMatch) (*prSignedBy, error) { if !keyType.IsValid() { return nil, InvalidPolicyFormatError(fmt.Sprintf("invalid keyType \"%s\"", keyType)) } @@ -323,33 +323,37 @@ func newPRSignedBy(keyType sbKeyType, keyPath string, keyData []byte, signedIden if signedIdentity == nil { return nil, InvalidPolicyFormatError("signedIdentity not specified") } + if referencesByDigest != SBKeyTypeUntaggedReferenceAllow && referencesByDigest != SBKeyTypeUntaggedReferenceReject { + return nil, InvalidPolicyFormatError("referencesByDigest accepts either allow or reject") + } return &prSignedBy{ - prCommon: prCommon{Type: prTypeSignedBy}, - KeyType: keyType, - KeyPath: keyPath, - KeyData: keyData, - SignedIdentity: signedIdentity, + prCommon: prCommon{Type: prTypeSignedBy}, + KeyType: keyType, + KeyPath: keyPath, + KeyData: keyData, + SignedIdentity: signedIdentity, + ReferencesByDigest: referencesByDigest, }, nil } // newPRSignedByKeyPath is NewPRSignedByKeyPath, except it returns the private type. -func newPRSignedByKeyPath(keyType sbKeyType, keyPath string, signedIdentity PolicyReferenceMatch) (*prSignedBy, error) { - return newPRSignedBy(keyType, keyPath, nil, signedIdentity) +func newPRSignedByKeyPath(keyType sbKeyType, keyPath string, signedIdentity PolicyReferenceMatch, referencesByDigest sbUntaggedReferenceMatch) (*prSignedBy, error) { + return newPRSignedBy(keyType, keyPath, nil, signedIdentity, referencesByDigest) } // NewPRSignedByKeyPath returns a new "signedBy" PolicyRequirement using a KeyPath -func NewPRSignedByKeyPath(keyType sbKeyType, keyPath string, signedIdentity PolicyReferenceMatch) (PolicyRequirement, error) { - return newPRSignedByKeyPath(keyType, keyPath, signedIdentity) +func NewPRSignedByKeyPath(keyType sbKeyType, keyPath string, signedIdentity PolicyReferenceMatch, referencesByDigest sbUntaggedReferenceMatch) (PolicyRequirement, error) { + return newPRSignedByKeyPath(keyType, keyPath, signedIdentity, referencesByDigest) } // newPRSignedByKeyData is NewPRSignedByKeyData, except it returns the private type. -func newPRSignedByKeyData(keyType sbKeyType, keyData []byte, signedIdentity PolicyReferenceMatch) (*prSignedBy, error) { - return newPRSignedBy(keyType, "", keyData, signedIdentity) +func newPRSignedByKeyData(keyType sbKeyType, keyData []byte, signedIdentity PolicyReferenceMatch, referencesByDigest sbUntaggedReferenceMatch) (*prSignedBy, error) { + return newPRSignedBy(keyType, "", keyData, signedIdentity, referencesByDigest) } // NewPRSignedByKeyData returns a new "signedBy" PolicyRequirement using a KeyData -func NewPRSignedByKeyData(keyType sbKeyType, keyData []byte, signedIdentity PolicyReferenceMatch) (PolicyRequirement, error) { - return newPRSignedByKeyData(keyType, keyData, signedIdentity) +func NewPRSignedByKeyData(keyType sbKeyType, keyData []byte, signedIdentity PolicyReferenceMatch, referencesByDigest sbUntaggedReferenceMatch) (PolicyRequirement, error) { + return newPRSignedByKeyData(keyType, keyData, signedIdentity, referencesByDigest) } // Compile-time check that prSignedBy implements json.Unmarshaler. @@ -375,6 +379,8 @@ func (pr *prSignedBy) UnmarshalJSON(data []byte) error { return &tmp.KeyData case "signedIdentity": return &signedIdentity + case "referencesByDigest": + return &tmp.ReferencesByDigest default: return nil } @@ -395,15 +401,19 @@ func (pr *prSignedBy) UnmarshalJSON(data []byte) error { tmp.SignedIdentity = si } + if tmp.ReferencesByDigest == "" { + tmp.ReferencesByDigest = SBKeyTypeUntaggedReferenceAllow + } + var res *prSignedBy var err error switch { case gotKeyPath && gotKeyData: return InvalidPolicyFormatError("keyPath and keyData cannot be used simultaneously") case gotKeyPath && !gotKeyData: - res, err = newPRSignedByKeyPath(tmp.KeyType, tmp.KeyPath, tmp.SignedIdentity) + res, err = newPRSignedByKeyPath(tmp.KeyType, tmp.KeyPath, tmp.SignedIdentity, tmp.ReferencesByDigest) case !gotKeyPath && gotKeyData: - res, err = newPRSignedByKeyData(tmp.KeyType, tmp.KeyData, tmp.SignedIdentity) + res, err = newPRSignedByKeyData(tmp.KeyType, tmp.KeyData, tmp.SignedIdentity, tmp.ReferencesByDigest) case !gotKeyPath && !gotKeyData: return InvalidPolicyFormatError("At least one of keyPath and keyData mus be specified") default: // Coverage: This should never happen @@ -501,7 +511,7 @@ func (pr *prSignedBaseLayer) UnmarshalJSON(data []byte) error { return nil } -// newPolicyRequirementFromJSON parses JSON data into a PolicyReferenceMatch implementation. +// newPolicyReferenceFromJSON parses JSON data into a PolicyReferenceMatch implementation. func newPolicyReferenceMatchFromJSON(data []byte) (PolicyReferenceMatch, error) { var typeField prmCommon if err := json.Unmarshal(data, &typeField); err != nil { diff --git a/signature/policy_config_test.go b/signature/policy_config_test.go index 8af3003d24..9eaa9a8496 100644 --- a/signature/policy_config_test.go +++ b/signature/policy_config_test.go @@ -204,7 +204,7 @@ func tryUnmarshalModifiedPolicy(t *testing.T, p *Policy, validJSON []byte, modif // xNewPRSignedByKeyPath is like NewPRSignedByKeyPath, except it must not fail. func xNewPRSignedByKeyPath(keyType sbKeyType, keyPath string, signedIdentity PolicyReferenceMatch) PolicyRequirement { - pr, err := NewPRSignedByKeyPath(keyType, keyPath, signedIdentity) + pr, err := NewPRSignedByKeyPath(keyType, keyPath, signedIdentity, SBKeyTypeUntaggedReferenceAllow) if err != nil { panic("xNewPRSignedByKeyPath failed") } @@ -213,7 +213,7 @@ func xNewPRSignedByKeyPath(keyType sbKeyType, keyPath string, signedIdentity Pol // xNewPRSignedByKeyData is like NewPRSignedByKeyData, except it must not fail. func xNewPRSignedByKeyData(keyType sbKeyType, keyData []byte, signedIdentity PolicyReferenceMatch) PolicyRequirement { - pr, err := NewPRSignedByKeyData(keyType, keyData, signedIdentity) + pr, err := NewPRSignedByKeyData(keyType, keyData, signedIdentity, SBKeyTypeUntaggedReferenceAllow) if err != nil { panic("xNewPRSignedByKeyData failed") } @@ -635,7 +635,7 @@ func TestNewPRSignedBy(t *testing.T) { testIdentity := NewPRMMatchExact() // Success - pr, err := newPRSignedBy(SBKeyTypeGPGKeys, testPath, nil, testIdentity) + pr, err := newPRSignedBy(SBKeyTypeGPGKeys, testPath, nil, testIdentity, SBKeyTypeUntaggedReferenceAllow) require.NoError(t, err) assert.Equal(t, &prSignedBy{ prCommon: prCommon{prTypeSignedBy}, @@ -644,7 +644,7 @@ func TestNewPRSignedBy(t *testing.T) { KeyData: nil, SignedIdentity: testIdentity, }, pr) - pr, err = newPRSignedBy(SBKeyTypeGPGKeys, "", testData, testIdentity) + pr, err = newPRSignedBy(SBKeyTypeGPGKeys, "", testData, testIdentity, SBKeyTypeUntaggedReferenceAllow) require.NoError(t, err) assert.Equal(t, &prSignedBy{ prCommon: prCommon{prTypeSignedBy}, @@ -655,23 +655,23 @@ func TestNewPRSignedBy(t *testing.T) { }, pr) // Invalid keyType - pr, err = newPRSignedBy(sbKeyType(""), testPath, nil, testIdentity) + pr, err = newPRSignedBy(sbKeyType(""), testPath, nil, testIdentity, SBKeyTypeUntaggedReferenceAllow) assert.Error(t, err) - pr, err = newPRSignedBy(sbKeyType("this is invalid"), testPath, nil, testIdentity) + pr, err = newPRSignedBy(sbKeyType("this is invalid"), testPath, nil, testIdentity, SBKeyTypeUntaggedReferenceAllow) assert.Error(t, err) // Both keyPath and keyData specified - pr, err = newPRSignedBy(SBKeyTypeGPGKeys, testPath, testData, testIdentity) + pr, err = newPRSignedBy(SBKeyTypeGPGKeys, testPath, testData, testIdentity, SBKeyTypeUntaggedReferenceAllow) assert.Error(t, err) // Invalid signedIdentity - pr, err = newPRSignedBy(SBKeyTypeGPGKeys, testPath, nil, nil) + pr, err = newPRSignedBy(SBKeyTypeGPGKeys, testPath, nil, nil, SBKeyTypeUntaggedReferenceAllow) assert.Error(t, err) } func TestNewPRSignedByKeyPath(t *testing.T) { const testPath = "/foo/bar" - _pr, err := NewPRSignedByKeyPath(SBKeyTypeGPGKeys, testPath, NewPRMMatchExact()) + _pr, err := NewPRSignedByKeyPath(SBKeyTypeGPGKeys, testPath, NewPRMMatchExact(), SBKeyTypeUntaggedReferenceAllow) require.NoError(t, err) pr, ok := _pr.(*prSignedBy) require.True(t, ok) @@ -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, NewPRMMatchExact(), SBKeyTypeUntaggedReferenceAllow) require.NoError(t, err) pr, ok := _pr.(*prSignedBy) require.True(t, ok) @@ -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"), NewPRMMatchExact(), SBKeyTypeUntaggedReferenceAllow) require.NoError(t, err) validJSON, err := json.Marshal(validPR) require.NoError(t, err) @@ -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", NewPRMMatchExact(), SBKeyTypeUntaggedReferenceAllow) require.NoError(t, err) testJSON, err := json.Marshal(kpPR) require.NoError(t, err) @@ -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", NewPRMMatchExact(), SBKeyTypeUntaggedReferenceAllow) require.NoError(t, err) testJSON, err = json.Marshal(pathPR) require.NoError(t, err) diff --git a/signature/policy_eval.go b/signature/policy_eval.go index 4828ecc81b..e65e0562b1 100644 --- a/signature/policy_eval.go +++ b/signature/policy_eval.go @@ -70,7 +70,10 @@ type PolicyReferenceMatch interface { // matchesDockerReference decides whether a specific image identity is accepted for an image // (or, usually, for the image's Reference().DockerReference()). Note that // image.Reference().DockerReference() may be nil. - matchesDockerReference(image types.UnparsedImage, signatureDockerReference string) bool + // + // TODO(runcom): document byDigest + // TODO(runcom): document digest + matchesDockerReference(image types.UnparsedImage, signatureDockerReference, signatureManifstDigest string, byDigest bool) bool } // PolicyContext encapsulates a policy and possible cached state diff --git a/signature/policy_eval_signedby.go b/signature/policy_eval_signedby.go index 7b1ffac710..d611a689ee 100644 --- a/signature/policy_eval_signedby.go +++ b/signature/policy_eval_signedby.go @@ -69,8 +69,8 @@ func (pr *prSignedBy) isSignatureAuthorAccepted(image types.UnparsedImage, sig [ // not be reachable. return PolicyRequirementError(fmt.Sprintf("Signature by key %s is not accepted", keyIdentity)) }, - validateSignedDockerReference: func(ref string) error { - if !pr.SignedIdentity.matchesDockerReference(image, ref) { + validateSignedDockerReference: func(ref, digest string) error { + if !pr.SignedIdentity.matchesDockerReference(image, ref, digest, pr.ReferencesByDigest == SBKeyTypeUntaggedReferenceAllow) { return PolicyRequirementError(fmt.Sprintf("Signature for identity %s is not accepted", ref)) } return nil diff --git a/signature/policy_eval_signedby_test.go b/signature/policy_eval_signedby_test.go index d21ee9c17f..a11d353da9 100644 --- a/signature/policy_eval_signedby_test.go +++ b/signature/policy_eval_signedby_test.go @@ -54,7 +54,7 @@ func TestPRSignedByIsSignatureAuthorAccepted(t *testing.T) { require.NoError(t, err) // Successful validation, with KeyData and KeyPath - pr, err := NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm) + pr, err := NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm, SBKeyTypeUntaggedReferenceAllow) require.NoError(t, err) sar, parsedSig, err := pr.isSignatureAuthorAccepted(testImage, testImageSig) assertSARAccepted(t, sar, parsedSig, err, Signature{ @@ -64,7 +64,7 @@ func TestPRSignedByIsSignatureAuthorAccepted(t *testing.T) { keyData, err := ioutil.ReadFile("fixtures/public-key.gpg") require.NoError(t, err) - pr, err = NewPRSignedByKeyData(ktGPG, keyData, prm) + pr, err = NewPRSignedByKeyData(ktGPG, keyData, prm, SBKeyTypeUntaggedReferenceAllow) require.NoError(t, err) sar, parsedSig, err = pr.isSignatureAuthorAccepted(testImage, testImageSig) assertSARAccepted(t, sar, parsedSig, err, Signature{ @@ -101,7 +101,7 @@ func TestPRSignedByIsSignatureAuthorAccepted(t *testing.T) { assertSARRejected(t, sar, parsedSig, err) // Invalid KeyPath - pr, err = NewPRSignedByKeyPath(ktGPG, "/this/does/not/exist", prm) + pr, err = NewPRSignedByKeyPath(ktGPG, "/this/does/not/exist", prm, SBKeyTypeUntaggedReferenceAllow) require.NoError(t, err) // Pass nil pointers to, kind of, test that the return value does not depend on the parameters. sar, parsedSig, err = pr.isSignatureAuthorAccepted(nil, nil) @@ -110,14 +110,14 @@ func TestPRSignedByIsSignatureAuthorAccepted(t *testing.T) { // Errors initializing the temporary GPG directory and mechanism are not obviously easy to reach. // KeyData has no public keys. - pr, err = NewPRSignedByKeyData(ktGPG, []byte{}, prm) + pr, err = NewPRSignedByKeyData(ktGPG, []byte{}, prm, SBKeyTypeUntaggedReferenceAllow) require.NoError(t, err) // Pass nil pointers to, kind of, test that the return value does not depend on the parameters. sar, parsedSig, err = pr.isSignatureAuthorAccepted(nil, nil) assertSARRejectedPolicyRequirement(t, sar, parsedSig, err) // A signature which does not GPG verify - pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm) + pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm, SBKeyTypeUntaggedReferenceAllow) require.NoError(t, err) // Pass a nil pointer to, kind of, test that the return value does not depend on the image parmater.. sar, parsedSig, err = pr.isSignatureAuthorAccepted(nil, []byte("invalid signature")) @@ -126,7 +126,7 @@ func TestPRSignedByIsSignatureAuthorAccepted(t *testing.T) { // A valid signature using an unknown key. // (This is (currently?) rejected through the "mech.Verify fails" path, not the "!identityFound" path, // because we use a temporary directory and only import the trusted keys.) - pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm) + pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm, SBKeyTypeUntaggedReferenceAllow) require.NoError(t, err) sig, err := ioutil.ReadFile("fixtures/unknown-key.signature") require.NoError(t, err) @@ -135,7 +135,7 @@ func TestPRSignedByIsSignatureAuthorAccepted(t *testing.T) { assertSARRejected(t, sar, parsedSig, err) // A valid signature of an invalid JSON. - pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm) + pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm, SBKeyTypeUntaggedReferenceAllow) require.NoError(t, err) sig, err = ioutil.ReadFile("fixtures/invalid-blob.signature") require.NoError(t, err) @@ -147,7 +147,7 @@ func TestPRSignedByIsSignatureAuthorAccepted(t *testing.T) { // A valid signature with a rejected identity. nonmatchingPRM, err := NewPRMExactReference("this/doesnt:match") require.NoError(t, err) - pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", nonmatchingPRM) + pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", nonmatchingPRM, SBKeyTypeUntaggedReferenceAllow) require.NoError(t, err) sar, parsedSig, err = pr.isSignatureAuthorAccepted(testImage, testImageSig) assertSARRejectedPolicyRequirement(t, sar, parsedSig, err) @@ -157,7 +157,7 @@ func TestPRSignedByIsSignatureAuthorAccepted(t *testing.T) { defer image.Close() sig, err = ioutil.ReadFile("fixtures/dir-img-no-manifest/signature-1") require.NoError(t, err) - pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm) + pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm, SBKeyTypeUntaggedReferenceAllow) require.NoError(t, err) sar, parsedSig, err = pr.isSignatureAuthorAccepted(image, sig) assertSARRejected(t, sar, parsedSig, err) @@ -167,7 +167,7 @@ func TestPRSignedByIsSignatureAuthorAccepted(t *testing.T) { defer image.Close() sig, err = ioutil.ReadFile("fixtures/dir-img-manifest-digest-error/signature-1") require.NoError(t, err) - pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm) + pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm, SBKeyTypeUntaggedReferenceAllow) require.NoError(t, err) sar, parsedSig, err = pr.isSignatureAuthorAccepted(image, sig) assertSARRejected(t, sar, parsedSig, err) @@ -177,7 +177,7 @@ func TestPRSignedByIsSignatureAuthorAccepted(t *testing.T) { defer image.Close() sig, err = ioutil.ReadFile("fixtures/dir-img-modified-manifest/signature-1") require.NoError(t, err) - pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm) + pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm, SBKeyTypeUntaggedReferenceAllow) require.NoError(t, err) sar, parsedSig, err = pr.isSignatureAuthorAccepted(image, sig) assertSARRejectedPolicyRequirement(t, sar, parsedSig, err) @@ -207,7 +207,7 @@ func TestPRSignedByIsRunningImageAllowed(t *testing.T) { // A simple success case: single valid signature. image := dirImageMock(t, "fixtures/dir-img-valid", "testing/manifest:latest") defer image.Close() - pr, err := NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm) + pr, err := NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm, SBKeyTypeUntaggedReferenceAllow) require.NoError(t, err) allowed, err := pr.isRunningImageAllowed(image) assertRunningAllowed(t, allowed, err) @@ -217,7 +217,7 @@ func TestPRSignedByIsRunningImageAllowed(t *testing.T) { defer os.RemoveAll(invalidSigDir) image = dirImageMock(t, invalidSigDir, "testing/manifest:latest") defer image.Close() - pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm) + pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm, SBKeyTypeUntaggedReferenceAllow) require.NoError(t, err) allowed, err = pr.isRunningImageAllowed(image) assertRunningRejected(t, allowed, err) @@ -225,7 +225,7 @@ func TestPRSignedByIsRunningImageAllowed(t *testing.T) { // No signatures image = dirImageMock(t, "fixtures/dir-img-unsigned", "testing/manifest:latest") defer image.Close() - pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm) + pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm, SBKeyTypeUntaggedReferenceAllow) require.NoError(t, err) allowed, err = pr.isRunningImageAllowed(image) assertRunningRejectedPolicyRequirement(t, allowed, err) @@ -233,7 +233,7 @@ func TestPRSignedByIsRunningImageAllowed(t *testing.T) { // 1 invalid signature: use dir-img-valid, but a non-matching Docker reference image = dirImageMock(t, "fixtures/dir-img-valid", "testing/manifest:notlatest") defer image.Close() - pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm) + pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm, SBKeyTypeUntaggedReferenceAllow) require.NoError(t, err) allowed, err = pr.isRunningImageAllowed(image) assertRunningRejectedPolicyRequirement(t, allowed, err) @@ -241,7 +241,7 @@ func TestPRSignedByIsRunningImageAllowed(t *testing.T) { // 2 valid signatures image = dirImageMock(t, "fixtures/dir-img-valid-2", "testing/manifest:latest") defer image.Close() - pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm) + pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm, SBKeyTypeUntaggedReferenceAllow) require.NoError(t, err) allowed, err = pr.isRunningImageAllowed(image) assertRunningAllowed(t, allowed, err) @@ -249,7 +249,7 @@ func TestPRSignedByIsRunningImageAllowed(t *testing.T) { // One invalid, one valid signature (in this order) image = dirImageMock(t, "fixtures/dir-img-mixed", "testing/manifest:latest") defer image.Close() - pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm) + pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm, SBKeyTypeUntaggedReferenceAllow) require.NoError(t, err) allowed, err = pr.isRunningImageAllowed(image) assertRunningAllowed(t, allowed, err) @@ -257,7 +257,7 @@ func TestPRSignedByIsRunningImageAllowed(t *testing.T) { // 2 invalid signatures: use dir-img-valid-2, but a non-matching Docker reference image = dirImageMock(t, "fixtures/dir-img-valid-2", "testing/manifest:notlatest") defer image.Close() - pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm) + pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm, SBKeyTypeUntaggedReferenceAllow) require.NoError(t, err) allowed, err = pr.isRunningImageAllowed(image) assertRunningRejectedPolicyRequirement(t, allowed, err) diff --git a/signature/policy_reference_match.go b/signature/policy_reference_match.go index aedda8d09b..9cfa10e61d 100644 --- a/signature/policy_reference_match.go +++ b/signature/policy_reference_match.go @@ -24,11 +24,19 @@ func parseImageAndDockerReference(image types.UnparsedImage, s2 string) (referen return r1, r2, nil } -func (prm *prmMatchExact) matchesDockerReference(image types.UnparsedImage, signatureDockerReference string) bool { +func (prm *prmMatchExact) matchesDockerReference(image types.UnparsedImage, signatureDockerReference, signatureManifestDigest string, byDigest bool) bool { intended, signature, err := parseImageAndDockerReference(image, signatureDockerReference) if err != nil { return false } + + if intendedCanonical, ok := intended.(reference.Canonical); ok { + if byDigest { + return signature.Name() == intended.Name() && + signatureManifestDigest == intendedCanonical.Digest().String() + } + } + // Do not add default tags: image.Reference().DockerReference() should contain it already, and signatureDockerReference should be exact; so, verify that now. if reference.IsNameOnly(intended) || reference.IsNameOnly(signature) { return false @@ -36,11 +44,18 @@ func (prm *prmMatchExact) matchesDockerReference(image types.UnparsedImage, sign return signature.String() == intended.String() } -func (prm *prmMatchRepository) matchesDockerReference(image types.UnparsedImage, signatureDockerReference string) bool { +func (prm *prmMatchRepository) matchesDockerReference(image types.UnparsedImage, signatureDockerReference, signatureManifestDigest string, byDigest bool) bool { intended, signature, err := parseImageAndDockerReference(image, signatureDockerReference) if err != nil { return false } + if intendedCanonical, ok := intended.(reference.Canonical); ok { + if byDigest { + if signatureManifestDigest != intendedCanonical.Digest().String() { + return false + } + } + } return signature.Name() == intended.Name() } @@ -57,7 +72,7 @@ func parseDockerReferences(s1, s2 string) (reference.Named, reference.Named, err return r1, r2, nil } -func (prm *prmExactReference) matchesDockerReference(image types.UnparsedImage, signatureDockerReference string) bool { +func (prm *prmExactReference) matchesDockerReference(image types.UnparsedImage, signatureDockerReference, signatureManifestDigest string, byDigest bool) bool { intended, signature, err := parseDockerReferences(prm.DockerReference, signatureDockerReference) if err != nil { return false @@ -66,13 +81,26 @@ func (prm *prmExactReference) matchesDockerReference(image types.UnparsedImage, if reference.IsNameOnly(intended) || reference.IsNameOnly(signature) { return false } + if intendedCanonical, ok := intended.(reference.Canonical); ok { + if byDigest { + return signature.Name() == intended.Name() && + signatureManifestDigest == intendedCanonical.Digest().String() + } + } return signature.String() == intended.String() } -func (prm *prmExactRepository) matchesDockerReference(image types.UnparsedImage, signatureDockerReference string) bool { +func (prm *prmExactRepository) matchesDockerReference(image types.UnparsedImage, signatureDockerReference, signatureManifestDigest string, byDigest bool) bool { intended, signature, err := parseDockerReferences(prm.DockerRepository, signatureDockerReference) if err != nil { return false } + if intendedCanonical, ok := intended.(reference.Canonical); ok { + if byDigest { + if signatureManifestDigest != intendedCanonical.Digest().String() { + return false + } + } + } return signature.Name() == intended.Name() } diff --git a/signature/policy_reference_match_test.go b/signature/policy_reference_match_test.go index 784cd3b669..9851d659bf 100644 --- a/signature/policy_reference_match_test.go +++ b/signature/policy_reference_match_test.go @@ -196,11 +196,11 @@ func TestPRMMatchExactMatchesDockerReference(t *testing.T) { if err != nil { continue } - res := prm.matchesDockerReference(refImageMock{imageRef}, test.sigRef) + res := prm.matchesDockerReference(refImageMock{imageRef}, test.sigRef, "", false) assert.Equal(t, test.result, res, fmt.Sprintf("%s vs. %s", test.imageRef, test.sigRef)) } // Even if they are signed with an empty string as a reference, unidentified images are rejected. - res := prm.matchesDockerReference(refImageMock{nil}, "") + res := prm.matchesDockerReference(refImageMock{nil}, "", "", false) assert.False(t, res, `unidentified vs. ""`) } @@ -213,11 +213,11 @@ func TestPRMMatchRepositoryMatchesDockerReference(t *testing.T) { if err != nil { continue } - res := prm.matchesDockerReference(refImageMock{imageRef}, test.sigRef) + res := prm.matchesDockerReference(refImageMock{imageRef}, test.sigRef, "", false) assert.Equal(t, test.result, res, fmt.Sprintf("%s vs. %s", test.imageRef, test.sigRef)) } // Even if they are signed with an empty string as a reference, unidentified images are rejected. - res := prm.matchesDockerReference(refImageMock{nil}, "") + res := prm.matchesDockerReference(refImageMock{nil}, "", "", false) assert.False(t, res, `unidentified vs. ""`) } @@ -267,7 +267,7 @@ func TestPRMExactReferenceMatchesDockerReference(t *testing.T) { // Do not use NewPRMExactReference, we want to also test the case with an invalid DockerReference, // even though NewPRMExactReference should never let it happen. prm := prmExactReference{DockerReference: test.imageRef} - res := prm.matchesDockerReference(forbiddenImageMock{}, test.sigRef) + res := prm.matchesDockerReference(forbiddenImageMock{}, test.sigRef, "", false) assert.Equal(t, test.result, res, fmt.Sprintf("%s vs. %s", test.imageRef, test.sigRef)) } } @@ -277,7 +277,7 @@ func TestPRMExactRepositoryMatchesDockerReference(t *testing.T) { // Do not use NewPRMExactRepository, we want to also test the case with an invalid DockerReference, // even though NewPRMExactRepository should never let it happen. prm := prmExactRepository{DockerRepository: test.imageRef} - res := prm.matchesDockerReference(forbiddenImageMock{}, test.sigRef) + res := prm.matchesDockerReference(forbiddenImageMock{}, test.sigRef, "", false) assert.Equal(t, test.result, res, fmt.Sprintf("%s vs. %s", test.imageRef, test.sigRef)) } } diff --git a/signature/policy_types.go b/signature/policy_types.go index 5775ab21dd..f2f3da9259 100644 --- a/signature/policy_types.go +++ b/signature/policy_types.go @@ -79,8 +79,18 @@ type prSignedBy struct { // SignedIdentity specifies what image identity the signature must be claiming about the image. // Defaults to "match-exact" if not specified. SignedIdentity PolicyReferenceMatch `json:"signedIdentity"` + + // ReferencesByDigest specifies whether the signature is valid when the reference isn't tagged (e.g. canonical). + ReferencesByDigest sbUntaggedReferenceMatch `json:"referencesByDigest"` } +type sbUntaggedReferenceMatch string + +const ( + SBKeyTypeUntaggedReferenceAllow sbUntaggedReferenceMatch = "allow" + SBKeyTypeUntaggedReferenceReject sbUntaggedReferenceMatch = "reject" +) + // sbKeyType are the allowed values for prSignedBy.KeyType type sbKeyType string @@ -143,3 +153,24 @@ type prmExactRepository struct { prmCommon DockerRepository string `json:"dockerRepository"` } + +// TODO(runcom): document stuff below + +const ( + prmTypeUntaggedProhibit prmTypeIdentifier = "prohibit" + prmTypeUntaggedMatchAnyTag prmTypeIdentifier = "matchAnyTag" + prmTypeUntaggedMatchPrecisely prmTypeIdentifier = "matchPrecisely" +) + +type prmUntaggedProhibit struct { + prmCommon +} + +type prmUntaggedMatchAnyTag struct { + prmCommon +} + +type prmUntaggedMatchPrecisely struct { + prmCommon + DockerRepository string `json:"dockerRepository"` +} diff --git a/signature/signature.go b/signature/signature.go index 2309e4998d..c42d8f5333 100644 --- a/signature/signature.go +++ b/signature/signature.go @@ -161,7 +161,7 @@ func (s privateSignature) sign(mech SigningMechanism, keyIdentity string) ([]byt // named members of this struct are more explicit. type signatureAcceptanceRules struct { validateKeyIdentity func(string) error - validateSignedDockerReference func(string) error + validateSignedDockerReference func(string, string) error validateSignedDockerManifestDigest func(string) error } @@ -183,7 +183,7 @@ func verifyAndExtractSignature(mech SigningMechanism, unverifiedSignature []byte if err := rules.validateSignedDockerManifestDigest(unmatchedSignature.DockerManifestDigest); err != nil { return nil, err } - if err := rules.validateSignedDockerReference(unmatchedSignature.DockerReference); err != nil { + if err := rules.validateSignedDockerReference(unmatchedSignature.DockerReference, unmatchedSignature.DockerManifestDigest); err != nil { return nil, err } signature := unmatchedSignature.Signature // Policy OK. diff --git a/signature/signature_test.go b/signature/signature_test.go index db63abee55..edee3cbccc 100644 --- a/signature/signature_test.go +++ b/signature/signature_test.go @@ -157,7 +157,7 @@ func TestSign(t *testing.T) { } return nil }, - validateSignedDockerReference: func(signedDockerReference string) error { + validateSignedDockerReference: func(signedDockerReference, signedManifestDigest string) error { if signedDockerReference != sig.DockerReference { return fmt.Errorf("Unexpected signedDockerReference") } @@ -199,7 +199,7 @@ func TestVerifyAndExtractSignature(t *testing.T) { } return nil }, - validateSignedDockerReference: func(signedDockerReference string) error { + validateSignedDockerReference: func(signedDockerReference, signedManifestDigest string) error { recorded.signedDockerReference = signedDockerReference if signedDockerReference != wanted.signedDockerReference { return fmt.Errorf("signedDockerReference mismatch")