Merge pull request #17 from MetaMask/varsig

crypto: integrate varsig
This commit is contained in:
Michael Muré
2025-08-05 16:25:39 +02:00
committed by GitHub
30 changed files with 451 additions and 123 deletions

View File

@@ -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"
)
@@ -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
@@ -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,
@@ -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,
@@ -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))
}
@@ -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) {
@@ -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)
})
}
}
@@ -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")

View File

@@ -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"
@@ -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")
@@ -73,11 +74,19 @@ 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
}
@@ -85,9 +94,12 @@ func (p PrivateKey) SignToBytes(message []byte, opts ...crypto.SigningOption) ([
// 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)

View File

@@ -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"
@@ -101,10 +102,16 @@ 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)
}
@@ -112,10 +119,16 @@ func (p PublicKey) VerifyBytes(message, signature []byte, opts ...crypto.Signing
// 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

View File

@@ -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
@@ -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")
}
// New returns a new hash.Hash calculating the given hash function. New panics
// if the hash function is not linked into the binary.
@@ -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 <= maxStdHash {
panic("crypto: RegisterHash of standard hash function")
if h < maxHash {
return hashVarsigs[h]
}
hashes[h-maxStdHash-1] = f
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")
}
}
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,
}

View File

@@ -1,7 +1,17 @@
package crypto
import (
"github.com/ucan-wg/go-varsig"
)
type SigningOpts struct {
Hash Hash
hash Hash
payloadEncoding varsig.PayloadEncoding
// if WithVarsig is used
algo varsig.Algorithm
curve uint64
keyLen uint64
}
func CollectSigningOptions(opts []SigningOption) SigningOpts {
@@ -13,10 +23,34 @@ func CollectSigningOptions(opts []SigningOption) SigningOpts {
}
func (opts SigningOpts) HashOrDefault(_default Hash) Hash {
if opts.Hash == 0 {
if opts.hash == 0 {
return _default
}
return opts.Hash
return opts.hash
}
func (opts SigningOpts) PayloadEncoding() varsig.PayloadEncoding {
if opts.payloadEncoding == 0 {
return varsig.PayloadEncodingVerbatim
}
return opts.payloadEncoding
}
func (opts SigningOpts) VarsigMatch(algo varsig.Algorithm, curve uint64, keyLength uint64) bool {
// This is relatively ugly, but we get cyclic import otherwise
switch opts.algo {
case 0:
// not varsig to compare
return true
case varsig.AlgorithmECDSA:
return algo == varsig.AlgorithmECDSA && opts.curve == curve
case varsig.AlgorithmEdDSA:
return algo == varsig.AlgorithmEdDSA && opts.curve == curve
case varsig.AlgorithmRSA:
return algo == varsig.AlgorithmRSA && opts.keyLen == keyLength
default:
panic("unreachable")
}
}
type SigningOption func(opts *SigningOpts)
@@ -24,6 +58,34 @@ 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
opts.hash = hash
}
}
// WithPayloadEncoding specify the encoding that was used on the message before signing it.
// This will be included in the resulting varsig.
func WithPayloadEncoding(encoding varsig.PayloadEncoding) SigningOption {
return func(opts *SigningOpts) {
opts.payloadEncoding = encoding
}
}
// WithVarsig configure the signing or verification parameters from a varsig.
// If you use WithVarsig, you should NOT use other options.
func WithVarsig(vsig varsig.Varsig) SigningOption {
return func(opts *SigningOpts) {
opts.payloadEncoding = vsig.PayloadEncoding()
opts.hash = FromVarsigHash(vsig.Hash())
opts.algo = vsig.Algorithm()
switch vsig := vsig.(type) {
case varsig.EdDSAVarsig:
opts.curve = uint64(vsig.Curve())
case varsig.ECDSAVarsig:
opts.curve = uint64(vsig.Curve())
case varsig.RSAVarsig:
opts.keyLen = vsig.KeyLength()
default:
panic("unreachable")
}
}
}

View File

