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 1 commit
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
232 changes: 232 additions & 0 deletions bindings-go/apis/v2/cdutils/normalize.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
package cdutils

import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"sort"

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

type Entry map[string]interface{}
enrico-kaack-comp marked this conversation as resolved.
Show resolved Hide resolved

func AddDigestsToComponentDescriptor(cd *v2.ComponentDescriptor, compRefResolver func(v2.ComponentDescriptor, v2.ComponentReference) v2.DigestSpec,
enrico-kaack-comp marked this conversation as resolved.
Show resolved Hide resolved
resResolver func(v2.ComponentDescriptor, v2.Resource) v2.DigestSpec) {

for i, reference := range cd.ComponentReferences {
if reference.Digest.Algorithm == "" || reference.Digest.Value == "" {
cd.ComponentReferences[i].Digest = compRefResolver(*cd, reference)
}
}

for i, res := range cd.Resources {
if res.Digest.Algorithm == "" || res.Digest.Value == "" {
cd.Resources[i].Digest = resResolver(*cd, res)
}
}
}

// HashForComponentDescriptor return the hash for the component-descriptor, if it is normaliseable
// (= componentReferences and resources contain digest field)
func HashForComponentDescriptor(cd v2.ComponentDescriptor) (string, error) {
normalisedComponentDescriptor, err := normalizeComponentDescriptor(cd)
if err != nil {
return "", fmt.Errorf("failed normalising component descriptor %w", err)
}
hash := sha256.Sum256(normalisedComponentDescriptor)
enrico-kaack-comp marked this conversation as resolved.
Show resolved Hide resolved

return hex.EncodeToString(hash[:]), nil
}

func SignComponentDescriptor(cd *v2.ComponentDescriptor, signer signatures.Signer) error {
enrico-kaack-comp marked this conversation as resolved.
Show resolved Hide resolved
hashCd, err := HashForComponentDescriptor(*cd)
if err != nil {
return fmt.Errorf("failed getting hash for cd: %w", err)
}
decodedHash, err := hex.DecodeString(hashCd)
if err != nil {
return fmt.Errorf("failed decoding hash to bytes")
}

signature, err := signer.Sign(decodedHash)
if err != nil {
return fmt.Errorf("failed signing hash of normalised component descriptor, %w", err)
}
cd.Signatures = append(cd.Signatures, v2.Signature{NormalisationType: v2.NormalisationTypeV1, Digest: v2.DigestSpec{
Algorithm: "sha256",
enrico-kaack-comp marked this conversation as resolved.
Show resolved Hide resolved
Value: hashCd,
},
Signature: *signature,
})
return nil
}

// VerifySignedComponentDescriptor verifies the signature and hash of the component-descriptor.
// Returns error if verification fails.
func VerifySignedComponentDescriptor(cd *v2.ComponentDescriptor, verifier signatures.Verifier) error {
//Verify hash with signature
err := verifier.Verify(cd.Signatures[0])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use named signatures

if err != nil {
return fmt.Errorf("failed verifying: %w", err)
}

//Verify normalised cd to given (and verified) hash
hashCd, err := HashForComponentDescriptor(*cd)
if err != nil {
return fmt.Errorf("failed getting hash for cd: %w", err)
}
if hashCd != cd.Signatures[0].Digest.Value {
return fmt.Errorf("normalised component-descriptor does not match signed hash")
}

return nil
}

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

var normalizedComponentDescriptor []Entry

meta := []Entry{
{"schemaVersion": cd.Metadata.Version},
}
normalizedComponentDescriptor = append(normalizedComponentDescriptor, Entry{"meta": meta})

