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)
}