@@ -9,6 +9,8 @@ import (
"fmt"
"math/big"
"github.com/ucan-wg/go-varsig"
"github.com/MetaMask/go-did-it/crypto"
)
@@ -99,6 +101,12 @@ func (p *PrivateKey) ToPKCS8PEM() string {
}))
}
// The default signing hash is SHA-256.
func (p *PrivateKey) Varsig(opts ...crypto.SigningOption) varsig.Varsig {
params := crypto.CollectSigningOptions(opts)
return varsig.NewECDSAVarsig(varsig.CurveP256, params.HashOrDefault(crypto.SHA256).ToVarsigHash(), params.PayloadEncoding())
}
// The default signing hash is SHA-256.
func (p *PrivateKey) SignToBytes(message []byte, opts ...crypto.SigningOption) ([]byte, error) {
params := crypto.CollectSigningOptions(opts)

View File

@@ -8,6 +8,8 @@ import (
"fmt"
"math/big"
"github.com/ucan-wg/go-varsig"
"github.com/MetaMask/go-did-it/crypto"
helpers "github.com/MetaMask/go-did-it/crypto/internal"
)
@@ -148,6 +150,10 @@ func (p *PublicKey) VerifyBytes(message, signature []byte, opts ...crypto.Signin
func (p *PublicKey) VerifyASN1(message, signature []byte, opts ...crypto.SigningOption) bool {
params := crypto.CollectSigningOptions(opts)
if !params.VarsigMatch(varsig.AlgorithmECDSA, uint64(varsig.CurveP256), 0) {
return false
}
hasher := params.HashOrDefault(crypto.SHA256).New()
hasher.Write(message)
hash := hasher.Sum(nil)

View File

@@ -9,6 +9,8 @@ import (
"fmt"
"math/big"
"github.com/ucan-wg/go-varsig"
"github.com/MetaMask/go-did-it/crypto"
)
@@ -99,6 +101,12 @@ func (p *PrivateKey) ToPKCS8PEM() string {
}))
}
// The default signing hash is SHA-384.
func (p *PrivateKey) Varsig(opts ...crypto.SigningOption) varsig.Varsig {
params := crypto.CollectSigningOptions(opts)
return varsig.NewECDSAVarsig(varsig.CurveP384, params.HashOrDefault(crypto.SHA384).ToVarsigHash(), params.PayloadEncoding())
}
// The default signing hash is SHA-384.
func (p *PrivateKey) SignToBytes(message []byte, opts ...crypto.SigningOption) ([]byte, error) {
params := crypto.CollectSigningOptions(opts)

View File

@@ -8,6 +8,8 @@ import (
"fmt"
"math/big"
"github.com/ucan-wg/go-varsig"
"github.com/MetaMask/go-did-it/crypto"
helpers "github.com/MetaMask/go-did-it/crypto/internal"
)
@@ -148,6 +150,10 @@ func (p *PublicKey) VerifyBytes(message, signature []byte, opts ...crypto.Signin
func (p *PublicKey) VerifyASN1(message, signature []byte, opts ...crypto.SigningOption) bool {
params := crypto.CollectSigningOptions(opts)
if !params.VarsigMatch(varsig.AlgorithmECDSA, uint64(varsig.CurveP384), 0) {
return false
}
hasher := params.HashOrDefault(crypto.SHA384).New()
hasher.Write(message)
hash := hasher.Sum(nil)

View File

@@ -9,6 +9,8 @@ import (
"fmt"
"math/big"
"github.com/ucan-wg/go-varsig"
"github.com/MetaMask/go-did-it/crypto"
)
@@ -99,6 +101,12 @@ func (p *PrivateKey) ToPKCS8PEM() string {
}))
}
// The default signing hash is SHA-512.
func (p *PrivateKey) Varsig(opts ...crypto.SigningOption) varsig.Varsig {
params := crypto.CollectSigningOptions(opts)
return varsig.NewECDSAVarsig(varsig.CurveP521, params.HashOrDefault(crypto.SHA512).ToVarsigHash(), params.PayloadEncoding())
}
// The default signing hash is SHA-512.
func (p *PrivateKey) SignToBytes(message []byte, opts ...crypto.SigningOption) ([]byte, error) {
params := crypto.CollectSigningOptions(opts)

View File

@@ -8,6 +8,8 @@ import (
"fmt"
"math/big"
"github.com/ucan-wg/go-varsig"
"github.com/MetaMask/go-did-it/crypto"
helpers "github.com/MetaMask/go-did-it/crypto/internal"
)
@@ -148,6 +150,10 @@ func (p *PublicKey) VerifyBytes(message, signature []byte, opts ...crypto.Signin
func (p *PublicKey) VerifyASN1(message, signature []byte, opts ...crypto.SigningOption) bool {
params := crypto.CollectSigningOptions(opts)
if !params.VarsigMatch(varsig.AlgorithmECDSA, uint64(varsig.CurveP521), 0) {
return false
}
hasher := params.HashOrDefault(crypto.SHA512).New()
hasher.Write(message)
hash := hasher.Sum(nil)

View File

@@ -1,48 +1,6 @@
package crypto
// Public Key
type PublicKey interface {
// Equal returns true if other is the same PublicKey
Equal(other PublicKey) bool
// ToPublicKeyMultibase format the PublicKey into a string compatible with a PublicKeyMultibase field
// in a DID Document.
ToPublicKeyMultibase() string
// ToX509DER serializes the PublicKey into the X.509 DER (binary) format.
ToX509DER() []byte
// ToX509PEM serializes the PublicKey into the X.509 PEM (string) format.
ToX509PEM() string
}
type PublicKeyToBytes interface {
PublicKey
// ToBytes serializes the PublicKey into "raw bytes", without metadata or structure.
// 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.
ToBytes() []byte
}
type PublicKeySigningBytes interface {
PublicKey
// 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, opts ...SigningOption) bool
}
type PublicKeySigningASN1 interface {
PublicKey
// VerifyASN1 checks a signature in the ASN.1 format.
VerifyASN1(message, signature []byte, opts ...SigningOption) bool
}
// Private Key
import "github.com/ucan-wg/go-varsig"
type PrivateKey interface {
// Equal returns true if other is the same PrivateKey
@@ -70,6 +28,9 @@ type PrivateKeyToBytes interface {
type PrivateKeySigningBytes interface {
PrivateKey
// Varsig returns the varsig.Varsig corresponding to the given parameters and private key.
Varsig(opts ...SigningOption) varsig.Varsig
// 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.
@@ -79,6 +40,9 @@ type PrivateKeySigningBytes interface {
type PrivateKeySigningASN1 interface {
PrivateKey
// Varsig returns the varsig.Varsig corresponding to the given parameters and private key.
Varsig(opts ...SigningOption) varsig.Varsig
// SignToASN1 creates a signature in the ASN.1 format.
SignToASN1(message []byte, opts ...SigningOption) ([]byte, error)
}

41
crypto/public.go Normal file
View File

@@ -0,0 +1,41 @@
package crypto
type PublicKey interface {
// Equal returns true if other is the same PublicKey
Equal(other PublicKey) bool
// ToPublicKeyMultibase format the PublicKey into a string compatible with a PublicKeyMultibase field
// in a DID Document.
ToPublicKeyMultibase() string
// ToX509DER serializes the PublicKey into the X.509 DER (binary) format.
ToX509DER() []byte
// ToX509PEM serializes the PublicKey into the X.509 PEM (string) format.
ToX509PEM() string
}
type PublicKeyToBytes interface {
PublicKey
// ToBytes serializes the PublicKey into "raw bytes", without metadata or structure.
// 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.
ToBytes() []byte
}
type PublicKeySigningBytes interface {
PublicKey
// 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, opts ...SigningOption) bool
}
type PublicKeySigningASN1 interface {
PublicKey
// VerifyASN1 checks a signature in the ASN.1 format.
VerifyASN1(message, signature []byte, opts ...SigningOption) bool
}

View File

@@ -9,6 +9,8 @@ import (
"fmt"
"math/big"
"github.com/ucan-wg/go-varsig"
"github.com/MetaMask/go-did-it/crypto"
)
@@ -67,8 +69,8 @@ func PrivateKeyFromPKCS8PEM(str string) (*PrivateKey, error) {
return PrivateKeyFromPKCS8DER(block.Bytes)
}
func (p *PrivateKey) BitLen() int {
return p.k.N.BitLen()
func (p *PrivateKey) KeyLength() uint64 {
return uint64((p.k.N.BitLen() + 7) / 8) // Round up to the nearest byte
}
func (p *PrivateKey) DBytes() []byte {
@@ -147,6 +149,16 @@ func (p *PrivateKey) ToPKCS8PEM() string {
}))
}
// The default signing hash is:
// - SHA-256 for keys of length 2048 bits and under
// - SHA-384 for keys of length 3072 bits and under
// - SHA-512 for higher key length
func (p *PrivateKey) Varsig(opts ...crypto.SigningOption) varsig.Varsig {
params := crypto.CollectSigningOptions(opts)
hashCode := params.HashOrDefault(defaultSigHash(p.k.N.BitLen()))
return varsig.NewRSAVarsig(hashCode.ToVarsigHash(), uint64(p.KeyLength()), params.PayloadEncoding())
}
// SignToASN1 produce a PKCS#1 v1.5 signature.
// The default signing hash is:
// - SHA-256 for keys of length 2048 bits and under

View File

@@ -8,6 +8,8 @@ import (
"fmt"
"math/big"
"github.com/ucan-wg/go-varsig"
"github.com/MetaMask/go-did-it/crypto"
helpers "github.com/MetaMask/go-did-it/crypto/internal"
)
@@ -93,8 +95,8 @@ func PublicKeyFromX509PEM(str string) (*PublicKey, error) {
return PublicKeyFromX509DER(block.Bytes)
}
func (p *PublicKey) BitLen() int {
return p.k.N.BitLen()
func (p *PublicKey) KeyLength() uint64 {
return uint64((p.k.N.BitLen() + 7) / 8) // Round up to the nearest byte
}
func (p *PublicKey) NBytes() []byte {
@@ -138,6 +140,10 @@ func (p *PublicKey) ToX509PEM() string {
func (p *PublicKey) VerifyASN1(message, signature []byte, opts ...crypto.SigningOption) bool {
params := crypto.CollectSigningOptions(opts)
if !params.VarsigMatch(varsig.AlgorithmRSA, 0, p.KeyLength()) {
return false
}
hashCode := params.HashOrDefault(defaultSigHash(p.k.N.BitLen()))
hasher := hashCode.New()
hasher.Write(message)

View File

@@ -8,6 +8,7 @@ import (
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa"
"github.com/ucan-wg/go-varsig"
"github.com/MetaMask/go-did-it/crypto"
)
@@ -172,6 +173,12 @@ func (p *PrivateKey) ToPKCS8PEM() string {
}))
}
// The default signing hash is SHA-256.
func (p *PrivateKey) Varsig(opts ...crypto.SigningOption) varsig.Varsig {
params := crypto.CollectSigningOptions(opts)
return varsig.NewECDSAVarsig(varsig.CurveSecp256k1, params.HashOrDefault(crypto.SHA256).ToVarsigHash(), params.PayloadEncoding())
}
// The default signing hash is SHA-256.
func (p *PrivateKey) SignToBytes(message []byte, opts ...crypto.SigningOption) ([]byte, error) {
params := crypto.CollectSigningOptions(opts)

View File

@@ -8,6 +8,7 @@ import (
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa"
"github.com/ucan-wg/go-varsig"
"github.com/MetaMask/go-did-it/crypto"
helpers "github.com/MetaMask/go-did-it/crypto/internal"
@@ -176,6 +177,10 @@ func (p *PublicKey) VerifyBytes(message, signature []byte, opts ...crypto.Signin
params := crypto.CollectSigningOptions(opts)
if !params.VarsigMatch(varsig.AlgorithmECDSA, uint64(varsig.CurveSecp256k1), 0) {
return false
}
hasher := params.HashOrDefault(crypto.SHA256).New()
hasher.Write(message)
hash := hasher.Sum(nil)
@@ -191,6 +196,10 @@ func (p *PublicKey) VerifyBytes(message, signature []byte, opts ...crypto.Signin
func (p *PublicKey) VerifyASN1(message, signature []byte, opts ...crypto.SigningOption) bool {
params := crypto.CollectSigningOptions(opts)
if !params.VarsigMatch(varsig.AlgorithmECDSA, uint64(varsig.CurveSecp256k1), 0) {
return false
}
hasher := params.HashOrDefault(crypto.SHA256).New()
hasher.Write(message)
hash := hasher.Sum(nil)

View File

@@ -23,7 +23,7 @@ func Example_signature() {
// 3) Use the appropriate set of verification methods (ex: verify a signature for authentication purpose)
sig, _ := base64.StdEncoding.DecodeString("nhpkr5a7juUM2eDpDRSJVdEE++0SYqaZXHtuvyafVFUx8zsOdDSrij+vHmd/ARwUOmi/ysmSD+b3K9WTBtmmBQ==")
if ok, method := did.TryAllVerify(doc.Authentication(), []byte("message"), sig); ok {
if ok, method := did.TryAllVerifyBytes(doc.Authentication(), []byte("message"), sig); ok {
fmt.Println("Signature is valid, verified with method:", method.Type(), method.ID())
} else {
fmt.Println("Signature is invalid")

9
go.mod
View File

@@ -1,8 +1,8 @@
module github.com/MetaMask/go-did-it
go 1.23.0
go 1.24.4
toolchain go1.23.1
toolchain go1.24.5
require (
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0
@@ -10,7 +10,8 @@ require (
github.com/multiformats/go-multibase v0.2.0
github.com/multiformats/go-varint v0.0.7
github.com/stretchr/testify v1.10.0
golang.org/x/crypto v0.39.0
github.com/ucan-wg/go-varsig v1.0.0
golang.org/x/crypto v0.40.0
)
require (
@@ -18,6 +19,6 @@ require (
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
golang.org/x/sys v0.34.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

10
go.sum
View File

@@ -18,10 +18,12 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
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=
github.com/ucan-wg/go-varsig v1.0.0 h1:Hrc437Zg+B5Eoajg+qZQZI3Q3ocPyjlnp3/Bz9ZnlWw=
github.com/ucan-wg/go-varsig v1.0.0/go.mod h1:Sakln6IPooDPH+ClQ0VvR09TuwUhHcfLqcPiPkMZGh0=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.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=

View File

@@ -104,8 +104,11 @@ type VerificationMethod interface {
type VerificationMethodSignature interface {
VerificationMethod
// Verify checks that 'sig' is a valid signature of 'data'.
Verify(data []byte, sig []byte) (bool, error)
// VerifyBytes checks that 'sig' is a valid "raw bytes" signature of 'data'.
VerifyBytes(data []byte, sig []byte, opts ...crypto.SigningOption) (bool, error)
// VerifyASN1 checks that 'sig' is a valid ASN.1 signature of 'data'.
VerifyASN1(data []byte, sig []byte, opts ...crypto.SigningOption) (bool, error)
}
// VerificationMethodKeyAgreement is a VerificationMethod implementing a shared key agreement.

View File

@@ -6,12 +6,24 @@ import (
"github.com/MetaMask/go-did-it/crypto"
)
// TryAllVerify tries to verify the signature with all the methods in the slice.
// TryAllVerifyBytes tries to verify the signature as bytes with all the methods in the slice.
// It returns true if the signature is verified, and the method that verified it.
// If no method verifies the signature, it returns false and nil.
func TryAllVerify(methods []VerificationMethodSignature, data []byte, sig []byte) (bool, VerificationMethodSignature) {
func TryAllVerifyBytes(methods []VerificationMethodSignature, data []byte, sig []byte, opts ...crypto.SigningOption) (bool, VerificationMethodSignature) {
for _, method := range methods {
if valid, err := method.Verify(data, sig); err == nil && valid {
if valid, err := method.VerifyBytes(data, sig, opts...); err == nil && valid {
return true, method
}
}
return false, nil
}
// TryAllVerifyASN1 tries to verify the signature as ASN.1 with all the methods in the slice.
// It returns true if the signature is verified, and the method that verified it.
// If no method verifies the signature, it returns false and nil.
func TryAllVerifyASN1(methods []VerificationMethodSignature, data []byte, sig []byte, opts ...crypto.SigningOption) (bool, VerificationMethodSignature) {
for _, method := range methods {
if valid, err := method.VerifyASN1(data, sig, opts...); err == nil && valid {
return true, method
}
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/mr-tron/base58"
"github.com/MetaMask/go-did-it"
"github.com/MetaMask/go-did-it/crypto"
"github.com/MetaMask/go-did-it/crypto/ed25519"
)
@@ -97,6 +98,10 @@ func (v VerificationKey2018) JsonLdContext() string {
return JsonLdContext2018
}
func (v VerificationKey2018) Verify(data []byte, sig []byte) (bool, error) {
return v.pubkey.VerifyBytes(data, sig), nil
func (v VerificationKey2018) VerifyBytes(data []byte, sig []byte, opts ...crypto.SigningOption) (bool, error) {
return v.pubkey.VerifyBytes(data, sig, opts...), nil
}
func (v VerificationKey2018) VerifyASN1(data []byte, sig []byte, opts ...crypto.SigningOption) (bool, error) {
return v.pubkey.VerifyASN1(data, sig, opts...), nil
}

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"github.com/MetaMask/go-did-it"
"github.com/MetaMask/go-did-it/crypto"
"github.com/MetaMask/go-did-it/crypto/ed25519"
)
@@ -91,6 +92,10 @@ func (v VerificationKey2020) JsonLdContext() string {
return JsonLdContext2020
}
func (v VerificationKey2020) Verify(data []byte, sig []byte) (bool, error) {
return v.pubkey.VerifyBytes(data, sig), nil
func (v VerificationKey2020) VerifyBytes(data []byte, sig []byte, opts ...crypto.SigningOption) (bool, error) {
return v.pubkey.VerifyBytes(data, sig, opts...), nil
}
func (v VerificationKey2020) VerifyASN1(data []byte, sig []byte, opts ...crypto.SigningOption) (bool, error) {
return v.pubkey.VerifyASN1(data, sig, opts...), nil
}

View File

@@ -93,9 +93,16 @@ func (j JsonWebKey2020) JsonLdContext() string {
return JsonLdContext
}
func (j JsonWebKey2020) Verify(data []byte, sig []byte) (bool, error) {
func (j JsonWebKey2020) VerifyBytes(data []byte, sig []byte, opts ...crypto.SigningOption) (bool, error) {
if pub, ok := j.pubkey.(crypto.PublicKeySigningBytes); ok {
return pub.VerifyBytes(data, sig), nil
return pub.VerifyBytes(data, sig, opts...), nil
}
return false, errors.New("not a signing public key")
}
func (j JsonWebKey2020) VerifyASN1(data []byte, sig []byte, opts ...crypto.SigningOption) (bool, error) {
if pub, ok := j.pubkey.(crypto.PublicKeySigningASN1); ok {
return pub.VerifyASN1(data, sig, opts...), nil
}
return false, errors.New("not a signing public key")
}

View File

@@ -96,9 +96,16 @@ func (m MultiKey) JsonLdContext() string {
return JsonLdContext
}
func (m MultiKey) Verify(data []byte, sig []byte) (bool, error) {
func (m MultiKey) VerifyBytes(data []byte, sig []byte, opts ...crypto.SigningOption) (bool, error) {
if pub, ok := m.pubkey.(crypto.PublicKeySigningBytes); ok {
return pub.VerifyBytes(data, sig), nil
return pub.VerifyBytes(data, sig, opts...), nil
}
return false, errors.New("not a signing public key")
}
func (m MultiKey) VerifyASN1(data []byte, sig []byte, opts ...crypto.SigningOption) (bool, error) {
if pub, ok := m.pubkey.(crypto.PublicKeySigningASN1); ok {
return pub.VerifyASN1(data, sig, opts...), nil
}
return false, errors.New("not a signing public key")
}

View File

@@ -101,8 +101,12 @@ func (m Key2021) JsonLdContext() string {
return JsonLdContext2021
}
func (m Key2021) Verify(data []byte, sig []byte) (bool, error) {
return m.pubkey.VerifyBytes(data, sig), nil
func (m Key2021) VerifyBytes(data []byte, sig []byte, opts ...crypto.SigningOption) (bool, error) {
return m.pubkey.VerifyBytes(data, sig, opts...), nil
}
func (m Key2021) VerifyASN1(data []byte, sig []byte, opts ...crypto.SigningOption) (bool, error) {
return m.pubkey.VerifyASN1(data, sig, opts...), nil
}
func (m Key2021) PrivateKeyIsCompatible(local crypto.PrivateKeyKeyExchange) bool {

View File

@@ -101,8 +101,12 @@ func (vm VerificationKey2019) JsonLdContext() string {
return JsonLdContext
}
func (vm VerificationKey2019) Verify(data []byte, sig []byte) (bool, error) {
return vm.pubkey.VerifyBytes(data, sig), nil
func (vm VerificationKey2019) VerifyBytes(data []byte, sig []byte, opts ...crypto.SigningOption) (bool, error) {
return vm.pubkey.VerifyBytes(data, sig, opts...), nil
}
func (vm VerificationKey2019) VerifyASN1(data []byte, sig []byte, opts ...crypto.SigningOption) (bool, error) {
return vm.pubkey.VerifyASN1(data, sig, opts...), nil
}
func (vm VerificationKey2019) PrivateKeyIsCompatible(local crypto.PrivateKeyKeyExchange) bool {

View File

@@ -32,7 +32,7 @@ func ExampleGenerateKeyPair() {
// Resolve the DID and verify a signature
doc, err := dk.Document()
handleErr(err)
ok, _ := did.TryAllVerify(doc.Authentication(), msg, sig)
ok, _ := did.TryAllVerifyBytes(doc.Authentication(), msg, sig)
fmt.Println("Signature verified:", ok)
}