componentReferences := [][]Entry{}
for _, ref := range cd.ComponentSpec.ComponentReferences {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
componentReferences := [][]Entry{}
for _, ref := range cd.ComponentSpec.ComponentReferences {
componentReferences := make([][]Entry, len(cd.ComponentSpec.ComponentReferences))
for i, ref := range cd.ComponentSpec.ComponentReferences {

extraIdentity := buildExtraIdentity(ref.ExtraIdentity)

digest := []Entry{
{"algorithm": ref.Digest.Algorithm},
{"value": ref.Digest.Value},
}

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

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

digest := []Entry{
{"algorithm": res.Digest.Algorithm},
{"value": res.Digest.Value},
}

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

sources := [][]Entry{}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we sign sources?

for _, src := range cd.ComponentSpec.Sources {
extraIdentity := buildExtraIdentity(src.ExtraIdentity)

source := []Entry{
{"name": src.Name},
{"version": src.Version},
{"extraIdentity": extraIdentity},
}
sources = append(sources, source)
}

var componentSpec []Entry

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use array

componentSpec = append(componentSpec, Entry{"name": cd.ComponentSpec.Name})
componentSpec = append(componentSpec, Entry{"version": cd.ComponentSpec.Version})
componentSpec = append(componentSpec, Entry{"componentReferences": componentReferences})
componentSpec = append(componentSpec, Entry{"resources": resources})
componentSpec = append(componentSpec, Entry{"sources": sources})

normalizedComponentDescriptor = append(normalizedComponentDescriptor, Entry{"component": componentSpec})
deepSort(normalizedComponentDescriptor)
normalizedString, err := json.Marshal(normalizedComponentDescriptor)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
normalizedString, err := json.Marshal(normalizedComponentDescriptor)
normalizedBytes, err := json.Marshal(normalizedComponentDescriptor)

if err != nil {
return nil, err
}

return normalizedString, 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{}) {
switch castIn := in.(type) {
case []Entry:
for _, entry := range castIn {
var val interface{}
for _, v := range entry {
val = v
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useless?

deepSort(val)

}
sort.SliceStable(castIn, func(i, j int) bool {
var keyI string
for k := range castIn[i] {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment that in maps this gets the first element (only element for maps with only 1 element)

keyI = k
}

var keyJ string
for k := range castIn[j] {
keyJ = k
}

return keyI < keyJ
})
case Entry:
var val interface{}
for _, v := range castIn {
val = v
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like general function. maybe also a method on the Entry struct would be suitable

deepSort(val)
case [][]Entry:
for _, v := range castIn {
deepSort(v)
}
case string:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should also handle nil here

break
default:
fmt.Println("unknow type")
}
}

// isNormaliseable checks if componentReferences and resources contain digest.
// Does NOT verify the digest is correct
func isNormaliseable(cd v2.ComponentDescriptor) error {
// check for digests on component references
for _, reference := range cd.ComponentReferences {
if reference.Digest.Algorithm == "" || 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 {
if res.Digest.Algorithm == "" || res.Digest.Value == "" {
return fmt.Errorf("missing digest in resource for %s:%s", res.Name, res.Version)
}
}
return nil
}
38 changes: 38 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"`
}

// 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 component.
enrico-kaack-comp marked this conversation as resolved.
Show resolved Hide resolved
// +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"`
enrico-kaack-comp marked this conversation as resolved.
Show resolved Hide resolved
// Labels defines an optional set of additional labels
// describing the object.
// +optional
Expand Down Expand Up @@ -427,3 +437,31 @@ 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 {
Algorithm string `json:"algorithm"`
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"`
Data string `json:"data"`
}
enrico-kaack-comp marked this conversation as resolved.
Show resolved Hide resolved

type NormalisationType string

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docstring missing


const (
NormalisationTypeV1 NormalisationType = "v1"
)

type Signature struct {
NormalisationType NormalisationType `json:"normalisationType"`
enrico-kaack-comp marked this conversation as resolved.
Show resolved Hide resolved
Digest DigestSpec `json:"digest"`
Signature SignatureSpec `json:"signature"`
enrico-kaack-comp marked this conversation as resolved.
Show resolved Hide resolved
}
86 changes: 86 additions & 0 deletions bindings-go/apis/v2/signatures/rsa.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package signatures

import (
"crypto"
"crypto/rsa"
"crypto/x509"
"encoding/hex"
"encoding/pem"
"fmt"
"io/ioutil"

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

type RsaSigner struct {
enrico-kaack-comp marked this conversation as resolved.
Show resolved Hide resolved
privateKey rsa.PrivateKey
}

func CreateRsaSignerFromKeyFile(pathToPrivateKey string) (*RsaSigner, error) {
enrico-kaack-comp marked this conversation as resolved.
Show resolved Hide resolved
privKeyFile, err := ioutil.ReadFile(pathToPrivateKey)
if err != nil {
return nil, fmt.Errorf("failed opening private key file %w", err)
}

block, _ := pem.Decode([]byte(privKeyFile))
if block == nil {
return nil, fmt.Errorf("failed decoding PEM formatted block in key %w", err)
}
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed parsing key %w", err)
}
return &RsaSigner{
privateKey: *key,
}, nil
}

func (s RsaSigner) Sign(data []byte) (*v2.SignatureSpec, error) {
signature, err := rsa.SignPKCS1v15(nil, &s.privateKey, crypto.SHA256, data)
if err != nil {
return nil, fmt.Errorf("failed signing hash, %w", err)
}
return &v2.SignatureSpec{
Algorithm: "RSASSA-PKCS1-V1_5-SIGN", //TODO: check
Data: hex.EncodeToString(signature),
}, nil
}

type RsaVerifier struct {
enrico-kaack-comp marked this conversation as resolved.
Show resolved Hide resolved
publicKey rsa.PublicKey
schrodit marked this conversation as resolved.
Show resolved Hide resolved
}

func CreateRsaVerifierFromKeyFile(pathToPublicKey string) (*RsaVerifier, error) {
publicKey, err := ioutil.ReadFile(pathToPublicKey)
if err != nil {
return nil, fmt.Errorf("failed opening public key file %w", err)
}
block, _ := pem.Decode([]byte(publicKey))
if block == nil {
return nil, fmt.Errorf("failed decoding PEM formatted block in key %w", err)
}
untypedKey, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed parsing key %w", err)
}
key := untypedKey.(*rsa.PublicKey)
return &RsaVerifier{
publicKey: *key,
}, nil
}

func (v RsaVerifier) Verify(signature v2.Signature) error {
decodedHash, err := hex.DecodeString(signature.Digest.Value)
if err != nil {
return fmt.Errorf("failed decoding hash %s: %w", signature.Digest.Value, err)
}
decodedSignature, err := hex.DecodeString(signature.Signature.Data)
if err != nil {
return fmt.Errorf("failed decoding hash %s: %w", signature.Digest.Value, err)
}
err = rsa.VerifyPKCS1v15(&v.publicKey, crypto.SHA256, decodedHash, decodedSignature)
if err != nil {
return fmt.Errorf("signature verification failed, %w", err)
}
return nil
}
11 changes: 11 additions & 0 deletions bindings-go/apis/v2/signatures/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package signatures

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

type Signer interface {
Sign(data []byte) (*v2.SignatureSpec, error)
}

type Verifier interface {
Verify(signature v2.Signature) error
}
enrico-kaack-comp marked this conversation as resolved.
Show resolved Hide resolved
Loading