crypto: allow to select the hashing algorithm for sign/verify

This commit is contained in:
Michael Muré
2025-07-08 12:57:06 +02:00
parent 98da79494e
commit e6d39009ba
17 changed files with 247 additions and 81 deletions

View File

@@ -3,6 +3,7 @@ package ed25519
import (
"testing"
"github.com/INFURA/go-did/crypto"
"github.com/INFURA/go-did/crypto/_testsuite"
)
@@ -17,6 +18,8 @@ var harness = testsuite.TestHarness[PublicKey, PrivateKey]{
PrivateKeyFromPKCS8DER: PrivateKeyFromPKCS8DER,
PrivateKeyFromPKCS8PEM: PrivateKeyFromPKCS8PEM,
MultibaseCode: MultibaseCode,
DefaultHash: crypto.SHA512,
OtherHashes: nil,
PublicKeyBytesSize: PublicKeyBytesSize,
PrivateKeyBytesSize: PrivateKeyBytesSize,
SignatureBytesSize: SignatureBytesSize,

View File

@@ -68,13 +68,21 @@ func (p PrivateKey) Public() crypto.PublicKey {
return PublicKey{k: p.k.Public().(ed25519.PublicKey)}
}
func (p PrivateKey) SignToBytes(message []byte) ([]byte, error) {
func (p PrivateKey) SignToBytes(message []byte, opts ...crypto.SigningOption) ([]byte, error) {
params := crypto.CollectSigningOptions(opts)
if params.Hash != crypto.Hash(0) && params.Hash != crypto.SHA512 {
return nil, fmt.Errorf("ed25519 does not support custom hash functions")
}
return ed25519.Sign(p.k, message), nil
}
// SignToASN1 creates a signature with ASN.1 encoding.
// This ASN.1 encoding uses a BIT STRING, which would be correct for an X.509 certificate.
func (p PrivateKey) SignToASN1(message []byte) ([]byte, error) {
func (p PrivateKey) SignToASN1(message []byte, opts ...crypto.SigningOption) ([]byte, error) {
params := crypto.CollectSigningOptions(opts)
if params.Hash != crypto.Hash(0) && params.Hash != crypto.SHA512 {
return nil, fmt.Errorf("ed25519 does not support custom hash functions")
}
sig := ed25519.Sign(p.k, message)
var b cryptobyte.Builder
b.AddASN1BitString(sig)

View File

@@ -98,13 +98,23 @@ func (p PublicKey) Equal(other crypto.PublicKey) bool {
return false
}
func (p PublicKey) VerifyBytes(message, signature []byte) bool {
func (p PublicKey) VerifyBytes(message, signature []byte, opts ...crypto.SigningOption) bool {
params := crypto.CollectSigningOptions(opts)
if params.Hash != crypto.Hash(0) && params.Hash != crypto.SHA512 {
// ed25519 does not support custom hash functions
return false
}
return ed25519.Verify(p.k, message, signature)
}
// VerifyASN1 verifies a signature with ASN.1 encoding.
// This ASN.1 encoding uses a BIT STRING, which would be correct for an X.509 certificate.
func (p PublicKey) VerifyASN1(message, signature []byte) bool {
func (p PublicKey) VerifyASN1(message, signature []byte, opts ...crypto.SigningOption) bool {
params := crypto.CollectSigningOptions(opts)
if params.Hash != crypto.Hash(0) && params.Hash != crypto.SHA512 {
// ed25519 does not support custom hash functions
return false
}
var s cryptobyte.String = signature
var bitString asn1.BitString

102
crypto/hash.go Normal file
View File

@@ -0,0 +1,102 @@
package crypto
import (
stdcrypto "crypto"
"hash"
"strconv"
"golang.org/x/crypto/sha3"
)
// As the standard crypto library prohibits from registering additional hash algorithm (like keccak),
// below is essentially an extension of that mechanism to allow it.
func init() {
RegisterHash(KECCAK_256, sha3.NewLegacyKeccak256)
RegisterHash(KECCAK_512, sha3.NewLegacyKeccak512)
}
type Hash uint
// HashFunc simply returns the value of h so that [Hash] implements [SignerOpts].
func (h Hash) HashFunc() Hash {
return h
}
func (h Hash) String() string {
if h < maxStdHash {
return stdcrypto.Hash(h).String()
}
// Extensions
switch h {
case KECCAK_256:
return "Keccak-256"
case KECCAK_512:
return "Keccak-512"
default:
return "unknown hash value " + strconv.Itoa(int(h))
}
}
const (
// From "crypto"
MD4 Hash = 1 + iota // import golang.org/x/crypto/md4
MD5 // import crypto/md5
SHA1 // import crypto/sha1
SHA224 // import crypto/sha256
SHA256 // import crypto/sha256
SHA384 // import crypto/sha512
SHA512 // import crypto/sha512
MD5SHA1 // no implementation; MD5+SHA1 used for TLS RSA
RIPEMD160 // import golang.org/x/crypto/ripemd160
SHA3_224 // import golang.org/x/crypto/sha3
SHA3_256 // import golang.org/x/crypto/sha3
SHA3_384 // import golang.org/x/crypto/sha3
SHA3_512 // import golang.org/x/crypto/sha3
SHA512_224 // import crypto/sha512
SHA512_256 // import crypto/sha512
BLAKE2s_256 // import golang.org/x/crypto/blake2s
BLAKE2b_256 // import golang.org/x/crypto/blake2b
BLAKE2b_384 // import golang.org/x/crypto/blake2b
BLAKE2b_512 // import golang.org/x/crypto/blake2b
maxStdHash
// Extensions
KECCAK_256
KECCAK_512
maxHash
)
var hashes = make([]func() hash.Hash, maxHash-maxStdHash-1)
// New returns a new hash.Hash calculating the given hash function. New panics
// if the hash function is not linked into the binary.
func (h Hash) New() hash.Hash {
if h > 0 && h < maxStdHash {
return stdcrypto.Hash(h).New()
}
if h > maxStdHash && h < maxHash {
f := hashes[h-maxStdHash-1]
if f != nil {
return f()
}
}
panic("crypto: requested hash function #" + strconv.Itoa(int(h)) + " is unavailable")
}
// RegisterHash registers a function that returns a new instance of the given
// hash function. This is intended to be called from the init function in
// packages that implement hash functions.
func RegisterHash(h Hash, f func() hash.Hash) {
if h >= maxHash {
panic("crypto: RegisterHash of unknown hash function")
}
if h <= maxStdHash {
panic("crypto: RegisterHash of standard hash function")
}
hashes[h-maxStdHash-1] = f
}

View File

@@ -32,10 +32,10 @@ type PublicKeySigning interface {
// VerifyBytes checks a signature in the "raw bytes" format.
// This format can make some assumptions and may not be what you expect.
// Ideally, this format is defined by the same specification as the underlying crypto scheme.
VerifyBytes(message, signature []byte) bool
VerifyBytes(message, signature []byte, opts ...SigningOption) bool
// VerifyASN1 checks a signature in the ASN.1 format.
VerifyASN1(message, signature []byte) bool
VerifyASN1(message, signature []byte, opts ...SigningOption) bool
}
// Private Key
@@ -69,10 +69,10 @@ type PrivateKeySigning interface {
// SignToBytes creates a signature in the "raw bytes" format.
// This format can make some assumptions and may not be what you expect.
// Ideally, this format is defined by the same specification as the underlying crypto scheme.
SignToBytes(message []byte) ([]byte, error)
SignToBytes(message []byte, opts ...SigningOption) ([]byte, error)
// SignToASN1 creates a signature in the ASN.1 format.
SignToASN1(message []byte) ([]byte, error)
SignToASN1(message []byte, opts ...SigningOption) ([]byte, error)
}
type PrivateKeyKeyExchange interface {

29
crypto/options.go Normal file
View File

@@ -0,0 +1,29 @@
package crypto
type SigningOpts struct {
Hash Hash
}
func CollectSigningOptions(opts []SigningOption) SigningOpts {
res := SigningOpts{}
for _, opt := range opts {
opt(&res)
}
return res
}
func (opts SigningOpts) HashOrDefault(_default Hash) Hash {
if opts.Hash == 0 {
return _default
}
return opts.Hash
}
type SigningOption func(opts *SigningOpts)
// WithSigningHash specify the hash algorithm to be used for signatures
func WithSigningHash(hash Hash) SigningOption {
return func(opts *SigningOpts) {
opts.Hash = hash
}
}

View File

@@ -3,6 +3,7 @@ package p256
import (
"testing"
"github.com/INFURA/go-did/crypto"
"github.com/INFURA/go-did/crypto/_testsuite"
)
@@ -17,6 +18,8 @@ var harness = testsuite.TestHarness[*PublicKey, *PrivateKey]{
PrivateKeyFromPKCS8DER: PrivateKeyFromPKCS8DER,
PrivateKeyFromPKCS8PEM: PrivateKeyFromPKCS8PEM,
MultibaseCode: MultibaseCode,
DefaultHash: crypto.SHA256,
OtherHashes: []crypto.Hash{crypto.SHA224, crypto.SHA384, crypto.SHA512},
PublicKeyBytesSize: PublicKeyBytesSize,
PrivateKeyBytesSize: PrivateKeyBytesSize,
SignatureBytesSize: SignatureBytesSize,

View File

@@ -4,7 +4,6 @@ import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"crypto/x509"
"encoding/pem"
"fmt"
@@ -96,15 +95,13 @@ func (p *PrivateKey) ToPKCS8PEM() string {
}))
}
/*
Note: signatures for the crypto.PrivateKeySigning interface assumes SHA256,
which should be correct almost always. If there is a need to use a different
hash function, we can add separate functions that have that flexibility.
*/
// The default signing hash is SHA-256.
func (p *PrivateKey) SignToBytes(message []byte, opts ...crypto.SigningOption) ([]byte, error) {
params := crypto.CollectSigningOptions(opts)
func (p *PrivateKey) SignToBytes(message []byte) ([]byte, error) {
// Hash the message with SHA-256
hash := sha256.Sum256(message)
hasher := params.HashOrDefault(crypto.SHA256).New()
hasher.Write(message)
hash := hasher.Sum(nil)
r, s, err := ecdsa.Sign(rand.Reader, p.k, hash[:])
if err != nil {
@@ -118,9 +115,13 @@ func (p *PrivateKey) SignToBytes(message []byte) ([]byte, error) {
return sig, nil
}
func (p *PrivateKey) SignToASN1(message []byte) ([]byte, error) {
// Hash the message with SHA-256
hash := sha256.Sum256(message)
// The default signing hash is SHA-256.
func (p *PrivateKey) SignToASN1(message []byte, opts ...crypto.SigningOption) ([]byte, error) {
params := crypto.CollectSigningOptions(opts)
hasher := params.HashOrDefault(crypto.SHA256).New()
hasher.Write(message)
hash := hasher.Sum(nil)
return ecdsa.SignASN1(rand.Reader, p.k, hash[:])
}

View File

@@ -3,7 +3,6 @@ package p256
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/sha256"
"crypto/x509"
"encoding/pem"
"fmt"
@@ -124,13 +123,8 @@ func (p *PublicKey) ToX509PEM() string {
}))
}
/*
Note: signatures for the crypto.PrivateKeySigning interface assumes SHA256,
which should be correct almost always. If there is a need to use a different
hash function, we can add separate functions that have that flexibility.
*/
func (p *PublicKey) VerifyBytes(message, signature []byte) bool {
// The default signing hash is SHA-256.
func (p *PublicKey) VerifyBytes(message, signature []byte, opts ...crypto.SigningOption) bool {
if len(signature) != SignatureBytesSize {
return false
}
@@ -142,12 +136,16 @@ func (p *PublicKey) VerifyBytes(message, signature []byte) bool {
return false
}
return p.VerifyASN1(message, sigAsn1)
return p.VerifyASN1(message, sigAsn1, opts...)
}
func (p *PublicKey) VerifyASN1(message, signature []byte) bool {
// Hash the message with SHA-256
hash := sha256.Sum256(message)
// The default signing hash is SHA-256.
func (p *PublicKey) VerifyASN1(message, signature []byte, opts ...crypto.SigningOption) bool {
params := crypto.CollectSigningOptions(opts)
hasher := params.HashOrDefault(crypto.SHA256).New()
hasher.Write(message)
hash := hasher.Sum(nil)
return ecdsa.VerifyASN1(p.k, hash[:], signature)
}

View File

@@ -3,6 +3,7 @@ package p384
import (
"testing"
"github.com/INFURA/go-did/crypto"
"github.com/INFURA/go-did/crypto/_testsuite"
)
@@ -17,6 +18,8 @@ var harness = testsuite.TestHarness[*PublicKey, *PrivateKey]{
PrivateKeyFromPKCS8DER: PrivateKeyFromPKCS8DER,
PrivateKeyFromPKCS8PEM: PrivateKeyFromPKCS8PEM,
MultibaseCode: MultibaseCode,
DefaultHash: crypto.SHA384,
OtherHashes: []crypto.Hash{crypto.SHA256, crypto.SHA512},
PublicKeyBytesSize: PublicKeyBytesSize,
PrivateKeyBytesSize: PrivateKeyBytesSize,
SignatureBytesSize: SignatureBytesSize,

View File

@@ -4,7 +4,6 @@ import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha512"
"crypto/x509"
"encoding/pem"
"fmt"
@@ -96,15 +95,13 @@ func (p *PrivateKey) ToPKCS8PEM() string {
}))
}
/*
Note: signatures for the crypto.PrivateKeySigning interface assumes SHA384,
which should be correct almost always. If there is a need to use a different
hash function, we can add separate functions that have that flexibility.
*/
// The default signing hash is SHA-384.
func (p *PrivateKey) SignToBytes(message []byte, opts ...crypto.SigningOption) ([]byte, error) {
params := crypto.CollectSigningOptions(opts)
func (p *PrivateKey) SignToBytes(message []byte) ([]byte, error) {
// Hash the message with SHA-384
hash := sha512.Sum384(message)
hasher := params.HashOrDefault(crypto.SHA384).New()
hasher.Write(message)
hash := hasher.Sum(nil)
r, s, err := ecdsa.Sign(rand.Reader, p.k, hash[:])
if err != nil {
@@ -118,9 +115,13 @@ func (p *PrivateKey) SignToBytes(message []byte) ([]byte, error) {
return sig, nil
}
func (p *PrivateKey) SignToASN1(message []byte) ([]byte, error) {
// Hash the message with SHA-384
hash := sha512.Sum384(message)
// The default signing hash is SHA-384.
func (p *PrivateKey) SignToASN1(message []byte, opts ...crypto.SigningOption) ([]byte, error) {
params := crypto.CollectSigningOptions(opts)
hasher := params.HashOrDefault(crypto.SHA384).New()
hasher.Write(message)
hash := hasher.Sum(nil)
return ecdsa.SignASN1(rand.Reader, p.k, hash[:])
}

View File

@@ -3,7 +3,6 @@ package p384
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/sha512"
"crypto/x509"
"encoding/pem"
"fmt"
@@ -124,13 +123,8 @@ func (p *PublicKey) ToX509PEM() string {
}))
}
/*
Note: signatures for the crypto.PrivateKeySigning interface assumes SHA384,
which should be correct almost always. If there is a need to use a different
hash function, we can add separate functions that have that flexibility.
*/
func (p *PublicKey) VerifyBytes(message, signature []byte) bool {
// The default signing hash is SHA-384.
func (p *PublicKey) VerifyBytes(message, signature []byte, opts ...crypto.SigningOption) bool {
if len(signature) != SignatureBytesSize {
return false
}
@@ -142,12 +136,16 @@ func (p *PublicKey) VerifyBytes(message, signature []byte) bool {
return false
}
return p.VerifyASN1(message, sigAsn1)
return p.VerifyASN1(message, sigAsn1, opts...)
}
func (p *PublicKey) VerifyASN1(message, signature []byte) bool {
// Hash the message with SHA-384
hash := sha512.Sum384(message)
// The default signing hash is SHA-384.
func (p *PublicKey) VerifyASN1(message, signature []byte, opts ...crypto.SigningOption) bool {
params := crypto.CollectSigningOptions(opts)
hasher := params.HashOrDefault(crypto.SHA384).New()
hasher.Write(message)
hash := hasher.Sum(nil)
return ecdsa.VerifyASN1(p.k, hash[:], signature)
}

View File

@@ -3,6 +3,7 @@ package p521
import (
"testing"
"github.com/INFURA/go-did/crypto"
"github.com/INFURA/go-did/crypto/_testsuite"
)
@@ -17,6 +18,8 @@ var harness = testsuite.TestHarness[*PublicKey, *PrivateKey]{
PrivateKeyFromPKCS8DER: PrivateKeyFromPKCS8DER,
PrivateKeyFromPKCS8PEM: PrivateKeyFromPKCS8PEM,
MultibaseCode: MultibaseCode,
DefaultHash: crypto.SHA512,
OtherHashes: []crypto.Hash{crypto.SHA384},
PublicKeyBytesSize: PublicKeyBytesSize,
PrivateKeyBytesSize: PrivateKeyBytesSize,
SignatureBytesSize: SignatureBytesSize,

View File

@@ -4,7 +4,6 @@ import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha512"
"crypto/x509"
"encoding/pem"
"fmt"
@@ -96,15 +95,13 @@ func (p *PrivateKey) ToPKCS8PEM() string {
}))
}
/*
Note: signatures for the crypto.PrivateKeySigning interface assumes SHA512,
which should be correct almost always. If there is a need to use a different
hash function, we can add separate functions that have that flexibility.
*/
// The default signing hash is SHA-512.
func (p *PrivateKey) SignToBytes(message []byte, opts ...crypto.SigningOption) ([]byte, error) {
params := crypto.CollectSigningOptions(opts)
func (p *PrivateKey) SignToBytes(message []byte) ([]byte, error) {
// Hash the message with SHA-512
hash := sha512.Sum512(message)
hasher := params.HashOrDefault(crypto.SHA512).New()
hasher.Write(message)
hash := hasher.Sum(nil)
r, s, err := ecdsa.Sign(rand.Reader, p.k, hash[:])
if err != nil {
@@ -118,9 +115,13 @@ func (p *PrivateKey) SignToBytes(message []byte) ([]byte, error) {
return sig, nil
}
func (p *PrivateKey) SignToASN1(message []byte) ([]byte, error) {
// Hash the message with SHA-512
hash := sha512.Sum512(message)
// The default signing hash is SHA-512.
func (p *PrivateKey) SignToASN1(message []byte, opts ...crypto.SigningOption) ([]byte, error) {
params := crypto.CollectSigningOptions(opts)
hasher := params.HashOrDefault(crypto.SHA512).New()
hasher.Write(message)
hash := hasher.Sum(nil)
return ecdsa.SignASN1(rand.Reader, p.k, hash[:])
}

View File

@@ -3,7 +3,6 @@ package p521
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/sha512"
"crypto/x509"
"encoding/pem"
"fmt"
@@ -124,13 +123,8 @@ func (p *PublicKey) ToX509PEM() string {
}))
}
/*
Note: signatures for the crypto.PrivateKeySigning interface assumes SHA512,
which should be correct almost always. If there is a need to use a different
hash function, we can add separate functions that have that flexibility.
*/
func (p *PublicKey) VerifyBytes(message, signature []byte) bool {
// The default signing hash is SHA-512.
func (p *PublicKey) VerifyBytes(message, signature []byte, opts ...crypto.SigningOption) bool {
if len(signature) != SignatureBytesSize {
return false
}
@@ -142,12 +136,16 @@ func (p *PublicKey) VerifyBytes(message, signature []byte) bool {
return false
}
return p.VerifyASN1(message, sigAsn1)
return p.VerifyASN1(message, sigAsn1, opts...)
}
func (p *PublicKey) VerifyASN1(message, signature []byte) bool {
// Hash the message with SHA-512
hash := sha512.Sum512(message)
// The default signing hash is SHA-512.
func (p *PublicKey) VerifyASN1(message, signature []byte, opts ...crypto.SigningOption) bool {
params := crypto.CollectSigningOptions(opts)
hasher := params.HashOrDefault(crypto.SHA512).New()
hasher.Write(message)
hash := hasher.Sum(nil)
return ecdsa.VerifyASN1(p.k, hash[:], signature)
}

4
go.mod
View File

@@ -5,6 +5,8 @@ go 1.23.0
toolchain go1.23.1
require (
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0
github.com/mr-tron/base58 v1.1.0
github.com/multiformats/go-multibase v0.2.0
github.com/multiformats/go-varint v0.0.7
github.com/stretchr/testify v1.10.0
@@ -13,9 +15,9 @@ require (
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/mr-tron/base58 v1.1.0 // indirect
github.com/multiformats/go-base32 v0.0.3 // indirect
github.com/multiformats/go-base36 v0.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sys v0.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

6
go.sum
View File

@@ -1,5 +1,9 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/mr-tron/base58 v1.1.0 h1:Y51FGVJ91WBqCEabAi5OPUz38eAx8DakuAm5svLcsfQ=
github.com/mr-tron/base58 v1.1.0/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8=
github.com/multiformats/go-base32 v0.0.3 h1:tw5+NhuwaOjJCC5Pp82QuXbrmLzWg7uxlMFp8Nq/kkI=
@@ -16,6 +20,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=