From ffcd0be0487999c6bd47614e56111d6b08ad4603 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Mon, 20 Mar 2017 21:12:46 +0100 Subject: [PATCH 1/3] Add a JSON schema for the JSON data embedded in a signature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Note that this is NOT a replacement for the atomic-signature.md documentation. Signed-off-by: Miloslav Trmač --- docs/atomic-signature-embedded-json.json | 66 ++++++++++++++++++++++++ signature/signature.go | 2 +- 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 docs/atomic-signature-embedded-json.json diff --git a/docs/atomic-signature-embedded-json.json b/docs/atomic-signature-embedded-json.json new file mode 100644 index 0000000000..ccb4eda096 --- /dev/null +++ b/docs/atomic-signature-embedded-json.json @@ -0,0 +1,66 @@ +{ + "title": "JSON embedded in an atomic container signature", + "description": "This schema is a supplement to atomic-signature.md in this directory.\n\nConsumers of the JSON MUST use the processing rules documented in atomic-signature.md, especially the requirements for the 'critical' subjobject.\n\nWhenever this schema and atomic-signature.md, or the github.com/containers/image/signature implementation, differ,\nit is the atomic-signature.md document, or the github.com/containers/image/signature implementation, which governs.\n\nUsers are STRONGLY RECOMMENDED to use the github.com/containeres/image/signature implementation instead of writing\ntheir own, ESPECIALLY when consuming signatures, so that the policy.json format can be shared by all image consumers.\n", + "type": "object", + "required": [ + "critical", + "optional" + ], + "additionalProperties": false, + "properties": { + "critical": { + "type": "object", + "required": [ + "type", + "image", + "identity" + ], + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "enum": [ + "atomic container signature" + ] + }, + "image": { + "type": "object", + "required": [ + "docker-manifest-digest" + ], + "additionalProperties": false, + "properties": { + "docker-manifest-digest": { + "type": "string" + } + } + }, + "identity": { + "type": "object", + "required": [ + "docker-reference" + ], + "additionalProperties": false, + "properties": { + "docker-reference": { + "type": "string" + } + } + } + } + }, + "optional": { + "type": "object", + "description": "All members are optional, but if they are included, they must be valid.", + "additionalProperties": true, + "properties": { + "creator": { + "type": "string" + }, + "timestamp": { + "type": "integer" + } + } + } + } +} \ No newline at end of file diff --git a/signature/signature.go b/signature/signature.go index 1fed265340..f6219bec87 100644 --- a/signature/signature.go +++ b/signature/signature.go @@ -1,6 +1,6 @@ // Note: Consider the API unstable until the code supports at least three different image formats or transports. -// NOTE: Keep this in sync with docs/atomic-signature.md! +// NOTE: Keep this in sync with docs/atomic-signature.md and docs/atomic-signature-embedded.json! package signature From 079f5b6f8e3d8d2adb4069e73af53135c4ca34d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Mon, 20 Mar 2017 22:17:48 +0100 Subject: [PATCH 2/3] Rework untrustedSignature.UnmarshalJSON testing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of an one-shot tryUnmarshalModifiedSignature and testing the resulting error value, use a separate modifiedUntrustedSignatureJSON helper, and a pair of successfullyUnmarshalUntrustedSignature / assertUnmarshalUntrustedSignatureFails helpers for the expected success / failure cases. This does not change behavior right now, but it will make it easier to add testing the JSON schema in the future. Signed-off-by: Miloslav Trmač --- signature/signature_test.go | 53 ++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/signature/signature_test.go b/signature/signature_test.go index 711cd5798c..7268abc8a9 100644 --- a/signature/signature_test.go +++ b/signature/signature_test.go @@ -78,33 +78,48 @@ func TestMarshalJSON(t *testing.T) { } } -// Return the result of modifying validJSON with fn and unmarshaling it into *sig -func tryUnmarshalModifiedSignature(t *testing.T, sig *untrustedSignature, validJSON []byte, modifyFn func(mSI)) error { +// Return the result of modifying validJSON with fn +func modifiedUntrustedSignatureJSON(t *testing.T, validJSON []byte, modifyFn func(mSI)) []byte { var tmp mSI err := json.Unmarshal(validJSON, &tmp) require.NoError(t, err) modifyFn(tmp) - testJSON, err := json.Marshal(tmp) + modifiedJSON, err := json.Marshal(tmp) require.NoError(t, err) + return modifiedJSON +} + +// Verify that input can be unmarshaled as an untrustedSignature, and that it passes JSON schema validation, and return the unmarshaled untrustedSignature. +func succesfullyUnmarshalUntrustedSignature(t *testing.T, input []byte) untrustedSignature { + inputString := string(input) - *sig = untrustedSignature{} - return json.Unmarshal(testJSON, sig) + var s untrustedSignature + err := json.Unmarshal(input, &s) + require.NoError(t, err, inputString) + return s } -func TestUnmarshalJSON(t *testing.T) { +// Verify that input can't be unmashaled as an untrusted signature, and that it fails JSON schema validation. +func assertUnmarshalUntrustedSignatureFails(t *testing.T, input []byte) { + inputString := string(input) + var s untrustedSignature + err := json.Unmarshal(input, &s) + assert.Error(t, err, inputString) +} + +func TestUnmarshalJSON(t *testing.T) { // Invalid input. Note that json.Unmarshal is guaranteed to validate input before calling our // UnmarshalJSON implementation; so test that first, then test our error handling for completeness. - err := json.Unmarshal([]byte("&"), &s) - assert.Error(t, err) - err = s.UnmarshalJSON([]byte("&")) + assertUnmarshalUntrustedSignatureFails(t, []byte("&")) + var s untrustedSignature + err := s.UnmarshalJSON([]byte("&")) assert.Error(t, err) // Not an object - err = json.Unmarshal([]byte("1"), &s) - assert.Error(t, err) + assertUnmarshalUntrustedSignatureFails(t, []byte("1")) // Start with a valid JSON. validSig := newUntrustedSignature("digest!@#", "reference#@!") @@ -112,9 +127,7 @@ func TestUnmarshalJSON(t *testing.T) { require.NoError(t, err) // Success - s = untrustedSignature{} - err = json.Unmarshal(validJSON, &s) - require.NoError(t, err) + s = succesfullyUnmarshalUntrustedSignature(t, validJSON) assert.Equal(t, validSig, s) // Various ways to corrupt the JSON @@ -156,8 +169,8 @@ func TestUnmarshalJSON(t *testing.T) { func(v mSI) { x(v, "optional")["timestamp"] = 0.5 }, // Fractional input } for _, fn := range breakFns { - err = tryUnmarshalModifiedSignature(t, &s, validJSON, fn) - assert.Error(t, err) + testJSON := modifiedUntrustedSignatureJSON(t, validJSON, fn) + assertUnmarshalUntrustedSignatureFails(t, testJSON) } // Modifications to unrecognized fields in "optional" are allowed and ignored @@ -166,8 +179,8 @@ func TestUnmarshalJSON(t *testing.T) { func(v mSI) { x(v, "optional")["unexpected"] = 1 }, } for _, fn := range allowedModificationFns { - err = tryUnmarshalModifiedSignature(t, &s, validJSON, fn) - require.NoError(t, err) + testJSON := modifiedUntrustedSignatureJSON(t, validJSON, fn) + s := succesfullyUnmarshalUntrustedSignature(t, testJSON) assert.Equal(t, validSig, s) } @@ -180,9 +193,7 @@ func TestUnmarshalJSON(t *testing.T) { } validJSON, err = validSig.MarshalJSON() require.NoError(t, err) - s = untrustedSignature{} - err = json.Unmarshal(validJSON, &s) - require.NoError(t, err) + s = succesfullyUnmarshalUntrustedSignature(t, validJSON) assert.Equal(t, validSig, s) } From 5012539a04c222055f6954f3c3e4e442b6fea34b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Mon, 20 Mar 2017 22:24:39 +0100 Subject: [PATCH 3/3] Add tests for docs/atomics-signature-embedded-json.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reuse the existing untrustedSignature.UnmarshalJSON tests. NOTE: The schema at schemaPath is NOT authoritative; docs/atomic-signature.json and the code is, rather! The schemaPath references are not testing that the code follows the behavior declared by the schema, they are testing that the schema follows the behavior of the code! Signed-off-by: Miloslav Trmač --- signature/signature_test.go | 35 ++++++++++++++++++++++++++--------- vendor.conf | 2 ++ 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/signature/signature_test.go b/signature/signature_test.go index 7268abc8a9..412a03df32 100644 --- a/signature/signature_test.go +++ b/signature/signature_test.go @@ -3,6 +3,7 @@ package signature import ( "encoding/json" "io/ioutil" + "path/filepath" "testing" "time" @@ -11,6 +12,7 @@ import ( "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/xeipuuv/gojsonschema" ) func TestInvalidSignatureError(t *testing.T) { @@ -92,34 +94,49 @@ func modifiedUntrustedSignatureJSON(t *testing.T, validJSON []byte, modifyFn fun } // Verify that input can be unmarshaled as an untrustedSignature, and that it passes JSON schema validation, and return the unmarshaled untrustedSignature. -func succesfullyUnmarshalUntrustedSignature(t *testing.T, input []byte) untrustedSignature { +func succesfullyUnmarshalUntrustedSignature(t *testing.T, schemaLoader gojsonschema.JSONLoader, input []byte) untrustedSignature { inputString := string(input) var s untrustedSignature err := json.Unmarshal(input, &s) require.NoError(t, err, inputString) + + res, err := gojsonschema.Validate(schemaLoader, gojsonschema.NewStringLoader(inputString)) + assert.True(t, err == nil, inputString) + assert.True(t, res.Valid(), inputString) + return s } // Verify that input can't be unmashaled as an untrusted signature, and that it fails JSON schema validation. -func assertUnmarshalUntrustedSignatureFails(t *testing.T, input []byte) { +func assertUnmarshalUntrustedSignatureFails(t *testing.T, schemaLoader gojsonschema.JSONLoader, input []byte) { inputString := string(input) var s untrustedSignature err := json.Unmarshal(input, &s) assert.Error(t, err, inputString) + + res, err := gojsonschema.Validate(schemaLoader, gojsonschema.NewStringLoader(inputString)) + assert.True(t, err != nil || !res.Valid(), inputString) } func TestUnmarshalJSON(t *testing.T) { + // NOTE: The schema at schemaPath is NOT authoritative; docs/atomic-signature.json and the code is, rather! + // The schemaPath references are not testing that the code follows the behavior declared by the schema, + // they are testing that the schema follows the behavior of the code! + schemaPath, err := filepath.Abs("../docs/atomic-signature-embedded-json.json") + require.NoError(t, err) + schemaLoader := gojsonschema.NewReferenceLoader("file://" + schemaPath) + // Invalid input. Note that json.Unmarshal is guaranteed to validate input before calling our // UnmarshalJSON implementation; so test that first, then test our error handling for completeness. - assertUnmarshalUntrustedSignatureFails(t, []byte("&")) + assertUnmarshalUntrustedSignatureFails(t, schemaLoader, []byte("&")) var s untrustedSignature - err := s.UnmarshalJSON([]byte("&")) + err = s.UnmarshalJSON([]byte("&")) assert.Error(t, err) // Not an object - assertUnmarshalUntrustedSignatureFails(t, []byte("1")) + assertUnmarshalUntrustedSignatureFails(t, schemaLoader, []byte("1")) // Start with a valid JSON. validSig := newUntrustedSignature("digest!@#", "reference#@!") @@ -127,7 +144,7 @@ func TestUnmarshalJSON(t *testing.T) { require.NoError(t, err) // Success - s = succesfullyUnmarshalUntrustedSignature(t, validJSON) + s = succesfullyUnmarshalUntrustedSignature(t, schemaLoader, validJSON) assert.Equal(t, validSig, s) // Various ways to corrupt the JSON @@ -170,7 +187,7 @@ func TestUnmarshalJSON(t *testing.T) { } for _, fn := range breakFns { testJSON := modifiedUntrustedSignatureJSON(t, validJSON, fn) - assertUnmarshalUntrustedSignatureFails(t, testJSON) + assertUnmarshalUntrustedSignatureFails(t, schemaLoader, testJSON) } // Modifications to unrecognized fields in "optional" are allowed and ignored @@ -180,7 +197,7 @@ func TestUnmarshalJSON(t *testing.T) { } for _, fn := range allowedModificationFns { testJSON := modifiedUntrustedSignatureJSON(t, validJSON, fn) - s := succesfullyUnmarshalUntrustedSignature(t, testJSON) + s := succesfullyUnmarshalUntrustedSignature(t, schemaLoader, testJSON) assert.Equal(t, validSig, s) } @@ -193,7 +210,7 @@ func TestUnmarshalJSON(t *testing.T) { } validJSON, err = validSig.MarshalJSON() require.NoError(t, err) - s = succesfullyUnmarshalUntrustedSignature(t, validJSON) + s = succesfullyUnmarshalUntrustedSignature(t, schemaLoader, validJSON) assert.Equal(t, validSig, s) } diff --git a/vendor.conf b/vendor.conf index 1685422100..616d2ed1d4 100644 --- a/vendor.conf +++ b/vendor.conf @@ -29,3 +29,5 @@ gopkg.in/cheggaaa/pb.v1 d7e6ca3010b6f084d8056847f55d7f572f180678 gopkg.in/yaml.v2 a3f3340b5840cee44f372bddb5880fcbc419b46a k8s.io/client-go bcde30fb7eaed76fd98a36b4120321b94995ffb6 github.com/xeipuuv/gojsonschema master +github.com/xeipuuv/gojsonreference master +github.com/xeipuuv/gojsonpointer master