Skip to content

Commit

Permalink
feat: Support ECDSA Secp256k1 linked data signature suite
Browse files Browse the repository at this point in the history
closes hyperledger-archives#1529

Signed-off-by: Dmitriy Kinoshenko <dkinoshenko@gmail.com>
  • Loading branch information
kdimak committed Apr 6, 2020
1 parent 0da8e85 commit 510aabe
Show file tree
Hide file tree
Showing 7 changed files with 496 additions and 8 deletions.
@@ -0,0 +1,103 @@
/*
Copyright SecureKey Technologies Inc. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

package ecdsasecp256k1signature2019

import (
"crypto"
"crypto/ecdsa"
"errors"
"fmt"
"math/big"

"github.com/btcsuite/btcd/btcec"

sigverifier "github.com/hyperledger/aries-framework-go/pkg/doc/signature/verifier"
)

const (
secp256k1KeySize = 32
)

// PublicKeyVerifier verifies a secp256k1 signature taking public key bytes and JSON Web Key as input.
// NOTE: this verifier is present for backward compatibility reasons and can be removed later.
// Please use CryptoVerifier or your own verifier.
type PublicKeyVerifier struct {
}

// Verify will verify a signature.
func (v *PublicKeyVerifier) Verify(pubKey *sigverifier.PublicKey, msg, signature []byte) error {
err := v.validatePublicKey(pubKey)
if err != nil {
return err
}

if len(signature) != 2*secp256k1KeySize {
return errors.New("ecdsa: invalid signature size")
}

curve := btcec.S256()

btcecPubKey, err := btcec.ParsePubKey(pubKey.Value, curve)
if err != nil {
return errors.New("ecdsa: invalid public key")
}

ecdsaPubKey := btcecPubKey.ToECDSA()
hasher := crypto.SHA256.New()

_, err = hasher.Write(msg)
if err != nil {
return errors.New("ecdsa: hash error")
}

hash := hasher.Sum(nil)

r := big.NewInt(0).SetBytes(signature[:secp256k1KeySize])
s := big.NewInt(0).SetBytes(signature[secp256k1KeySize:])

verified := ecdsa.Verify(ecdsaPubKey, hash, r, s)
if !verified {
return errors.New("ecdsa: invalid signature")
}

return nil
}

func (v *PublicKeyVerifier) validatePublicKey(pubKey *sigverifier.PublicKey) error {
// A presence of JSON Web Key is mandatory (due to EcdsaSecp256k1VerificationKey2019 type).
if pubKey.JWK == nil {
return ErrJWKNotPresent
}

if pubKey.Type != jwkType {
return ErrTypeNotEcdsaSecp256k1VerificationKey2019
}

if pubKey.JWK.Kty != "EC" {
return fmt.Errorf("unsupported key type: '%s'", pubKey.JWK.Kty)
}

if pubKey.JWK.Crv != "" && pubKey.JWK.Crv != "secp256k1" {
return fmt.Errorf("ecdsa: not secp256k1 curve: '%s'", pubKey.JWK.Crv)
}

if pubKey.JWK.Algorithm != "" && pubKey.JWK.Algorithm != "ES256K" {
return fmt.Errorf("ecdsa: not ES256K EC algorithm: '%s'", pubKey.JWK.Algorithm)
}

return nil
}

var (
// ErrJWKNotPresent is returned when no JWK is defined in a public key
// (must be defined for EcdsaSecp256k1VerificationKey2019).
ErrJWKNotPresent = errors.New("JWK is not present")

// ErrTypeNotEcdsaSecp256k1VerificationKey2019 is returned when a public key passed for signature verification
// has a type different from EcdsaSecp256k1VerificationKey2019.
ErrTypeNotEcdsaSecp256k1VerificationKey2019 = errors.New("a type of public key is not EcdsaSecp256k1VerificationKey2019") //nolint:lll
)
@@ -0,0 +1,185 @@
/*
Copyright SecureKey Technologies Inc. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

package ecdsasecp256k1signature2019

import (
"crypto"
"crypto/ecdsa"
"crypto/rand"
"testing"

"github.com/btcsuite/btcd/btcec"
gojose "github.com/square/go-jose/v3"
"github.com/stretchr/testify/require"

"github.com/hyperledger/aries-framework-go/pkg/doc/jose"
sigverifier "github.com/hyperledger/aries-framework-go/pkg/doc/signature/verifier"
)

func TestPublicKeyVerifier_Verify(t *testing.T) {
btcecPrivKey, err := btcec.NewPrivateKey(btcec.S256())
require.NoError(t, err)

ecdsaPrivKey := btcecPrivKey.ToECDSA()

ecdsaPubKey := &ecdsaPrivKey.PublicKey

msg := []byte("test message")

pubKeyBytes := btcecPrivKey.PubKey().SerializeCompressed()

pubKey := &sigverifier.PublicKey{
Type: "EcdsaSecp256k1VerificationKey2019",
Value: pubKeyBytes,

JWK: &jose.JWK{
JSONWebKey: gojose.JSONWebKey{
Algorithm: "ES256K",
},
Crv: "secp256k1",
Kty: "EC",
},
}

v := &PublicKeyVerifier{}
signature := getSignature(&ecdsa.PrivateKey{
PublicKey: ecdsa.PublicKey{
Curve: ecdsaPubKey.Curve,
X: ecdsaPubKey.X,
Y: ecdsaPubKey.Y,
},
D: btcecPrivKey.D,
}, msg)

v = &PublicKeyVerifier{}
signature = getSignature(ecdsaPrivKey, msg)

err = v.Verify(pubKey, msg, signature)
require.NoError(t, err)

t.Run("undefined JWK", func(t *testing.T) {
verifyError := v.Verify(&sigverifier.PublicKey{
Type: "EcdsaSecp256k1VerificationKey2019",
Value: pubKeyBytes,
}, msg, signature)
require.Error(t, verifyError)
require.Equal(t, verifyError, ErrJWKNotPresent)
})

t.Run("JWK is invalid type", func(t *testing.T) {
verifyError := v.Verify(&sigverifier.PublicKey{
Type: "Ed25519Signature2018",
Value: pubKeyBytes,
JWK: &jose.JWK{},
}, msg, signature)
require.Error(t, verifyError)
require.Equal(t, verifyError, ErrTypeNotEcdsaSecp256k1VerificationKey2019)
})

t.Run("JWK with unsupported key type", func(t *testing.T) {
verifyError := v.Verify(&sigverifier.PublicKey{
Type: "EcdsaSecp256k1VerificationKey2019",
Value: pubKeyBytes,
JWK: &jose.JWK{
Kty: "unknown",
},
}, msg, signature)
require.Error(t, verifyError)
require.EqualError(t, verifyError, "unsupported key type: 'unknown'")
})

t.Run("invalid curve", func(t *testing.T) {
verifyError := v.Verify(&sigverifier.PublicKey{
Type: "EcdsaSecp256k1VerificationKey2019",
Value: pubKeyBytes,
JWK: &jose.JWK{
JSONWebKey: gojose.JSONWebKey{
Algorithm: "ES256K",
},
Crv: "unsupported",
Kty: "EC",
},
}, msg, signature)
require.Error(t, verifyError)
require.EqualError(t, verifyError, "ecdsa: not secp256k1 curve: 'unsupported'")
})

t.Run("invalid algorithm", func(t *testing.T) {
verifyError := v.Verify(&sigverifier.PublicKey{
Type: "EcdsaSecp256k1VerificationKey2019",
Value: pubKeyBytes,
JWK: &jose.JWK{
JSONWebKey: gojose.JSONWebKey{
Algorithm: "ES512",
},
Crv: "secp256k1",
Kty: "EC",
},
}, msg, signature)
require.Error(t, verifyError)
require.EqualError(t, verifyError, "ecdsa: not ES256K EC algorithm: 'ES512'")
})

t.Run("invalid public key", func(t *testing.T) {
verifyError := v.Verify(&sigverifier.PublicKey{
Type: "EcdsaSecp256k1VerificationKey2019",
Value: []byte("invalid public key"),
JWK: &jose.JWK{
JSONWebKey: gojose.JSONWebKey{
Algorithm: "ES256K",
},
Crv: "secp256k1",
Kty: "EC",
},
}, msg, signature)
require.Error(t, verifyError)
require.EqualError(t, verifyError, "ecdsa: invalid public key")
})

t.Run("invalid signature", func(t *testing.T) {
verifyError := v.Verify(pubKey, msg, []byte("signature of invalid size"))
require.Error(t, verifyError)
require.EqualError(t, verifyError, "ecdsa: invalid signature size")

emptySig := make([]byte, 64)
verifyError = v.Verify(pubKey, msg, emptySig)
require.Error(t, verifyError)
require.EqualError(t, verifyError, "ecdsa: invalid signature")
})
}

func getSignature(privKey *ecdsa.PrivateKey, payload []byte) []byte {
hasher := crypto.SHA256.New()

_, err := hasher.Write(payload)
if err != nil {
panic(err)
}

hashed := hasher.Sum(nil)

r, s, err := ecdsa.Sign(rand.Reader, privKey, hashed)
if err != nil {
panic(err)
}

curveBits := privKey.Curve.Params().BitSize

keyBytes := curveBits / 8
if curveBits%8 > 0 {
keyBytes++
}

copyPadded := func(source []byte, size int) []byte {
dest := make([]byte, size)
copy(dest[size-len(source):], source)

return dest
}

return append(copyPadded(r.Bytes(), keyBytes), copyPadded(s.Bytes(), keyBytes)...)
}
70 changes: 70 additions & 0 deletions pkg/doc/signature/suite/ecdsasecp256k1signature2019/suite.go
@@ -0,0 +1,70 @@
/*
Copyright SecureKey Technologies Inc. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

// Package ecdsasecp256k1signature2019 implements the EcdsaSecp256k1Signature2019 signature suite
// for the Linked Data Signatures specification (https://w3c-dvcg.github.io/lds-ecdsa-secp256k1-2019/).
// It uses the RDF Dataset Normalization Algorithm to transform the input document into its canonical form.
// It uses SHA-256 [RFC6234] as the message digest algorithm.
// Supported signature algorithms depend on the signer/verifier provided as options to the New().
package ecdsasecp256k1signature2019

import (
"crypto/sha256"

"github.com/piprate/json-gold/ld"

"github.com/hyperledger/aries-framework-go/pkg/doc/signature/suite"
)

// Suite implements EcdsaSecp256k1Signature2019 signature suite.
type Suite struct {
suite.SignatureSuite
}

const (
signatureType = "EcdsaSecp256k1Signature2019"
jwkType = "EcdsaSecp256k1VerificationKey2019"
format = "application/n-quads"
algorithm = "URDNA2015"
)

// New an instance of Linked Data Signatures for JWS suite.
func New(opts ...suite.Opt) *Suite {
s := &Suite{}

suite.InitSuiteOptions(&s.SignatureSuite, opts...)

return s
}

// GetCanonicalDocument will return normalized/canonical version of the document.
// EcdsaSecp256k1Signature2019 signature suite uses RDF Dataset Normalization as canonicalization algorithm.
func (s *Suite) GetCanonicalDocument(doc map[string]interface{}) ([]byte, error) {
proc := ld.NewJsonLdProcessor()
options := ld.NewJsonLdOptions("")
options.ProcessingMode = ld.JsonLd_1_1
options.Format = format
options.ProduceGeneralizedRdf = true
options.Algorithm = algorithm

canonicalDoc, err := proc.Normalize(doc, options)
if err != nil {
return nil, err
}

return []byte(canonicalDoc.(string)), nil
}

// GetDigest returns document digest.
func (s *Suite) GetDigest(doc []byte) []byte {
digest := sha256.Sum256(doc)
return digest[:]
}

// Accept will accept only EcdsaSecp256k1Signature2019 signature type.
func (s *Suite) Accept(t string) bool {
return t == signatureType
}

0 comments on commit 510aabe

Please sign in to comment.