diff --git a/go/v1/resource_descriptor.go b/go/v1/resource_descriptor.go index 5ebeea35..51654e95 100644 --- a/go/v1/resource_descriptor.go +++ b/go/v1/resource_descriptor.go @@ -4,9 +4,29 @@ Wrapper APIs for in-toto attestation ResourceDescriptor protos. package v1 -import "errors" +import ( + "encoding/hex" + "errors" + "fmt" +) -var ErrRDRequiredField = errors.New("at least one of name, URI, or digest are required") +var ( + 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") +) + +// 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 +} func (d *ResourceDescriptor) Validate() error { // at least one of name, URI or digest are required @@ -14,5 +34,28 @@ func (d *ResourceDescriptor) Validate() error { return ErrRDRequiredField } + if len(d.GetDigest()) > 0 { + for alg, digest := range d.GetDigest() { + + // 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 := 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 fmt.Errorf("%w (%s: %s)", ErrInvalidDigestEncoding, alg, digest) + } + + // check the length of the digest + if len(hashBytes) != size { + return fmt.Errorf("%w: got %d bytes, want %d bytes (%s: %s)", ErrIncorrectDigestLength, len(hashBytes), size, alg, digest) + } + } + } + } + return nil } diff --git a/go/v1/resource_descriptor_test.go b/go/v1/resource_descriptor_test.go index 7c7eb240..a1e1e61a 100644 --- a/go/v1/resource_descriptor_test.go +++ b/go/v1/resource_descriptor_test.go @@ -15,8 +15,14 @@ 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"}` + +const badRdDigestLength = `{"digest":{"sha256":"abc123"},"downloadLocation":"https://example.com/test.zip","mediaType":"theMediaType"}` + func createTestResourceDescriptor() (*ResourceDescriptor, error) { // Create a ResourceDescriptor a, err := structpb.NewStruct(map[string]interface{}{ @@ -56,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) @@ -65,3 +81,23 @@ func TestBadResourceDescriptor(t *testing.T) { err = got.Validate() assert.ErrorIs(t, err, ErrRDRequiredField, "created malformed ResourceDescriptor") } + +func TestBadResourceDescriptorDigestEncoding(t *testing.T) { + got := &ResourceDescriptor{} + err := protojson.Unmarshal([]byte(badRdDigestEncoding), got) + + assert.NoError(t, err, "Error during JSON unmarshalling") + + err = got.Validate() + assert.ErrorIs(t, err, ErrInvalidDigestEncoding, "did not get expected error when validating 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, "did not get expected error when validating ResourceDescriptor with incorrect digest length") +}