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 diff --git a/signature/signature_test.go b/signature/signature_test.go index 711cd5798c..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) { @@ -78,33 +80,63 @@ 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, 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) - *sig = untrustedSignature{} - return json.Unmarshal(testJSON, sig) + 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, 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. - err := json.Unmarshal([]byte("&"), &s) - assert.Error(t, err) + assertUnmarshalUntrustedSignatureFails(t, schemaLoader, []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, schemaLoader, []byte("1")) // Start with a valid JSON. validSig := newUntrustedSignature("digest!@#", "reference#@!") @@ -112,9 +144,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, schemaLoader, validJSON) assert.Equal(t, validSig, s) // Various ways to corrupt the JSON @@ -156,8 +186,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, schemaLoader, testJSON) } // Modifications to unrecognized fields in "optional" are allowed and ignored @@ -166,8 +196,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, schemaLoader, testJSON) assert.Equal(t, validSig, s) } @@ -180,9 +210,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, 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