diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 810bdc6..2b65271 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,6 +16,3 @@ jobs: uses: actions/checkout@v2 - name: Test run: go test -v ./... - - name: Test Blake3 - run: go test -v ./... - working-directory: blake3 diff --git a/algorithm.go b/algorithm.go index 6298363..704cf9c 100644 --- a/algorithm.go +++ b/algorithm.go @@ -34,23 +34,22 @@ const ( // project. Other digests may be used but this one is the primary storage // digest. Canonical = SHA256 - // BLAKE3 is the blake3 algorithm with the default 256-bit output size - // github.com/opencontainers/go-digest/blake3 should be imported to make it available - BLAKE3 Algorithm = "blake3" ) var ( algorithmRegexp = regexp.MustCompile(`^[a-z0-9]+([+._-][a-z0-9]+)*$`) ) -// CryptoHash is the interface that any hash algorithm must implement +// CryptoHash is the interface that any digest algorithm must implement type CryptoHash interface { - // Available reports whether the given hash function is usable in the current binary. + // Available reports whether the given hash function is usable in the + // current binary. Available() bool - // Size returns the length, in bytes, of a digest resulting from the given hash function. + // Size returns the length, in bytes, of a digest resulting from the given + // hash function. Size() int - // New returns a new hash.Hash calculating the given hash function. If the hash function is not - // available, it may panic. + // New returns a new hash.Hash calculating the given hash function. If the + // hash function is not available, it may panic. New() hash.Hash } @@ -69,20 +68,22 @@ var ( algorithmsLock sync.RWMutex ) -// RegisterAlgorithm may be called to dynamically register an algorithm. The implementation is a CryptoHash, and -// the regex is meant to match the hash portion of the algorithm. If a duplicate algorithm is already registered, -// the return value is false, otherwise if registration was successful the return value is true. +// RegisterAlgorithm may be called to dynamically register an algorithm. The +// implementation is a CryptoHash, and the regex is meant to match the hash +// portion of the algorithm. If a duplicate algorithm is already registered, the +// return value is false, otherwise if registration was successful the return +// value is true. // // The algorithm encoding format must be based on hex. // -// The algorithm name must be conformant to the BNF specification in the OCI image-spec, otherwise the function -// will panic. +// The algorithm name must be conformant to the BNF specification in the OCI +// image-spec, otherwise the function will panic. func RegisterAlgorithm(algorithm Algorithm, implementation CryptoHash) bool { algorithmsLock.Lock() defer algorithmsLock.Unlock() if !algorithmRegexp.MatchString(string(algorithm)) { - panic(fmt.Sprintf("Algorithm %s has a name which does not fit within the allowed grammar", algorithm)) + panic(fmt.Sprintf("algorithm %s has a name which does not fit within the allowed grammar", algorithm)) } if _, ok := algorithms[algorithm]; ok { @@ -90,8 +91,10 @@ func RegisterAlgorithm(algorithm Algorithm, implementation CryptoHash) bool { } algorithms[algorithm] = implementation - // We can do this since the Digest function below only implements a hex digest. If we open this in the future - // we need to allow for alternative digest algorithms to be implemented and for the user to pass their own + + // We can do this since the Digest function below only implements a hex + // digest. If we open this in the future we need to allow for alternative + // digest algorithms to be implemented and for the user to pass their own // custom regexp. anchoredEncodedRegexps[algorithm] = hexDigestRegex(implementation) return true @@ -166,7 +169,7 @@ func (a Algorithm) Hash() hash.Hash { if !a.Available() { // Empty algorithm string is invalid if a == "" { - panic(fmt.Sprintf("empty digest algorithm, validate before calling Algorithm.Hash()")) + panic("empty digest algorithm, validate before calling Algorithm.Hash()") } // NOTE(stevvooe): A missing hash is usually a programming error that diff --git a/blake3/blake3.go b/blake3.go similarity index 77% rename from blake3/blake3.go rename to blake3.go index 9aa30c2..9f62908 100644 --- a/blake3/blake3.go +++ b/blake3.go @@ -11,17 +11,24 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -package blake3 +package digest import ( "hash" - "github.com/opencontainers/go-digest" "github.com/zeebo/blake3" ) +const ( + // Blake3 is the blake3 algorithm with the default 256-bit output size + Blake3 Algorithm = "blake3" + + // BLAKE3 is deprecated. Use the symbol "Blake3" instead. + BLAKE3 = Blake3 +) + func init() { - digest.RegisterAlgorithm(digest.BLAKE3, &blake3hash{}) + RegisterAlgorithm(Blake3, &blake3hash{}) } type blake3hash struct{} diff --git a/blake3/blake3_test.go b/blake3/blake3_test.go deleted file mode 100644 index 111a831..0000000 --- a/blake3/blake3_test.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2021 OCI Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -package blake3 - -import ( - "testing" - - "github.com/opencontainers/go-digest" - "github.com/opencontainers/go-digest/testdigest" -) - -func TestBLAKE3(t *testing.T) { - testdigest.RunTestCase(t, testdigest.TestCase{ - Input: "blake3:af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262", - Algorithm: "blake3", - Encoded: "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262", - }) -} - -func TestBLAKE3Vector(t *testing.T) { - // From the BLAKE3 test vectors. - testvector := digest.BLAKE3.FromBytes([]byte{0, 1, 2, 3, 4}) - expected := "blake3:b40b44dfd97e7a84a996a91af8b85188c66c126940ba7aad2e7ae6b385402aa2" - if string(testvector) != expected { - t.Fatalf("Expected: %s; Got: %s", expected, testvector) - } -} diff --git a/blake3/go.mod b/blake3/go.mod deleted file mode 100644 index e542bff..0000000 --- a/blake3/go.mod +++ /dev/null @@ -1,12 +0,0 @@ -module github.com/opencontainers/go-digest/blake3 - -go 1.15 - -require ( - github.com/opencontainers/go-digest v0.0.0 - github.com/zeebo/blake3 v0.1.1 -) - -replace ( - github.com/opencontainers/go-digest => ../ -) diff --git a/digest_test.go b/digest_test.go new file mode 100644 index 0000000..483e183 --- /dev/null +++ b/digest_test.go @@ -0,0 +1,157 @@ +// Copyright 2019, 2020 OCI Contributors +// Copyright 2017 Docker, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package digest + +import ( + "testing" +) + +func TestParseDigest(t *testing.T) { + for _, testcase := range []struct { + input string + err error + algorithm Algorithm + encoded string + }{ + { + input: "sha256:e58fcf7418d4390dec8e8fb69d88c06ec07039d651fedd3aa72af9972e7d046b", + algorithm: "sha256", + encoded: "e58fcf7418d4390dec8e8fb69d88c06ec07039d651fedd3aa72af9972e7d046b", + }, + { + input: "sha384:d3fc7881460b7e22e3d172954463dddd7866d17597e7248453c48b3e9d26d9596bf9c4a9cf8072c9d5bad76e19af801d", + algorithm: "sha384", + encoded: "d3fc7881460b7e22e3d172954463dddd7866d17597e7248453c48b3e9d26d9596bf9c4a9cf8072c9d5bad76e19af801d", + }, + { + input: "blake3:af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262", + algorithm: "blake3", + encoded: "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262", + }, + { + // empty + input: "", + err: ErrDigestInvalidFormat, + }, + { + // whitespace only + input: " ", + err: ErrDigestInvalidFormat, + }, + { + // empty hex + input: "sha256:", + err: ErrDigestInvalidFormat, + }, + { + // hex with correct length, but whitespace only + input: "sha256: ", + err: ErrDigestInvalidFormat, + }, + { + // empty hex + input: ":", + err: ErrDigestInvalidFormat, + }, + { + // just hex + input: "d41d8cd98f00b204e9800998ecf8427e", + err: ErrDigestInvalidFormat, + }, + { + // not hex + input: "sha256:d41d8cd98f00b204e9800m98ecf8427e", + err: ErrDigestInvalidLength, + }, + { + // too short + input: "sha256:abcdef0123456789", + err: ErrDigestInvalidLength, + }, + { + // too short (from different algorithm) + input: "sha512:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", + err: ErrDigestInvalidLength, + }, + { + input: "foo:d41d8cd98f00b204e9800998ecf8427e", + err: ErrDigestUnsupported, + }, + { + // repeated separators + input: "sha384__foo+bar:d3fc7881460b7e22e3d172954463dddd7866d17597e7248453c48b3e9d26d9596bf9c4a9cf8072c9d5bad76e19af801d", + err: ErrDigestInvalidFormat, + }, + { + // ensure that we parse, but we don't have support for the algorithm + input: "sha384.foo+bar:d3fc7881460b7e22e3d172954463dddd7866d17597e7248453c48b3e9d26d9596bf9c4a9cf8072c9d5bad76e19af801d", + algorithm: "sha384.foo+bar", + encoded: "d3fc7881460b7e22e3d172954463dddd7866d17597e7248453c48b3e9d26d9596bf9c4a9cf8072c9d5bad76e19af801d", + err: ErrDigestUnsupported, + }, + { + input: "sha384_foo+bar:d3fc7881460b7e22e3d172954463dddd7866d17597e7248453c48b3e9d26d9596bf9c4a9cf8072c9d5bad76e19af801d", + algorithm: "sha384_foo+bar", + encoded: "d3fc7881460b7e22e3d172954463dddd7866d17597e7248453c48b3e9d26d9596bf9c4a9cf8072c9d5bad76e19af801d", + err: ErrDigestUnsupported, + }, + { + input: "sha256+b64:LCa0a2j_xo_5m0U8HTBBNBNCLXBkg7-g-YpeiGJm564", + algorithm: "sha256+b64", + encoded: "LCa0a2j_xo_5m0U8HTBBNBNCLXBkg7-g-YpeiGJm564", + err: ErrDigestUnsupported, + }, + { + input: "sha256:E58FCF7418D4390DEC8E8FB69D88C06EC07039D651FEDD3AA72AF9972E7D046B", + err: ErrDigestInvalidFormat, + }, + } { + t.Run(testcase.input, func(t *testing.T) { + digest, err := Parse(testcase.input) + if err != testcase.err { + t.Fatalf("error differed from expected while parsing %q: %v != %v", testcase.input, err, testcase.err) + } + + if testcase.err != nil { + return + } + + if digest.Algorithm() != testcase.algorithm { + t.Fatalf("incorrect algorithm for parsed digest: %q != %q", digest.Algorithm(), testcase.algorithm) + } + + if digest.Encoded() != testcase.encoded { + t.Fatalf("incorrect hex for parsed digest: %q != %q", digest.Encoded(), testcase.encoded) + } + + // Parse string return value and check equality + newParsed, err := Parse(digest.String()) + + if err != nil { + t.Fatalf("unexpected error parsing input %q: %v", testcase.input, err) + } + + if newParsed != digest { + t.Fatalf("expected equal: %q != %q", newParsed, digest) + } + + newFromHex := NewDigestFromEncoded(newParsed.Algorithm(), newParsed.Encoded()) + if newFromHex != digest { + t.Fatalf("%v != %v", newFromHex, digest) + } + }) + } +} diff --git a/go.mod b/go.mod index cf5d7b1..25f89e5 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/opencontainers/go-digest go 1.13 + +require github.com/zeebo/blake3 v0.2.0 diff --git a/blake3/go.sum b/go.sum similarity index 57% rename from blake3/go.sum rename to go.sum index 0eb623a..abb7b05 100644 --- a/blake3/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ +github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= -github.com/zeebo/blake3 v0.1.1 h1:Nbsts7DdKThRHHd+YNlqiGlRqGEF2bE2eXN+xQ1hsEs= -github.com/zeebo/blake3 v0.1.1/go.mod h1:G9pM4qQwjRzF1/v7+vabMj/c5mWpGZ2Wzo3Eb4z0pb4= +github.com/zeebo/blake3 v0.2.0 h1:1SGx3IvKWFUU/xl+/7kjdcjjMcvVSm+3dMo/N42afC8= +github.com/zeebo/blake3 v0.2.0/go.mod h1:G9pM4qQwjRzF1/v7+vabMj/c5mWpGZ2Wzo3Eb4z0pb4= +github.com/zeebo/pcg v1.0.0 h1:dt+dx+HvX8g7Un32rY9XWoYnd0NmKmrIzpHF7qiTDj0= github.com/zeebo/pcg v1.0.0/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= golang.org/x/sys v0.0.0-20201014080544-cc95f250f6bc h1:HVFDs9bKvTxP6bh1Rj9MCSo+UmafQtI8ZWDPVwVk9g4= golang.org/x/sys v0.0.0-20201014080544-cc95f250f6bc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/testdigest/testdigest.go b/testdigest/testdigest.go deleted file mode 100644 index cc004e3..0000000 --- a/testdigest/testdigest.go +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2021 OCI Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// testdigest is a separate package, because it has some testing utilities in it that may be useful -// to other internal Algorithm implementors. -// -// It is not a stable interface and not meant for consumption outside of digest developers. - -package testdigest - -import ( - "testing" - - pkgdigest "github.com/opencontainers/go-digest" -) - -type TestCase struct { - // Input the formal format of the hash, for example sha256:e58fcf7418d4390dec8e8fb69d88c06ec07039d651fedd3aa72af9972e7d046b - Input string - // If err is non-nil, then the parsing of Input is expected to return this error - Err error - // Algorithm should be an available or registered algorithm - Algorithm pkgdigest.Algorithm - // Encoded is the the encoded portion of the digest to expect, for example e58fcf7418d4390dec8e8fb69d88c06ec07039d651fedd3aa72af9972e7d046b - Encoded string -} - -func RunTestCase(t *testing.T, testcase TestCase) { - digest, err := pkgdigest.Parse(testcase.Input) - if err != testcase.Err { - t.Fatalf("error differed from expected while parsing %q: %v != %v", testcase.Input, err, testcase.Err) - } - - if testcase.Err != nil { - return - } - - if digest.Algorithm() != testcase.Algorithm { - t.Fatalf("incorrect Algorithm for parsed digest: %q != %q", digest.Algorithm(), testcase.Algorithm) - } - - if digest.Encoded() != testcase.Encoded { - t.Fatalf("incorrect hex for parsed digest: %q != %q", digest.Encoded(), testcase.Encoded) - } - - // Parse string return value and check equality - newParsed, err := pkgdigest.Parse(digest.String()) - - if err != nil { - t.Fatalf("unexpected error parsing Input %q: %v", testcase.Input, err) - } - - if newParsed != digest { - t.Fatalf("expected equal: %q != %q", newParsed, digest) - } - - newFromHex := pkgdigest.NewDigestFromEncoded(newParsed.Algorithm(), newParsed.Encoded()) - if newFromHex != digest { - t.Fatalf("%v != %v", newFromHex, digest) - } -} - -func RunTestCases(t *testing.T, testcases []TestCase) { - for _, testcase := range testcases { - t.Run(testcase.Input, func(t *testing.T) { - RunTestCase(t, testcase) - }) - } -} \ No newline at end of file diff --git a/testdigest/testdigest_test.go b/testdigest/testdigest_test.go deleted file mode 100644 index 8969892..0000000 --- a/testdigest/testdigest_test.go +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright 2021 OCI Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package testdigest - -import ( - "testing" - - _ "crypto/sha256" - _ "crypto/sha512" - "github.com/opencontainers/go-digest" -) - -func TestParseDigest(t *testing.T) { - RunTestCases(t, []TestCase{ - { - Input: "sha256:e58fcf7418d4390dec8e8fb69d88c06ec07039d651fedd3aa72af9972e7d046b", - Algorithm: "sha256", - Encoded: "e58fcf7418d4390dec8e8fb69d88c06ec07039d651fedd3aa72af9972e7d046b", - }, - { - Input: "sha384:d3fc7881460b7e22e3d172954463dddd7866d17597e7248453c48b3e9d26d9596bf9c4a9cf8072c9d5bad76e19af801d", - Algorithm: "sha384", - Encoded: "d3fc7881460b7e22e3d172954463dddd7866d17597e7248453c48b3e9d26d9596bf9c4a9cf8072c9d5bad76e19af801d", - }, - { - // empty - Input: "", - Err: digest.ErrDigestInvalidFormat, - }, - { - // whitespace only - Input: " ", - Err: digest.ErrDigestInvalidFormat, - }, - { - // empty hex - Input: "sha256:", - Err: digest.ErrDigestInvalidFormat, - }, - { - // hex with correct length, but whitespace only - Input: "sha256: ", - Err: digest.ErrDigestInvalidFormat, - }, - { - // empty hex - Input: ":", - Err: digest.ErrDigestInvalidFormat, - }, - { - // just hex - Input: "d41d8cd98f00b204e9800998ecf8427e", - Err: digest.ErrDigestInvalidFormat, - }, - { - // not hex - Input: "sha256:d41d8cd98f00b204e9800m98ecf8427e", - Err: digest. ErrDigestInvalidLength, - }, - { - // too short - Input: "sha256:abcdef0123456789", - Err: digest. ErrDigestInvalidLength, - }, - { - // too short (from different Algorithm) - Input: "sha512:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", - Err: digest.ErrDigestInvalidLength, - }, - { - Input: "foo:d41d8cd98f00b204e9800998ecf8427e", - Err: digest.ErrDigestUnsupported, - }, - { - // repeated separators - Input: "sha384__foo+bar:d3fc7881460b7e22e3d172954463dddd7866d17597e7248453c48b3e9d26d9596bf9c4a9cf8072c9d5bad76e19af801d", - Err: digest.ErrDigestInvalidFormat, - }, - { - // ensure that we parse, but we don't have support for the Algorithm - Input: "sha384.foo+bar:d3fc7881460b7e22e3d172954463dddd7866d17597e7248453c48b3e9d26d9596bf9c4a9cf8072c9d5bad76e19af801d", - Algorithm: "sha384.foo+bar", - Encoded: "d3fc7881460b7e22e3d172954463dddd7866d17597e7248453c48b3e9d26d9596bf9c4a9cf8072c9d5bad76e19af801d", - Err: digest. ErrDigestUnsupported, - }, - { - Input: "sha384_foo+bar:d3fc7881460b7e22e3d172954463dddd7866d17597e7248453c48b3e9d26d9596bf9c4a9cf8072c9d5bad76e19af801d", - Algorithm: "sha384_foo+bar", - Encoded: "d3fc7881460b7e22e3d172954463dddd7866d17597e7248453c48b3e9d26d9596bf9c4a9cf8072c9d5bad76e19af801d", - Err: digest. ErrDigestUnsupported, - }, - { - Input: "sha256+b64:LCa0a2j_xo_5m0U8HTBBNBNCLXBkg7-g-YpeiGJm564", - Algorithm: "sha256+b64", - Encoded: "LCa0a2j_xo_5m0U8HTBBNBNCLXBkg7-g-YpeiGJm564", - Err: digest. ErrDigestUnsupported, - }, - { - Input: "sha256:E58FCF7418D4390DEC8E8FB69D88C06EC07039D651FEDD3AA72AF9972E7D046B", - Err: digest. ErrDigestInvalidFormat, - }, - }) -} - diff --git a/verifiers_test.go b/verifiers_test.go index 0e35115..9157a27 100644 --- a/verifiers_test.go +++ b/verifiers_test.go @@ -37,6 +37,15 @@ func TestDigestVerifier(t *testing.T) { } } +func TestDigestBlakeVector(t *testing.T) { + // From the BLAKE3 test vectors. + testvector := Blake3.FromBytes([]byte{0, 1, 2, 3, 4}) + expected := "blake3:b40b44dfd97e7a84a996a91af8b85188c66c126940ba7aad2e7ae6b385402aa2" + if string(testvector) != expected { + t.Fatalf("Expected: %s; Got: %s", expected, testvector) + } +} + // TestVerifierUnsupportedDigest ensures that unsupported digest validation is // flowing through verifier creation. func TestVerifierUnsupportedDigest(t *testing.T) {