Skip to content

Commit 15e7ca2

Browse files
authored
Merge pull request #330 from marcelamelara/add-digest-set-validation
Add DigestSet hex encoding validation
2 parents ad3ec5f + dfaabea commit 15e7ca2

File tree

2 files changed

+81
-2
lines changed

2 files changed

+81
-2
lines changed

go/v1/resource_descriptor.go

+45-2
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,58 @@ Wrapper APIs for in-toto attestation ResourceDescriptor protos.
44

55
package v1
66

7-
import "errors"
7+
import (
8+
"encoding/hex"
9+
"errors"
10+
"fmt"
11+
)
812

9-
var ErrRDRequiredField = errors.New("at least one of name, URI, or digest are required")
13+
var (
14+
ErrIncorrectDigestLength = errors.New("digest has incorrect length")
15+
ErrInvalidDigestEncoding = errors.New("digest is not valid hex-encoded string")
16+
ErrRDRequiredField = errors.New("at least one of name, URI, or digest are required")
17+
)
18+
19+
// Indicates if a given fixed-size hash algorithm is supported by default and returns the algorithm's
20+
// digest size in bytes, if supported. We assume gitCommit and dirHash are aliases for sha1 and sha256, respectively.
21+
//
22+
// SHA digest sizes from https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.202.pdf
23+
// MD5 digest size from https://www.rfc-editor.org/rfc/rfc1321.html#section-1
24+
func isSupportedFixedSizeAlgorithm(alg string) (bool, int) {
25+
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}
26+
27+
size, ok := algos[alg]
28+
return ok, size
29+
}
1030

1131
func (d *ResourceDescriptor) Validate() error {
1232
// at least one of name, URI or digest are required
1333
if d.GetName() == "" && d.GetUri() == "" && len(d.GetDigest()) == 0 {
1434
return ErrRDRequiredField
1535
}
1636

37+
if len(d.GetDigest()) > 0 {
38+
for alg, digest := range d.GetDigest() {
39+
40+
// Per https://github.com/in-toto/attestation/blob/main/spec/v1/digest_set.md
41+
// check encoding and length for supported algorithms;
42+
// use of custom, unsupported algorithms is allowed and does not not generate validation errors.
43+
supported, size := isSupportedFixedSizeAlgorithm(alg)
44+
if supported {
45+
// the in-toto spec expects a hex-encoded string in DigestSets for supported algorithms
46+
hashBytes, err := hex.DecodeString(digest)
47+
48+
if err != nil {
49+
return fmt.Errorf("%w (%s: %s)", ErrInvalidDigestEncoding, alg, digest)
50+
}
51+
52+
// check the length of the digest
53+
if len(hashBytes) != size {
54+
return fmt.Errorf("%w: got %d bytes, want %d bytes (%s: %s)", ErrIncorrectDigestLength, len(hashBytes), size, alg, digest)
55+
}
56+
}
57+
}
58+
}
59+
1760
return nil
1861
}

go/v1/resource_descriptor_test.go

+36
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,14 @@ import (
1515

1616
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"}}}}`
1717

18+
const supportedRdDigest = `{"digest":{"sha256":"a1234567b1234567c1234567d1234567e1234567f1234567a1234567b1234567","custom":"myCustomEnvoding","sha1":"a1234567b1234567c1234567d1234567e1234567"}}`
19+
1820
const badRd = `{"downloadLocation":"https://example.com/test.zip","mediaType":"theMediaType"}`
1921

22+
const badRdDigestEncoding = `{"digest":{"sha256":"badDigest"},"downloadLocation":"https://example.com/test.zip","mediaType":"theMediaType"}`
23+
24+
const badRdDigestLength = `{"digest":{"sha256":"abc123"},"downloadLocation":"https://example.com/test.zip","mediaType":"theMediaType"}`
25+
2026
func createTestResourceDescriptor() (*ResourceDescriptor, error) {
2127
// Create a ResourceDescriptor
2228
a, err := structpb.NewStruct(map[string]interface{}{
@@ -56,6 +62,16 @@ func TestJsonUnmarshalResourceDescriptor(t *testing.T) {
5662
assert.True(t, proto.Equal(got, want), "Protos do not match")
5763
}
5864

65+
func TestSupportedResourceDescriptorDigest(t *testing.T) {
66+
got := &ResourceDescriptor{}
67+
err := protojson.Unmarshal([]byte(supportedRdDigest), got)
68+
69+
assert.NoError(t, err, "Error during JSON unmarshalling")
70+
71+
err = got.Validate()
72+
assert.NoError(t, err, "Error during validation of valid supported RD digests")
73+
}
74+
5975
func TestBadResourceDescriptor(t *testing.T) {
6076
got := &ResourceDescriptor{}
6177
err := protojson.Unmarshal([]byte(badRd), got)
@@ -65,3 +81,23 @@ func TestBadResourceDescriptor(t *testing.T) {
6581
err = got.Validate()
6682
assert.ErrorIs(t, err, ErrRDRequiredField, "created malformed ResourceDescriptor")
6783
}
84+
85+
func TestBadResourceDescriptorDigestEncoding(t *testing.T) {
86+
got := &ResourceDescriptor{}
87+
err := protojson.Unmarshal([]byte(badRdDigestEncoding), got)
88+
89+
assert.NoError(t, err, "Error during JSON unmarshalling")
90+
91+
err = got.Validate()
92+
assert.ErrorIs(t, err, ErrInvalidDigestEncoding, "did not get expected error when validating ResourceDescriptor with invalid digest encoding")
93+
}
94+
95+
func TestBadResourceDescriptorDigestLength(t *testing.T) {
96+
got := &ResourceDescriptor{}
97+
err := protojson.Unmarshal([]byte(badRdDigestLength), got)
98+
99+
assert.NoError(t, err, "Error during JSON unmarshalling")
100+
101+
err = got.Validate()
102+
assert.ErrorIs(t, err, ErrIncorrectDigestLength, "did not get expected error when validating ResourceDescriptor with incorrect digest length")
103+
}

0 commit comments

Comments
 (0)