Skip to content

Commit

Permalink
algorithm: ErrDigestInvalidFormat on Validate() even for unknown algo…
Browse files Browse the repository at this point in the history
…rithms

In some cases (e.g. Algorithm("foo").Validate("!")) we know the
encoded portion is invalid even if we do not have an entry for foo in
anchoredEncodedRegexps.  Whatever algorithm-specific additional
restrictions are applied via anchoredEncodedRegexps, the encoded
portion must satisfy the generic encoded-portion restrictions from the
DigestRegexp.  This commit adjusts the Algorithm("foo").Validate("!")
result to match the current Digest("foo:!").Validate() result, and
factors the encoded portion of DigestRegexp out into encodedRegexp and
encodedRegexpAnchored.

It also guards the Size()-based length check with Available(), because
Size() returns zero for unavailable algorithms.  Now that we have a
check for algorithms that aren't in anchoredEncodedRegexps, we can't
rely on lookup-misses there to protect us from running the Size()
check on unavailable algorithms.  And equating anchoredEncodedRegexps
hits with Available was dicey anyway, since it depended on what
packages get loaded, etc.

Adding Algorithm.ValidateIdentifier() allows us to avoid doubling up
on the encoded check in Digest.Validate(), since both
DigestRegexpAnchored and algorithm.Validate() were covering the
encoded portion.  The new tests are based on digest_test.go's
TestParseDigest.

I've also dropped the Available check from Digest.Valid, because we
can tell:

  sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

is a valid digest even if Algorithm("sha256") happens to be
unavailable (e.g. because the consumer didn't import crypto/sha256).
Available-ness just affects whether you're capable of verifying
content against the digest, not digest-validity.

Running Algorithm.Validate before Algorithm.ValidateIdentifier uses
the presence of an identifier in anchoredEncodedRegexps as a hint to
skip the identifier validation.  This saves some time (ad Derek's
suggestion [1]) and is safe as long as we don't add any invalid
identifiers to that map.

[1]: opencontainers#34 (comment)

Signed-off-by: W. Trevor King <wking@tremily.us>
  • Loading branch information
wking committed Jun 9, 2017
1 parent 959fab8 commit 9a46ed6
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 14 deletions.
30 changes: 24 additions & 6 deletions algorithm.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ var (
SHA384: regexp.MustCompile(`^[a-f0-9]{96}$`),
SHA512: regexp.MustCompile(`^[a-f0-9]{128}$`),
}

algorithmRegexp = regexp.MustCompile(`[a-z0-9]+(?:[.+_-][a-z0-9]+)*`)
algorithmRegexpAnchored = regexp.MustCompile(`^` + algorithmRegexp.String() + `$`)
encodedRegexp = regexp.MustCompile(`[a-zA-Z0-9=_-]+`)
encodedRegexpAnchored = regexp.MustCompile(`^` + encodedRegexp.String() + `$`)
)

// Available returns true if the digest type is available for use. If this
Expand Down Expand Up @@ -178,15 +183,28 @@ func (a Algorithm) FromString(s string) Digest {
func (a Algorithm) Validate(encoded string) error {
r, ok := anchoredEncodedRegexps[a]
if !ok {
return ErrDigestUnsupported
r = encodedRegexpAnchored
}
// Digests much always be hex-encoded, ensuring that their hex portion will
// always be size*2
if a.Size()*2 != len(encoded) {
return ErrDigestInvalidLength
if a.Available() {
// Digests much always be hex-encoded, ensuring that their hex portion will
// always be size*2
if a.Size()*2 != len(encoded) {
return ErrDigestInvalidLength
}
}
if r.MatchString(encoded) {
return nil
if ok {
return nil
}
return ErrDigestUnsupported
}
return ErrDigestInvalidFormat
}

// ValidateIdentifier validates the algorithm name.
func (a Algorithm) ValidateIdentifier() error {
if !algorithmRegexpAnchored.MatchString(a.String()) {
return ErrDigestInvalidFormat
}
return nil
}
43 changes: 42 additions & 1 deletion algorithm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ func TestValidate(t *testing.T) {
// unsupported, but the encoded part cannot possibly be valid because it contains invalid characters
encoded: "!",
algorithm: "foo",
err: ErrDigestUnsupported,
err: ErrDigestInvalidFormat,
},
{
// uppercase
Expand All @@ -175,3 +175,44 @@ func TestValidate(t *testing.T) {
}
}
}

func TestValidateIdentifier(t *testing.T) {
for _, testcase := range []struct {
algorithm Algorithm
err error
}{
{
algorithm: "sha256",
},
{
algorithm: "sha384",
},
{
// empty identifier
algorithm: "",
err: ErrDigestInvalidFormat,
},
{
// repeated separators
algorithm: "sha384__foo+bar",
err: ErrDigestInvalidFormat,
},
{
// ensure that we parse valid separators
algorithm: "sha384.foo+bar",
},
{
// ensure that we parse valid separators
algorithm: "sha384_foo+bar",
},
{
// ensure that we parse valid separators
algorithm: "sha256+b64",
},
} {
err := testcase.algorithm.ValidateIdentifier()
if err != testcase.err {
t.Fatalf("error differed from expected while validating identifier %q: %v != %v", testcase.algorithm, err, testcase.err)
}
}
}
21 changes: 14 additions & 7 deletions digest.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func NewDigestFromEncoded(alg Algorithm, encoded string) Digest {
}

// DigestRegexp matches valid digest types.
var DigestRegexp = regexp.MustCompile(`[a-z0-9]+(?:[.+_-][a-z0-9]+)*:[a-zA-Z0-9=_-]+`)
var DigestRegexp = regexp.MustCompile(algorithmRegexp.String() + `:` + encodedRegexp.String())

// DigestRegexpAnchored matches valid digest types, anchored to the start and end of the match.
var DigestRegexpAnchored = regexp.MustCompile(`^` + DigestRegexp.String() + `$`)
Expand Down Expand Up @@ -99,20 +99,27 @@ func FromString(s string) Digest {

// Validate checks that the contents of d is a valid digest, returning an
// error if not.
func (d Digest) Validate() error {
func (d Digest) Validate() (err error) {
s := string(d)
i := strings.Index(s, ":")
if i <= 0 || i+1 == len(s) {
return ErrDigestInvalidFormat
}
algorithm, encoded := Algorithm(s[:i]), s[i+1:]
if !algorithm.Available() {
if !DigestRegexpAnchored.MatchString(s) {
return ErrDigestInvalidFormat

err = algorithm.Validate(encoded)
if err == ErrDigestUnsupported {
err2 := algorithm.ValidateIdentifier()
if err2 != nil {
return err2
}
return ErrDigestUnsupported
return err
}
if err != nil {
return err
}
return algorithm.Validate(encoded)

return algorithm.ValidateIdentifier()
}

// Algorithm returns the algorithm portion of the digest. This will panic if
Expand Down

0 comments on commit 9a46ed6

Please sign in to comment.