Skip to content

Commit

Permalink
Merge pull request #62 from sargun/hash-registry
Browse files Browse the repository at this point in the history
Implement dynamic hash registration
  • Loading branch information
vbatts authored Apr 21, 2021
2 parents 3499bfa + b9e02e0 commit 404628e
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 19 deletions.
90 changes: 71 additions & 19 deletions algorithm.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@
package digest

import (
"crypto"
"fmt"
"hash"
"io"
"regexp"
"sync"
)

// Algorithm identifies and implementation of a digester by an identifier.
Expand All @@ -30,41 +30,82 @@ type Algorithm string

// supported digest types
const (
SHA256 Algorithm = "sha256" // sha256 with hex encoding (lower case only)
SHA384 Algorithm = "sha384" // sha384 with hex encoding (lower case only)
SHA512 Algorithm = "sha512" // sha512 with hex encoding (lower case only)

// Canonical is the primary digest algorithm used with the distribution
// project. Other digests may be used but this one is the primary storage
// digest.
Canonical = SHA256
)

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.
algorithmRegexp = regexp.MustCompile(`^[a-z0-9]+([+._-][a-z0-9]+)*$`)
)

// CryptoHash is the interface that any hash algorithm must implement
type CryptoHash interface {
// 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() int
// New returns a new hash.Hash calculating the given hash function. If the hash function is not
// available, it may panic.
New() hash.Hash
}

// algorithms maps values to hash.Hash implementations. Other algorithms
var (
// algorithms maps values to CryptoHash 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,
}
//
// See: RegisterAlgorithm
algorithms = map[Algorithm]CryptoHash{}

// anchoredEncodedRegexps contains anchored regular expressions for hex-encoded digests.
// Note that /A-F/ disallowed.
anchoredEncodedRegexps = map[Algorithm]*regexp.Regexp{
SHA256: regexp.MustCompile(`^[a-f0-9]{64}$`),
SHA384: regexp.MustCompile(`^[a-f0-9]{96}$`),
SHA512: regexp.MustCompile(`^[a-f0-9]{128}$`),
}
anchoredEncodedRegexps = map[Algorithm]*regexp.Regexp{}

// algorithmsLock protects algorithms, and anchoredEncodedRegexps
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.
//
// 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.
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))
}

if _, ok := algorithms[algorithm]; ok {
return false
}

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
// custom regexp.
anchoredEncodedRegexps[algorithm] = hexDigestRegex(implementation)
return true
}

// hexDigestRegex can be used to generate a regex for RegisterAlgorithm.
func hexDigestRegex(cryptoHash CryptoHash) *regexp.Regexp {
hexdigestbytes := cryptoHash.Size() * 2
return regexp.MustCompile(fmt.Sprintf("^[a-f0-9]{%d}$", hexdigestbytes))
}

// 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 {
algorithmsLock.RLock()
defer algorithmsLock.RUnlock()

h, ok := algorithms[a]
if !ok {
return false
Expand All @@ -80,6 +121,9 @@ func (a Algorithm) String() string {

// Size returns number of bytes returned by the hash.
func (a Algorithm) Size() int {
algorithmsLock.RLock()
defer algorithmsLock.RUnlock()

h, ok := algorithms[a]
if !ok {
return 0
Expand Down Expand Up @@ -132,6 +176,8 @@ func (a Algorithm) Hash() hash.Hash {
panic(fmt.Sprintf("%v not available (make sure it is imported)", a))
}

algorithmsLock.RLock()
defer algorithmsLock.RUnlock()
return algorithms[a].New()
}

Expand All @@ -140,6 +186,9 @@ func (a Algorithm) Hash() hash.Hash {
func (a Algorithm) Encode(d []byte) string {
// TODO(stevvooe): Currently, all algorithms use a hex encoding. When we
// add support for back registration, we can modify this accordingly.
//
// We support dynamic registration now, but we do not allow for the user to
// specify their own custom format. Hash functions may only use hex encoding.
return fmt.Sprintf("%x", d)
}

Expand Down Expand Up @@ -177,6 +226,9 @@ func (a Algorithm) FromString(s string) Digest {

// Validate validates the encoded portion string
func (a Algorithm) Validate(encoded string) error {
algorithmsLock.RLock()
defer algorithmsLock.RUnlock()

r, ok := anchoredEncodedRegexps[a]
if !ok {
return ErrDigestUnsupported
Expand Down
37 changes: 37 additions & 0 deletions algorithm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package digest

import (
"bytes"
"crypto"
"crypto/rand"
_ "crypto/sha256"
_ "crypto/sha512"
Expand Down Expand Up @@ -113,3 +114,39 @@ func TestFroms(t *testing.T) {
}
}
}

func TestBadAlgorithmNameRegistration(t *testing.T) {
expectPanic := func(algorithm string) {
defer func() {
r := recover()
if r == nil {
t.Fatal("Expected panic and did not find one")
}
t.Logf("Captured panic: %v", r)
}()
// We just use SHA256 here as a test / stand-in
RegisterAlgorithm(Algorithm(algorithm), crypto.SHA256)
}

expectPanic("sha256-")
expectPanic("-")
expectPanic("SHA256")
expectPanic("sha25*")
}

func TestGoodAlgorithmNameRegistration(t *testing.T) {
expectNoPanic := func(algorithm string) {
defer func() {
r := recover()
if r != nil {
t.Fatalf("Expected panic and found one: %v", r)
}
}()

// We just use SHA256 here as a test / stand-in
RegisterAlgorithm(Algorithm(algorithm), crypto.SHA256)
}

expectNoPanic("sha256-test")
expectNoPanic("sha256_384")
}
17 changes: 17 additions & 0 deletions sha.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package digest

import (
"crypto"
)

const (
SHA256 Algorithm = "sha256" // sha256 with hex encoding (lower case only)
SHA384 Algorithm = "sha384" // sha384 with hex encoding (lower case only)
SHA512 Algorithm = "sha512" // sha512 with hex encoding (lower case only)
)

func init() {
RegisterAlgorithm(SHA256, crypto.SHA256)
RegisterAlgorithm(SHA384, crypto.SHA384)
RegisterAlgorithm(SHA512, crypto.SHA512)
}

0 comments on commit 404628e

Please sign in to comment.