From f4f2a959fc50e3b8cb692e8146d78791ca93a92f Mon Sep 17 00:00:00 2001 From: oguzturker8ijm8l Date: Sat, 17 Jan 2026 08:05:12 +0800 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 0000000..ccb4eda --- /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 1fed265..f6219be 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 e5d2487bc085002c17c4f3420f3536ce8b1ad110 Mon Sep 17 00:00:00 2001 From: oguzturker8ijm8l Date: Tue, 20 Jan 2026 01:36:52 +0800 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 711cd57..7268abc 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 101922c7c8810a6bf5b49183682f0f5366c4bbf0 Mon Sep 17 00:00:00 2001 From: oguzturker8ijm8l Date: Thu, 22 Jan 2026 02:13:12 +0800 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 7268abc..412a03d 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 1685422..616d2ed 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