From aa5d132c711f494a083509ac941495ae4c9cf71c Mon Sep 17 00:00:00 2001 From: Bob Callaway <bobcallaway@users.noreply.github.com> Date: Fri, 8 Apr 2022 13:02:03 -0400 Subject: [PATCH] Add the SHA256 digest of the intoto payload into the rekor entry (#764) * include hash of attestation in rekor entry Signed-off-by: Bob Callaway <bcallaway@google.com> * compute sha off of decoded attestation Signed-off-by: Bob Callaway <bcallaway@google.com> * change name to reflect DSSE terminology Signed-off-by: Bob Callaway <bcallaway@google.com> --- pkg/api/entries.go | 1 + pkg/generated/models/intoto_v001_schema.go | 156 ++++++++++++++++++ pkg/types/intoto/v0.0.1/entry.go | 20 ++- .../intoto/v0.0.1/intoto_v0_0_1_schema.json | 20 +++ tests/e2e_test.go | 22 ++- 5 files changed, 215 insertions(+), 4 deletions(-) diff --git a/pkg/api/entries.go b/pkg/api/entries.go index d88770f..a81da00 100644 --- a/pkg/api/entries.go +++ b/pkg/api/entries.go @@ -221,6 +221,7 @@ func createLogEntry(params entries.CreateLogEntryParams) (models.LogEntry, middl log.RequestIDLogger(params.HTTPRequest).Infof("no attestation for %s", uuid) return } + // TODO stop using uuid and use attestation hash if err := storeAttestation(context.Background(), uuid, attestation); err != nil { log.RequestIDLogger(params.HTTPRequest).Errorf("error storing attestation: %s", err) } diff --git a/pkg/generated/models/intoto_v001_schema.go b/pkg/generated/models/intoto_v001_schema.go index 58fc5fd..e43e699 100644 --- a/pkg/generated/models/intoto_v001_schema.go +++ b/pkg/generated/models/intoto_v001_schema.go @@ -153,6 +153,9 @@ type IntotoV001SchemaContent struct { // hash Hash *IntotoV001SchemaContentHash `json:"hash,omitempty"` + + // payload hash + PayloadHash *IntotoV001SchemaContentPayloadHash `json:"payloadHash,omitempty"` } // Validate validates this intoto v001 schema content @@ -163,6 +166,10 @@ func (m *IntotoV001SchemaContent) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validatePayloadHash(formats); err != nil { + res = append(res, err) + } + if len(res) > 0 { return errors.CompositeValidationError(res...) } @@ -188,6 +195,25 @@ func (m *IntotoV001SchemaContent) validateHash(formats strfmt.Registry) error { return nil } +func (m *IntotoV001SchemaContent) validatePayloadHash(formats strfmt.Registry) error { + if swag.IsZero(m.PayloadHash) { // not required + return nil + } + + if m.PayloadHash != nil { + if err := m.PayloadHash.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("content" + "." + "payloadHash") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("content" + "." + "payloadHash") + } + return err + } + } + + return nil +} + // ContextValidate validate this intoto v001 schema content based on the context it is used func (m *IntotoV001SchemaContent) ContextValidate(ctx context.Context, formats strfmt.Registry) error { var res []error @@ -196,6 +222,10 @@ func (m *IntotoV001SchemaContent) ContextValidate(ctx context.Context, formats s res = append(res, err) } + if err := m.contextValidatePayloadHash(ctx, formats); err != nil { + res = append(res, err) + } + if len(res) > 0 { return errors.CompositeValidationError(res...) } @@ -218,6 +248,22 @@ func (m *IntotoV001SchemaContent) contextValidateHash(ctx context.Context, forma return nil } +func (m *IntotoV001SchemaContent) contextValidatePayloadHash(ctx context.Context, formats strfmt.Registry) error { + + if m.PayloadHash != nil { + if err := m.PayloadHash.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("content" + "." + "payloadHash") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("content" + "." + "payloadHash") + } + return err + } + } + + return nil +} + // MarshalBinary interface implementation func (m *IntotoV001SchemaContent) MarshalBinary() ([]byte, error) { if m == nil { @@ -345,3 +391,113 @@ func (m *IntotoV001SchemaContentHash) UnmarshalBinary(b []byte) error { *m = res return nil } + +// IntotoV001SchemaContentPayloadHash Specifies the hash algorithm and value covering the payload within the DSSE envelope +// +// swagger:model IntotoV001SchemaContentPayloadHash +type IntotoV001SchemaContentPayloadHash struct { + + // The hashing function used to compute the hash value + // Required: true + // Enum: [sha256] + Algorithm *string `json:"algorithm"` + + // The hash value for the envelope's payload + // Required: true + Value *string `json:"value"` +} + +// Validate validates this intoto v001 schema content payload hash +func (m *IntotoV001SchemaContentPayloadHash) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateAlgorithm(formats); err != nil { + res = append(res, err) + } + + if err := m.validateValue(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +var intotoV001SchemaContentPayloadHashTypeAlgorithmPropEnum []interface{} + +func init() { + var res []string + if err := json.Unmarshal([]byte(`["sha256"]`), &res); err != nil { + panic(err) + } + for _, v := range res { + intotoV001SchemaContentPayloadHashTypeAlgorithmPropEnum = append(intotoV001SchemaContentPayloadHashTypeAlgorithmPropEnum, v) + } +} + +const ( + + // IntotoV001SchemaContentPayloadHashAlgorithmSha256 captures enum value "sha256" + IntotoV001SchemaContentPayloadHashAlgorithmSha256 string = "sha256" +) + +// prop value enum +func (m *IntotoV001SchemaContentPayloadHash) validateAlgorithmEnum(path, location string, value string) error { + if err := validate.EnumCase(path, location, value, intotoV001SchemaContentPayloadHashTypeAlgorithmPropEnum, true); err != nil { + return err + } + return nil +} + +func (m *IntotoV001SchemaContentPayloadHash) validateAlgorithm(formats strfmt.Registry) error { + + if err := validate.Required("content"+"."+"payloadHash"+"."+"algorithm", "body", m.Algorithm); err != nil { + return err + } + + // value enum + if err := m.validateAlgorithmEnum("content"+"."+"payloadHash"+"."+"algorithm", "body", *m.Algorithm); err != nil { + return err + } + + return nil +} + +func (m *IntotoV001SchemaContentPayloadHash) validateValue(formats strfmt.Registry) error { + + if err := validate.Required("content"+"."+"payloadHash"+"."+"value", "body", m.Value); err != nil { + return err + } + + return nil +} + +// ContextValidate validate this intoto v001 schema content payload hash based on the context it is used +func (m *IntotoV001SchemaContentPayloadHash) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// MarshalBinary interface implementation +func (m *IntotoV001SchemaContentPayloadHash) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *IntotoV001SchemaContentPayloadHash) UnmarshalBinary(b []byte) error { + var res IntotoV001SchemaContentPayloadHash + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/pkg/types/intoto/v0.0.1/entry.go b/pkg/types/intoto/v0.0.1/entry.go index 66cc065..fb4f230 100644 --- a/pkg/types/intoto/v0.0.1/entry.go +++ b/pkg/types/intoto/v0.0.1/entry.go @@ -23,7 +23,6 @@ import ( "encoding/base64" "encoding/hex" "encoding/json" - "errors" "fmt" "io/ioutil" "path/filepath" @@ -35,6 +34,8 @@ import ( "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" + "github.com/pkg/errors" + "github.com/sigstore/rekor/pkg/generated/models" "github.com/sigstore/rekor/pkg/log" "github.com/sigstore/rekor/pkg/pki" @@ -150,6 +151,18 @@ func (v *V001Entry) Canonicalize(ctx context.Context) ([]byte, error) { }, }, } + attestation := v.Attestation() + if attestation != nil { + decodedAttestation, err := base64.StdEncoding.DecodeString(string(attestation)) + if err != nil { + return nil, errors.Wrap(err, "decoding attestation") + } + attH := sha256.Sum256(decodedAttestation) + canonicalEntry.Content.PayloadHash = &models.IntotoV001SchemaContentPayloadHash{ + Algorithm: swag.String(models.IntotoV001SchemaContentHashAlgorithmSha256), + Value: swag.String(hex.EncodeToString(attH[:])), + } + } itObj := models.Intoto{} itObj.APIVersion = swag.String(APIVERSION) @@ -194,8 +207,9 @@ func (v *V001Entry) validate() error { } func (v *V001Entry) Attestation() []byte { - if len(v.env.Payload) > viper.GetInt("max_attestation_size") { - log.Logger.Infof("Skipping attestation storage, size %d is greater than max %d", len(v.env.Payload), viper.GetInt("max_attestation_size")) + storageSize := base64.StdEncoding.DecodedLen(len(v.env.Payload)) + if storageSize > viper.GetInt("max_attestation_size") { + log.Logger.Infof("Skipping attestation storage, size %d is greater than max %d", storageSize, viper.GetInt("max_attestation_size")) return nil } return []byte(v.env.Payload) diff --git a/pkg/types/intoto/v0.0.1/intoto_v0_0_1_schema.json b/pkg/types/intoto/v0.0.1/intoto_v0_0_1_schema.json index a8e8c05..39117a6 100644 --- a/pkg/types/intoto/v0.0.1/intoto_v0_0_1_schema.json +++ b/pkg/types/intoto/v0.0.1/intoto_v0_0_1_schema.json @@ -34,6 +34,26 @@ "value" ], "readOnly": true + }, + "payloadHash": { + "description": "Specifies the hash algorithm and value covering the payload within the DSSE envelope", + "type": "object", + "properties": { + "algorithm": { + "description": "The hashing function used to compute the hash value", + "type": "string", + "enum": [ "sha256" ] + }, + "value": { + "description": "The hash value for the envelope's payload", + "type": "string" + } + }, + "required": [ + "algorithm", + "value" + ], + "readOnly": true } } }, diff --git a/tests/e2e_test.go b/tests/e2e_test.go index c96aa9d..2f43e49 100644 --- a/tests/e2e_test.go +++ b/tests/e2e_test.go @@ -56,6 +56,7 @@ import ( "github.com/sigstore/rekor/pkg/generated/models" "github.com/sigstore/rekor/pkg/sharding" "github.com/sigstore/rekor/pkg/signer" + "github.com/sigstore/rekor/pkg/types" rekord "github.com/sigstore/rekor/pkg/types/rekord/v0.0.1" "github.com/sigstore/rekor/pkg/util" "github.com/sigstore/sigstore/pkg/cryptoutils" @@ -449,7 +450,7 @@ func TestIntoto(t *testing.T) { if err := json.Unmarshal([]byte(out), &g); err != nil { t.Fatal(err) } - // The atteestation should be stored at /var/run/attestations/$uuid + // The attestation should be stored at /var/run/attestations/$uuid got := in_toto.ProvenanceStatement{} if err := json.Unmarshal(g.Attestation, &got); err != nil { @@ -459,6 +460,25 @@ func TestIntoto(t *testing.T) { t.Errorf("diff: %s", diff) } + attHash := sha256.Sum256(g.Attestation) + + intotoModel := &models.IntotoV001Schema{} + if err := types.DecodeEntry(g.Body.(map[string]interface{})["IntotoObj"], intotoModel); err != nil { + t.Errorf("could not convert body into intoto type: %v", err) + } + if intotoModel.Content == nil || intotoModel.Content.PayloadHash == nil { + t.Errorf("could not find hash over attestation %v", intotoModel) + } + recordedPayloadHash, err := hex.DecodeString(*intotoModel.Content.PayloadHash.Value) + if err != nil { + t.Errorf("error converting attestation hash to []byte: %v", err) + } + + if !bytes.Equal(attHash[:], recordedPayloadHash) { + t.Fatal(fmt.Errorf("attestation hash %v doesnt match the payload we sent %v", hex.EncodeToString(attHash[:]), + *intotoModel.Content.PayloadHash.Value)) + } + out = runCli(t, "upload", "--artifact", attestationPath, "--type", "intoto", "--public-key", pubKeyPath) outputContains(t, out, "Entry already exists") -- GitLab