diff --git a/signature/fixtures/policy.json b/signature/fixtures/policy.json new file mode 100644 index 00000000..5e3d5bbb --- /dev/null +++ b/signature/fixtures/policy.json @@ -0,0 +1,84 @@ +{ + "default": [ + { + "type": "reject" + } + ], + "specific": { + "example.com/playground": [ + { + "type": "insecureAcceptAnything" + } + ], + "example.com/production": [ + { + "type": "signedBy", + "keyType": "GPGKeys", + "keyPath": "/keys/employee-gpg-keyring" + } + ], + "example.com/hardened": [ + { + "type": "signedBy", + "keyType": "GPGKeys", + "keyPath": "/keys/employee-gpg-keyring", + "signedIdentity": { + "type": "matchRepository" + } + }, + { + "type": "signedBy", + "keyType": "signedByGPGKeys", + "keyPath": "/keys/public-key-signing-gpg-keyring", + "signedIdentity": { + "type": "matchExact" + } + }, + { + "type": "signedBaseLayer", + "baseLayerIdentity": { + "type": "exactRepository", + "dockerRepository": "registry.access.redhat.com/rhel7/rhel" + } + } + ], + "example.com/hardened-x509": [ + { + "type": "signedBy", + "keyType": "X509Certificates", + "keyPath": "/keys/employee-cert-file", + "signedIdentity": { + "type": "matchRepository" + } + }, + { + "type": "signedBy", + "keyType": "signedByX509CAs", + "keyPath": "/keys/public-key-signing-ca-file" + } + ], + "registry.access.redhat.com": [ + { + "type": "signedBy", + "keyType": "signedByGPGKeys", + "keyPath": "/keys/RH-key-signing-key-gpg-keyring" + } + ], + "bogus/key-data-example": [ + { + "type": "signedBy", + "keyType": "signedByGPGKeys", + "keyData": "bm9uc2Vuc2U=" + } + ], + "bogus/signed-identity-example": [ + { + "type": "signedBaseLayer", + "baseLayerIdentity": { + "type": "exactReference", + "dockerReference": "registry.access.redhat.com/rhel7/rhel:latest" + } + } + ] + } +} \ No newline at end of file diff --git a/signature/policy_config.go b/signature/policy_config.go new file mode 100644 index 00000000..f4af51d4 --- /dev/null +++ b/signature/policy_config.go @@ -0,0 +1,612 @@ +// policy_config.go hanles creation of policy objects, either by parsing JSON +// or by programs building them programmatically. + +// The New* constructors are intended to be a stable API. FIXME: after an independent review. + +// Do not invoke the internals of the JSON marshaling/unmarshaling directly. + +// We can't just blindly call json.Unmarshal because that would silently ignore +// typos, and that would just not do for security policy. + +// FIXME? This is by no means an user-friendly parser: No location information in error messages, no other context. +// But at least it is not worse than blind json.Unmarshal()… + +package signature + +import ( + "encoding/json" + "fmt" + "io/ioutil" + + "github.com/projectatomic/skopeo/reference" +) + +// InvalidPolicyFormatError is returned when parsing an invalid policy configuration. +type InvalidPolicyFormatError string + +func (err InvalidPolicyFormatError) Error() string { + return string(err) +} + +// FIXME: NewDefaultPolicy, from default file (or environment if trusted?) + +// NewPolicyFromFile returns a policy configured in the specified file. +func NewPolicyFromFile(fileName string) (*Policy, error) { + contents, err := ioutil.ReadFile(fileName) + if err != nil { + return nil, err + } + return NewPolicyFromBytes(contents) +} + +// NewPolicyFromBytes returns a policy parsed from the specified blob. +// Use this function instead of calling json.Unmarshal directly. +func NewPolicyFromBytes(data []byte) (*Policy, error) { + p := Policy{} + if err := json.Unmarshal(data, &p); err != nil { + return nil, InvalidPolicyFormatError(err.Error()) + } + return &p, nil +} + +// Compile-time check that Policy implements json.Unmarshaler. +var _ json.Unmarshaler = (*Policy)(nil) + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (p *Policy) UnmarshalJSON(data []byte) error { + *p = Policy{} + specific := policySpecificMap{} + if err := paranoidUnmarshalJSONObject(data, func(key string) interface{} { + switch key { + case "default": + return &p.Default + case "specific": + return &specific + default: + return nil + } + }); err != nil { + return err + } + + if p.Default == nil { + return InvalidPolicyFormatError("Default policy is missing") + } + p.Specific = map[string]PolicyRequirements(specific) + return nil +} + +// policySpecificMap is a specialization of this map type for the strict JSON parsing semantics appropriate for the Policy.Specific member. +type policySpecificMap map[string]PolicyRequirements + +// Compile-time check that policySpecificMap implements json.Unmarshaler. +var _ json.Unmarshaler = (*policySpecificMap)(nil) + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (m *policySpecificMap) UnmarshalJSON(data []byte) error { + // We can't unmarshal directly into map values because it is not possible to take an address of a map value. + // So, use a temporary map of pointers-to-slices and convert. + tmpMap := map[string]*PolicyRequirements{} + if err := paranoidUnmarshalJSONObject(data, func(key string) interface{} { + // Check that the scope format is at least plausible. + if _, err := reference.ParseNamed(key); err != nil { + return nil // FIXME? This returns an "Unknown key" error instead of saying that the format is invalid. + } + // paranoidUnmarshalJSONObject detects key duplication for us, check just to be safe. + if _, ok := tmpMap[key]; ok { + return nil + } + ptr := &PolicyRequirements{} // This allocates a new instance on each call. + tmpMap[key] = ptr + return ptr + }); err != nil { + return err + } + for key, ptr := range tmpMap { + (*m)[key] = *ptr + } + return nil +} + +// Compile-time check that PolicyRequirements implements json.Unmarshaler. +var _ json.Unmarshaler = (*PolicyRequirements)(nil) + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (m *PolicyRequirements) UnmarshalJSON(data []byte) error { + reqJSONs := []json.RawMessage{} + if err := json.Unmarshal(data, &reqJSONs); err != nil { + return err + } + if len(reqJSONs) == 0 { + return InvalidPolicyFormatError("List of verification policy requirements must not be empty") + } + res := make([]PolicyRequirement, len(reqJSONs)) + for i, reqJSON := range reqJSONs { + req, err := newPolicyRequirementFromJSON(reqJSON) + if err != nil { + return err + } + res[i] = req + } + *m = res + return nil +} + +// newPolicyRequirementFromJSON parses JSON data into a PolicyRequirement implementation. +func newPolicyRequirementFromJSON(data []byte) (PolicyRequirement, error) { + var typeField prCommon + if err := json.Unmarshal(data, &typeField); err != nil { + return nil, err + } + var res PolicyRequirement + switch typeField.Type { + case prTypeInsecureAcceptAnything: + res = &prInsecureAcceptAnything{} + case prTypeReject: + res = &prReject{} + case prTypeSignedBy: + res = &prSignedBy{} + case prTypeSignedBaseLayer: + res = &prSignedBaseLayer{} + default: + return nil, InvalidPolicyFormatError(fmt.Sprintf("Unknown policy requirement type \"%s\"", typeField.Type)) + } + if err := json.Unmarshal(data, &res); err != nil { + return nil, err + } + return res, nil +} + +// newPRInsecureAcceptAnything is NewPRInsecureAcceptAnything, except it returns the private type. +func newPRInsecureAcceptAnything() *prInsecureAcceptAnything { + return &prInsecureAcceptAnything{prCommon{Type: prTypeInsecureAcceptAnything}} +} + +// NewPRInsecureAcceptAnything returns a new "insecureAcceptAnything" PolicyRequirement. +func NewPRInsecureAcceptAnything() PolicyRequirement { + return newPRInsecureAcceptAnything() +} + +// Compile-time check that prInsecureAcceptAnything implements json.Unmarshaler. +var _ json.Unmarshaler = (*prInsecureAcceptAnything)(nil) + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (pr *prInsecureAcceptAnything) UnmarshalJSON(data []byte) error { + *pr = prInsecureAcceptAnything{} + var tmp prInsecureAcceptAnything + if err := paranoidUnmarshalJSONObject(data, func(key string) interface{} { + switch key { + case "type": + return &tmp.Type + default: + return nil + } + }); err != nil { + return err + } + + if tmp.Type != prTypeInsecureAcceptAnything { + return InvalidPolicyFormatError(fmt.Sprintf("Unexpected policy requirement type \"%s\"", tmp.Type)) + } + *pr = *newPRInsecureAcceptAnything() + return nil +} + +// newPRReject is NewPRReject, except it returns the private type. +func newPRReject() *prReject { + return &prReject{prCommon{Type: prTypeReject}} +} + +// NewPRReject returns a new "reject" PolicyRequirement. +func NewPRReject() PolicyRequirement { + return newPRReject() +} + +// Compile-time check that prReject implements json.Unmarshaler. +var _ json.Unmarshaler = (*prReject)(nil) + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (pr *prReject) UnmarshalJSON(data []byte) error { + *pr = prReject{} + var tmp prReject + if err := paranoidUnmarshalJSONObject(data, func(key string) interface{} { + switch key { + case "type": + return &tmp.Type + default: + return nil + } + }); err != nil { + return err + } + + if tmp.Type != prTypeReject { + return InvalidPolicyFormatError(fmt.Sprintf("Unexpected policy requirement type \"%s\"", tmp.Type)) + } + *pr = *newPRReject() + return nil +} + +// newPRSignedBy returns a new prSignedBy if parameters are valid. +func newPRSignedBy(keyType sbKeyType, keyPath string, keyData []byte, signedIdentity PolicyReferenceMatch) (*prSignedBy, error) { + if !keyType.IsValid() { + return nil, InvalidPolicyFormatError(fmt.Sprintf("invalid keyType \"%s\"", keyType)) + } + if len(keyPath) > 0 && len(keyData) > 0 { + return nil, InvalidPolicyFormatError("keyType and keyData cannot be used simultaneously") + } + if signedIdentity == nil { + return nil, InvalidPolicyFormatError("signedIdentity not specified") + } + return &prSignedBy{ + prCommon: prCommon{Type: prTypeSignedBy}, + KeyType: keyType, + KeyPath: keyPath, + KeyData: keyData, + SignedIdentity: signedIdentity, + }, nil +} + +// newPRSignedByKeyPath is NewPRSignedByKeyPath, except it returns the private type. +func newPRSignedByKeyPath(keyType sbKeyType, keyPath string, signedIdentity PolicyReferenceMatch) (*prSignedBy, error) { + return newPRSignedBy(keyType, keyPath, nil, signedIdentity) +} + +// NewPRSignedByKeyPath returns a new "signedBy" PolicyRequirement using a KeyPath +func NewPRSignedByKeyPath(keyType sbKeyType, keyPath string, signedIdentity PolicyReferenceMatch) (PolicyRequirement, error) { + return newPRSignedByKeyPath(keyType, keyPath, signedIdentity) +} + +// newPRSignedByKeyData is NewPRSignedByKeyData, except it returns the private type. +func newPRSignedByKeyData(keyType sbKeyType, keyData []byte, signedIdentity PolicyReferenceMatch) (*prSignedBy, error) { + return newPRSignedBy(keyType, "", keyData, signedIdentity) +} + +// NewPRSignedByKeyData returns a new "signedBy" PolicyRequirement using a KeyData +func NewPRSignedByKeyData(keyType sbKeyType, keyData []byte, signedIdentity PolicyReferenceMatch) (PolicyRequirement, error) { + return newPRSignedByKeyData(keyType, keyData, signedIdentity) +} + +// Compile-time check that prSignedBy implements json.Unmarshaler. +var _ json.Unmarshaler = (*prSignedBy)(nil) + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (pr *prSignedBy) UnmarshalJSON(data []byte) error { + *pr = prSignedBy{} + var tmp prSignedBy + var gotKeyPath, gotKeyData = false, false + var signedIdentity json.RawMessage + if err := paranoidUnmarshalJSONObject(data, func(key string) interface{} { + switch key { + case "type": + return &tmp.Type + case "keyType": + return &tmp.KeyType + case "keyPath": + gotKeyPath = true + return &tmp.KeyPath + case "keyData": + gotKeyData = true + return &tmp.KeyData + case "signedIdentity": + return &signedIdentity + default: + return nil + } + }); err != nil { + return err + } + + if tmp.Type != prTypeSignedBy { + return InvalidPolicyFormatError(fmt.Sprintf("Unexpected policy requirement type \"%s\"", tmp.Type)) + } + if signedIdentity == nil { + tmp.SignedIdentity = NewPRMMatchExact() + } else { + si, err := newPolicyReferenceMatchFromJSON(signedIdentity) + if err != nil { + return err + } + tmp.SignedIdentity = si + } + + var res *prSignedBy + var err error + switch { + case gotKeyPath && gotKeyData: + return InvalidPolicyFormatError("keyPath and keyData cannot be used simultaneously") + case gotKeyPath && !gotKeyData: + res, err = newPRSignedByKeyPath(tmp.KeyType, tmp.KeyPath, tmp.SignedIdentity) + case !gotKeyPath && gotKeyData: + res, err = newPRSignedByKeyData(tmp.KeyType, tmp.KeyData, tmp.SignedIdentity) + case !gotKeyPath && !gotKeyData: + return InvalidPolicyFormatError("At least one of keyPath and keyData mus be specified") + default: // Coverage: This should never happen + return fmt.Errorf("Impossible keyPath/keyData presence combination!?") + } + if err != nil { + return err + } + *pr = *res + + return nil +} + +// IsValid returns true iff kt is a recognized value +func (kt sbKeyType) IsValid() bool { + switch kt { + case SBKeyTypeGPGKeys, SBKeyTypeSignedByGPGKeys, + SBKeyTypeX509Certificates, SBKeyTypeSignedByX509CAs: + return true + default: + return false + } +} + +// Compile-time check that sbKeyType implements json.Unmarshaler. +var _ json.Unmarshaler = (*sbKeyType)(nil) + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (kt *sbKeyType) UnmarshalJSON(data []byte) error { + *kt = sbKeyType("") + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + if !sbKeyType(s).IsValid() { + return InvalidPolicyFormatError(fmt.Sprintf("Unrecognized keyType value \"%s\"", s)) + } + *kt = sbKeyType(s) + return nil +} + +// newPRSignedBaseLayer is NewPRSignedBaseLayer, except it returns the private type. +func newPRSignedBaseLayer(baseLayerIdentity PolicyReferenceMatch) (*prSignedBaseLayer, error) { + if baseLayerIdentity == nil { + return nil, InvalidPolicyFormatError("baseLayerIdenitty not specified") + } + return &prSignedBaseLayer{ + prCommon: prCommon{Type: prTypeSignedBaseLayer}, + BaseLayerIdentity: baseLayerIdentity, + }, nil +} + +// NewPRSignedBaseLayer returns a new "signedBaseLayer" PolicyRequirement. +func NewPRSignedBaseLayer(baseLayerIdentity PolicyReferenceMatch) (PolicyRequirement, error) { + return newPRSignedBaseLayer(baseLayerIdentity) +} + +// Compile-time check that prSignedBaseLayer implements json.Unmarshaler. +var _ json.Unmarshaler = (*prSignedBaseLayer)(nil) + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (pr *prSignedBaseLayer) UnmarshalJSON(data []byte) error { + *pr = prSignedBaseLayer{} + var tmp prSignedBaseLayer + var baseLayerIdentity json.RawMessage + if err := paranoidUnmarshalJSONObject(data, func(key string) interface{} { + switch key { + case "type": + return &tmp.Type + case "baseLayerIdentity": + return &baseLayerIdentity + default: + return nil + } + }); err != nil { + return err + } + + if tmp.Type != prTypeSignedBaseLayer { + return InvalidPolicyFormatError(fmt.Sprintf("Unexpected policy requirement type \"%s\"", tmp.Type)) + } + if baseLayerIdentity == nil { + return InvalidPolicyFormatError(fmt.Sprintf("baseLayerIdentity not specified")) + } + bli, err := newPolicyReferenceMatchFromJSON(baseLayerIdentity) + if err != nil { + return err + } + res, err := newPRSignedBaseLayer(bli) + if err != nil { + // Coverage: This should never happen, newPolicyReferenceMatchFromJSON has ensured bli is valid. + return err + } + *pr = *res + return nil +} + +// newPolicyRequirementFromJSON parses JSON data into a PolicyReferenceMatch implementation. +func newPolicyReferenceMatchFromJSON(data []byte) (PolicyReferenceMatch, error) { + var typeField prmCommon + if err := json.Unmarshal(data, &typeField); err != nil { + return nil, err + } + var res PolicyReferenceMatch + switch typeField.Type { + case prmTypeMatchExact: + res = &prmMatchExact{} + case prmTypeMatchRepository: + res = &prmMatchRepository{} + case prmTypeExactReference: + res = &prmExactReference{} + case prmTypeExactRepository: + res = &prmExactRepository{} + default: + return nil, InvalidPolicyFormatError(fmt.Sprintf("Unknown policy reference match type \"%s\"", typeField.Type)) + } + if err := json.Unmarshal(data, &res); err != nil { + return nil, err + } + return res, nil +} + +// newPRMMatchExact is NewPRMMatchExact, except it resturns the private type. +func newPRMMatchExact() *prmMatchExact { + return &prmMatchExact{prmCommon{Type: prmTypeMatchExact}} +} + +// NewPRMMatchExact returns a new "matchExact" PolicyReferenceMatch. +func NewPRMMatchExact() PolicyReferenceMatch { + return newPRMMatchExact() +} + +// Compile-time check that prmMatchExact implements json.Unmarshaler. +var _ json.Unmarshaler = (*prmMatchExact)(nil) + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (prm *prmMatchExact) UnmarshalJSON(data []byte) error { + *prm = prmMatchExact{} + var tmp prmMatchExact + if err := paranoidUnmarshalJSONObject(data, func(key string) interface{} { + switch key { + case "type": + return &tmp.Type + default: + return nil + } + }); err != nil { + return err + } + + if tmp.Type != prmTypeMatchExact { + return InvalidPolicyFormatError(fmt.Sprintf("Unexpected policy requirement type \"%s\"", tmp.Type)) + } + *prm = *newPRMMatchExact() + return nil +} + +// newPRMMatchRepository is NewPRMMatchRepository, except it resturns the private type. +func newPRMMatchRepository() *prmMatchRepository { + return &prmMatchRepository{prmCommon{Type: prmTypeMatchRepository}} +} + +// NewPRMMatchRepository returns a new "matchRepository" PolicyReferenceMatch. +func NewPRMMatchRepository() PolicyReferenceMatch { + return newPRMMatchRepository() +} + +// Compile-time check that prmMatchRepository implements json.Unmarshaler. +var _ json.Unmarshaler = (*prmMatchRepository)(nil) + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (prm *prmMatchRepository) UnmarshalJSON(data []byte) error { + *prm = prmMatchRepository{} + var tmp prmMatchRepository + if err := paranoidUnmarshalJSONObject(data, func(key string) interface{} { + switch key { + case "type": + return &tmp.Type + default: + return nil + } + }); err != nil { + return err + } + + if tmp.Type != prmTypeMatchRepository { + return InvalidPolicyFormatError(fmt.Sprintf("Unexpected policy requirement type \"%s\"", tmp.Type)) + } + *prm = *newPRMMatchRepository() + return nil +} + +// newPRMExactReference is NewPRMExactReference, except it resturns the private type. +func newPRMExactReference(dockerReference string) (*prmExactReference, error) { + ref, err := reference.ParseNamed(dockerReference) + if err != nil { + return nil, InvalidPolicyFormatError(fmt.Sprintf("Invalid format of dockerReference %s: %s", dockerReference, err.Error())) + } + if reference.IsNameOnly(ref) { + return nil, InvalidPolicyFormatError(fmt.Sprintf("dockerReference %s contains neither a tag nor digest", dockerReference)) + } + return &prmExactReference{ + prmCommon: prmCommon{Type: prmTypeExactReference}, + DockerReference: dockerReference, + }, nil +} + +// NewPRMExactReference returns a new "exactReference" PolicyReferenceMatch. +func NewPRMExactReference(dockerReference string) (PolicyReferenceMatch, error) { + return newPRMExactReference(dockerReference) +} + +// Compile-time check that prmExactReference implements json.Unmarshaler. +var _ json.Unmarshaler = (*prmExactReference)(nil) + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (prm *prmExactReference) UnmarshalJSON(data []byte) error { + *prm = prmExactReference{} + var tmp prmExactReference + if err := paranoidUnmarshalJSONObject(data, func(key string) interface{} { + switch key { + case "type": + return &tmp.Type + case "dockerReference": + return &tmp.DockerReference + default: + return nil + } + }); err != nil { + return err + } + + if tmp.Type != prmTypeExactReference { + return InvalidPolicyFormatError(fmt.Sprintf("Unexpected policy requirement type \"%s\"", tmp.Type)) + } + + res, err := newPRMExactReference(tmp.DockerReference) + if err != nil { + return err + } + *prm = *res + return nil +} + +// newPRMExactRepository is NewPRMExactRepository, except it resturns the private type. +func newPRMExactRepository(dockerRepository string) (*prmExactRepository, error) { + if _, err := reference.ParseNamed(dockerRepository); err != nil { + return nil, InvalidPolicyFormatError(fmt.Sprintf("Invalid format of dockerRepository %s: %s", dockerRepository, err.Error())) + } + return &prmExactRepository{ + prmCommon: prmCommon{Type: prmTypeExactRepository}, + DockerRepository: dockerRepository, + }, nil +} + +// NewPRMExactRepository returns a new "exactRepository" PolicyRepositoryMatch. +func NewPRMExactRepository(dockerRepository string) (PolicyReferenceMatch, error) { + return newPRMExactRepository(dockerRepository) +} + +// Compile-time check that prmExactRepository implements json.Unmarshaler. +var _ json.Unmarshaler = (*prmExactRepository)(nil) + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (prm *prmExactRepository) UnmarshalJSON(data []byte) error { + *prm = prmExactRepository{} + var tmp prmExactRepository + if err := paranoidUnmarshalJSONObject(data, func(key string) interface{} { + switch key { + case "type": + return &tmp.Type + case "dockerRepository": + return &tmp.DockerRepository + default: + return nil + } + }); err != nil { + return err + } + + if tmp.Type != prmTypeExactRepository { + return InvalidPolicyFormatError(fmt.Sprintf("Unexpected policy requirement type \"%s\"", tmp.Type)) + } + + res, err := newPRMExactRepository(tmp.DockerRepository) + if err != nil { + return err + } + *prm = *res + return nil +} diff --git a/signature/policy_config_test.go b/signature/policy_config_test.go new file mode 100644 index 00000000..63fd8c74 --- /dev/null +++ b/signature/policy_config_test.go @@ -0,0 +1,1177 @@ +package signature + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// policyFixtureContents is a data structure equal to the contents of "fixtures/policy.json" +var policyFixtureContents = &Policy{ + Default: PolicyRequirements{NewPRReject()}, + Specific: map[string]PolicyRequirements{ + "example.com/playground": { + NewPRInsecureAcceptAnything(), + }, + "example.com/production": { + xNewPRSignedByKeyPath(SBKeyTypeGPGKeys, + "/keys/employee-gpg-keyring", + NewPRMMatchExact()), + }, + "example.com/hardened": { + xNewPRSignedByKeyPath(SBKeyTypeGPGKeys, + "/keys/employee-gpg-keyring", + NewPRMMatchRepository()), + xNewPRSignedByKeyPath(SBKeyTypeSignedByGPGKeys, + "/keys/public-key-signing-gpg-keyring", + NewPRMMatchExact()), + xNewPRSignedBaseLayer(xNewPRMExactRepository("registry.access.redhat.com/rhel7/rhel")), + }, + "example.com/hardened-x509": { + xNewPRSignedByKeyPath(SBKeyTypeX509Certificates, + "/keys/employee-cert-file", + NewPRMMatchRepository()), + xNewPRSignedByKeyPath(SBKeyTypeSignedByX509CAs, + "/keys/public-key-signing-ca-file", + NewPRMMatchExact()), + }, + "registry.access.redhat.com": { + xNewPRSignedByKeyPath(SBKeyTypeSignedByGPGKeys, + "/keys/RH-key-signing-key-gpg-keyring", + NewPRMMatchExact()), + }, + "bogus/key-data-example": { + xNewPRSignedByKeyData(SBKeyTypeSignedByGPGKeys, + []byte("nonsense"), + NewPRMMatchExact()), + }, + "bogus/signed-identity-example": { + xNewPRSignedBaseLayer(xNewPRMExactReference("registry.access.redhat.com/rhel7/rhel:latest")), + }, + }, +} + +func TestNewPolicyFromFile(t *testing.T) { + // Success + policy, err := NewPolicyFromFile("./fixtures/policy.json") + require.NoError(t, err) + assert.Equal(t, policyFixtureContents, policy) + + // Error reading file + _, err = NewPolicyFromFile("/this/doesnt/exist") + assert.Error(t, err) + + // A failure case; most are tested in the individual method unit tests. + _, err = NewPolicyFromFile("/dev/null") + require.Error(t, err) + assert.IsType(t, InvalidPolicyFormatError(""), err) +} + +func TestNewPolicyFromBytes(t *testing.T) { + // Success + bytes, err := ioutil.ReadFile("./fixtures/policy.json") + require.NoError(t, err) + policy, err := NewPolicyFromBytes(bytes) + require.NoError(t, err) + assert.Equal(t, policyFixtureContents, policy) + + // A failure case; most are tested in the individual method unit tests. + _, err = NewPolicyFromBytes([]byte("")) + require.Error(t, err) + assert.IsType(t, InvalidPolicyFormatError(""), err) +} + +// FIXME? There is quite a bit of duplication below. Factor some of it out? + +// addExtraJSONMember adds adds an additional member "$name": $extra, +// possibly with a duplicate name, to encoded. +// Errors, if any, are reported through t. +func addExtraJSONMember(t *testing.T, encoded []byte, name string, extra interface{}) []byte { + extraJSON, err := json.Marshal(extra) + require.NoError(t, err) + + require.True(t, bytes.HasSuffix(encoded, []byte("}"))) + preservedLen := len(encoded) - 1 + + return bytes.Join([][]byte{encoded[:preservedLen], []byte(`,"`), []byte(name), []byte(`":`), extraJSON, []byte("}")}, nil) +} + +func TestInvalidPolicyFormatError(t *testing.T) { + // A stupid test just to keep code coverage + s := "test" + err := InvalidPolicyFormatError(s) + assert.Equal(t, s, err.Error()) +} + +// Return the result of modifying validJSON with fn and unmarshaling it into *p +func tryUnmarshalModifiedPolicy(t *testing.T, p *Policy, validJSON []byte, modifyFn func(mSI)) error { + var tmp mSI + err := json.Unmarshal(validJSON, &tmp) + require.NoError(t, err) + + modifyFn(tmp) + + testJSON, err := json.Marshal(tmp) + require.NoError(t, err) + + *p = Policy{} + return json.Unmarshal(testJSON, p) +} + +// xNewPRSignedByKeyPath is like NewPRSignedByKeyPath, except it must not fail. +func xNewPRSignedByKeyPath(keyType sbKeyType, keyPath string, signedIdentity PolicyReferenceMatch) PolicyRequirement { + pr, err := NewPRSignedByKeyPath(keyType, keyPath, signedIdentity) + if err != nil { + panic("xNewPRSignedByKeyPath failed") + } + return pr +} + +// xNewPRSignedByKeyData is like NewPRSignedByKeyData, except it must not fail. +func xNewPRSignedByKeyData(keyType sbKeyType, keyData []byte, signedIdentity PolicyReferenceMatch) PolicyRequirement { + pr, err := NewPRSignedByKeyData(keyType, keyData, signedIdentity) + if err != nil { + panic("xNewPRSignedByKeyData failed") + } + return pr +} + +func TestPolicyUnmarshalJSON(t *testing.T) { + var p Policy + + // 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("&"), &p) + assert.Error(t, err) + err = p.UnmarshalJSON([]byte("&")) + assert.Error(t, err) + + // Not an object + err = json.Unmarshal([]byte("1"), &p) + assert.Error(t, err) + + // Start with a valid JSON. + validPolicy := Policy{ + Default: []PolicyRequirement{ + xNewPRSignedByKeyData(SBKeyTypeGPGKeys, []byte("abc"), NewPRMMatchExact()), + }, + Specific: map[string]PolicyRequirements{ + "library/busybox": []PolicyRequirement{ + xNewPRSignedByKeyData(SBKeyTypeGPGKeys, []byte("def"), NewPRMMatchExact()), + }, + "registry.access.redhat.com": []PolicyRequirement{ + xNewPRSignedByKeyData(SBKeyTypeSignedByGPGKeys, []byte("RH"), NewPRMMatchRepository()), + }, + }, + } + validJSON, err := json.Marshal(validPolicy) + require.NoError(t, err) + + // Success + p = Policy{} + err = json.Unmarshal(validJSON, &p) + require.NoError(t, err) + assert.Equal(t, validPolicy, p) + + // Various ways to corrupt the JSON + breakFns := []func(mSI){ + // The "default" field is missing + func(v mSI) { delete(v, "default") }, + // Extra top-level sub-object + func(v mSI) { v["unexpected"] = 1 }, + // "default" not an array + func(v mSI) { v["default"] = 1 }, + func(v mSI) { v["default"] = mSI{} }, + // "specific" not an object + func(v mSI) { v["specific"] = 1 }, + func(v mSI) { v["specific"] = []string{} }, + // "default" is an invalid PolicyRequirements + func(v mSI) { v["default"] = PolicyRequirements{} }, + // Invalid scope name in "specific". Uppercase is invalid in Docker reference components. + // Get valid PolicyRequirements by copying them from "library/buxybox". + func(v mSI) { x(v, "specific")["INVALIDUPPERCASE"] = x(v, "specific")["library/busybox"] }, + // A field in "specific" is an invalid PolicyRequirements + func(v mSI) { x(v, "specific")["library/busybox"] = PolicyRequirements{} }, + } + for _, fn := range breakFns { + err = tryUnmarshalModifiedPolicy(t, &p, validJSON, fn) + assert.Error(t, err) + } + + // Duplicated fields + for _, field := range []string{"default", "specific"} { + var tmp mSI + err := json.Unmarshal(validJSON, &tmp) + require.NoError(t, err) + + testJSON := addExtraJSONMember(t, validJSON, field, tmp[field]) + + p = Policy{} + err = json.Unmarshal(testJSON, &p) + assert.Error(t, err) + } + + // Various allowed modifications to the policy + allowedModificationFns := []func(mSI){ + // Delete the map of specific policies + func(v mSI) { delete(v, "specific") }, + // Use an empty map of specific policies + func(v mSI) { v["specific"] = map[string]PolicyRequirements{} }, + } + for _, fn := range allowedModificationFns { + err = tryUnmarshalModifiedPolicy(t, &p, validJSON, fn) + require.NoError(t, err) + } +} + +func TestPolicyRequirementsUnmarshalJSON(t *testing.T) { + var reqs PolicyRequirements + + // 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("&"), &reqs) + assert.Error(t, err) + err = reqs.UnmarshalJSON([]byte("&")) + assert.Error(t, err) + + // Not an array + err = json.Unmarshal([]byte("1"), &reqs) + assert.Error(t, err) + + // Start with a valid JSON. + validReqs := PolicyRequirements{ + xNewPRSignedByKeyData(SBKeyTypeGPGKeys, []byte("def"), NewPRMMatchExact()), + xNewPRSignedByKeyData(SBKeyTypeSignedByGPGKeys, []byte("RH"), NewPRMMatchRepository()), + } + validJSON, err := json.Marshal(validReqs) + require.NoError(t, err) + + // Success + reqs = PolicyRequirements{} + err = json.Unmarshal(validJSON, &reqs) + require.NoError(t, err) + assert.Equal(t, validReqs, reqs) + + for _, invalid := range [][]interface{}{ + // No requirements + {}, + // A member is not an object + {1}, + // A member has an invalid type + {prSignedBy{prCommon: prCommon{Type: "this is invalid"}}}, + // A member has a valid type but invalid contents + {prSignedBy{ + prCommon: prCommon{Type: prTypeSignedBy}, + KeyType: "this is invalid", + }}, + } { + testJSON, err := json.Marshal(invalid) + require.NoError(t, err) + + reqs = PolicyRequirements{} + err = json.Unmarshal(testJSON, &reqs) + assert.Error(t, err, string(testJSON)) + } +} + +func TestNewPolicyRequirementFromJSON(t *testing.T) { + // Sample success. Others tested in the individual PolicyRequirement.UnmarshalJSON implementations. + validReq := NewPRInsecureAcceptAnything() + validJSON, err := json.Marshal(validReq) + require.NoError(t, err) + req, err := newPolicyRequirementFromJSON(validJSON) + require.NoError(t, err) + assert.Equal(t, validReq, req) + + // Invalid + for _, invalid := range []interface{}{ + // Not an object + 1, + // Missing type + prCommon{}, + // Invalid type + prCommon{Type: "this is invalid"}, + // Valid type but invalid contents + prSignedBy{ + prCommon: prCommon{Type: prTypeSignedBy}, + KeyType: "this is invalid", + }, + } { + testJSON, err := json.Marshal(invalid) + require.NoError(t, err) + + _, err = newPolicyRequirementFromJSON(testJSON) + assert.Error(t, err, string(testJSON)) + } +} + +func TestNewPRInsecureAcceptAnything(t *testing.T) { + _pr := NewPRInsecureAcceptAnything() + pr, ok := _pr.(*prInsecureAcceptAnything) + require.True(t, ok) + assert.Equal(t, &prInsecureAcceptAnything{prCommon{prTypeInsecureAcceptAnything}}, pr) +} + +func TestPRInsecureAcceptAnythingUnmarshalJSON(t *testing.T) { + var pr prInsecureAcceptAnything + + // 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("&"), &pr) + assert.Error(t, err) + err = pr.UnmarshalJSON([]byte("&")) + assert.Error(t, err) + + // Not an object + err = json.Unmarshal([]byte("1"), &pr) + assert.Error(t, err) + + // Start with a valid JSON. + validPR := NewPRInsecureAcceptAnything() + validJSON, err := json.Marshal(validPR) + require.NoError(t, err) + + // Success + pr = prInsecureAcceptAnything{} + err = json.Unmarshal(validJSON, &pr) + require.NoError(t, err) + assert.Equal(t, validPR, &pr) + + // newPolicyRequirementFromJSON recognizes this type + _pr, err := newPolicyRequirementFromJSON(validJSON) + require.NoError(t, err) + assert.Equal(t, validPR, _pr) + + for _, invalid := range []mSI{ + // Missing "type" field + {}, + // Wrong "type" field + {"type": 1}, + {"type": "this is invalid"}, + // Extra fields + { + "type": string(prTypeInsecureAcceptAnything), + "unknown": "foo", + }, + } { + testJSON, err := json.Marshal(invalid) + require.NoError(t, err) + + pr = prInsecureAcceptAnything{} + err = json.Unmarshal(testJSON, &pr) + assert.Error(t, err, string(testJSON)) + } + + // Duplicated fields + for _, field := range []string{"type"} { + var tmp mSI + err := json.Unmarshal(validJSON, &tmp) + require.NoError(t, err) + + testJSON := addExtraJSONMember(t, validJSON, field, tmp[field]) + + pr = prInsecureAcceptAnything{} + err = json.Unmarshal(testJSON, &pr) + assert.Error(t, err) + } +} + +func TestNewPRReject(t *testing.T) { + _pr := NewPRReject() + pr, ok := _pr.(*prReject) + require.True(t, ok) + assert.Equal(t, &prReject{prCommon{prTypeReject}}, pr) +} + +func TestPRRejectUnmarshalJSON(t *testing.T) { + var pr prReject + + // 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("&"), &pr) + assert.Error(t, err) + err = pr.UnmarshalJSON([]byte("&")) + assert.Error(t, err) + + // Not an object + err = json.Unmarshal([]byte("1"), &pr) + assert.Error(t, err) + + // Start with a valid JSON. + validPR := NewPRReject() + validJSON, err := json.Marshal(validPR) + require.NoError(t, err) + + // Success + pr = prReject{} + err = json.Unmarshal(validJSON, &pr) + require.NoError(t, err) + assert.Equal(t, validPR, &pr) + + // newPolicyRequirementFromJSON recognizes this type + _pr, err := newPolicyRequirementFromJSON(validJSON) + require.NoError(t, err) + assert.Equal(t, validPR, _pr) + + for _, invalid := range []mSI{ + // Missing "type" field + {}, + // Wrong "type" field + {"type": 1}, + {"type": "this is invalid"}, + // Extra fields + { + "type": string(prTypeReject), + "unknown": "foo", + }, + } { + testJSON, err := json.Marshal(invalid) + require.NoError(t, err) + + pr = prReject{} + err = json.Unmarshal(testJSON, &pr) + assert.Error(t, err, string(testJSON)) + } + + // Duplicated fields + for _, field := range []string{"type"} { + var tmp mSI + err := json.Unmarshal(validJSON, &tmp) + require.NoError(t, err) + + testJSON := addExtraJSONMember(t, validJSON, field, tmp[field]) + + pr = prReject{} + err = json.Unmarshal(testJSON, &pr) + assert.Error(t, err) + } +} + +func TestNewPRSignedBy(t *testing.T) { + const testPath = "/foo/bar" + testData := []byte("abc") + testIdentity := NewPRMMatchExact() + + // Success + pr, err := newPRSignedBy(SBKeyTypeGPGKeys, testPath, nil, testIdentity) + require.NoError(t, err) + assert.Equal(t, &prSignedBy{ + prCommon: prCommon{prTypeSignedBy}, + KeyType: SBKeyTypeGPGKeys, + KeyPath: testPath, + KeyData: nil, + SignedIdentity: testIdentity, + }, pr) + pr, err = newPRSignedBy(SBKeyTypeGPGKeys, "", testData, testIdentity) + require.NoError(t, err) + assert.Equal(t, &prSignedBy{ + prCommon: prCommon{prTypeSignedBy}, + KeyType: SBKeyTypeGPGKeys, + KeyPath: "", + KeyData: testData, + SignedIdentity: testIdentity, + }, pr) + + // Invalid keyType + pr, err = newPRSignedBy(sbKeyType(""), testPath, nil, testIdentity) + assert.Error(t, err) + pr, err = newPRSignedBy(sbKeyType("this is invalid"), testPath, nil, testIdentity) + assert.Error(t, err) + + // Both keyPath and keyData specified + pr, err = newPRSignedBy(SBKeyTypeGPGKeys, testPath, testData, testIdentity) + assert.Error(t, err) + + // Invalid signedIdentity + pr, err = newPRSignedBy(SBKeyTypeGPGKeys, testPath, nil, nil) + assert.Error(t, err) +} + +func TestNewPRSignedByKeyPath(t *testing.T) { + const testPath = "/foo/bar" + _pr, err := NewPRSignedByKeyPath(SBKeyTypeGPGKeys, testPath, NewPRMMatchExact()) + require.NoError(t, err) + pr, ok := _pr.(*prSignedBy) + require.True(t, ok) + assert.Equal(t, testPath, pr.KeyPath) + // Failure cases tested in TestNewPRSignedBy. +} + +func TestNewPRSignedByKeyData(t *testing.T) { + testData := []byte("abc") + _pr, err := NewPRSignedByKeyData(SBKeyTypeGPGKeys, testData, NewPRMMatchExact()) + require.NoError(t, err) + pr, ok := _pr.(*prSignedBy) + require.True(t, ok) + assert.Equal(t, testData, pr.KeyData) + // Failure cases tested in TestNewPRSignedBy. +} + +// Return the result of modifying vaoidJSON with fn and unmarshalingit into *pr +func tryUnmarshalModifiedSignedBy(t *testing.T, pr *prSignedBy, validJSON []byte, modifyFn func(mSI)) error { + var tmp mSI + err := json.Unmarshal(validJSON, &tmp) + require.NoError(t, err) + + modifyFn(tmp) + + testJSON, err := json.Marshal(tmp) + require.NoError(t, err) + + *pr = prSignedBy{} + return json.Unmarshal(testJSON, &pr) +} + +func TestPRSignedByUnmarshalJSON(t *testing.T) { + var pr prSignedBy + + // 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("&"), &pr) + assert.Error(t, err) + err = pr.UnmarshalJSON([]byte("&")) + assert.Error(t, err) + + // Not an object + err = json.Unmarshal([]byte("1"), &pr) + assert.Error(t, err) + + // Start with a valid JSON. + validPR, err := NewPRSignedByKeyData(SBKeyTypeGPGKeys, []byte("abc"), NewPRMMatchExact()) + require.NoError(t, err) + validJSON, err := json.Marshal(validPR) + require.NoError(t, err) + + // Success with KeyData + pr = prSignedBy{} + err = json.Unmarshal(validJSON, &pr) + require.NoError(t, err) + assert.Equal(t, validPR, &pr) + + // Success with KeyPath + kpPR, err := NewPRSignedByKeyPath(SBKeyTypeGPGKeys, "/foo/bar", NewPRMMatchExact()) + require.NoError(t, err) + testJSON, err := json.Marshal(kpPR) + require.NoError(t, err) + pr = prSignedBy{} + err = json.Unmarshal(testJSON, &pr) + require.NoError(t, err) + assert.Equal(t, kpPR, &pr) + + // newPolicyRequirementFromJSON recognizes this type + _pr, err := newPolicyRequirementFromJSON(validJSON) + require.NoError(t, err) + assert.Equal(t, validPR, _pr) + + // Various ways to corrupt the JSON + breakFns := []func(mSI){ + // The "type" field is missing + func(v mSI) { delete(v, "type") }, + // Wrong "type" field + func(v mSI) { v["type"] = 1 }, + func(v mSI) { v["type"] = "this is invalid" }, + // Extra top-level sub-object + func(v mSI) { v["unexpected"] = 1 }, + // The "keyType" field is missing + func(v mSI) { delete(v, "keyType") }, + // Invalid "keyType" field + func(v mSI) { v["keyType"] = "this is invalid" }, + // Both "keyPath" and "keyData" is missing + func(v mSI) { delete(v, "keyData") }, + // Both "keyPath" and "keyData" is present + func(v mSI) { v["keyPath"] = "/foo/bar" }, + // Invalid "keyPath" field + func(v mSI) { delete(v, "keyData"); v["keyPath"] = 1 }, + func(v mSI) { v["type"] = "this is invalid" }, + // Invalid "keyData" field + func(v mSI) { v["keyData"] = 1 }, + func(v mSI) { v["keyData"] = "this is invalid base64" }, + // Invalid "signedIdentity" field + func(v mSI) { v["signedIdentity"] = "this is invalid" }, + // "signedIdentity" an explicit nil + func(v mSI) { v["signedIdentity"] = nil }, + } + for _, fn := range breakFns { + err = tryUnmarshalModifiedSignedBy(t, &pr, validJSON, fn) + assert.Error(t, err, string(testJSON)) + } + + // Duplicated fields + for _, field := range []string{"type", "keyType", "keyData", "signedIdentity"} { + var tmp mSI + err := json.Unmarshal(validJSON, &tmp) + require.NoError(t, err) + + testJSON := addExtraJSONMember(t, validJSON, field, tmp[field]) + + pr = prSignedBy{} + err = json.Unmarshal(testJSON, &pr) + assert.Error(t, err) + } + // Handle "keyPath", which is not in validJSON, specially + pathPR, err := NewPRSignedByKeyPath(SBKeyTypeGPGKeys, "/foo/bar", NewPRMMatchExact()) + require.NoError(t, err) + testJSON, err = json.Marshal(pathPR) + require.NoError(t, err) + testJSON = addExtraJSONMember(t, testJSON, "keyPath", pr.KeyPath) + pr = prSignedBy{} + err = json.Unmarshal(testJSON, &pr) + assert.Error(t, err) + + // Various allowed modifications to the requirement + allowedModificationFns := []func(mSI){ + // Delete the signedIdentity field + func(v mSI) { delete(v, "signedIdentity") }, + } + for _, fn := range allowedModificationFns { + err = tryUnmarshalModifiedSignedBy(t, &pr, validJSON, fn) + require.NoError(t, err) + } + +} + +func TestSBKeyTypeIsValid(t *testing.T) { + // Valid values + for _, s := range []sbKeyType{ + SBKeyTypeGPGKeys, + SBKeyTypeSignedByGPGKeys, + SBKeyTypeX509Certificates, + SBKeyTypeSignedByX509CAs, + } { + assert.True(t, s.IsValid()) + } + + // Invalid values + for _, s := range []string{"", "this is invalid"} { + assert.False(t, sbKeyType(s).IsValid()) + } +} + +func TestSBKeyTypeUnmarshalJSON(t *testing.T) { + var kt sbKeyType + + // 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("&"), &kt) + assert.Error(t, err) + err = kt.UnmarshalJSON([]byte("&")) + assert.Error(t, err) + + // Not a string + err = json.Unmarshal([]byte("1"), &kt) + assert.Error(t, err) + + // Valid values. + for _, v := range []sbKeyType{ + SBKeyTypeGPGKeys, + SBKeyTypeSignedByGPGKeys, + SBKeyTypeX509Certificates, + SBKeyTypeSignedByX509CAs, + } { + kt = sbKeyType("") + err = json.Unmarshal([]byte(`"`+string(v)+`"`), &kt) + assert.NoError(t, err) + } + + // Invalid values + kt = sbKeyType("") + err = json.Unmarshal([]byte(`""`), &kt) + assert.Error(t, err) + + kt = sbKeyType("") + err = json.Unmarshal([]byte(`"this is invalid"`), &kt) + assert.Error(t, err) +} + +// NewPRSignedBaseLayer is like NewPRSignedBaseLayer, except it must not fail. +func xNewPRSignedBaseLayer(baseLayerIdentity PolicyReferenceMatch) PolicyRequirement { + pr, err := NewPRSignedBaseLayer(baseLayerIdentity) + if err != nil { + panic("xNewPRSignedBaseLayer failed") + } + return pr +} + +func TestNewPRSignedBaseLayer(t *testing.T) { + testBLI := NewPRMMatchExact() + + // Success + _pr, err := NewPRSignedBaseLayer(testBLI) + require.NoError(t, err) + pr, ok := _pr.(*prSignedBaseLayer) + require.True(t, ok) + assert.Equal(t, &prSignedBaseLayer{ + prCommon: prCommon{prTypeSignedBaseLayer}, + BaseLayerIdentity: testBLI, + }, pr) + + // Invalid baseLayerIdentity + _, err = NewPRSignedBaseLayer(nil) + assert.Error(t, err) +} + +func TestPRSignedBaseLayerUnmarshalJSON(t *testing.T) { + var pr prSignedBaseLayer + + // 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("&"), &pr) + assert.Error(t, err) + err = pr.UnmarshalJSON([]byte("&")) + assert.Error(t, err) + + // Not an object + err = json.Unmarshal([]byte("1"), &pr) + assert.Error(t, err) + + // Start with a valid JSON. + baseIdentity, err := NewPRMExactReference("registry.access.redhat.com/rhel7/rhel:7.2.3") + require.NoError(t, err) + validPR, err := NewPRSignedBaseLayer(baseIdentity) + require.NoError(t, err) + validJSON, err := json.Marshal(validPR) + require.NoError(t, err) + + // Success + pr = prSignedBaseLayer{} + err = json.Unmarshal(validJSON, &pr) + require.NoError(t, err) + assert.Equal(t, validPR, &pr) + + // newPolicyRequirementFromJSON recognizes this type + _pr, err := newPolicyRequirementFromJSON(validJSON) + require.NoError(t, err) + assert.Equal(t, validPR, _pr) + + // Various ways to corrupt the JSON + breakFns := []func(mSI){ + // The "type" field is missing + func(v mSI) { delete(v, "type") }, + // Wrong "type" field + func(v mSI) { v["type"] = 1 }, + func(v mSI) { v["type"] = "this is invalid" }, + // Extra top-level sub-object + func(v mSI) { v["unexpected"] = 1 }, + // The "baseLayerIdentity" field is missing + func(v mSI) { delete(v, "baseLayerIdentity") }, + // Invalid "baseLayerIdentity" field + func(v mSI) { v["baseLayerIdentity"] = "this is invalid" }, + // Invalid "baseLayerIdentity" an explicit nil + func(v mSI) { v["baseLayerIdentity"] = nil }, + } + for _, fn := range breakFns { + var tmp mSI + err := json.Unmarshal(validJSON, &tmp) + require.NoError(t, err) + + fn(tmp) + + testJSON, err := json.Marshal(tmp) + require.NoError(t, err) + + pr = prSignedBaseLayer{} + err = json.Unmarshal(testJSON, &pr) + assert.Error(t, err) + } + + // Duplicated fields + for _, field := range []string{"type", "baseLayerIdentity"} { + var tmp mSI + err := json.Unmarshal(validJSON, &tmp) + require.NoError(t, err) + + testJSON := addExtraJSONMember(t, validJSON, field, tmp[field]) + + pr = prSignedBaseLayer{} + err = json.Unmarshal(testJSON, &pr) + assert.Error(t, err) + } +} + +func TestNewPolicyReferenceMatchFromJSON(t *testing.T) { + // Sample success. Others tested in the individual PolicyReferenceMatch.UnmarshalJSON implementations. + validPRM := NewPRMMatchExact() + validJSON, err := json.Marshal(validPRM) + require.NoError(t, err) + prm, err := newPolicyReferenceMatchFromJSON(validJSON) + require.NoError(t, err) + assert.Equal(t, validPRM, prm) + + // Invalid + for _, invalid := range []interface{}{ + // Not an object + 1, + // Missing type + prmCommon{}, + // Invalid type + prmCommon{Type: "this is invalid"}, + // Valid type but invalid contents + prmExactReference{ + prmCommon: prmCommon{Type: prmTypeExactReference}, + DockerReference: "", + }, + } { + testJSON, err := json.Marshal(invalid) + require.NoError(t, err) + + _, err = newPolicyReferenceMatchFromJSON(testJSON) + assert.Error(t, err, string(testJSON)) + } +} + +func TestNewPRMMatchExact(t *testing.T) { + _prm := NewPRMMatchExact() + prm, ok := _prm.(*prmMatchExact) + require.True(t, ok) + assert.Equal(t, &prmMatchExact{prmCommon{prmTypeMatchExact}}, prm) +} + +func TestPRMMatchExactUnmarshalJSON(t *testing.T) { + var prm prmMatchExact + + // 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("&"), &prm) + assert.Error(t, err) + err = prm.UnmarshalJSON([]byte("&")) + assert.Error(t, err) + + // Not an object + err = json.Unmarshal([]byte("1"), &prm) + assert.Error(t, err) + + // Start with a valid JSON. + validPR := NewPRMMatchExact() + validJSON, err := json.Marshal(validPR) + require.NoError(t, err) + + // Success + prm = prmMatchExact{} + err = json.Unmarshal(validJSON, &prm) + require.NoError(t, err) + assert.Equal(t, validPR, &prm) + + // newPolicyReferenceMatchFromJSON recognizes this type + _pr, err := newPolicyReferenceMatchFromJSON(validJSON) + require.NoError(t, err) + assert.Equal(t, validPR, _pr) + + for _, invalid := range []mSI{ + // Missing "type" field + {}, + // Wrong "type" field + {"type": 1}, + {"type": "this is invalid"}, + // Extra fields + { + "type": string(prmTypeMatchExact), + "unknown": "foo", + }, + } { + testJSON, err := json.Marshal(invalid) + require.NoError(t, err) + + prm = prmMatchExact{} + err = json.Unmarshal(testJSON, &prm) + assert.Error(t, err, string(testJSON)) + } + + // Duplicated fields + for _, field := range []string{"type"} { + var tmp mSI + err := json.Unmarshal(validJSON, &tmp) + require.NoError(t, err) + + testJSON := addExtraJSONMember(t, validJSON, field, tmp[field]) + + prm = prmMatchExact{} + err = json.Unmarshal(testJSON, &prm) + assert.Error(t, err) + } +} + +func TestNewPRMMatchRepository(t *testing.T) { + _prm := NewPRMMatchRepository() + prm, ok := _prm.(*prmMatchRepository) + require.True(t, ok) + assert.Equal(t, &prmMatchRepository{prmCommon{prmTypeMatchRepository}}, prm) +} + +func TestPRMMatchRepositoryUnmarshalJSON(t *testing.T) { + var prm prmMatchRepository + + // 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("&"), &prm) + assert.Error(t, err) + err = prm.UnmarshalJSON([]byte("&")) + assert.Error(t, err) + + // Not an object + err = json.Unmarshal([]byte("1"), &prm) + assert.Error(t, err) + + // Start with a valid JSON. + validPR := NewPRMMatchRepository() + validJSON, err := json.Marshal(validPR) + require.NoError(t, err) + + // Success + prm = prmMatchRepository{} + err = json.Unmarshal(validJSON, &prm) + require.NoError(t, err) + assert.Equal(t, validPR, &prm) + + // newPolicyReferenceMatchFromJSON recognizes this type + _pr, err := newPolicyReferenceMatchFromJSON(validJSON) + require.NoError(t, err) + assert.Equal(t, validPR, _pr) + + for _, invalid := range []mSI{ + // Missing "type" field + {}, + // Wrong "type" field + {"type": 1}, + {"type": "this is invalid"}, + // Extra fields + { + "type": string(prmTypeMatchRepository), + "unknown": "foo", + }, + } { + testJSON, err := json.Marshal(invalid) + require.NoError(t, err) + + prm = prmMatchRepository{} + err = json.Unmarshal(testJSON, &prm) + assert.Error(t, err, string(testJSON)) + } + + // Duplicated fields + for _, field := range []string{"type"} { + var tmp mSI + err := json.Unmarshal(validJSON, &tmp) + require.NoError(t, err) + + testJSON := addExtraJSONMember(t, validJSON, field, tmp[field]) + + prm = prmMatchRepository{} + err = json.Unmarshal(testJSON, &prm) + assert.Error(t, err) + } +} + +// xNewPRMExactReference is like NewPRMExactReference, except it must not fail. +func xNewPRMExactReference(dockerReference string) PolicyReferenceMatch { + pr, err := NewPRMExactReference(dockerReference) + if err != nil { + panic("xNewPRMExactReference failed") + } + return pr +} + +func TestNewPRMExactReference(t *testing.T) { + const testDR = "library/busybox:latest" + + // Success + _prm, err := NewPRMExactReference(testDR) + require.NoError(t, err) + prm, ok := _prm.(*prmExactReference) + require.True(t, ok) + assert.Equal(t, &prmExactReference{ + prmCommon: prmCommon{prmTypeExactReference}, + DockerReference: testDR, + }, prm) + + // Invalid dockerReference + _, err = NewPRMExactReference("") + assert.Error(t, err) + // Uppercase is invalid in Docker reference components. + _, err = NewPRMExactReference("INVALIDUPPERCASE:latest") + assert.Error(t, err) + // Missing tag + _, err = NewPRMExactReference("library/busybox") + assert.Error(t, err) +} + +func TestPRMExactReferenceUnmarshalJSON(t *testing.T) { + var prm prmExactReference + + // 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("&"), &prm) + assert.Error(t, err) + err = prm.UnmarshalJSON([]byte("&")) + assert.Error(t, err) + + // Not an object + err = json.Unmarshal([]byte("1"), &prm) + assert.Error(t, err) + + // Start with a valid JSON. + validPRM, err := NewPRMExactReference("library/buxybox:latest") + require.NoError(t, err) + validJSON, err := json.Marshal(validPRM) + require.NoError(t, err) + + // Success + prm = prmExactReference{} + err = json.Unmarshal(validJSON, &prm) + require.NoError(t, err) + assert.Equal(t, validPRM, &prm) + + // newPolicyReferenceMatchFromJSON recognizes this type + _prm, err := newPolicyReferenceMatchFromJSON(validJSON) + require.NoError(t, err) + assert.Equal(t, validPRM, _prm) + + // Various ways to corrupt the JSON + breakFns := []func(mSI){ + // The "type" field is missing + func(v mSI) { delete(v, "type") }, + // Wrong "type" field + func(v mSI) { v["type"] = 1 }, + func(v mSI) { v["type"] = "this is invalid" }, + // Extra top-level sub-object + func(v mSI) { v["unexpected"] = 1 }, + // The "dockerReference" field is missing + func(v mSI) { delete(v, "dockerReference") }, + // Invalid "dockerReference" field + func(v mSI) { v["dockerReference"] = 1 }, + } + for _, fn := range breakFns { + var tmp mSI + err := json.Unmarshal(validJSON, &tmp) + require.NoError(t, err) + + fn(tmp) + + testJSON, err := json.Marshal(tmp) + require.NoError(t, err) + + prm = prmExactReference{} + err = json.Unmarshal(testJSON, &prm) + assert.Error(t, err) + } + + // Duplicated fields + for _, field := range []string{"type", "baseLayerIdentity"} { + var tmp mSI + err := json.Unmarshal(validJSON, &tmp) + require.NoError(t, err) + + testJSON := addExtraJSONMember(t, validJSON, field, tmp[field]) + + prm = prmExactReference{} + err = json.Unmarshal(testJSON, &prm) + assert.Error(t, err) + } +} + +// xNewPRMExactRepository is like NewPRMExactRepository, except it must not fail. +func xNewPRMExactRepository(dockerRepository string) PolicyReferenceMatch { + pr, err := NewPRMExactRepository(dockerRepository) + if err != nil { + panic("xNewPRMExactRepository failed") + } + return pr +} + +func TestNewPRMExactRepository(t *testing.T) { + const testDR = "library/busybox:latest" + + // Success + _prm, err := NewPRMExactRepository(testDR) + require.NoError(t, err) + prm, ok := _prm.(*prmExactRepository) + require.True(t, ok) + assert.Equal(t, &prmExactRepository{ + prmCommon: prmCommon{prmTypeExactRepository}, + DockerRepository: testDR, + }, prm) + + // Invalid dockerRepository + _, err = NewPRMExactRepository("") + assert.Error(t, err) + // Uppercase is invalid in Docker reference components. + _, err = NewPRMExactRepository("INVALIDUPPERCASE") + assert.Error(t, err) +} + +func TestPRMExactRepositoryUnmarshalJSON(t *testing.T) { + var prm prmExactRepository + + // 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("&"), &prm) + assert.Error(t, err) + err = prm.UnmarshalJSON([]byte("&")) + assert.Error(t, err) + + // Not an object + err = json.Unmarshal([]byte("1"), &prm) + assert.Error(t, err) + + // Start with a valid JSON. + validPRM, err := NewPRMExactRepository("library/buxybox:latest") + require.NoError(t, err) + validJSON, err := json.Marshal(validPRM) + require.NoError(t, err) + + // Success + prm = prmExactRepository{} + err = json.Unmarshal(validJSON, &prm) + require.NoError(t, err) + assert.Equal(t, validPRM, &prm) + + // newPolicyReferenceMatchFromJSON recognizes this type + _prm, err := newPolicyReferenceMatchFromJSON(validJSON) + require.NoError(t, err) + assert.Equal(t, validPRM, _prm) + + // Various ways to corrupt the JSON + breakFns := []func(mSI){ + // The "type" field is missing + func(v mSI) { delete(v, "type") }, + // Wrong "type" field + func(v mSI) { v["type"] = 1 }, + func(v mSI) { v["type"] = "this is invalid" }, + // Extra top-level sub-object + func(v mSI) { v["unexpected"] = 1 }, + // The "dockerRepository" field is missing + func(v mSI) { delete(v, "dockerRepository") }, + // Invalid "dockerRepository" field + func(v mSI) { v["dockerRepository"] = 1 }, + } + for _, fn := range breakFns { + var tmp mSI + err := json.Unmarshal(validJSON, &tmp) + require.NoError(t, err) + + fn(tmp) + + testJSON, err := json.Marshal(tmp) + require.NoError(t, err) + + prm = prmExactRepository{} + err = json.Unmarshal(testJSON, &prm) + assert.Error(t, err) + } + + // Duplicated fields + for _, field := range []string{"type", "baseLayerIdentity"} { + var tmp mSI + err := json.Unmarshal(validJSON, &tmp) + require.NoError(t, err) + + testJSON := addExtraJSONMember(t, validJSON, field, tmp[field]) + + prm = prmExactRepository{} + err = json.Unmarshal(testJSON, &prm) + assert.Error(t, err) + } +} diff --git a/signature/policy_types.go b/signature/policy_types.go new file mode 100644 index 00000000..9365a37d --- /dev/null +++ b/signature/policy_types.go @@ -0,0 +1,136 @@ +// Note: Consider the API unstable until the code supports at least three different image formats or transports. + +// This defines types used to represent a signature verification policy in memory. +// Do not use the private types directly; either parse a configuration file, or construct a Policy from PolicyRequirements +// built using the constructor functions provided in policy_config.go. + +package signature + +// Policy defines requirements for considering a signature valid. +type Policy struct { + // Default applies to any image which does not have a matching policy in Specific. + Default PolicyRequirements `json:"default"` + // Specific applies to images matching scope, the map key. + // Scope is registry/server, namespace in a registry, single repository. + // FIXME: Scope syntax - should it be namespaced docker:something ? Or, in the worst case, a composite object (we couldn't use a JSON map) + // Most specific scope wins, duplication is prohibited (hard failure). + // Defaults to an empty map if not specified. + Specific map[string]PolicyRequirements `json:"specific"` +} + +// PolicyRequirements is a set of requirements applying to a set of images; each of them must be satisfied (though perhaps each by a different signature). +// Must not be empty, frequently will only contain a single element. +type PolicyRequirements []PolicyRequirement + +// PolicyRequirement is a rule which must be satisfied by at least one of the signatures of an image. +// The type is public, but its definition is private. +type PolicyRequirement interface{} // Will be expanded and moved elsewhere later. + +// prCommon is the common type field in a JSON encoding of PolicyRequirement. +type prCommon struct { + Type prTypeIdentifier `json:"type"` +} + +// prTypeIdentifier is string designating a kind of a PolicyRequirement. +type prTypeIdentifier string + +const ( + prTypeInsecureAcceptAnything prTypeIdentifier = "insecureAcceptAnything" + prTypeReject prTypeIdentifier = "reject" + prTypeSignedBy prTypeIdentifier = "signedBy" + prTypeSignedBaseLayer prTypeIdentifier = "signedBaseLayer" +) + +// prInsecureAcceptAnything is a PolicyRequirement with type = prTypeInsecureAcceptAnything: every image is accepted. +// Note that because PolicyRequirements are implicitly ANDed, this is necessary only if it is the only rule (to make the list non-empty and the policy explicit). +type prInsecureAcceptAnything struct { + prCommon +} + +// prReject is a PolicyRequirement with type = prTypeReject: every image is rejected. +type prReject struct { + prCommon +} + +// prSignedBy is a PolicyRequirement with type = prTypeSignedBy: the image is signed by trusted keys for a specified identity +type prSignedBy struct { + prCommon + + // KeyType specifies what kind of key reference KeyPath/KeyData is. + // Acceptable values are “GPGKeys” | “signedByGPGKeys” “X.509Certificates” | “signedByX.509CAs” + // FIXME: eventually also support GPGTOFU, X.509TOFU, with KeyPath only + KeyType sbKeyType `json:"keyType"` + + // KeyPath is a pathname to a local file containing the trusted key(s). Exactly one of KeyPath and KeyData must be specified. + KeyPath string `json:"keyPath,omitempty"` + // KeyData contains the trusted key(s), base64-encoded. Exactly one of KeyPath and KeyData must be specified. + KeyData []byte `json:"keyData,omitempty"` + + // SignedIdentity specifies what image identity the signature must be claiming about the image. + // Defaults to "match-exact" if not specified. + SignedIdentity PolicyReferenceMatch `json:"signedIdentity"` +} + +// sbKeyType are the allowed values for prSignedBy.KeyType +type sbKeyType string + +const ( + // SBKeyTypeGPGKeys refers to keys contained in a GPG keyring + SBKeyTypeGPGKeys sbKeyType = "GPGKeys" + // SBKeyTypeSignedByGPGKeys refers to keys signed by keys in a GPG keyring + SBKeyTypeSignedByGPGKeys sbKeyType = "signedByGPGKeys" + // SBKeyTypeX509Certificates refers to keys in a set of X.509 certificates + // FIXME: PEM, DER? + SBKeyTypeX509Certificates sbKeyType = "X509Certificates" + // SBKeyTypeSignedByX509CAs refers to keys signed by one of the X.509 CAs + // FIXME: PEM, DER? + SBKeyTypeSignedByX509CAs sbKeyType = "signedByX509CAs" +) + +// prSignedBaseLayer is a PolicyRequirement with type = prSignedBaseLayer: the image has a specified, correctly signed, base image. +type prSignedBaseLayer struct { + prCommon + // BaseLayerIdentity specifies the base image to look for. "match-exact" is rejected, "match-repository" is unlikely to be useful. + BaseLayerIdentity PolicyReferenceMatch `json:"baseLayerIdentity"` +} + +// PolicyReferenceMatch specifies a set of image identities accepted in PolicyRequirement. +// The type is public, but its implementation is private. +type PolicyReferenceMatch interface{} // Will be expanded and moved elsewhere later. + +// prmCommon is the common type field in a JSON encoding of PolicyReferenceMatch. +type prmCommon struct { + Type prmTypeIdentifier `json:"type"` +} + +// prmTypeIdentifier is string designating a kind of a PolicyReferenceMatch. +type prmTypeIdentifier string + +const ( + prmTypeMatchExact prmTypeIdentifier = "matchExact" + prmTypeMatchRepository prmTypeIdentifier = "matchRepository" + prmTypeExactReference prmTypeIdentifier = "exactReference" + prmTypeExactRepository prmTypeIdentifier = "exactRepository" +) + +// prmMatchExact is a PolicyReferenceMatch with type = prmMatchExact: the two references must match exactly. +type prmMatchExact struct { + prmCommon +} + +// prmMatchRepository is a PolicyReferenceMatch with type = prmMatchRepository: the two references must use the same repository, may differ in the tag. +type prmMatchRepository struct { + prmCommon +} + +// prmExactReference is a PolicyReferenceMatch with type = prmExactReference: matches a specified reference exactly. +type prmExactReference struct { + prmCommon + DockerReference string `json:"dockerReference"` +} + +// prmExactRepository is a PolicyReferenceMatch with type = prmExactRepository: matches a specified repository, with any tag. +type prmExactRepository struct { + prmCommon + DockerRepository string `json:"dockerRepository"` +}