From 7e1b9d7ec78f5b203e3610a9556b3ed5d2a7e0b8 Mon Sep 17 00:00:00 2001 From: Marcela Melara Date: Fri, 1 Mar 2024 14:45:28 -0800 Subject: [PATCH 1/5] Add DigestSet hex encoding validation Signed-off-by: Marcela Melara --- go/v1/resource_descriptor.go | 22 ++++++++++++++++++++-- go/v1/resource_descriptor_test.go | 12 ++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/go/v1/resource_descriptor.go b/go/v1/resource_descriptor.go index 5ebeea35..377c1bb8 100644 --- a/go/v1/resource_descriptor.go +++ b/go/v1/resource_descriptor.go @@ -4,9 +4,15 @@ Wrapper APIs for in-toto attestation ResourceDescriptor protos. package v1 -import "errors" +import ( + "encoding/hex" + "errors" +) -var ErrRDRequiredField = errors.New("at least one of name, URI, or digest are required") +var ( + ErrInvalidDigestEncoding = errors.New("digest is not valid hex-encoded string") + ErrRDRequiredField = errors.New("at least one of name, URI, or digest are required") +) func (d *ResourceDescriptor) Validate() error { // at least one of name, URI or digest are required @@ -14,5 +20,17 @@ func (d *ResourceDescriptor) Validate() error { return ErrRDRequiredField } + // the in-toto spec expects a hex-encoded string in DigestSets + // https://github.com/in-toto/attestation/blob/main/spec/v1/digest_set.md + if len(d.GetDigest()) > 0 { + for _, digest := range d.GetDigest() { + _, err := hex.DecodeString(digest) + + if err != nil { + return ErrInvalidDigestEncoding + } + } + } + return nil } diff --git a/go/v1/resource_descriptor_test.go b/go/v1/resource_descriptor_test.go index 7c7eb240..ba3f9cf2 100644 --- a/go/v1/resource_descriptor_test.go +++ b/go/v1/resource_descriptor_test.go @@ -17,6 +17,8 @@ const wantFullRd = `{"name":"theName","uri":"https://example.com","digest":{"alg const badRd = `{"downloadLocation":"https://example.com/test.zip","mediaType":"theMediaType"}` +const badRdDigest = `{"digest":{"alg1":"badDigest"},"downloadLocation":"https://example.com/test.zip","mediaType":"theMediaType"}` + func createTestResourceDescriptor() (*ResourceDescriptor, error) { // Create a ResourceDescriptor a, err := structpb.NewStruct(map[string]interface{}{ @@ -65,3 +67,13 @@ func TestBadResourceDescriptor(t *testing.T) { err = got.Validate() assert.ErrorIs(t, err, ErrRDRequiredField, "created malformed ResourceDescriptor") } + +func TestBadResourceDescriptorDigest(t *testing.T) { + got := &ResourceDescriptor{} + err := protojson.Unmarshal([]byte(badRdDigest), got) + + assert.NoError(t, err, "Error during JSON unmarshalling") + + err = got.Validate() + assert.ErrorIs(t, err, ErrInvalidDigestEncoding, "created ResourceDescriptor with invalid digest encoding") +} From a3e6e7b22ba02f2f2eae5c63f928729294930236 Mon Sep 17 00:00:00 2001 From: Marcela Melara Date: Thu, 4 Apr 2024 14:00:22 -0700 Subject: [PATCH 2/5] Check encoding and length for supported hash algorithms Signed-off-by: Marcela Melara --- go/v1/resource_descriptor.go | 34 +++++++++++++++++++++++++------ go/v1/resource_descriptor_test.go | 18 +++++++++++++--- 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/go/v1/resource_descriptor.go b/go/v1/resource_descriptor.go index 377c1bb8..c91cfd6d 100644 --- a/go/v1/resource_descriptor.go +++ b/go/v1/resource_descriptor.go @@ -5,29 +5,51 @@ Wrapper APIs for in-toto attestation ResourceDescriptor protos. package v1 import ( + "crypto/md5" + "crypto/sha1" + "crypto/sha512" "encoding/hex" "errors" ) var ( + ErrIncorrectDigestLength = errors.New("digest is not correct length") ErrInvalidDigestEncoding = errors.New("digest is not valid hex-encoded string") ErrRDRequiredField = errors.New("at least one of name, URI, or digest are required") ) +// Supported standard hash algorithms +func isSupportedAlgorithm(alg string) (bool, int) { + algos := map[string]int{"md5": md5.Size, "sha1": sha1.Size, "shake128": md5.Size, "sha224": sha512.Size224, "sha3_224": sha512.Size224, "sha512_224": sha512.Size224, "sha256": sha512.Size256, "sha3_256": sha512.Size256, "sha512_256": sha512.Size256, "shake256": sha512.Size256, "sha384": sha512.Size384, "sha3_384": sha512.Size384, "sha512_384": sha512.Size384, "sha512": sha512.Size, "sha3_512": sha512.Size, "dirHash": sha512.Size256, "gitCommit": sha1.Size} + + size, ok := algos[alg] + return ok, size +} + func (d *ResourceDescriptor) Validate() error { // at least one of name, URI or digest are required if d.GetName() == "" && d.GetUri() == "" && len(d.GetDigest()) == 0 { return ErrRDRequiredField } - // the in-toto spec expects a hex-encoded string in DigestSets - // https://github.com/in-toto/attestation/blob/main/spec/v1/digest_set.md if len(d.GetDigest()) > 0 { - for _, digest := range d.GetDigest() { - _, err := hex.DecodeString(digest) + for alg, digest := range d.GetDigest() { + + // check encoding and length for supported algorithms + supported, size := isSupportedAlgorithm(alg) + if supported { + // the in-toto spec expects a hex-encoded string in DigestSets for supported algorithms + // https://github.com/in-toto/attestation/blob/main/spec/v1/digest_set.md + hashBytes, err := hex.DecodeString(digest) + + if err != nil { + return ErrInvalidDigestEncoding + } - if err != nil { - return ErrInvalidDigestEncoding + // check the length of the digest + if len(hashBytes) != size { + return ErrIncorrectDigestLength + } } } } diff --git a/go/v1/resource_descriptor_test.go b/go/v1/resource_descriptor_test.go index ba3f9cf2..16cf3a99 100644 --- a/go/v1/resource_descriptor_test.go +++ b/go/v1/resource_descriptor_test.go @@ -17,7 +17,9 @@ const wantFullRd = `{"name":"theName","uri":"https://example.com","digest":{"alg const badRd = `{"downloadLocation":"https://example.com/test.zip","mediaType":"theMediaType"}` -const badRdDigest = `{"digest":{"alg1":"badDigest"},"downloadLocation":"https://example.com/test.zip","mediaType":"theMediaType"}` +const badRdDigestEncoding = `{"digest":{"sha256":"badDigest"},"downloadLocation":"https://example.com/test.zip","mediaType":"theMediaType"}` + +const badRdDigestLength = `{"digest":{"sha256":"abc123"},"downloadLocation":"https://example.com/test.zip","mediaType":"theMediaType"}` func createTestResourceDescriptor() (*ResourceDescriptor, error) { // Create a ResourceDescriptor @@ -68,12 +70,22 @@ func TestBadResourceDescriptor(t *testing.T) { assert.ErrorIs(t, err, ErrRDRequiredField, "created malformed ResourceDescriptor") } -func TestBadResourceDescriptorDigest(t *testing.T) { +func TestBadResourceDescriptorDigestEncoding(t *testing.T) { got := &ResourceDescriptor{} - err := protojson.Unmarshal([]byte(badRdDigest), got) + err := protojson.Unmarshal([]byte(badRdDigestEncoding), got) assert.NoError(t, err, "Error during JSON unmarshalling") err = got.Validate() assert.ErrorIs(t, err, ErrInvalidDigestEncoding, "created ResourceDescriptor with invalid digest encoding") } + +func TestBadResourceDescriptorDigestLength(t *testing.T) { + got := &ResourceDescriptor{} + err := protojson.Unmarshal([]byte(badRdDigestLength), got) + + assert.NoError(t, err, "Error during JSON unmarshalling") + + err = got.Validate() + assert.ErrorIs(t, err, ErrIncorrectDigestLength, "created ResourceDescriptor with incorrect digest length") +} From b8f895b2f7133abe1f2c9721151b6bf9d9c5171c Mon Sep 17 00:00:00 2001 From: Marcela Melara Date: Thu, 4 Apr 2024 16:19:11 -0700 Subject: [PATCH 3/5] Add comment about custom digest algorithm validation and valid digest test case Signed-off-by: Marcela Melara --- go/v1/resource_descriptor.go | 5 +++-- go/v1/resource_descriptor_test.go | 12 ++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/go/v1/resource_descriptor.go b/go/v1/resource_descriptor.go index c91cfd6d..8d23c7cf 100644 --- a/go/v1/resource_descriptor.go +++ b/go/v1/resource_descriptor.go @@ -35,11 +35,12 @@ func (d *ResourceDescriptor) Validate() error { if len(d.GetDigest()) > 0 { for alg, digest := range d.GetDigest() { - // check encoding and length for supported algorithms + // Per https://github.com/in-toto/attestation/blob/main/spec/v1/digest_set.md + // check encoding and length for supported algorithms; + // use of custom, unsupported algorithms is allowed and does not not generate validation errors. supported, size := isSupportedAlgorithm(alg) if supported { // the in-toto spec expects a hex-encoded string in DigestSets for supported algorithms - // https://github.com/in-toto/attestation/blob/main/spec/v1/digest_set.md hashBytes, err := hex.DecodeString(digest) if err != nil { diff --git a/go/v1/resource_descriptor_test.go b/go/v1/resource_descriptor_test.go index 16cf3a99..48410830 100644 --- a/go/v1/resource_descriptor_test.go +++ b/go/v1/resource_descriptor_test.go @@ -15,6 +15,8 @@ import ( const wantFullRd = `{"name":"theName","uri":"https://example.com","digest":{"alg1":"abc123"},"content":"Ynl0ZXNjb250ZW50","downloadLocation":"https://example.com/test.zip","mediaType":"theMediaType","annotations":{"a1":{"keyNum": 13,"keyStr":"value1"},"a2":{"keyObj":{"subKey":"subVal"}}}}` +const supportedRdDigest = `{"digest":{"sha256":"a1234567b1234567c1234567d1234567e1234567f1234567a1234567b1234567","custom":"myCustomEnvoding","sha1":"a1234567b1234567c1234567d1234567e1234567"}}` + const badRd = `{"downloadLocation":"https://example.com/test.zip","mediaType":"theMediaType"}` const badRdDigestEncoding = `{"digest":{"sha256":"badDigest"},"downloadLocation":"https://example.com/test.zip","mediaType":"theMediaType"}` @@ -60,6 +62,16 @@ func TestJsonUnmarshalResourceDescriptor(t *testing.T) { assert.True(t, proto.Equal(got, want), "Protos do not match") } +func TestSupportedResourceDescriptorDigest(t *testing.T) { + got := &ResourceDescriptor{} + err := protojson.Unmarshal([]byte(supportedRdDigest), got) + + assert.NoError(t, err, "Error during JSON unmarshalling") + + err = got.Validate() + assert.NoError(t, err, "Error during validation of valid supported RD digests") +} + func TestBadResourceDescriptor(t *testing.T) { got := &ResourceDescriptor{} err := protojson.Unmarshal([]byte(badRd), got) From 9fd2ef13fd18658cebd698ec50b343d022405423 Mon Sep 17 00:00:00 2001 From: Marcela Melara Date: Fri, 5 Apr 2024 15:09:41 -0700 Subject: [PATCH 4/5] Improve support hash algorithm documentation, remove support for extendable-output functions (shake), parameterize error outputs by algorithm/size Signed-off-by: Marcela Melara --- go/v1/resource_descriptor.go | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/go/v1/resource_descriptor.go b/go/v1/resource_descriptor.go index 8d23c7cf..e4851f09 100644 --- a/go/v1/resource_descriptor.go +++ b/go/v1/resource_descriptor.go @@ -5,22 +5,24 @@ Wrapper APIs for in-toto attestation ResourceDescriptor protos. package v1 import ( - "crypto/md5" - "crypto/sha1" - "crypto/sha512" "encoding/hex" "errors" + "fmt" ) var ( - ErrIncorrectDigestLength = errors.New("digest is not correct length") + ErrIncorrectDigestLength = errors.New("digest has incorrect length") ErrInvalidDigestEncoding = errors.New("digest is not valid hex-encoded string") ErrRDRequiredField = errors.New("at least one of name, URI, or digest are required") ) -// Supported standard hash algorithms -func isSupportedAlgorithm(alg string) (bool, int) { - algos := map[string]int{"md5": md5.Size, "sha1": sha1.Size, "shake128": md5.Size, "sha224": sha512.Size224, "sha3_224": sha512.Size224, "sha512_224": sha512.Size224, "sha256": sha512.Size256, "sha3_256": sha512.Size256, "sha512_256": sha512.Size256, "shake256": sha512.Size256, "sha384": sha512.Size384, "sha3_384": sha512.Size384, "sha512_384": sha512.Size384, "sha512": sha512.Size, "sha3_512": sha512.Size, "dirHash": sha512.Size256, "gitCommit": sha1.Size} +// Indicates if a given fixed-size hash algorithm is supported by default and returns the algorithm's +// digest size in bytes, if supported. We assume gitCommit and dirHash are aliases for sha1 and sha256, respectively. +// +// SHA digest sizes from https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.202.pdf +// MD5 digest size from https://www.rfc-editor.org/rfc/rfc1321.html#section-1 +func isSupportedFixedSizeAlgorithm(alg string) (bool, int) { + algos := map[string]int{"md5": 16, "sha1": 20, "sha224": 28, "sha512_224": 28, "sha256": 32, "sha512_256": 32, "sha384": 48, "sha512": 64, "sha3_224": 28, "sha3_256": 32, "sha3_384": 48, "sha3_512": 64, "gitCommit": 20, "dirHash": 32} size, ok := algos[alg] return ok, size @@ -38,18 +40,18 @@ func (d *ResourceDescriptor) Validate() error { // Per https://github.com/in-toto/attestation/blob/main/spec/v1/digest_set.md // check encoding and length for supported algorithms; // use of custom, unsupported algorithms is allowed and does not not generate validation errors. - supported, size := isSupportedAlgorithm(alg) + supported, size := isSupportedFixedSizeAlgorithm(alg) if supported { // the in-toto spec expects a hex-encoded string in DigestSets for supported algorithms hashBytes, err := hex.DecodeString(digest) if err != nil { - return ErrInvalidDigestEncoding + return fmt.Errorf("%w: %s", ErrInvalidDigestEncoding, alg) } // check the length of the digest if len(hashBytes) != size { - return ErrIncorrectDigestLength + return fmt.Errorf("%w: %s (got %d bytes, want %d bytes", ErrIncorrectDigestLength, alg, len(hashBytes), size) } } } From dfaabeadb02f7bfbfb3bcd39130b938a0f7cc710 Mon Sep 17 00:00:00 2001 From: Marcela Melara Date: Mon, 8 Apr 2024 08:18:41 -0700 Subject: [PATCH 5/5] Include digest in RD validation error message Signed-off-by: Marcela Melara --- go/v1/resource_descriptor.go | 4 ++-- go/v1/resource_descriptor_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go/v1/resource_descriptor.go b/go/v1/resource_descriptor.go index e4851f09..51654e95 100644 --- a/go/v1/resource_descriptor.go +++ b/go/v1/resource_descriptor.go @@ -46,12 +46,12 @@ func (d *ResourceDescriptor) Validate() error { hashBytes, err := hex.DecodeString(digest) if err != nil { - return fmt.Errorf("%w: %s", ErrInvalidDigestEncoding, alg) + return fmt.Errorf("%w (%s: %s)", ErrInvalidDigestEncoding, alg, digest) } // check the length of the digest if len(hashBytes) != size { - return fmt.Errorf("%w: %s (got %d bytes, want %d bytes", ErrIncorrectDigestLength, alg, len(hashBytes), size) + return fmt.Errorf("%w: got %d bytes, want %d bytes (%s: %s)", ErrIncorrectDigestLength, len(hashBytes), size, alg, digest) } } } diff --git a/go/v1/resource_descriptor_test.go b/go/v1/resource_descriptor_test.go index 48410830..a1e1e61a 100644 --- a/go/v1/resource_descriptor_test.go +++ b/go/v1/resource_descriptor_test.go @@ -89,7 +89,7 @@ func TestBadResourceDescriptorDigestEncoding(t *testing.T) { assert.NoError(t, err, "Error during JSON unmarshalling") err = got.Validate() - assert.ErrorIs(t, err, ErrInvalidDigestEncoding, "created ResourceDescriptor with invalid digest encoding") + assert.ErrorIs(t, err, ErrInvalidDigestEncoding, "did not get expected error when validating ResourceDescriptor with invalid digest encoding") } func TestBadResourceDescriptorDigestLength(t *testing.T) { @@ -99,5 +99,5 @@ func TestBadResourceDescriptorDigestLength(t *testing.T) { assert.NoError(t, err, "Error during JSON unmarshalling") err = got.Validate() - assert.ErrorIs(t, err, ErrIncorrectDigestLength, "created ResourceDescriptor with incorrect digest length") + assert.ErrorIs(t, err, ErrIncorrectDigestLength, "did not get expected error when validating ResourceDescriptor with incorrect digest length") }