From b9e02e015be61903bbee58e3fd349114fa28e0b4 Mon Sep 17 00:00:00 2001 From: Sargun Dhillon Date: Mon, 15 Feb 2021 00:29:21 -0800 Subject: [PATCH] Implement dynamic hash registration This adds a new interface to go-digest allowing for it to act as a registry for various hash implementations. It includes the new "CryptoHash" interface which hashers must implement. By default the SHA variants that were historically available are available. The cryptoHash interface mimics crypto.Hash, but is a subset of the methods that we require. crypto.Hash is a concrete type that makes it hard to bring new hash function implementations in. The primary impetus is to allow for out-of-tree hash implementations to be added. For example, if someone wanted to add the out-of-tree BLAKE3 AVX implementation, they could do that. Right now, if two versions of the same implementation are added, the one that is added first will take precedence, but in the future, we can add the ability to force registration. Signed-off-by: Sargun Dhillon --- algorithm.go | 90 +++++++++++++++++++++++++++++++++++++---------- algorithm_test.go | 37 +++++++++++++++++++ sha.go | 17 +++++++++ 3 files changed, 125 insertions(+), 19 deletions(-) create mode 100644 sha.go diff --git a/algorithm.go b/algorithm.go index 490951d..f69b5ec 100644 --- a/algorithm.go +++ b/algorithm.go @@ -16,11 +16,11 @@ package digest import ( - "crypto" "fmt" "hash" "io" "regexp" + "sync" ) // Algorithm identifies and implementation of a digester by an identifier. @@ -30,10 +30,6 @@ 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. @@ -41,30 +37,75 @@ 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. + 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 @@ -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 @@ -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() } @@ -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) } @@ -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 diff --git a/algorithm_test.go b/algorithm_test.go index 192e058..060819e 100644 --- a/algorithm_test.go +++ b/algorithm_test.go @@ -17,6 +17,7 @@ package digest import ( "bytes" + "crypto" "crypto/rand" _ "crypto/sha256" _ "crypto/sha512" @@ -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") +} diff --git a/sha.go b/sha.go new file mode 100644 index 0000000..0e2201f --- /dev/null +++ b/sha.go @@ -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) +}