diff --git a/api/swagger-spec/oapi-v1.json b/api/swagger-spec/oapi-v1.json index 362fe90ac5f9..0e94379427b0 100644 --- a/api/swagger-spec/oapi-v1.json +++ b/api/swagger-spec/oapi-v1.json @@ -21059,6 +21059,13 @@ "$ref": "v1.ImageLayer" }, "description": "DockerImageLayers represents the layers in the image. May not be set if the image does not define that data." + }, + "signatures": { + "type": "array", + "items": { + "$ref": "v1.ImageSignature" + }, + "description": "Signatures holds all signatures of the image." } } }, @@ -21081,6 +21088,123 @@ } } }, + "v1.ImageSignature": { + "id": "v1.ImageSignature", + "description": "ImageSignature holds a signature of an image. It allows to verify image identity and possibly other claims as long as the signature is trusted. Based on this information it is possible to restrict runnable images to those matching cluster-wide policy. There are two mandatory fields provided by client: Type and Content. They should be parsed by clients doing image verification. The others are parsed from signature's content by the server. They serve just an informative purpose.", + "required": [ + "type", + "content" + ], + "properties": { + "type": { + "type": "string", + "description": "Required: Describes a type of stored blob." + }, + "content": { + "type": "array", + "items": { + "$ref": "integer" + }, + "description": "Required: An opaque binary string which is an image's signature." + }, + "conditions": { + "type": "array", + "items": { + "$ref": "v1.SignatureCondition" + }, + "description": "Conditions represent the latest available observations of a signature's current state." + }, + "imageIdentity": { + "type": "string", + "description": "A human readable string representing image's identity. It could be a product name and version, or an image pull spec (e.g. \"registry.access.redhat.com/rhel7/rhel:7.2\")." + }, + "signedClaims": { + "type": "any", + "description": "Contains claims from the signature." + }, + "created": { + "type": "string", + "description": "If specified, it is the time of signature's creation." + }, + "issuedBy": { + "$ref": "v1.SignatureIssuer", + "description": "If specified, it holds information about an issuer of signing certificate or key (a person or entity who signed the signing certificate or key)." + }, + "issuedTo": { + "$ref": "v1.SignatureSubject", + "description": "If specified, it holds information about a subject of signing certificate or key (a person or entity who signed the image)." + } + } + }, + "v1.SignatureCondition": { + "id": "v1.SignatureCondition", + "description": "SignatureCondition describes an image signature condition of particular kind at particular probe time.", + "required": [ + "type", + "status" + ], + "properties": { + "type": { + "type": "string", + "description": "Type of job condition, Complete or Failed." + }, + "status": { + "type": "string", + "description": "Status of the condition, one of True, False, Unknown." + }, + "lastProbeTime": { + "type": "string", + "description": "Last time the condition was checked." + }, + "lastTransitionTime": { + "type": "string", + "description": "Last time the condition transit from one status to another." + }, + "reason": { + "type": "string", + "description": "(brief) reason for the condition's last transition." + }, + "message": { + "type": "string", + "description": "Human readable message indicating details about last transition." + } + } + }, + "v1.SignatureIssuer": { + "id": "v1.SignatureIssuer", + "description": "SignatureIssuer holds information about an issuer of signing certificate or key.", + "properties": { + "organization": { + "type": "string", + "description": "Organization name." + }, + "commonName": { + "type": "string", + "description": "Common name (e.g. openshift-signing-service)." + } + } + }, + "v1.SignatureSubject": { + "id": "v1.SignatureSubject", + "description": "SignatureSubject holds information about a person or entity who created the signature.", + "required": [ + "publicKeyID" + ], + "properties": { + "organization": { + "type": "string", + "description": "Organization name." + }, + "commonName": { + "type": "string", + "description": "Common name (e.g. openshift-signing-service)." + }, + "publicKeyID": { + "type": "string", + "description": "If present, it is a human readable key id of public key belonging to the subject used to verify image signature. It should contain at least 64 lowest bits of public key's fingerprint (e.g. 0x685ebe62bf278440)." + } + } + }, "v1.ImageStreamImage": { "id": "v1.ImageStreamImage", "description": "ImageStreamImage represents an Image that is retrieved by image name from an ImageStream.", diff --git a/pkg/image/api/deep_copy_generated.go b/pkg/image/api/deep_copy_generated.go index 01886fa724a2..9a8bd069bc68 100644 --- a/pkg/image/api/deep_copy_generated.go +++ b/pkg/image/api/deep_copy_generated.go @@ -26,6 +26,7 @@ func init() { DeepCopy_api_ImageImportStatus, DeepCopy_api_ImageLayer, DeepCopy_api_ImageList, + DeepCopy_api_ImageSignature, DeepCopy_api_ImageStream, DeepCopy_api_ImageStreamImage, DeepCopy_api_ImageStreamImport, @@ -39,6 +40,10 @@ func init() { DeepCopy_api_ImageStreamTagList, DeepCopy_api_RepositoryImportSpec, DeepCopy_api_RepositoryImportStatus, + DeepCopy_api_SignatureCondition, + DeepCopy_api_SignatureGenericEntity, + DeepCopy_api_SignatureIssuer, + DeepCopy_api_SignatureSubject, DeepCopy_api_TagEvent, DeepCopy_api_TagEventCondition, DeepCopy_api_TagEventList, @@ -312,6 +317,17 @@ func DeepCopy_api_Image(in Image, out *Image, c *conversion.Cloner) error { } else { out.DockerImageLayers = nil } + if in.Signatures != nil { + in, out := in.Signatures, &out.Signatures + *out = make([]ImageSignature, len(in)) + for i := range in { + if err := DeepCopy_api_ImageSignature(in[i], &(*out)[i], c); err != nil { + return err + } + } + } else { + out.Signatures = nil + } return nil } @@ -379,6 +395,66 @@ func DeepCopy_api_ImageList(in ImageList, out *ImageList, c *conversion.Cloner) return nil } +func DeepCopy_api_ImageSignature(in ImageSignature, out *ImageSignature, c *conversion.Cloner) error { + out.Type = in.Type + if in.Content != nil { + in, out := in.Content, &out.Content + *out = make([]byte, len(in)) + copy(*out, in) + } else { + out.Content = nil + } + if in.Conditions != nil { + in, out := in.Conditions, &out.Conditions + *out = make([]SignatureCondition, len(in)) + for i := range in { + if err := DeepCopy_api_SignatureCondition(in[i], &(*out)[i], c); err != nil { + return err + } + } + } else { + out.Conditions = nil + } + out.ImageIdentity = in.ImageIdentity + if in.SignedClaims != nil { + in, out := in.SignedClaims, &out.SignedClaims + *out = make(map[string]string) + for key, val := range in { + (*out)[key] = val + } + } else { + out.SignedClaims = nil + } + if in.Created != nil { + in, out := in.Created, &out.Created + *out = new(unversioned.Time) + if err := unversioned.DeepCopy_unversioned_Time(*in, *out, c); err != nil { + return err + } + } else { + out.Created = nil + } + if in.IssuedBy != nil { + in, out := in.IssuedBy, &out.IssuedBy + *out = new(SignatureIssuer) + if err := DeepCopy_api_SignatureIssuer(*in, *out, c); err != nil { + return err + } + } else { + out.IssuedBy = nil + } + if in.IssuedTo != nil { + in, out := in.IssuedTo, &out.IssuedTo + *out = new(SignatureSubject) + if err := DeepCopy_api_SignatureSubject(*in, *out, c); err != nil { + return err + } + } else { + out.IssuedTo = nil + } + return nil +} + func DeepCopy_api_ImageStream(in ImageStream, out *ImageStream, c *conversion.Cloner) error { if err := unversioned.DeepCopy_unversioned_TypeMeta(in.TypeMeta, &out.TypeMeta, c); err != nil { return err @@ -645,6 +721,41 @@ func DeepCopy_api_RepositoryImportStatus(in RepositoryImportStatus, out *Reposit return nil } +func DeepCopy_api_SignatureCondition(in SignatureCondition, out *SignatureCondition, c *conversion.Cloner) error { + out.Type = in.Type + out.Status = in.Status + if err := unversioned.DeepCopy_unversioned_Time(in.LastProbeTime, &out.LastProbeTime, c); err != nil { + return err + } + if err := unversioned.DeepCopy_unversioned_Time(in.LastTransitionTime, &out.LastTransitionTime, c); err != nil { + return err + } + out.Reason = in.Reason + out.Message = in.Message + return nil +} + +func DeepCopy_api_SignatureGenericEntity(in SignatureGenericEntity, out *SignatureGenericEntity, c *conversion.Cloner) error { + out.Organization = in.Organization + out.CommonName = in.CommonName + return nil +} + +func DeepCopy_api_SignatureIssuer(in SignatureIssuer, out *SignatureIssuer, c *conversion.Cloner) error { + if err := DeepCopy_api_SignatureGenericEntity(in.SignatureGenericEntity, &out.SignatureGenericEntity, c); err != nil { + return err + } + return nil +} + +func DeepCopy_api_SignatureSubject(in SignatureSubject, out *SignatureSubject, c *conversion.Cloner) error { + if err := DeepCopy_api_SignatureGenericEntity(in.SignatureGenericEntity, &out.SignatureGenericEntity, c); err != nil { + return err + } + out.PublicKeyID = in.PublicKeyID + return nil +} + func DeepCopy_api_TagEvent(in TagEvent, out *TagEvent, c *conversion.Cloner) error { if err := unversioned.DeepCopy_unversioned_Time(in.Created, &out.Created, c); err != nil { return err diff --git a/pkg/image/api/types.go b/pkg/image/api/types.go index cdc5499d9c12..2ae58574c6e8 100644 --- a/pkg/image/api/types.go +++ b/pkg/image/api/types.go @@ -66,6 +66,8 @@ type Image struct { DockerImageManifest string // DockerImageLayers represents the layers in the image. May not be set if the image does not define that data. DockerImageLayers []ImageLayer + // Signatures holds all signatures of the image. + Signatures []ImageSignature } // ImageLayer represents a single layer of the image. Some images may have multiple layers. Some may have none. @@ -76,6 +78,99 @@ type ImageLayer struct { Size int64 } +const ( + // The supported type of image signature. + ImageSignatureTypeAtomicImageV1 string = "AtomicImageV1" +) + +// ImageSignature holds a signature of an image. It allows to verify image identity and possibly other claims +// as long as the signature is trusted. Based on this information it is possible to restrict runnable images +// to those matching cluster-wide policy. +// There are two mandatory fields provided by client: Type and Content. They should be parsed by clients doing +// image verification. The others are parsed from signature's content by the server. They serve just an +// informative purpose. +type ImageSignature struct { + // Required: Describes a type of stored blob. + Type string + // Required: An opaque binary string which is an image's signature. + Content []byte + // Conditions represent the latest available observations of a signature's current state. + Conditions []SignatureCondition + + // Following metadata fields will be set by server if the signature content is successfully parsed and + // the information available. + + // A human readable string representing image's identity. It could be a product name and version, or an + // image pull spec (e.g. "registry.access.redhat.com/rhel7/rhel:7.2"). + ImageIdentity string + // Contains claims from the signature. + SignedClaims map[string]string + // If specified, it is the time of signature's creation. + Created *unversioned.Time + // If specified, it holds information about an issuer of signing certificate or key (a person or entity + // who signed the signing certificate or key). + IssuedBy *SignatureIssuer + // If specified, it holds information about a subject of signing certificate or key (a person or entity + // who signed the image). + IssuedTo *SignatureSubject +} + +// These are valid conditions of an image signature. +const ( + // SignatureTrusted means the signing key or certificate was valid and the signature matched the image at + // the probe time. + SignatureTrusted = "Trusted" + // SignatureForImage means the signature matches image object containing it. + SignatureForImage = "ForImage" + // SignatureExpired means the signature or its signing key or certificate had been expired at the probe + // time. + SignatureExpired = "Expired" + // SignatureRevoked means the signature or its signing key or certificate has been revoked. + SignatureRevoked = "Revoked" +) + +/// SignatureConditionType is a type of image signature condition. +type SignatureConditionType string + +// SignatureCondition describes an image signature condition of particular kind at particular probe time. +type SignatureCondition struct { + // Type of job condition, Complete or Failed. + Type SignatureConditionType + // Status of the condition, one of True, False, Unknown. + Status kapi.ConditionStatus + // Last time the condition was checked. + LastProbeTime unversioned.Time + // Last time the condition transit from one status to another. + LastTransitionTime unversioned.Time + // (brief) reason for the condition's last transition. + Reason string + // Human readable message indicating details about last transition. + Message string +} + +// SignatureGenericEntity holds a generic information about a person or entity who is an issuer or a subject +// of signing certificate or key. +type SignatureGenericEntity struct { + // Organization name. + Organization string + // Common name (e.g. openshift-signing-service). + CommonName string +} + +// SignatureIssuer holds information about an issuer of signing certificate or key. +type SignatureIssuer struct { + SignatureGenericEntity +} + +// SignatureSubject holds information about a person or entity who created the signature. +type SignatureSubject struct { + SignatureGenericEntity + // If present, it is a human readable key id of public key belonging to the subject used to verify image + // signature. It should contain at least 64 lowest bits of public key's fingerprint (e.g. + // 0x685ebe62bf278440). + PublicKeyID string +} + // ImageStreamList is a list of ImageStream objects. type ImageStreamList struct { unversioned.TypeMeta diff --git a/pkg/image/api/v1/conversion.go b/pkg/image/api/v1/conversion.go index 9e4c43a5c52e..c43f7c4c6668 100644 --- a/pkg/image/api/v1/conversion.go +++ b/pkg/image/api/v1/conversion.go @@ -52,6 +52,17 @@ func Convert_api_Image_To_v1_Image(in *newer.Image, out *Image, s conversion.Sco out.DockerImageLayers = nil } + if in.Signatures != nil { + out.Signatures = make([]ImageSignature, len(in.Signatures)) + for i := range in.Signatures { + if err := s.Convert(&in.Signatures[i], &out.Signatures[i], 0); err != nil { + return err + } + } + } else { + out.Signatures = nil + } + return nil } @@ -92,6 +103,17 @@ func Convert_v1_Image_To_api_Image(in *Image, out *newer.Image, s conversion.Sco out.DockerImageLayers = nil } + if in.Signatures != nil { + out.Signatures = make([]newer.ImageSignature, len(in.Signatures)) + for i := range in.Signatures { + if err := s.Convert(&in.Signatures[i], &out.Signatures[i], 0); err != nil { + return err + } + } + } else { + out.Signatures = nil + } + return nil } diff --git a/pkg/image/api/v1/conversion_generated.go b/pkg/image/api/v1/conversion_generated.go index 366e49afab33..4ba3467ae5d0 100644 --- a/pkg/image/api/v1/conversion_generated.go +++ b/pkg/image/api/v1/conversion_generated.go @@ -7,6 +7,7 @@ package v1 import ( image_api "github.com/openshift/origin/pkg/image/api" api "k8s.io/kubernetes/pkg/api" + unversioned "k8s.io/kubernetes/pkg/api/unversioned" api_v1 "k8s.io/kubernetes/pkg/api/v1" conversion "k8s.io/kubernetes/pkg/conversion" reflect "reflect" @@ -26,6 +27,8 @@ func init() { Convert_api_ImageLayer_To_v1_ImageLayer, Convert_v1_ImageList_To_api_ImageList, Convert_api_ImageList_To_v1_ImageList, + Convert_v1_ImageSignature_To_api_ImageSignature, + Convert_api_ImageSignature_To_v1_ImageSignature, Convert_v1_ImageStream_To_api_ImageStream, Convert_api_ImageStream_To_v1_ImageStream, Convert_v1_ImageStreamImage_To_api_ImageStreamImage, @@ -52,6 +55,14 @@ func init() { Convert_api_RepositoryImportSpec_To_v1_RepositoryImportSpec, Convert_v1_RepositoryImportStatus_To_api_RepositoryImportStatus, Convert_api_RepositoryImportStatus_To_v1_RepositoryImportStatus, + Convert_v1_SignatureCondition_To_api_SignatureCondition, + Convert_api_SignatureCondition_To_v1_SignatureCondition, + Convert_v1_SignatureGenericEntity_To_api_SignatureGenericEntity, + Convert_api_SignatureGenericEntity_To_v1_SignatureGenericEntity, + Convert_v1_SignatureIssuer_To_api_SignatureIssuer, + Convert_api_SignatureIssuer_To_v1_SignatureIssuer, + Convert_v1_SignatureSubject_To_api_SignatureSubject, + Convert_api_SignatureSubject_To_v1_SignatureSubject, Convert_v1_TagEvent_To_api_TagEvent, Convert_api_TagEvent_To_v1_TagEvent, Convert_v1_TagEventCondition_To_api_TagEventCondition, @@ -288,6 +299,132 @@ func Convert_api_ImageList_To_v1_ImageList(in *image_api.ImageList, out *ImageLi return autoConvert_api_ImageList_To_v1_ImageList(in, out, s) } +func autoConvert_v1_ImageSignature_To_api_ImageSignature(in *ImageSignature, out *image_api.ImageSignature, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*ImageSignature))(in) + } + out.Type = in.Type + if err := conversion.Convert_Slice_byte_To_Slice_byte(&in.Content, &out.Content, s); err != nil { + return err + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]image_api.SignatureCondition, len(*in)) + for i := range *in { + if err := Convert_v1_SignatureCondition_To_api_SignatureCondition(&(*in)[i], &(*out)[i], s); err != nil { + return err + } + } + } else { + out.Conditions = nil + } + out.ImageIdentity = in.ImageIdentity + if in.SignedClaims != nil { + in, out := &in.SignedClaims, &out.SignedClaims + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } else { + out.SignedClaims = nil + } + if in.Created != nil { + in, out := &in.Created, &out.Created + *out = new(unversioned.Time) + if err := api.Convert_unversioned_Time_To_unversioned_Time(*in, *out, s); err != nil { + return err + } + } else { + out.Created = nil + } + if in.IssuedBy != nil { + in, out := &in.IssuedBy, &out.IssuedBy + *out = new(image_api.SignatureIssuer) + if err := Convert_v1_SignatureIssuer_To_api_SignatureIssuer(*in, *out, s); err != nil { + return err + } + } else { + out.IssuedBy = nil + } + if in.IssuedTo != nil { + in, out := &in.IssuedTo, &out.IssuedTo + *out = new(image_api.SignatureSubject) + if err := Convert_v1_SignatureSubject_To_api_SignatureSubject(*in, *out, s); err != nil { + return err + } + } else { + out.IssuedTo = nil + } + return nil +} + +func Convert_v1_ImageSignature_To_api_ImageSignature(in *ImageSignature, out *image_api.ImageSignature, s conversion.Scope) error { + return autoConvert_v1_ImageSignature_To_api_ImageSignature(in, out, s) +} + +func autoConvert_api_ImageSignature_To_v1_ImageSignature(in *image_api.ImageSignature, out *ImageSignature, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*image_api.ImageSignature))(in) + } + out.Type = in.Type + if err := conversion.Convert_Slice_byte_To_Slice_byte(&in.Content, &out.Content, s); err != nil { + return err + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]SignatureCondition, len(*in)) + for i := range *in { + if err := Convert_api_SignatureCondition_To_v1_SignatureCondition(&(*in)[i], &(*out)[i], s); err != nil { + return err + } + } + } else { + out.Conditions = nil + } + out.ImageIdentity = in.ImageIdentity + if in.SignedClaims != nil { + in, out := &in.SignedClaims, &out.SignedClaims + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } else { + out.SignedClaims = nil + } + if in.Created != nil { + in, out := &in.Created, &out.Created + *out = new(unversioned.Time) + if err := api.Convert_unversioned_Time_To_unversioned_Time(*in, *out, s); err != nil { + return err + } + } else { + out.Created = nil + } + if in.IssuedBy != nil { + in, out := &in.IssuedBy, &out.IssuedBy + *out = new(SignatureIssuer) + if err := Convert_api_SignatureIssuer_To_v1_SignatureIssuer(*in, *out, s); err != nil { + return err + } + } else { + out.IssuedBy = nil + } + if in.IssuedTo != nil { + in, out := &in.IssuedTo, &out.IssuedTo + *out = new(SignatureSubject) + if err := Convert_api_SignatureSubject_To_v1_SignatureSubject(*in, *out, s); err != nil { + return err + } + } else { + out.IssuedTo = nil + } + return nil +} + +func Convert_api_ImageSignature_To_v1_ImageSignature(in *image_api.ImageSignature, out *ImageSignature, s conversion.Scope) error { + return autoConvert_api_ImageSignature_To_v1_ImageSignature(in, out, s) +} + func autoConvert_v1_ImageStream_To_api_ImageStream(in *ImageStream, out *image_api.ImageStream, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*ImageStream))(in) @@ -888,6 +1025,132 @@ func Convert_api_RepositoryImportStatus_To_v1_RepositoryImportStatus(in *image_a return autoConvert_api_RepositoryImportStatus_To_v1_RepositoryImportStatus(in, out, s) } +func autoConvert_v1_SignatureCondition_To_api_SignatureCondition(in *SignatureCondition, out *image_api.SignatureCondition, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*SignatureCondition))(in) + } + out.Type = image_api.SignatureConditionType(in.Type) + out.Status = api.ConditionStatus(in.Status) + if err := api.Convert_unversioned_Time_To_unversioned_Time(&in.LastProbeTime, &out.LastProbeTime, s); err != nil { + return err + } + if err := api.Convert_unversioned_Time_To_unversioned_Time(&in.LastTransitionTime, &out.LastTransitionTime, s); err != nil { + return err + } + out.Reason = in.Reason + out.Message = in.Message + return nil +} + +func Convert_v1_SignatureCondition_To_api_SignatureCondition(in *SignatureCondition, out *image_api.SignatureCondition, s conversion.Scope) error { + return autoConvert_v1_SignatureCondition_To_api_SignatureCondition(in, out, s) +} + +func autoConvert_api_SignatureCondition_To_v1_SignatureCondition(in *image_api.SignatureCondition, out *SignatureCondition, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*image_api.SignatureCondition))(in) + } + out.Type = SignatureConditionType(in.Type) + out.Status = api_v1.ConditionStatus(in.Status) + if err := api.Convert_unversioned_Time_To_unversioned_Time(&in.LastProbeTime, &out.LastProbeTime, s); err != nil { + return err + } + if err := api.Convert_unversioned_Time_To_unversioned_Time(&in.LastTransitionTime, &out.LastTransitionTime, s); err != nil { + return err + } + out.Reason = in.Reason + out.Message = in.Message + return nil +} + +func Convert_api_SignatureCondition_To_v1_SignatureCondition(in *image_api.SignatureCondition, out *SignatureCondition, s conversion.Scope) error { + return autoConvert_api_SignatureCondition_To_v1_SignatureCondition(in, out, s) +} + +func autoConvert_v1_SignatureGenericEntity_To_api_SignatureGenericEntity(in *SignatureGenericEntity, out *image_api.SignatureGenericEntity, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*SignatureGenericEntity))(in) + } + out.Organization = in.Organization + out.CommonName = in.CommonName + return nil +} + +func Convert_v1_SignatureGenericEntity_To_api_SignatureGenericEntity(in *SignatureGenericEntity, out *image_api.SignatureGenericEntity, s conversion.Scope) error { + return autoConvert_v1_SignatureGenericEntity_To_api_SignatureGenericEntity(in, out, s) +} + +func autoConvert_api_SignatureGenericEntity_To_v1_SignatureGenericEntity(in *image_api.SignatureGenericEntity, out *SignatureGenericEntity, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*image_api.SignatureGenericEntity))(in) + } + out.Organization = in.Organization + out.CommonName = in.CommonName + return nil +} + +func Convert_api_SignatureGenericEntity_To_v1_SignatureGenericEntity(in *image_api.SignatureGenericEntity, out *SignatureGenericEntity, s conversion.Scope) error { + return autoConvert_api_SignatureGenericEntity_To_v1_SignatureGenericEntity(in, out, s) +} + +func autoConvert_v1_SignatureIssuer_To_api_SignatureIssuer(in *SignatureIssuer, out *image_api.SignatureIssuer, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*SignatureIssuer))(in) + } + if err := Convert_v1_SignatureGenericEntity_To_api_SignatureGenericEntity(&in.SignatureGenericEntity, &out.SignatureGenericEntity, s); err != nil { + return err + } + return nil +} + +func Convert_v1_SignatureIssuer_To_api_SignatureIssuer(in *SignatureIssuer, out *image_api.SignatureIssuer, s conversion.Scope) error { + return autoConvert_v1_SignatureIssuer_To_api_SignatureIssuer(in, out, s) +} + +func autoConvert_api_SignatureIssuer_To_v1_SignatureIssuer(in *image_api.SignatureIssuer, out *SignatureIssuer, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*image_api.SignatureIssuer))(in) + } + if err := Convert_api_SignatureGenericEntity_To_v1_SignatureGenericEntity(&in.SignatureGenericEntity, &out.SignatureGenericEntity, s); err != nil { + return err + } + return nil +} + +func Convert_api_SignatureIssuer_To_v1_SignatureIssuer(in *image_api.SignatureIssuer, out *SignatureIssuer, s conversion.Scope) error { + return autoConvert_api_SignatureIssuer_To_v1_SignatureIssuer(in, out, s) +} + +func autoConvert_v1_SignatureSubject_To_api_SignatureSubject(in *SignatureSubject, out *image_api.SignatureSubject, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*SignatureSubject))(in) + } + if err := Convert_v1_SignatureGenericEntity_To_api_SignatureGenericEntity(&in.SignatureGenericEntity, &out.SignatureGenericEntity, s); err != nil { + return err + } + out.PublicKeyID = in.PublicKeyID + return nil +} + +func Convert_v1_SignatureSubject_To_api_SignatureSubject(in *SignatureSubject, out *image_api.SignatureSubject, s conversion.Scope) error { + return autoConvert_v1_SignatureSubject_To_api_SignatureSubject(in, out, s) +} + +func autoConvert_api_SignatureSubject_To_v1_SignatureSubject(in *image_api.SignatureSubject, out *SignatureSubject, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*image_api.SignatureSubject))(in) + } + if err := Convert_api_SignatureGenericEntity_To_v1_SignatureGenericEntity(&in.SignatureGenericEntity, &out.SignatureGenericEntity, s); err != nil { + return err + } + out.PublicKeyID = in.PublicKeyID + return nil +} + +func Convert_api_SignatureSubject_To_v1_SignatureSubject(in *image_api.SignatureSubject, out *SignatureSubject, s conversion.Scope) error { + return autoConvert_api_SignatureSubject_To_v1_SignatureSubject(in, out, s) +} + func autoConvert_v1_TagEvent_To_api_TagEvent(in *TagEvent, out *image_api.TagEvent, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*TagEvent))(in) diff --git a/pkg/image/api/v1/deep_copy_generated.go b/pkg/image/api/v1/deep_copy_generated.go index 264cd5d95cdd..5e348918441e 100644 --- a/pkg/image/api/v1/deep_copy_generated.go +++ b/pkg/image/api/v1/deep_copy_generated.go @@ -20,6 +20,7 @@ func init() { DeepCopy_v1_ImageImportStatus, DeepCopy_v1_ImageLayer, DeepCopy_v1_ImageList, + DeepCopy_v1_ImageSignature, DeepCopy_v1_ImageStream, DeepCopy_v1_ImageStreamImage, DeepCopy_v1_ImageStreamImport, @@ -34,6 +35,10 @@ func init() { DeepCopy_v1_NamedTagEventList, DeepCopy_v1_RepositoryImportSpec, DeepCopy_v1_RepositoryImportStatus, + DeepCopy_v1_SignatureCondition, + DeepCopy_v1_SignatureGenericEntity, + DeepCopy_v1_SignatureIssuer, + DeepCopy_v1_SignatureSubject, DeepCopy_v1_TagEvent, DeepCopy_v1_TagEventCondition, DeepCopy_v1_TagImportPolicy, @@ -77,6 +82,17 @@ func DeepCopy_v1_Image(in Image, out *Image, c *conversion.Cloner) error { } else { out.DockerImageLayers = nil } + if in.Signatures != nil { + in, out := in.Signatures, &out.Signatures + *out = make([]ImageSignature, len(in)) + for i := range in { + if err := DeepCopy_v1_ImageSignature(in[i], &(*out)[i], c); err != nil { + return err + } + } + } else { + out.Signatures = nil + } return nil } @@ -144,6 +160,66 @@ func DeepCopy_v1_ImageList(in ImageList, out *ImageList, c *conversion.Cloner) e return nil } +func DeepCopy_v1_ImageSignature(in ImageSignature, out *ImageSignature, c *conversion.Cloner) error { + out.Type = in.Type + if in.Content != nil { + in, out := in.Content, &out.Content + *out = make([]byte, len(in)) + copy(*out, in) + } else { + out.Content = nil + } + if in.Conditions != nil { + in, out := in.Conditions, &out.Conditions + *out = make([]SignatureCondition, len(in)) + for i := range in { + if err := DeepCopy_v1_SignatureCondition(in[i], &(*out)[i], c); err != nil { + return err + } + } + } else { + out.Conditions = nil + } + out.ImageIdentity = in.ImageIdentity + if in.SignedClaims != nil { + in, out := in.SignedClaims, &out.SignedClaims + *out = make(map[string]string) + for key, val := range in { + (*out)[key] = val + } + } else { + out.SignedClaims = nil + } + if in.Created != nil { + in, out := in.Created, &out.Created + *out = new(unversioned.Time) + if err := unversioned.DeepCopy_unversioned_Time(*in, *out, c); err != nil { + return err + } + } else { + out.Created = nil + } + if in.IssuedBy != nil { + in, out := in.IssuedBy, &out.IssuedBy + *out = new(SignatureIssuer) + if err := DeepCopy_v1_SignatureIssuer(*in, *out, c); err != nil { + return err + } + } else { + out.IssuedBy = nil + } + if in.IssuedTo != nil { + in, out := in.IssuedTo, &out.IssuedTo + *out = new(SignatureSubject) + if err := DeepCopy_v1_SignatureSubject(*in, *out, c); err != nil { + return err + } + } else { + out.IssuedTo = nil + } + return nil +} + func DeepCopy_v1_ImageStream(in ImageStream, out *ImageStream, c *conversion.Cloner) error { if err := unversioned.DeepCopy_unversioned_TypeMeta(in.TypeMeta, &out.TypeMeta, c); err != nil { return err @@ -432,6 +508,41 @@ func DeepCopy_v1_RepositoryImportStatus(in RepositoryImportStatus, out *Reposito return nil } +func DeepCopy_v1_SignatureCondition(in SignatureCondition, out *SignatureCondition, c *conversion.Cloner) error { + out.Type = in.Type + out.Status = in.Status + if err := unversioned.DeepCopy_unversioned_Time(in.LastProbeTime, &out.LastProbeTime, c); err != nil { + return err + } + if err := unversioned.DeepCopy_unversioned_Time(in.LastTransitionTime, &out.LastTransitionTime, c); err != nil { + return err + } + out.Reason = in.Reason + out.Message = in.Message + return nil +} + +func DeepCopy_v1_SignatureGenericEntity(in SignatureGenericEntity, out *SignatureGenericEntity, c *conversion.Cloner) error { + out.Organization = in.Organization + out.CommonName = in.CommonName + return nil +} + +func DeepCopy_v1_SignatureIssuer(in SignatureIssuer, out *SignatureIssuer, c *conversion.Cloner) error { + if err := DeepCopy_v1_SignatureGenericEntity(in.SignatureGenericEntity, &out.SignatureGenericEntity, c); err != nil { + return err + } + return nil +} + +func DeepCopy_v1_SignatureSubject(in SignatureSubject, out *SignatureSubject, c *conversion.Cloner) error { + if err := DeepCopy_v1_SignatureGenericEntity(in.SignatureGenericEntity, &out.SignatureGenericEntity, c); err != nil { + return err + } + out.PublicKeyID = in.PublicKeyID + return nil +} + func DeepCopy_v1_TagEvent(in TagEvent, out *TagEvent, c *conversion.Cloner) error { if err := unversioned.DeepCopy_unversioned_Time(in.Created, &out.Created, c); err != nil { return err diff --git a/pkg/image/api/v1/swagger_doc.go b/pkg/image/api/v1/swagger_doc.go index 079ddb195e73..8998fed66f83 100644 --- a/pkg/image/api/v1/swagger_doc.go +++ b/pkg/image/api/v1/swagger_doc.go @@ -26,6 +26,7 @@ var map_Image = map[string]string{ "dockerImageMetadataVersion": "DockerImageMetadataVersion conveys the version of the object, which if empty defaults to \"1.0\"", "dockerImageManifest": "DockerImageManifest is the raw JSON of the manifest", "dockerImageLayers": "DockerImageLayers represents the layers in the image. May not be set if the image does not define that data.", + "signatures": "Signatures holds all signatures of the image.", } func (Image) SwaggerDoc() map[string]string { @@ -75,6 +76,22 @@ func (ImageList) SwaggerDoc() map[string]string { return map_ImageList } +var map_ImageSignature = map[string]string{ + "": "ImageSignature holds a signature of an image. It allows to verify image identity and possibly other claims as long as the signature is trusted. Based on this information it is possible to restrict runnable images to those matching cluster-wide policy. There are two mandatory fields provided by client: Type and Content. They should be parsed by clients doing image verification. The others are parsed from signature's content by the server. They serve just an informative purpose.", + "type": "Required: Describes a type of stored blob.", + "content": "Required: An opaque binary string which is an image's signature.", + "conditions": "Conditions represent the latest available observations of a signature's current state.", + "imageIdentity": "A human readable string representing image's identity. It could be a product name and version, or an image pull spec (e.g. \"registry.access.redhat.com/rhel7/rhel:7.2\").", + "signedClaims": "Contains claims from the signature.", + "created": "If specified, it is the time of signature's creation.", + "issuedBy": "If specified, it holds information about an issuer of signing certificate or key (a person or entity who signed the signing certificate or key).", + "issuedTo": "If specified, it holds information about a subject of signing certificate or key (a person or entity who signed the image).", +} + +func (ImageSignature) SwaggerDoc() map[string]string { + return map_ImageSignature +} + var map_ImageStream = map[string]string{ "": "ImageStream stores a mapping of tags to images, metadata overrides that are applied when images are tagged in a stream, and an optional reference to a Docker image repository on a registry.", "metadata": "Standard object's metadata.", @@ -226,6 +243,47 @@ func (RepositoryImportStatus) SwaggerDoc() map[string]string { return map_RepositoryImportStatus } +var map_SignatureCondition = map[string]string{ + "": "SignatureCondition describes an image signature condition of particular kind at particular probe time.", + "type": "Type of job condition, Complete or Failed.", + "status": "Status of the condition, one of True, False, Unknown.", + "lastProbeTime": "Last time the condition was checked.", + "lastTransitionTime": "Last time the condition transit from one status to another.", + "reason": "(brief) reason for the condition's last transition.", + "message": "Human readable message indicating details about last transition.", +} + +func (SignatureCondition) SwaggerDoc() map[string]string { + return map_SignatureCondition +} + +var map_SignatureGenericEntity = map[string]string{ + "": "SignatureGenericEntity holds a generic information about a person or entity who is an issuer or a subject of signing certificate or key.", + "organization": "Organization name.", + "commonName": "Common name (e.g. openshift-signing-service).", +} + +func (SignatureGenericEntity) SwaggerDoc() map[string]string { + return map_SignatureGenericEntity +} + +var map_SignatureIssuer = map[string]string{ + "": "SignatureIssuer holds information about an issuer of signing certificate or key.", +} + +func (SignatureIssuer) SwaggerDoc() map[string]string { + return map_SignatureIssuer +} + +var map_SignatureSubject = map[string]string{ + "": "SignatureSubject holds information about a person or entity who created the signature.", + "publicKeyID": "If present, it is a human readable key id of public key belonging to the subject used to verify image signature. It should contain at least 64 lowest bits of public key's fingerprint (e.g. 0x685ebe62bf278440).", +} + +func (SignatureSubject) SwaggerDoc() map[string]string { + return map_SignatureSubject +} + var map_TagEvent = map[string]string{ "": "TagEvent is used by ImageStreamStatus to keep a historical record of images associated with a tag.", "created": "Created holds the time the TagEvent was created", diff --git a/pkg/image/api/v1/types.go b/pkg/image/api/v1/types.go index a29c4561f327..f577b7cd7996 100644 --- a/pkg/image/api/v1/types.go +++ b/pkg/image/api/v1/types.go @@ -32,6 +32,8 @@ type Image struct { DockerImageManifest string `json:"dockerImageManifest,omitempty"` // DockerImageLayers represents the layers in the image. May not be set if the image does not define that data. DockerImageLayers []ImageLayer `json:"dockerImageLayers"` + // Signatures holds all signatures of the image. + Signatures []ImageSignature `json:"signatures,omitempty"` } // ImageLayer represents a single layer of the image. Some images may have multiple layers. Some may have none. @@ -42,6 +44,80 @@ type ImageLayer struct { Size int64 `json:"size"` } +// ImageSignature holds a signature of an image. It allows to verify image identity and possibly other claims +// as long as the signature is trusted. Based on this information it is possible to restrict runnable images +// to those matching cluster-wide policy. +// There are two mandatory fields provided by client: Type and Content. They should be parsed by clients doing +// image verification. The others are parsed from signature's content by the server. They serve just an +// informative purpose. +type ImageSignature struct { + // Required: Describes a type of stored blob. + Type string `json:"type"` + // Required: An opaque binary string which is an image's signature. + Content []byte `json:"content"` + // Conditions represent the latest available observations of a signature's current state. + Conditions []SignatureCondition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` + + // Following metadata fields will be set by server if the signature content is successfully parsed and + // the information available. + + // A human readable string representing image's identity. It could be a product name and version, or an + // image pull spec (e.g. "registry.access.redhat.com/rhel7/rhel:7.2"). + ImageIdentity string `json:"imageIdentity,omitempty"` + // Contains claims from the signature. + SignedClaims map[string]string `json:"signedClaims,omitempty"` + // If specified, it is the time of signature's creation. + Created *unversioned.Time `json:"created,omitempty"` + // If specified, it holds information about an issuer of signing certificate or key (a person or entity + // who signed the signing certificate or key). + IssuedBy *SignatureIssuer `json:"issuedBy,omitempty"` + // If specified, it holds information about a subject of signing certificate or key (a person or entity + // who signed the image). + IssuedTo *SignatureSubject `json:"issuedTo,omitempty"` +} + +/// SignatureConditionType is a type of image signature condition. +type SignatureConditionType string + +// SignatureCondition describes an image signature condition of particular kind at particular probe time. +type SignatureCondition struct { + // Type of job condition, Complete or Failed. + Type SignatureConditionType `json:"type"` + // Status of the condition, one of True, False, Unknown. + Status kapi.ConditionStatus `json:"status"` + // Last time the condition was checked. + LastProbeTime unversioned.Time `json:"lastProbeTime,omitempty"` + // Last time the condition transit from one status to another. + LastTransitionTime unversioned.Time `json:"lastTransitionTime,omitempty"` + // (brief) reason for the condition's last transition. + Reason string `json:"reason,omitempty"` + // Human readable message indicating details about last transition. + Message string `json:"message,omitempty"` +} + +// SignatureGenericEntity holds a generic information about a person or entity who is an issuer or a subject +// of signing certificate or key. +type SignatureGenericEntity struct { + // Organization name. + Organization string `json:"organization,omitempty"` + // Common name (e.g. openshift-signing-service). + CommonName string `json:"commonName,omitempty"` +} + +// SignatureIssuer holds information about an issuer of signing certificate or key. +type SignatureIssuer struct { + SignatureGenericEntity `json:",inline"` +} + +// SignatureSubject holds information about a person or entity who created the signature. +type SignatureSubject struct { + SignatureGenericEntity `json:",inline"` + // If present, it is a human readable key id of public key belonging to the subject used to verify image + // signature. It should contain at least 64 lowest bits of public key's fingerprint (e.g. + // 0x685ebe62bf278440). + PublicKeyID string `json:"publicKeyID"` +} + // ImageStreamList is a list of ImageStream objects. type ImageStreamList struct { unversioned.TypeMeta `json:",inline"` diff --git a/pkg/image/api/v1beta3/types.go b/pkg/image/api/v1beta3/types.go index 7bc48df18530..3d072c8b4c4b 100644 --- a/pkg/image/api/v1beta3/types.go +++ b/pkg/image/api/v1beta3/types.go @@ -29,6 +29,8 @@ type Image struct { DockerImageManifest string `json:"dockerImageManifest,omitempty"` // DockerImageLayers represents the layers in the image. May not be set if the image does not define that data. DockerImageLayers []ImageLayer `json:"dockerImageLayers"` + // Signatures holds all signatures of the image. + Signatures []ImageSignature } // ImageLayer represents a single layer of the image. Some images may have multiple layers. Some may have none. @@ -39,6 +41,80 @@ type ImageLayer struct { Size int64 `json:"size"` } +// ImageSignature holds a signature of an image. It allows to verify image identity and possibly other claims +// as long as the signature is trusted. Based on this information it is possible to restrict runnable images +// to those matching cluster-wide policy. +// There are two mandatory fields provided by client: Type and Content. They should be parsed by clients doing +// image verification. The others are parsed from signature's content by the server. They serve just an +// informative purpose. +type ImageSignature struct { + // Required: Describes a type of stored blob. + Type string `json:"type"` + // Required: An opaque binary string which is an image's signature. + Content []byte `json:"content"` + // Conditions represent the latest available observations of a signature's current state. + Conditions []SignatureCondition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` + + // Following metadata fields will be set by server if the signature content is successfully parsed and + // the information available. + + // A human readable string representing image's identity. It could be a product name and version, or an + // image pull spec (e.g. "registry.access.redhat.com/rhel7/rhel:7.2"). + ImageIdentity string `json:"imageIdentity,omitempty"` + // Contains claims from the signature. + SignedClaims map[string]string `json:"signedClaims,omitempty"` + // If specified, it is the time of signature's creation. + Created *unversioned.Time `json:"created,omitempty"` + // If specified, it holds information about an issuer of signing certificate or key (a person or entity + // who signed the signing certificate or key). + IssuedBy *SignatureIssuer `json:"issuedBy,omitempty"` + // If specified, it holds information about a subject of signing certificate or key (a person or entity + // who signed the image). + IssuedTo *SignatureSubject `json:"issuedTo,omitempty"` +} + +/// SignatureConditionType is a type of image signature condition. +type SignatureConditionType string + +// SignatureCondition describes an image signature condition of particular kind at particular probe time. +type SignatureCondition struct { + // Type of job condition, Complete or Failed. + Type SignatureConditionType `json:"type"` + // Status of the condition, one of True, False, Unknown. + Status kapi.ConditionStatus `json:"status"` + // Last time the condition was checked. + LastProbeTime unversioned.Time `json:"lastProbeTime,omitempty"` + // Last time the condition transit from one status to another. + LastTransitionTime unversioned.Time `json:"lastTransitionTime,omitempty"` + // (brief) reason for the condition's last transition. + Reason string `json:"reason,omitempty"` + // Human readable message indicating details about last transition. + Message string `json:"message,omitempty"` +} + +// SignatureGenericEntity holds a generic information about a person or entity who is an issuer or a subject +// of signing certificate or key. +type SignatureGenericEntity struct { + // Organization name. + Organization string `json:"organization,omitempty"` + // Common name (e.g. openshift-signing-service). + CommonName string `json:"commonName,omitempty"` +} + +// SignatureIssuer holds information about an issuer of signing certificate or key. +type SignatureIssuer struct { + SignatureGenericEntity `json:",inline"` +} + +// SignatureSubject holds information about a person or entity who created the signature. +type SignatureSubject struct { + SignatureGenericEntity `json:",inline"` + // If present, it is a human readable key id of public key belonging to the subject used to verify image + // signature. It should contain at least 64 lowest bits of public key's fingerprint (e.g. + // 0x685ebe62bf278440). + PublicKeyID string `json:"publicKeyID"` +} + // ImageStreamList is a list of ImageStream objects. type ImageStreamList struct { unversioned.TypeMeta `json:",inline"` diff --git a/pkg/image/api/validation/validation.go b/pkg/image/api/validation/validation.go index ae4987b7daea..6916e6b1863d 100644 --- a/pkg/image/api/validation/validation.go +++ b/pkg/image/api/validation/validation.go @@ -59,9 +59,59 @@ func validateImage(image *api.Image, fldPath *field.Path) field.ErrorList { } } + for i, sig := range image.Signatures { + result = append(result, validateImageSignature(&sig, fldPath.Child("signatures").Index(i))...) + } + return result } +func validateImageSignature(signature *api.ImageSignature, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if len(signature.Type) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("type"), "")) + } + if len(signature.Content) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("content"), "")) + } + + var trustedCondition, forImageCondition *api.SignatureCondition + for i := range signature.Conditions { + cond := &signature.Conditions[i] + if cond.Type == api.SignatureTrusted && (trustedCondition == nil || !cond.LastProbeTime.Before(trustedCondition.LastProbeTime)) { + trustedCondition = cond + } else if cond.Type == api.SignatureForImage && forImageCondition == nil || !cond.LastProbeTime.Before(forImageCondition.LastProbeTime) { + forImageCondition = cond + } + } + + if trustedCondition != nil && forImageCondition == nil { + msg := fmt.Sprintf("missing %q condition type", api.SignatureForImage) + allErrs = append(allErrs, field.Invalid(fldPath.Child("conditions"), signature.Conditions, msg)) + } else if forImageCondition != nil && trustedCondition == nil { + msg := fmt.Sprintf("missing %q condition type", api.SignatureTrusted) + allErrs = append(allErrs, field.Invalid(fldPath.Child("conditions"), signature.Conditions, msg)) + } + + if trustedCondition == nil || trustedCondition.Status == kapi.ConditionUnknown { + if len(signature.ImageIdentity) != 0 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("imageIdentity"), signature.ImageIdentity, "must be unset for unknown signature state")) + } + if len(signature.SignedClaims) != 0 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("signedClaims"), signature.SignedClaims, "must be unset for unknown signature state")) + } + if signature.IssuedBy != nil { + allErrs = append(allErrs, field.Invalid(fldPath.Child("issuedBy"), signature.IssuedBy, "must be unset for unknown signature state")) + } + if signature.IssuedTo != nil { + allErrs = append(allErrs, field.Invalid(fldPath.Child("issuedTo"), signature.IssuedTo, "must be unset for unknown signature state")) + } + } + + return allErrs +} + func ValidateImageUpdate(newImage, oldImage *api.Image) field.ErrorList { result := validation.ValidateObjectMetaUpdate(&newImage.ObjectMeta, &oldImage.ObjectMeta, field.NewPath("metadata")) result = append(result, ValidateImage(newImage)...) diff --git a/pkg/image/api/validation/validation_test.go b/pkg/image/api/validation/validation_test.go index b2cd3bc4422b..dc01d1caa82a 100644 --- a/pkg/image/api/validation/validation_test.go +++ b/pkg/image/api/validation/validation_test.go @@ -1,14 +1,16 @@ package validation import ( + "fmt" "reflect" "strings" "testing" - "github.com/openshift/origin/pkg/image/api" kapi "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/util/diff" "k8s.io/kubernetes/pkg/util/validation/field" + + "github.com/openshift/origin/pkg/image/api" ) func TestValidateImageOK(t *testing.T) { @@ -68,6 +70,149 @@ func TestValidateImageMissingFields(t *testing.T) { } } +func TestValidateImageSignature(t *testing.T) { + for _, tc := range []struct { + name string + signature api.ImageSignature + expected field.ErrorList + }{ + { + name: "valid", + signature: api.ImageSignature{ + Type: "valid", + Content: []byte("blob"), + }, + expected: field.ErrorList{}, + }, + + { + name: "valid trusted", + signature: api.ImageSignature{ + Type: "valid", + Content: []byte("blob"), + Conditions: []api.SignatureCondition{ + { + Type: api.SignatureTrusted, + Status: kapi.ConditionTrue, + }, + { + Type: api.SignatureForImage, + Status: kapi.ConditionTrue, + }, + }, + ImageIdentity: "registry.company.ltd/app/core:v1.2", + }, + expected: field.ErrorList{}, + }, + + { + name: "valid untrusted", + signature: api.ImageSignature{ + Type: "valid", + Content: []byte("blob"), + Conditions: []api.SignatureCondition{ + { + Type: api.SignatureTrusted, + Status: kapi.ConditionTrue, + }, + { + Type: api.SignatureForImage, + Status: kapi.ConditionFalse, + }, + // compare the latest condition + { + Type: api.SignatureTrusted, + Status: kapi.ConditionFalse, + }, + }, + ImageIdentity: "registry.company.ltd/app/core:v1.2", + }, + expected: field.ErrorList{}, + }, + + { + name: "missing type", + signature: api.ImageSignature{ + Content: []byte("blob"), + }, + expected: field.ErrorList{ + field.Required(field.NewPath("type"), ""), + }, + }, + + { + name: "missing content", + signature: api.ImageSignature{ + Type: "invalid", + }, + expected: field.ErrorList{ + field.Required(field.NewPath("content"), ""), + }, + }, + + { + name: "missing ForImage condition", + signature: api.ImageSignature{ + Type: "invalid", + Content: []byte("blob"), + Conditions: []api.SignatureCondition{ + { + Type: api.SignatureTrusted, + Status: kapi.ConditionTrue, + }, + }, + ImageIdentity: "registry.company.ltd/app/core:v1.2", + }, + expected: field.ErrorList{field.Invalid(field.NewPath("conditions"), + []api.SignatureCondition{ + { + Type: api.SignatureTrusted, + Status: kapi.ConditionTrue, + }, + }, + fmt.Sprintf("missing %q condition type", api.SignatureForImage))}, + }, + + { + name: "filled metadata for unknown signature state", + signature: api.ImageSignature{ + Type: "invalid", + Content: []byte("blob"), + Conditions: []api.SignatureCondition{ + { + Type: api.SignatureTrusted, + Status: kapi.ConditionUnknown, + }, + { + Type: api.SignatureForImage, + Status: kapi.ConditionUnknown, + }, + }, + ImageIdentity: "registry.company.ltd/app/core:v1.2", + SignedClaims: map[string]string{"claim": "value"}, + IssuedBy: &api.SignatureIssuer{ + SignatureGenericEntity: api.SignatureGenericEntity{Organization: "org"}, + }, + IssuedTo: &api.SignatureSubject{PublicKeyID: "id"}, + }, + expected: field.ErrorList{ + field.Invalid(field.NewPath("imageIdentity"), "registry.company.ltd/app/core:v1.2", "must be unset for unknown signature state"), + field.Invalid(field.NewPath("signedClaims"), map[string]string{"claim": "value"}, "must be unset for unknown signature state"), + field.Invalid(field.NewPath("issuedBy"), &api.SignatureIssuer{ + SignatureGenericEntity: api.SignatureGenericEntity{Organization: "org"}, + }, "must be unset for unknown signature state"), + field.Invalid(field.NewPath("issuedTo"), &api.SignatureSubject{PublicKeyID: "id"}, "must be unset for unknown signature state"), + }, + }, + } { + errs := validateImageSignature(&tc.signature, nil) + if e, a := tc.expected, errs; !reflect.DeepEqual(a, e) { + t.Errorf("[%s] unexpected errors: %s", tc.name, diff.ObjectDiff(e, a)) + } + } + +} + func TestValidateImageStreamMappingNotOK(t *testing.T) { errorCases := map[string]struct { I api.ImageStreamMapping diff --git a/pkg/image/registry/image/strategy.go b/pkg/image/registry/image/strategy.go index 2b4dd76db9c9..53153d68bc8b 100644 --- a/pkg/image/registry/image/strategy.go +++ b/pkg/image/registry/image/strategy.go @@ -32,12 +32,14 @@ func (imageStrategy) NamespaceScoped() bool { // PrepareForCreate clears fields that are not allowed to be set by end users on creation. // It extracts the latest information from the manifest (if available) and sets that onto the object. -func (imageStrategy) PrepareForCreate(obj runtime.Object) { +func (s imageStrategy) PrepareForCreate(obj runtime.Object) { newImage := obj.(*api.Image) // ignore errors, change in place if err := api.ImageWithMetadata(newImage); err != nil { utilruntime.HandleError(fmt.Errorf("Unable to update image metadata for %q: %v", newImage.Name, err)) } + + s.clearSignatureDetails(newImage) } // Validate validates a new image. @@ -72,6 +74,7 @@ func (imageStrategy) PrepareForUpdate(obj, old runtime.Object) { newImage.DockerImageMetadata = oldImage.DockerImageMetadata newImage.DockerImageMetadataVersion = oldImage.DockerImageMetadataVersion newImage.DockerImageLayers = oldImage.DockerImageLayers + newImage.Signatures = oldImage.Signatures // allow an image update that results in the manifest matching the digest (the name) newManifest := newImage.DockerImageManifest @@ -95,6 +98,20 @@ func (imageStrategy) ValidateUpdate(ctx kapi.Context, obj, old runtime.Object) f return validation.ValidateImageUpdate(old.(*api.Image), obj.(*api.Image)) } +// clearSignatureDetails removes signature details from all the signatures of given image. It also clear all +// the validation data. These data will be set by the server once the signature parsing support is added. +func (imageStrategy) clearSignatureDetails(image *api.Image) { + for i := range image.Signatures { + signature := &image.Signatures[i] + signature.Conditions = nil + signature.ImageIdentity = "" + signature.SignedClaims = nil + signature.Created = nil + signature.IssuedBy = nil + signature.IssuedTo = nil + } +} + // MatchImage returns a generic matcher for a given label and field selector. func MatchImage(label labels.Selector, field fields.Selector) generic.Matcher { return generic.MatcherFunc(func(obj runtime.Object) (bool, error) {