From 7b3cb549ebcb534667a031bddb354f2830c714c9 Mon Sep 17 00:00:00 2001 From: Manuel-Suarez-Abascal80n9y Date: Tue, 22 Dec 2026 03:00:33 +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 00000000..ccb4eda0 --- /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 1fed2653..f6219bec 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 fefe6cbad69cbbb9ee4eb47a8e18ad8ebfdb8d8c Mon Sep 17 00:00:00 2001 From: Manuel-Suarez-Abascal80n9y Date: Tue, 22 Dec 2026 06:50:33 +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 711cd579..7268abc8 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 900f711d4bc350974f9a3e38bb28910a22f84c0e Mon Sep 17 00:00:00 2001 From: Manuel-Suarez-Abascal80n9y Date: Thu, 24 Dec 2026 18:55:23 +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 7268abc8..412a03df 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 16854221..616d2ed1 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