From f97edc8fa724b95596f1575798c8c15dea5993c4 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 7 Mar 2017 14:06:14 -0800 Subject: [PATCH] *: Replace implicit hex hash encoding with Algorithm-based encoding The docs for 'Algorithm' (now 'algorithm') made it clear that the algorithm identifier was intended to cover both the hash and encoding algorithms. Stephen confirmed this interpretation in recent comments [1,2] as well. The idea is that a future algorithm may chose a non-hex encoding like base 64 [1]. The previous implementation, on the other hand, baked the hex encoding into key locations (e.g. in NewDigestFromBytes and Digest.Validate). This commit makes the encoding internal to Agorithm instances, adding a new Encoding interface and an Algorithm.Encoding method. In order support external algorithms with different encoding, I've defined an Algorithm interface with the public API and made renamed the local implementation of that interface to 'algorithm'. And now that Algorithm is a stand-alone interface, I've made the identifier-to-Algorithm registry public by renaming 'algorithms' to 'Algorithm'. I've also split out the flag binding into its own AlgorithmFlag structure, because the old Algorithm.Set pointer assignment no longer works now that Algorithm is an interface. Having a separate interface for the flag.Value also simplifies the Algorithm interface, which makes it easier to write external Algorithm implementations while still benefiting from the flag.Value helper. API changes: * Additions * Algorithms, a newly-public registry of known algorithm identifiers. * Algorithm.HashSize, which allows us to avoid a previously-hardcoded hex assumption in Digest.Validate(). * Algorithm.Encoding, which allows us to implement NewDigester and avoid a previously-hardcoded hex assumption in NewDigestFromBytes. * Digest.Hash, as a better name for Digest.Hex. * Encoding, a new interface backing Algorithm.Encoding. * Hex, the lowercase base 16 Encoding. * NewDigester, as a more flexible replacement for NewDigest. * NewDigestFromHash, as a better name for NewDigestFromHex. * Adjustments * Algorithm.Hash now returns nil on unavailable algorithms, to mirror the old API for Algorithm.Digester. * Deprecations * NewDigest, because NewDigester is a more flexible form of the same thing that is just about as compact. * NewDigestFromHex, because NewDigestFromHash is a better name for this function. * Digest.Hex, because Hash is a better name for this method. * Removals * Algorithm.Set, because it is not possible to support this now that Algorithm is an interface. I also switched to black-box testing (with digest_test package names) because I was getting Hex-undefined errors with white-box testing, and we don't actually need any white-box access in our test suite. [1]: https://github.com/opencontainers/go-digest/issues/3#issuecomment-267724212 [2]: https://github.com/opencontainers/go-digest/issues/6#issuecomment-267727752 Signed-off-by: W. Trevor King --- algorithm.go | 177 ++++++++++++++++++++++++----------------- algorithm_flag.go | 45 +++++++++++ algorithm_flag_test.go | 77 ++++++++++++++++++ algorithm_test.go | 70 +++------------- digest.go | 55 ++++++++----- digest_test.go | 60 +++++++------- digester.go | 27 ++++++- doc.go | 4 +- encoding.go | 46 +++++++++++ encoding_test.go | 49 ++++++++++++ verifiers.go | 9 +-- verifiers_test.go | 22 ++--- 12 files changed, 442 insertions(+), 199 deletions(-) create mode 100644 algorithm_flag.go create mode 100644 algorithm_flag_test.go create mode 100644 encoding.go create mode 100644 encoding_test.go diff --git a/algorithm.go b/algorithm.go index bdff42d..601bdc1 100644 --- a/algorithm.go +++ b/algorithm.go @@ -21,16 +21,70 @@ import ( "io" ) -// Algorithm identifies and implementation of a digester by an identifier. +type Algorithm interface { + // Available returns true if the digest type is available for use. If this + // returns false, Digester and Hash will return nil. + Available() bool + + // String returns the canonical algorithm identifier for this algorithm. + String() string + + // Size returns number of bytes in the raw (unencoded) hash. This + // gives the size of the hash space. For SHA-256, this will be 32. + Size() int + + // HashSize returns number of bytes in the encoded hash. For + // algorithms which use a base b encoding, HashSize will Size() * 8 / + // log2(b), possibly rounded up depending on padding. + HashSize() int + + // Encoding returns the algorithm's Encoding. + Encoding() Encoding + + // Digester returns a new digester for the algorithm. + Digester() Digester + + // Hash returns a new hash as used by the algorithm. + Hash() hash.Hash + + // FromReader returns the digest of the reader using the algorithm. + FromReader(rd io.Reader) (Digest, error) + + // FromBytes digests the input and returns a Digest. + FromBytes(p []byte) Digest + + // FromString digests the string input and returns a Digest. + FromString(s string) Digest +} + +// algorithm identifies and implementation of a digester by an identifier. // Note the that this defines both the hash algorithm used and the string // encoding. -type Algorithm string +type algorithm struct { + name string + hash crypto.Hash + encoding Encoding +} // supported digest types -const ( - SHA256 Algorithm = "sha256" // sha256 with hex encoding - SHA384 Algorithm = "sha384" // sha384 with hex encoding - SHA512 Algorithm = "sha512" // sha512 with hex encoding +var ( + SHA256 = algorithm{ + name: "sha256", + hash: crypto.SHA256, + encoding: Hex, + } + + SHA384 = algorithm{ + name: "sha384", + hash: crypto.SHA384, + encoding: Hex, + } + + SHA512 = algorithm{ + name: "sha512", + hash: crypto.SHA512, + encoding: Hex, + } // Canonical is the primary digest algorithm used with the distribution // project. Other digests may be used but this one is the primary storage @@ -39,94 +93,73 @@ const ( ) var ( - // TODO(stevvooe): Follow the pattern of the standard crypto package for - // registration of digests. Effectively, we are a registerable set and - // common symbol access. - - // algorithms maps values to hash.Hash implementations. Other algorithms - // may be available but they cannot be calculated by the digest package. - algorithms = map[Algorithm]crypto.Hash{ - SHA256: crypto.SHA256, - SHA384: crypto.SHA384, - SHA512: crypto.SHA512, + // Algorithms is a registerable set of Algorithm instances. + Algorithms = map[string]Algorithm{ + "sha256": SHA256, + "sha384": SHA384, + "sha512": SHA512, } ) -// Available returns true if the digest type is available for use. If this -// returns false, Digester and Hash will return nil. -func (a Algorithm) Available() bool { - h, ok := algorithms[a] - if !ok { - return false - } - +// Available returns true if the algorithm hash is available for +// use. If this returns false, Digester and Hash will return nil. +func (a algorithm) Available() bool { // check availability of the hash, as well - return h.Available() + return a.hash.Available() } -func (a Algorithm) String() string { - return string(a) +func (a algorithm) String() string { + return a.name } -// Size returns number of bytes returned by the hash. -func (a Algorithm) Size() int { - h, ok := algorithms[a] - if !ok { - return 0 - } - return h.Size() +// Size returns number of bytes in the raw (unencoded) hash. This +// gives the size of the hash space. For SHA-256, this will be 32. +func (a algorithm) Size() int { + return a.hash.Size() } -// Set implemented to allow use of Algorithm as a command line flag. -func (a *Algorithm) Set(value string) error { - if value == "" { - *a = Canonical - } else { - // just do a type conversion, support is queried with Available. - *a = Algorithm(value) +// HashSize returns number of bytes in the encoded hash. For +// algorithms which use a base b encoding, HashSize will Size() * 8 / +// log2(b), possibly rounded up depending on padding. +func (a algorithm) HashSize() int { + if a.encoding == Hex { + return 2 * a.Size() } + panic(fmt.Sprintf("unrecognized encoding %v", a.encoding)) +} + +// Encoding returns the algorithm's Encoding. +func (a algorithm) Encoding() Encoding { + return a.encoding +} +// Digester returns a new digester for the algorithm. If the algorithm +// is unavailable, nil will be returned. This can be checked by +// calling Available before calling Digester. +func (a algorithm) Digester() Digester { if !a.Available() { - return ErrDigestUnsupported + return nil } - return nil -} - -// Digester returns a new digester for the specified algorithm. If the algorithm -// does not have a digester implementation, nil will be returned. This can be -// checked by calling Available before calling Digester. -func (a Algorithm) Digester() Digester { return &digester{ - alg: a, - hash: a.Hash(), + name: a.name, + hash: a.Hash(), + encoding: a.encoding, } } -// Hash returns a new hash as used by the algorithm. If not available, the -// method will panic. Check Algorithm.Available() before calling. -func (a Algorithm) Hash() hash.Hash { +// Hash returns a new hash as used by the algorithm. If the algorithm +// is unavailable, nil will be returned. This can be checked by +// calling Available before calling Hash(). +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()")) - } - - // NOTE(stevvooe): A missing hash is usually a programming error that - // must be resolved at compile time. We don't import in the digest - // package to allow users to choose their hash implementation (such as - // when using stevvooe/resumable or a hardware accelerated package). - // - // Applications that may want to resolve the hash at runtime should - // call Algorithm.Available before call Algorithm.Hash(). - panic(fmt.Sprintf("%v not available (make sure it is imported)", a)) + return nil } - - return algorithms[a].New() + return a.hash.New() } // FromReader returns the digest of the reader using the algorithm. -func (a Algorithm) FromReader(rd io.Reader) (Digest, error) { +func (a algorithm) FromReader(rd io.Reader) (Digest, error) { digester := a.Digester() if _, err := io.Copy(digester.Hash(), rd); err != nil { @@ -137,7 +170,7 @@ func (a Algorithm) FromReader(rd io.Reader) (Digest, error) { } // FromBytes digests the input and returns a Digest. -func (a Algorithm) FromBytes(p []byte) Digest { +func (a algorithm) FromBytes(p []byte) Digest { digester := a.Digester() if _, err := digester.Hash().Write(p); err != nil { @@ -153,6 +186,6 @@ func (a Algorithm) FromBytes(p []byte) Digest { } // FromString digests the string input and returns a Digest. -func (a Algorithm) FromString(s string) Digest { +func (a algorithm) FromString(s string) Digest { return a.FromBytes([]byte(s)) } diff --git a/algorithm_flag.go b/algorithm_flag.go new file mode 100644 index 0000000..c1fd2bb --- /dev/null +++ b/algorithm_flag.go @@ -0,0 +1,45 @@ +// Copyright 2017 go-digest 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 digest + +type AlgorithmFlag struct { + Algorithm Algorithm +} + +// String implements the flag.Value interface for Algorithms. +// https://golang.org/pkg/flag/#Value +func (flag *AlgorithmFlag) String() string { + if flag.Algorithm == nil { + return "unset" + } + return flag.Algorithm.String() +} + +// Set implements the flag.Value interface for Algorithms. +// https://golang.org/pkg/flag/#Value +func (flag *AlgorithmFlag) Set(value string) error { + if value == "" { + flag.Algorithm = Canonical + } else { + alg, ok := Algorithms[value] + if !ok || !alg.Available() { + return ErrDigestUnsupported + } + + flag.Algorithm = alg + } + + return nil +} diff --git a/algorithm_flag_test.go b/algorithm_flag_test.go new file mode 100644 index 0000000..d2309a5 --- /dev/null +++ b/algorithm_flag_test.go @@ -0,0 +1,77 @@ +// Copyright 2017 go-digest 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 digest_test + +import ( + _ "crypto/sha256" + _ "crypto/sha512" + "flag" + "strings" + "testing" + + "github.com/opencontainers/go-digest" +) + +func TestFlagInterface(t *testing.T) { + var ( + algFlag digest.AlgorithmFlag + flagSet flag.FlagSet + ) + + flagSet.Var(&algFlag, "algorithm", "set the digest algorithm") + for _, testcase := range []struct { + Name string + Args []string + Err error + Expected digest.Algorithm + }{ + { + Name: "Invalid", + Args: []string{"-algorithm", "bean"}, + Err: digest.ErrDigestUnsupported, + }, + { + Name: "Default", + Args: []string{"unrelated"}, + Expected: digest.SHA256, + }, + { + Name: "Other", + Args: []string{"-algorithm", "sha512"}, + Expected: digest.SHA512, + }, + } { + t.Run(testcase.Name, func(t *testing.T) { + algFlag = digest.AlgorithmFlag{ + Algorithm: digest.Canonical, + } + if err := flagSet.Parse(testcase.Args); err != testcase.Err { + if testcase.Err == nil { + t.Fatal("unexpected error", err) + } + + // check that flag package returns correct error + if !strings.Contains(err.Error(), testcase.Err.Error()) { + t.Fatalf("unexpected error: %v != %v", err, testcase.Err) + } + return + } + + if algFlag.Algorithm != testcase.Expected { + t.Fatalf("unexpected algorithm: %v != %v", algFlag.Algorithm, testcase.Expected) + } + }) + } +} diff --git a/algorithm_test.go b/algorithm_test.go index d50e849..0a53e5d 100644 --- a/algorithm_test.go +++ b/algorithm_test.go @@ -12,97 +12,47 @@ // See the License for the specific language governing permissions and // limitations under the License. -package digest +package digest_test import ( "bytes" "crypto/rand" _ "crypto/sha256" _ "crypto/sha512" - "flag" "fmt" - "strings" "testing" -) - -func TestFlagInterface(t *testing.T) { - var ( - alg Algorithm - flagSet flag.FlagSet - ) - - flagSet.Var(&alg, "algorithm", "set the digest algorithm") - for _, testcase := range []struct { - Name string - Args []string - Err error - Expected Algorithm - }{ - { - Name: "Invalid", - Args: []string{"-algorithm", "bean"}, - Err: ErrDigestUnsupported, - }, - { - Name: "Default", - Args: []string{"unrelated"}, - Expected: "sha256", - }, - { - Name: "Other", - Args: []string{"-algorithm", "sha512"}, - Expected: "sha512", - }, - } { - t.Run(testcase.Name, func(t *testing.T) { - alg = Canonical - if err := flagSet.Parse(testcase.Args); err != testcase.Err { - if testcase.Err == nil { - t.Fatal("unexpected error", err) - } - - // check that flag package returns correct error - if !strings.Contains(err.Error(), testcase.Err.Error()) { - t.Fatalf("unexpected error: %v != %v", err, testcase.Err) - } - return - } - if alg != testcase.Expected { - t.Fatalf("unexpected algorithm: %v != %v", alg, testcase.Expected) - } - }) - } -} + "github.com/opencontainers/go-digest" +) func TestFroms(t *testing.T) { p := make([]byte, 1<<20) rand.Read(p) - for alg := range algorithms { + for _, alg := range digest.Algorithms { h := alg.Hash() h.Write(p) - expected := Digest(fmt.Sprintf("%s:%x", alg, h.Sum(nil))) + expected := digest.Digest(fmt.Sprintf("%s:%x", alg, h.Sum(nil))) readerDgst, err := alg.FromReader(bytes.NewReader(p)) if err != nil { t.Fatalf("error calculating hash from reader: %v", err) } - dgsts := []Digest{ + dgsts := []digest.Digest{ alg.FromBytes(p), alg.FromString(string(p)), readerDgst, } - if alg == Canonical { - readerDgst, err := FromReader(bytes.NewReader(p)) + if alg == digest.Canonical { + readerDgst, err := digest.FromReader(bytes.NewReader(p)) if err != nil { t.Fatalf("error calculating hash from reader: %v", err) } dgsts = append(dgsts, - FromBytes(p), - FromString(string(p)), + digest.FromBytes(p), + digest.FromString(string(p)), readerDgst) } for _, dgst := range dgsts { diff --git a/digest.go b/digest.go index 69e1d2b..80b9c19 100644 --- a/digest.go +++ b/digest.go @@ -22,9 +22,9 @@ import ( "strings" ) -// Digest allows simple protection of hex formatted digest strings, prefixed -// by their algorithm. Strings of type Digest have some guarantee of being in -// the correct format and it provides quick access to the components of a +// Digest allows simple protection of algorithm-prefixed +// hashes. Strings of type Digest have some guarantee of being in the +// correct format and it provides quick access to the components of a // digest string. // // The following is an example of the contents of Digest types: @@ -35,9 +35,9 @@ import ( // terms. type Digest string -// NewDigest returns a Digest from alg and a hash.Hash object. +// NewDigest is deprecated. Use NewDigester(...).Digest() instead. func NewDigest(alg Algorithm, h hash.Hash) Digest { - return NewDigestFromBytes(alg, h.Sum(nil)) + return NewDigester(alg, h).Digest() } // NewDigestFromBytes returns a new digest from the byte contents of p. @@ -45,12 +45,17 @@ func NewDigest(alg Algorithm, h hash.Hash) Digest { // functions. This is also useful for rebuilding digests from binary // serializations. func NewDigestFromBytes(alg Algorithm, p []byte) Digest { - return Digest(fmt.Sprintf("%s:%x", alg, p)) + return NewDigestFromHash(alg.String(), alg.Encoding().EncodeToString(p)) } -// NewDigestFromHex returns a Digest from alg and a the hex encoded digest. +// NewDigestFromHash returns a Digest from the algorithm and hash. +func NewDigestFromHash(alg, hash string) Digest { + return Digest(fmt.Sprintf("%s:%s", alg, hash)) +} + +// NewDigestFromHex is deprecated. Use NewDigestFromHash instead. func NewDigestFromHex(alg, hex string) Digest { - return Digest(fmt.Sprintf("%s:%s", alg, hex)) + return NewDigestFromHash(alg, hex) } // DigestRegexp matches valid digest types. @@ -104,14 +109,12 @@ func (d Digest) Validate() error { return ErrDigestInvalidFormat } - algorithm := Algorithm(s[:i]) - if !algorithm.Available() { + algorithm, ok := Algorithms[s[:i]] + if !ok || !algorithm.Available() { return ErrDigestUnsupported } - // Digests much always be hex-encoded, ensuring that their hex portion will - // always be size*2 - if algorithm.Size()*2 != len(s[i+1:]) { + if algorithm.HashSize() != len(s[i+1:]) { return ErrDigestInvalidLength } @@ -121,24 +124,40 @@ func (d Digest) Validate() error { // Algorithm returns the algorithm portion of the digest. This will panic if // the underlying digest is not in a valid format. func (d Digest) Algorithm() Algorithm { - return Algorithm(d[:d.sepIndex()]) + identifier := string(d)[:d.sepIndex()] + if identifier == "" { + panic(fmt.Sprintf("empty digest algorithm for %v", d)) + } + alg, ok := Algorithms[identifier] + if !ok { + panic(fmt.Sprintf("unrecognized algorithm %v", identifier)) + } + if !alg.Available() { + panic(fmt.Sprintf("unavailable algorithm %v", identifier)) + } + return alg } // Verifier returns a writer object that can be used to verify a stream of // content against the digest. If the digest is invalid, the method will panic. func (d Digest) Verifier() Verifier { return hashVerifier{ - hash: d.Algorithm().Hash(), - digest: d, + digest: d, + digester: d.Algorithm().Digester(), } } -// Hex returns the hex digest portion of the digest. This will panic if the +// Hash returns the hash portion of the digest. This will panic if the // underlying digest is not in a valid format. -func (d Digest) Hex() string { +func (d Digest) Hash() string { return string(d[d.sepIndex()+1:]) } +// Hex is is deprecated. Use Hash instead. +func (d Digest) Hex() string { + return d.Hash() +} + func (d Digest) String() string { return string(d) } diff --git a/digest_test.go b/digest_test.go index 182f2dd..0c7b62c 100644 --- a/digest_test.go +++ b/digest_test.go @@ -12,65 +12,67 @@ // See the License for the specific language governing permissions and // limitations under the License. -package digest +package digest_test import ( "testing" + + "github.com/opencontainers/go-digest" ) func TestParseDigest(t *testing.T) { for _, testcase := range []struct { input string err error - algorithm Algorithm - hex string + algorithm digest.Algorithm + hash string }{ { input: "sha256:e58fcf7418d4390dec8e8fb69d88c06ec07039d651fedd3aa72af9972e7d046b", - algorithm: "sha256", - hex: "e58fcf7418d4390dec8e8fb69d88c06ec07039d651fedd3aa72af9972e7d046b", + algorithm: digest.SHA256, + hash: "e58fcf7418d4390dec8e8fb69d88c06ec07039d651fedd3aa72af9972e7d046b", }, { input: "sha384:d3fc7881460b7e22e3d172954463dddd7866d17597e7248453c48b3e9d26d9596bf9c4a9cf8072c9d5bad76e19af801d", - algorithm: "sha384", - hex: "d3fc7881460b7e22e3d172954463dddd7866d17597e7248453c48b3e9d26d9596bf9c4a9cf8072c9d5bad76e19af801d", + algorithm: digest.SHA384, + hash: "d3fc7881460b7e22e3d172954463dddd7866d17597e7248453c48b3e9d26d9596bf9c4a9cf8072c9d5bad76e19af801d", }, { - // empty hex + // empty hash input: "sha256:", - err: ErrDigestInvalidFormat, + err: digest.ErrDigestInvalidFormat, }, { - // empty hex + // empty hash input: ":", - err: ErrDigestInvalidFormat, + err: digest.ErrDigestInvalidFormat, }, { - // just hex + // just hash input: "d41d8cd98f00b204e9800998ecf8427e", - err: ErrDigestInvalidFormat, + err: digest.ErrDigestInvalidFormat, }, { - // not hex + // not hash input: "sha256:d41d8cd98f00b204e9800m98ecf8427e", - err: ErrDigestInvalidFormat, + err: digest.ErrDigestInvalidFormat, }, { // too short input: "sha256:abcdef0123456789", - err: ErrDigestInvalidLength, + err: digest.ErrDigestInvalidLength, }, { // too short (from different algorithm) input: "sha512:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", - err: ErrDigestInvalidLength, + err: digest.ErrDigestInvalidLength, }, { input: "foo:d41d8cd98f00b204e9800998ecf8427e", - err: ErrDigestUnsupported, + err: digest.ErrDigestUnsupported, }, } { - digest, err := Parse(testcase.input) + dgst, err := digest.Parse(testcase.input) if err != testcase.err { t.Fatalf("error differed from expected while parsing %q: %v != %v", testcase.input, err, testcase.err) } @@ -79,28 +81,28 @@ func TestParseDigest(t *testing.T) { continue } - if digest.Algorithm() != testcase.algorithm { - t.Fatalf("incorrect algorithm for parsed digest: %q != %q", digest.Algorithm(), testcase.algorithm) + if dgst.Algorithm() != testcase.algorithm { + t.Fatalf("incorrect algorithm for parsed digest: %q != %q", dgst.Algorithm(), testcase.algorithm) } - if digest.Hex() != testcase.hex { - t.Fatalf("incorrect hex for parsed digest: %q != %q", digest.Hex(), testcase.hex) + if dgst.Hash() != testcase.hash { + t.Fatalf("incorrect hash for parsed digest: %q != %q", dgst.Hash(), testcase.hash) } // Parse string return value and check equality - newParsed, err := Parse(digest.String()) + newParsed, err := digest.Parse(dgst.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) + if newParsed != dgst { + t.Fatalf("expected equal: %q != %q", newParsed, dgst) } - newFromHex := NewDigestFromHex(newParsed.Algorithm().String(), newParsed.Hex()) - if newFromHex != digest { - t.Fatalf("%v != %v", newFromHex, digest) + newFromHash := digest.NewDigestFromHash(newParsed.Algorithm().String(), newParsed.Hash()) + if newFromHash != dgst { + t.Fatalf("%v != %v", newFromHash, dgst) } } } diff --git a/digester.go b/digester.go index 36fa272..8704cf1 100644 --- a/digester.go +++ b/digester.go @@ -20,20 +20,39 @@ import "hash" // to the return value of Hash, while calling Digest will return the current // value of the digest. type Digester interface { - Hash() hash.Hash // provides direct access to underlying hash instance. + // Hash provides direct access to the underlying Hash instance. + Hash() hash.Hash + + // Digest returns the digest of the currently-hashed content. Digest() Digest } // digester provides a simple digester definition that embeds a hasher. type digester struct { - alg Algorithm - hash hash.Hash + name string + hash hash.Hash + encoding Encoding +} + +func NewDigester(alg Algorithm, hash hash.Hash) Digester { + return &digester{ + name: alg.String(), + hash: hash, + encoding: alg.Encoding(), + } } +// Hash provides direct access to the underlying Hash instance. func (d *digester) Hash() hash.Hash { return d.hash } +// hashString returns the current encoded hash. +func (d *digester) hashString() string { + return d.encoding.EncodeToString(d.hash.Sum(nil)) +} + +// Digest returns the digest of the currently-hashed content. func (d *digester) Digest() Digest { - return NewDigest(d.alg, d.hash) + return NewDigestFromHash(d.name, d.hashString()) } diff --git a/doc.go b/doc.go index 491ea1e..17f06eb 100644 --- a/doc.go +++ b/doc.go @@ -23,14 +23,14 @@ // The format of a digest is simply a string with two parts, dubbed the // "algorithm" and the "digest", separated by a colon: // -// : +// : // // An example of a sha256 digest representation follows: // // sha256:7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc // // In this case, the string "sha256" is the algorithm and the hex bytes are -// the "digest". +// the hash. // // Because the Digest type is simply a string, once a valid Digest is // obtained, comparisons are cheap, quick and simple to express with the diff --git a/encoding.go b/encoding.go new file mode 100644 index 0000000..163e645 --- /dev/null +++ b/encoding.go @@ -0,0 +1,46 @@ +// Copyright 2017 go-digest 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 digest + +import ( + _hex "encoding/hex" +) + +// Encoding identifies a hash encoding used by an Algorithm. +type Encoding interface { + // EncodeToString encodes src to a string. + EncodeToString(src []byte) string + + // DecodeString decodes s to a byte array. + DecodeString(s string) (raw []byte, err error) +} + +type hex struct{} + +var ( + // Hex is a lowercase version of the base 16 encoding defined in RFC + // 4648. https://tools.ietf.org/html/rfc4648#section-8 + Hex = hex{} +) + +// EncodeToString encodes src to a lowecase base 16 string. +func (h hex) EncodeToString(src []byte) string { + return _hex.EncodeToString(src) +} + +// DecodeString decodes a case-insensitive base 16 string to a byte array. +func (h hex) DecodeString(s string) (raw []byte, err error) { + return _hex.DecodeString(s) +} diff --git a/encoding_test.go b/encoding_test.go new file mode 100644 index 0000000..afc595a --- /dev/null +++ b/encoding_test.go @@ -0,0 +1,49 @@ +// Copyright 2017 go-digest 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 digest_test + +import ( + "bytes" + "testing" + + "github.com/opencontainers/go-digest" +) + +func TestHexEncode(t *testing.T) { + string := digest.Hex.EncodeToString([]byte{0x66, 0x6f, 0x6f}) + if string != "666f6f" { + t.Fatalf("error encoding 66 6f 6f to hex: %v", string) + } +} + +func TestHexDecode(t *testing.T) { + raw, err := digest.Hex.DecodeString("666f6f") + if err != nil { + t.Fatalf("error decoding 666f6f from hex: %v", err) + } + if !bytes.Equal(raw, []byte{0x66, 0x6f, 0x6f}) { + t.Fatalf("error decoding 666f6f from hex: %v", raw) + } +} + +func TestUppercaseHexDecode(t *testing.T) { + raw, err := digest.Hex.DecodeString("666F6F") + if err != nil { + t.Fatalf("error decoding 666F6F from hex: %v", err) + } + if !bytes.Equal(raw, []byte{0x66, 0x6f, 0x6f}) { + t.Fatalf("error decoding 666F6F from hex: %v", raw) + } +} diff --git a/verifiers.go b/verifiers.go index 32125e9..0d2a964 100644 --- a/verifiers.go +++ b/verifiers.go @@ -15,7 +15,6 @@ package digest import ( - "hash" "io" ) @@ -32,14 +31,14 @@ type Verifier interface { } type hashVerifier struct { - digest Digest - hash hash.Hash + digest Digest + digester Digester } func (hv hashVerifier) Write(p []byte) (n int, err error) { - return hv.hash.Write(p) + return hv.digester.Hash().Write(p) } func (hv hashVerifier) Verified() bool { - return hv.digest == NewDigest(hv.digest.Algorithm(), hv.hash) + return hv.digest == hv.digester.Digest() } diff --git a/verifiers_test.go b/verifiers_test.go index d67bb1b..d142ae0 100644 --- a/verifiers_test.go +++ b/verifiers_test.go @@ -12,22 +12,26 @@ // See the License for the specific language governing permissions and // limitations under the License. -package digest +package digest_test import ( "bytes" "crypto/rand" + _ "crypto/sha256" + _ "crypto/sha512" "io" "reflect" "testing" + + "github.com/opencontainers/go-digest" ) func TestDigestVerifier(t *testing.T) { p := make([]byte, 1<<20) rand.Read(p) - digest := FromBytes(p) + dgst := digest.FromBytes(p) - verifier := digest.Verifier() + verifier := dgst.Verifier() io.Copy(verifier, bytes.NewReader(p)) @@ -41,7 +45,7 @@ func TestDigestVerifier(t *testing.T) { func TestVerifierUnsupportedDigest(t *testing.T) { for _, testcase := range []struct { Name string - Digest Digest + Digest digest.Digest Expected interface{} // expected panic target }{ { @@ -52,17 +56,17 @@ func TestVerifierUnsupportedDigest(t *testing.T) { { Name: "EmptyAlg", Digest: ":", - Expected: "empty digest algorithm, validate before calling Algorithm.Hash()", + Expected: "empty digest algorithm for :", }, { Name: "Unsupported", - Digest: Digest("bean:0123456789abcdef"), - Expected: "bean not available (make sure it is imported)", + Digest: digest.Digest("bean:0123456789abcdef"), + Expected: "unrecognized algorithm bean", }, { Name: "Garbage", - Digest: Digest("sha256-garbage:pure"), - Expected: "sha256-garbage not available (make sure it is imported)", + Digest: digest.Digest("sha256-garbage:pure"), + Expected: "unrecognized algorithm sha256-garbage", }, } { t.Run(testcase.Name, func(t *testing.T) {