Skip to content
This repository has been archived by the owner on Feb 12, 2024. It is now read-only.

Commit

Permalink
merge Normalisation and Signing of Component Descriptor (#47)
Browse files Browse the repository at this point in the history
extend cd with digests and signatures field, normalisation, signer, verifier, rsa signer
  • Loading branch information
enrico-kaack-comp authored Feb 28, 2022
1 parent 4307809 commit 7eb2343
Show file tree
Hide file tree
Showing 13 changed files with 1,167 additions and 1 deletion.
28 changes: 28 additions & 0 deletions bindings-go/apis/v2/accesstypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,31 @@ func NewGitHubAccess(url, ref, commit string) *GitHubAccess {
func (a GitHubAccess) GetType() string {
return GitHubAccessType
}

// S3AccessType is the type of a s3 access.
const S3AccessType = "s3"

// S3AccessType describes a s3 resource access.
type S3Access struct {
ObjectType `json:",inline"`

// BucketName is the name of the s3 bucket.
BucketName string `json:"bucketName"`
// ObjectKey describes the referenced object.
ObjectKey string `json:"objectKey"`
}

// NewS3Access creates a new s3 accessor
func NewS3Access(bucketName, objectKey string) *S3Access {
return &S3Access{
ObjectType: ObjectType{
Type: S3AccessType,
},
BucketName: bucketName,
ObjectKey: objectKey,
}
}

func (a S3Access) GetType() string {
return S3AccessType
}
45 changes: 45 additions & 0 deletions bindings-go/apis/v2/componentdescriptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ type ComponentDescriptor struct {
Metadata Metadata `json:"meta"`
// Spec contains the specification of the component.
ComponentSpec `json:"component"`

// Signatures contains a list of signatures for the ComponentDescriptor
Signatures []Signature `json:"signatures,omitempty"`
}

// ComponentSpec defines a virtual component with
Expand Down Expand Up @@ -351,6 +354,10 @@ type SourceRef struct {
type Resource struct {
IdentityObjectMeta `json:",inline"`

// Digest is the optional digest of the referenced resource.
// +optional
Digest *DigestSpec `json:"digest,omitempty"`

// Relation describes the relation of the resource to the component.
// Can be a local or external resource
Relation ResourceRelation `json:"relation,omitempty"`
Expand All @@ -377,6 +384,9 @@ type ComponentReference struct {
// ExtraIdentity is the identity of an object.
// An additional label with key "name" ist not allowed
ExtraIdentity Identity `json:"extraIdentity,omitempty"`
// Digest is the optional digest of the referenced component.
// +optional
Digest *DigestSpec `json:"digest,omitempty"`
// Labels defines an optional set of additional labels
// describing the object.
// +optional
Expand Down Expand Up @@ -427,3 +437,38 @@ func (o *ComponentReference) GetIdentity() Identity {
func (o *ComponentReference) GetIdentityDigest() []byte {
return o.GetIdentity().Digest()
}

// DigestSpec defines the digest and algorithm.
// +k8s:deepcopy-gen=true
// +k8s:openapi-gen=true
type DigestSpec struct {
HashAlgorithm string `json:"hashAlgorithm"`
NormalisationAlgorithm string `json:"normalisationAlgorithm"`
Value string `json:"value"`
}

// SignatureSpec defines the signature and algorithm.
// +k8s:deepcopy-gen=true
// +k8s:openapi-gen=true
type SignatureSpec struct {
Algorithm string `json:"algorithm"`
Value string `json:"value"`
}

// NormalisationAlgorithm types and versions the algorithm used for digest generation.
type NormalisationAlgorithm string

const (
JsonNormalisationV1 NormalisationAlgorithm = "jsonNormalisation/V1"
ManifestDigestV1 NormalisationAlgorithm = "manifestDigest/V1"
GenericBlobDigestV1 NormalisationAlgorithm = "genericBlobDigest/V1"
)

// Signature defines a digest and corresponding signature, identifyable by name.
// +k8s:deepcopy-gen=true
// +k8s:openapi-gen=true
type Signature struct {
Name string `json:"name"`
Digest DigestSpec `json:"digest"`
Signature SignatureSpec `json:"signature"`
}
233 changes: 233 additions & 0 deletions bindings-go/apis/v2/signatures/normalize.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
package signatures

import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"fmt"
"sort"

v2 "github.com/gardener/component-spec/bindings-go/apis/v2"
)

// Entry is used for normalisation and has to contain one key
type Entry map[string]interface{}

// AddDigestsToComponentDescriptor adds digest to componentReferences and resources as returned in the resolver functions
func AddDigestsToComponentDescriptor(ctx context.Context, cd *v2.ComponentDescriptor,
compRefResolver func(context.Context, v2.ComponentDescriptor, v2.ComponentReference) (*v2.DigestSpec, error),
resResolver func(context.Context, v2.ComponentDescriptor, v2.Resource) (*v2.DigestSpec, error)) error {

for i, reference := range cd.ComponentReferences {
if reference.Digest == nil || reference.Digest.HashAlgorithm == "" || reference.Digest.NormalisationAlgorithm == "" || reference.Digest.Value == "" {
digest, err := compRefResolver(ctx, *cd, reference)
if err != nil {
return fmt.Errorf("failed resolving componentReference for %s:%s: %w", reference.Name, reference.Version, err)
}
cd.ComponentReferences[i].Digest = digest
}
}

for i, res := range cd.Resources {
if res.Digest == nil || res.Digest.HashAlgorithm == "" || res.Digest.NormalisationAlgorithm == "" || res.Digest.Value == "" {
digest, err := resResolver(ctx, *cd, res)
if err != nil {
return fmt.Errorf("failed resolving resource for %s:%s: %w", res.Name, res.Version, err)
}
cd.Resources[i].Digest = digest
}
}
return nil
}

// HashForComponentDescriptor return the hash for the component-descriptor, if it is normaliseable
// (= componentReferences and resources contain digest field)
func HashForComponentDescriptor(cd v2.ComponentDescriptor, hash Hasher) (*v2.DigestSpec, error) {
normalisedComponentDescriptor, err := normalizeComponentDescriptor(cd)
if err != nil {
return nil, fmt.Errorf("failed normalising component descriptor %w", err)
}
hash.HashFunction.Reset()
if _, err = hash.HashFunction.Write(normalisedComponentDescriptor); err != nil {
return nil, fmt.Errorf("failed hashing the normalisedComponentDescriptorJson: %w", err)
}
return &v2.DigestSpec{
HashAlgorithm: hash.AlgorithmName,
NormalisationAlgorithm: string(v2.JsonNormalisationV1),
Value: hex.EncodeToString(hash.HashFunction.Sum(nil)),
}, nil
}

func normalizeComponentDescriptor(cd v2.ComponentDescriptor) ([]byte, error) {
if err := isNormaliseableUnsafe(cd); err != nil {
return nil, fmt.Errorf("can not normalise component-descriptor %s:%s: %w", cd.Name, cd.Version, err)
}

meta := []Entry{
{"schemaVersion": cd.Metadata.Version},
}

componentReferences := []interface{}{}
for _, ref := range cd.ComponentSpec.ComponentReferences {
extraIdentity := buildExtraIdentity(ref.ExtraIdentity)

digest := []Entry{
{"hashAlgorithm": ref.Digest.HashAlgorithm},
{"normalisationAlgorithm": ref.Digest.NormalisationAlgorithm},
{"value": ref.Digest.Value},
}

componentReference := []Entry{
{"name": ref.Name},
{"version": ref.Version},
{"extraIdentity": extraIdentity},
{"digest": digest},
}
componentReferences = append(componentReferences, componentReference)
}

resources := []interface{}{}
for _, res := range cd.ComponentSpec.Resources {
extraIdentity := buildExtraIdentity(res.ExtraIdentity)

//ignore access.type=None for normalisation and hash calculation
if res.Access == nil || res.Access.Type == "None" {
resource := []Entry{
{"name": res.Name},
{"version": res.Version},
{"extraIdentity": extraIdentity},
}
resources = append(resources, resource)
continue
}

//ignore a resource without digests
if res.Digest == nil {
resource := []Entry{
{"name": res.Name},
{"version": res.Version},
{"extraIdentity": extraIdentity},
}
resources = append(resources, resource)
continue
}

digest := []Entry{
{"hashAlgorithm": res.Digest.HashAlgorithm},
{"normalisationAlgorithm": res.Digest.NormalisationAlgorithm},
{"value": res.Digest.Value},
}

resource := []Entry{
{"name": res.Name},
{"version": res.Version},
{"extraIdentity": extraIdentity},
{"digest": digest},
}
resources = append(resources, resource)
}

componentSpec := []Entry{
{"name": cd.ComponentSpec.Name},
{"version": cd.ComponentSpec.Version},
{"componentReferences": componentReferences},
{"resources": resources},
}

normalizedComponentDescriptor := []Entry{
{"meta": meta},
{"component": componentSpec},
}

if err := deepSort(normalizedComponentDescriptor); err != nil {
return nil, fmt.Errorf("failed sorting during normalisation: %w", err)
}

byteBuffer := bytes.NewBuffer([]byte{})
encoder := json.NewEncoder(byteBuffer)
encoder.SetEscapeHTML(false)

if err := encoder.Encode(normalizedComponentDescriptor); err != nil {
return nil, err
}

normalizedJson := byteBuffer.Bytes()

// encoder.Encode appends a newline that we do not want
if normalizedJson[len(normalizedJson)-1] == 10 {
normalizedJson = normalizedJson[:len(normalizedJson)-1]
}

return normalizedJson, nil
}

func buildExtraIdentity(identity v2.Identity) []Entry {
var extraIdentities []Entry
for k, v := range identity {
extraIdentities = append(extraIdentities, Entry{k: v})
}
return extraIdentities
}

// deepSort sorts Entry, []Enry and [][]Entry interfaces recursively, lexicographicly by key(Entry).
func deepSort(in interface{}) error {
switch castIn := in.(type) {
case []Entry:
// sort the values recursively for every entry
for _, entry := range castIn {
val := getOnlyValueInEntry(entry)
if err := deepSort(val); err != nil {
return err
}
}
// sort the entries based on the key
sort.SliceStable(castIn, func(i, j int) bool {
return getOnlyKeyInEntry(castIn[i]) < getOnlyKeyInEntry(castIn[j])
})
case Entry:
val := getOnlyValueInEntry(castIn)
if err := deepSort(val); err != nil {
return err
}
case []interface{}:
for _, v := range castIn {
if err := deepSort(v); err != nil {
return err
}
}
case string:
break
default:
return fmt.Errorf("unknown type in sorting. This should not happen")
}
return nil
}

func getOnlyKeyInEntry(entry Entry) string {
var key string
for k := range entry {
key = k
}
return key
}

func getOnlyValueInEntry(entry Entry) interface{} {
var value interface{}
for _, v := range entry {
value = v
}
return value
}

// isNormaliseableUnsafe checks if componentReferences contain digest. It does not check resources for containing digests.
// Does NOT verify if the digests are correct
func isNormaliseableUnsafe(cd v2.ComponentDescriptor) error {
// check for digests on component references
for _, reference := range cd.ComponentReferences {
if reference.Digest == nil || reference.Digest.HashAlgorithm == "" || reference.Digest.NormalisationAlgorithm == "" || reference.Digest.Value == "" {
return fmt.Errorf("missing digest in componentReference for %s:%s", reference.Name, reference.Version)
}
}
return nil
}
Loading

0 comments on commit 7eb2343

Please sign in to comment.