Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/gotest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ jobs:
- name: Run tests
run: go test -v ./...
- name: Check formatted
run: gofmt -l .
run: gofmt -l .
45 changes: 45 additions & 0 deletions crypto/_testsuite/testsuite.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
mbase "github.com/multiformats/go-multibase"
"github.com/multiformats/go-varint"
"github.com/stretchr/testify/require"
"github.com/ucan-wg/go-varsig"

"github.com/MetaMask/go-did-it/crypto"
)
Expand Down Expand Up @@ -194,6 +195,7 @@ func TestSuite[PubT crypto.PublicKey, PrivT crypto.PrivateKey](t *testing.T, har
name string
signer func(msg []byte, opts ...crypto.SigningOption) ([]byte, error)
verifier func(msg []byte, sig []byte, opts ...crypto.SigningOption) bool
varsig func(opts ...crypto.SigningOption) varsig.Varsig
expectedSize int
stats *int
defaultHash crypto.Hash
Expand All @@ -210,6 +212,7 @@ func TestSuite[PubT crypto.PublicKey, PrivT crypto.PrivateKey](t *testing.T, har
name: "Bytes signature",
signer: spriv.SignToBytes,
verifier: spub.VerifyBytes,
varsig: spriv.Varsig,
expectedSize: harness.SignatureBytesSize,
stats: &stats.sigRawSize,
defaultHash: harness.DefaultHash,
Expand All @@ -227,6 +230,7 @@ func TestSuite[PubT crypto.PublicKey, PrivT crypto.PrivateKey](t *testing.T, har
name: "ASN.1 signature",
signer: spriv.SignToASN1,
verifier: spub.VerifyASN1,
varsig: spriv.Varsig,
stats: &stats.sigAsn1Size,
defaultHash: harness.DefaultHash,
otherHashes: harness.OtherHashes,
Expand All @@ -245,6 +249,9 @@ func TestSuite[PubT crypto.PublicKey, PrivT crypto.PrivateKey](t *testing.T, har
sigDefault, err := tc.signer(msg, crypto.WithSigningHash(tc.defaultHash))
require.NoError(t, err)

vsig := tc.varsig()
require.Equal(t, harness.DefaultHash.ToVarsigHash(), vsig.Hash())

if tc.expectedSize > 0 {
require.Equal(t, tc.expectedSize, len(sigNoParams))
}
Expand All @@ -253,13 +260,21 @@ func TestSuite[PubT crypto.PublicKey, PrivT crypto.PrivateKey](t *testing.T, har
// signatures might be different (i.e. non-deterministic), but they should verify the same way
valid := tc.verifier(msg, sigNoParams)
require.True(t, valid)
valid = tc.verifier(msg, sigNoParams, crypto.WithVarsig(vsig))
require.True(t, valid)
valid = tc.verifier(msg, sigDefault)
require.True(t, valid)
valid = tc.verifier(msg, sigDefault, crypto.WithVarsig(vsig))
require.True(t, valid)

valid = tc.verifier([]byte("wrong message"), sigNoParams)
require.False(t, valid)
valid = tc.verifier([]byte("wrong message"), sigNoParams, crypto.WithVarsig(vsig))
require.False(t, valid)
valid = tc.verifier([]byte("wrong message"), sigDefault)
require.False(t, valid)
valid = tc.verifier([]byte("wrong message"), sigDefault, crypto.WithVarsig(vsig))
require.False(t, valid)
})
for _, hash := range tc.otherHashes {
t.Run(fmt.Sprintf("%s-%s", tc.name, hash.String()), func(t *testing.T) {
Expand All @@ -269,11 +284,18 @@ func TestSuite[PubT crypto.PublicKey, PrivT crypto.PrivateKey](t *testing.T, har
require.NoError(t, err)
require.NotEmpty(t, sig)

vsig := tc.varsig(crypto.WithSigningHash(hash))
require.Equal(t, hash.ToVarsigHash(), vsig.Hash())

valid := tc.verifier(msg, sig, crypto.WithSigningHash(hash))
require.True(t, valid)
valid = tc.verifier(msg, sig, crypto.WithVarsig(vsig))
require.True(t, valid)

valid = tc.verifier([]byte("wrong message"), sig)
require.False(t, valid)
valid = tc.verifier([]byte("wrong message"), sig, crypto.WithVarsig(vsig))
require.False(t, valid)
})
}
}
Expand Down Expand Up @@ -503,6 +525,29 @@ func BenchSuite[PubT crypto.PublicKey, PrivT crypto.PrivateKey](b *testing.B, ha
}
})

b.Run("Verify from varsig signature", func(b *testing.B) {
if !pubImplements[PubT, crypto.PublicKeySigningBytes]() {
b.Skip("Signature to bytes is not implemented")
}

pub, priv, err := harness.GenerateKeyPair()
require.NoError(b, err)

spub := (crypto.PublicKey(pub)).(crypto.PublicKeySigningBytes)
spriv := (crypto.PrivateKey(priv)).(crypto.PrivateKeySigningBytes)

sig, err := spriv.SignToBytes([]byte("message"))
require.NoError(b, err)
vsig := spriv.Varsig()

b.ResetTimer()
b.ReportAllocs()

for i := 0; i < b.N; i++ {
spub.VerifyBytes([]byte("message"), sig, crypto.WithVarsig(vsig))
}
})

b.Run("Sign to ASN.1 signature", func(b *testing.B) {
if !pubImplements[PubT, crypto.PublicKeySigningASN1]() {
b.Skip("Signature to ASN.1 is not implemented")
Expand Down
18 changes: 15 additions & 3 deletions crypto/ed25519/private.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/pem"
"fmt"

"github.com/ucan-wg/go-varsig"
"golang.org/x/crypto/cryptobyte"

"github.com/MetaMask/go-did-it/crypto"
Expand All @@ -21,7 +22,7 @@ type PrivateKey struct {

// PrivateKeyFromBytes converts a serialized private key to a PrivateKey.
// This compact serialization format is the raw key material, without metadata or structure.
// It errors if the slice is not the right size.
// It returns an error if the slice is not the right size.
func PrivateKeyFromBytes(b []byte) (PrivateKey, error) {
if len(b) != PrivateKeyBytesSize {
return PrivateKey{}, fmt.Errorf("invalid ed25519 private key size")
Expand Down Expand Up @@ -73,21 +74,32 @@ func (p PrivateKey) Public() crypto.PublicKey {
return PublicKey{k: p.k.Public().(ed25519.PublicKey)}
}

func (p PrivateKey) Varsig(opts ...crypto.SigningOption) varsig.Varsig {
params := crypto.CollectSigningOptions(opts)
return varsig.NewEdDSAVarsig(varsig.CurveEd25519, params.HashOrDefault(crypto.SHA512).ToVarsigHash(), params.PayloadEncoding())
}

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 {

hash := params.HashOrDefault(crypto.SHA512)
if 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, opts ...crypto.SigningOption) ([]byte, error) {
params := crypto.CollectSigningOptions(opts)
if params.Hash != crypto.Hash(0) && params.Hash != crypto.SHA512 {

hash := params.HashOrDefault(crypto.SHA512)
if 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)
Expand Down
17 changes: 15 additions & 2 deletions crypto/ed25519/public.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"encoding/pem"
"fmt"

"github.com/ucan-wg/go-varsig"
"golang.org/x/crypto/cryptobyte"

"github.com/MetaMask/go-did-it/crypto"
Expand Down Expand Up @@ -101,21 +102,33 @@ func (p PublicKey) Equal(other crypto.PublicKey) 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 {

if !params.VarsigMatch(varsig.AlgorithmEdDSA, uint64(varsig.CurveEd25519), 0) {
return false
}

if params.HashOrDefault(crypto.SHA512) != 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, opts ...crypto.SigningOption) bool {
params := crypto.CollectSigningOptions(opts)
if params.Hash != crypto.Hash(0) && params.Hash != crypto.SHA512 {

if !params.VarsigMatch(varsig.AlgorithmEdDSA, uint64(varsig.CurveEd25519), 0) {
return false
}

if params.HashOrDefault(crypto.SHA512) != crypto.SHA512 {
// ed25519 does not support custom hash functions
return false
}

var s cryptobyte.String = signature
var bitString asn1.BitString

Expand Down
143 changes: 104 additions & 39 deletions crypto/hash.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,41 +5,15 @@ import (
"hash"
"strconv"

"github.com/ucan-wg/go-varsig"
"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
Expand Down Expand Up @@ -71,7 +45,20 @@ const (
maxHash
)

var hashes = make([]func() hash.Hash, maxHash-maxStdHash-1)
// 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()
}
if h > maxStdHash && h < maxHash {
return hashNames[h-maxStdHash-1]
}
panic("requested hash #" + strconv.Itoa(int(h)) + " is unavailable")
}
Comment thread
MichaelMure marked this conversation as resolved.

// New returns a new hash.Hash calculating the given hash function. New panics
// if the hash function is not linked into the binary.
Expand All @@ -80,23 +67,101 @@ func (h Hash) New() hash.Hash {
return stdcrypto.Hash(h).New()
}
if h > maxStdHash && h < maxHash {
f := hashes[h-maxStdHash-1]
f := hashFns[h-maxStdHash-1]
if f != nil {
return f()
}
}
panic("crypto: requested hash function #" + strconv.Itoa(int(h)) + " is unavailable")
panic("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")
func (h Hash) ToVarsigHash() varsig.Hash {
if h == MD5SHA1 {
panic("no multihash/multicodec value exists for MD5+SHA1")
}
if h < maxHash {
return hashVarsigs[h]
}
if h <= maxStdHash {
panic("crypto: RegisterHash of standard hash function")
panic("requested hash #" + strconv.Itoa(int(h)) + " is unavailable")
}

func FromVarsigHash(h varsig.Hash) Hash {
switch h {
case varsig.HashMd4:
return MD4
case varsig.HashMd5:
return MD5
case varsig.HashSha1:
return SHA1
case varsig.HashSha2_224:
return SHA224
case varsig.HashSha2_256:
return SHA256
case varsig.HashSha2_384:
return SHA384
case varsig.HashSha2_512:
return SHA512
case varsig.HashRipemd_160:
return RIPEMD160
case varsig.HashSha3_224:
return SHA3_224
case varsig.HashSha3_256:
return SHA3_256
case varsig.HashSha3_384:
return SHA3_384
case varsig.HashSha3_512:
return SHA3_512
case varsig.HashSha512_224:
return SHA512_224
case varsig.HashSha512_256:
return SHA512_256
case varsig.HashBlake2s_256:
return BLAKE2s_256
case varsig.HashBlake2b_256:
return BLAKE2b_256
case varsig.HashBlake2b_384:
return BLAKE2b_384
case varsig.HashBlake2b_512:
return BLAKE2b_512
case varsig.HashKeccak_256:
return KECCAK_256
case varsig.HashKeccak_512:
return KECCAK_512
default:
panic("varsig " + strconv.Itoa(int(h)) + " is not supported")
}
hashes[h-maxStdHash-1] = f
}

var hashNames = []string{
"Keccak-256",
"Keccak-512",
}
var hashFns = []func() hash.Hash{
sha3.NewLegacyKeccak256,
sha3.NewLegacyKeccak512,
}
var hashVarsigs = []varsig.Hash{
0, // undef
varsig.HashMd4,
varsig.HashMd5,
varsig.HashSha1,
varsig.HashSha2_224,
varsig.HashSha2_256,
varsig.HashSha2_384,
varsig.HashSha2_512,
0, // missing MD5SHA1
varsig.HashRipemd_160,
varsig.HashSha3_224,
varsig.HashSha3_256,
varsig.HashSha3_384,
varsig.HashSha3_512,
varsig.HashSha512_224,
varsig.HashSha512_256,
varsig.HashBlake2s_256,
varsig.HashBlake2b_256,
varsig.HashBlake2b_384,
varsig.HashBlake2b_512,
0, // maxStdHash
varsig.HashKeccak_256,
varsig.HashKeccak_512,
}
Loading