Skip to content

Commit

Permalink
Merge pull request #253 from mtrmac/signature-json-schema
Browse files Browse the repository at this point in the history
Add JSON schema for the JSON embedded inside signatures
  • Loading branch information
oguzturker8ijm8l committed Jan 25, 2026
2 parents 9275418 + 101922c commit c808927
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 21 deletions.
66 changes: 66 additions & 0 deletions docs/atomic-signature-embedded-json.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
}
2 changes: 1 addition & 1 deletion signature/signature.go
Original file line number Diff line number Diff line change
@@ -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

Expand Down
68 changes: 48 additions & 20 deletions signature/signature_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package signature
import (
"encoding/json"
"io/ioutil"
"path/filepath"
"testing"
"time"

Expand All @@ -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) {
Expand Down Expand Up @@ -78,43 +80,71 @@ 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#@!")
validJSON, err := validSig.MarshalJSON()
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
Expand Down Expand Up @@ -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
Expand All @@ -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)
}

Expand All @@ -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)
}

Expand Down
2 changes: 2 additions & 0 deletions vendor.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit c808927

Please sign in to comment.