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

Normalisation and Signing of Component Descriptor #47

Merged
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
a06bf79
First implementation
enrico-kaack-comp Oct 20, 2021
ac96863
Feedbacj improvements
enrico-kaack-comp Oct 22, 2021
f9fc5f5
improvements syntax
enrico-kaack-comp Oct 25, 2021
946c8c3
hash function now exchangeable
enrico-kaack-comp Oct 26, 2021
6c0da39
add context to digestToComponentDescriptor resolvers
enrico-kaack-comp Oct 26, 2021
bc2cc68
add error to componentReference and resourceResolver callbacks
enrico-kaack-comp Oct 26, 2021
bdc91cf
normalisation type to normalisation version
enrico-kaack-comp Oct 27, 2021
3b38896
adopt example to modified spec
enrico-kaack-comp Oct 27, 2021
5c6836e
check public key to be a rsa public key
enrico-kaack-comp Nov 4, 2021
8aa01b9
removed unused hash algorithms and todos
enrico-kaack-comp Nov 4, 2021
67a43fc
omit signatures in json if empty
enrico-kaack-comp Nov 4, 2021
8a942c2
add tests
enrico-kaack-comp Nov 4, 2021
36b076e
improve example
enrico-kaack-comp Nov 4, 2021
a3c3fc2
Feedback PR Review
enrico-kaack-comp Nov 10, 2021
0e3c2bd
make format
enrico-kaack-comp Nov 10, 2021
d0799b8
deepSort fails on unknown type, so does normalising
enrico-kaack-comp Nov 10, 2021
b2d695a
change digest to pointer
enrico-kaack-comp Nov 10, 2021
507e1a6
make SelectSignatureByName public
enrico-kaack-comp Dec 9, 2021
9435823
update to latest spec changes
enrico-kaack-comp Dec 9, 2021
1b370ec
improved normalisation
enrico-kaack-comp Jan 20, 2022
1c6ec8c
Add Resource Digster interface and localOciBlobV1 type
enrico-kaack-comp Jan 24, 2022
5fa1b73
ignore digest for res.access.type == None
enrico-kaack-comp Feb 14, 2022
aa4954d
add s3 types
enrico-kaack-comp Feb 14, 2022
f31d5ff
allow missing res digests in normailsation
enrico-kaack-comp Feb 14, 2022
7b74c2a
hash algorithm to lowercase
enrico-kaack-comp Feb 22, 2022
5cce38c
update tests to current logic
enrico-kaack-comp Feb 22, 2022
8f95d2a
changed naming of normalisation algorithms
enrico-kaack-comp Feb 22, 2022
ee17899
addapt python and json schema to new signature properties
enrico-kaack-comp Feb 23, 2022
ef3ab49
fix updated python and json schema adoption
enrico-kaack-comp Feb 24, 2022
8137ce2
python improvement
enrico-kaack-comp Feb 24, 2022
4f8daa0
adapt tests to changed normalisation type
enrico-kaack-comp Feb 25, 2022
20aeac9
removed unused code
enrico-kaack-comp Feb 25, 2022
6ebc5b9
docstring
enrico-kaack-comp Feb 25, 2022
5206bc9
error handlingimprovement
enrico-kaack-comp Feb 25, 2022
e0968b5
typos
enrico-kaack-comp Feb 25, 2022
d3ec35e
introduced const for hash algorithm
enrico-kaack-comp Feb 25, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 acces.
const S3AccessType = "s3"

// S3AccessType describes a github repository 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"`
}

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

func (a S3Access) GetType() string {
return S3AccessType
}
46 changes: 46 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,39 @@ 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 {
enrico-kaack-comp marked this conversation as resolved.
Show resolved Hide resolved
Algorithm string `json:"algorithm"`
Value string `json:"value"`
}

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

const (
JsonNormalisationV1 NormalisationAlgorithm = "jsonNormalisationV1"
enrico-kaack-comp marked this conversation as resolved.
Show resolved Hide resolved
ManifestDigestV1 NormalisationAlgorithm = "manifestDigestV1"
LocalOciBlobDigestV1 NormalisationAlgorithm = "localOciBlobDigestV1"
GenericBlobDigestV1 NormalisationAlgorithm = "genericBlobDigestV1"
)

// 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"`
}
257 changes: 257 additions & 0 deletions bindings-go/apis/v2/signatures/normalize.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
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.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)

err := encoder.Encode(normalizedComponentDescriptor)
if 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
}

// isNormaliseable checks if componentReferences and resources contain digest
// Does NOT verify if the digests are correct
func isNormaliseable(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)
}
}

// check for digests on resources
for _, res := range cd.Resources {
//ignore access.type=None for normalisation and hash calculation
if res.Access.Type == "None" {
continue
}
if res.Digest == nil || res.Digest.HashAlgorithm == "" || res.Digest.NormalisationAlgorithm == "" || res.Digest.Value == "" {
return fmt.Errorf("missing digest in resource for %s:%s", res.Name, res.Version)
}
}
return nil
}

// 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