From a77ef67a8b05689a74ccbac1c69bf7b96c8e22c3 Mon Sep 17 00:00:00 2001 From: Dmitriy Kinoshenko Date: Mon, 6 Apr 2020 23:09:17 +0300 Subject: [PATCH] feat: Support ECDSA Secp256k1 linked data signature suite closes #1529 Signed-off-by: Dmitriy Kinoshenko --- .../public_key_verifier.go | 103 ++++++++++ .../public_key_verifier_test.go | 185 ++++++++++++++++++ .../ecdsasecp256k1signature2019/suite.go | 70 +++++++ .../ecdsasecp256k1signature2019/suite_test.go | 70 +++++++ pkg/doc/verifiable/credential_ldp_test.go | 49 +++++ pkg/doc/verifiable/embedded_proof.go | 4 +- pkg/doc/verifiable/support_test.go | 23 ++- 7 files changed, 496 insertions(+), 8 deletions(-) create mode 100644 pkg/doc/signature/suite/ecdsasecp256k1signature2019/public_key_verifier.go create mode 100644 pkg/doc/signature/suite/ecdsasecp256k1signature2019/public_key_verifier_test.go create mode 100644 pkg/doc/signature/suite/ecdsasecp256k1signature2019/suite.go create mode 100644 pkg/doc/signature/suite/ecdsasecp256k1signature2019/suite_test.go diff --git a/pkg/doc/signature/suite/ecdsasecp256k1signature2019/public_key_verifier.go b/pkg/doc/signature/suite/ecdsasecp256k1signature2019/public_key_verifier.go new file mode 100644 index 0000000000..12b237a5a5 --- /dev/null +++ b/pkg/doc/signature/suite/ecdsasecp256k1signature2019/public_key_verifier.go @@ -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 +) diff --git a/pkg/doc/signature/suite/ecdsasecp256k1signature2019/public_key_verifier_test.go b/pkg/doc/signature/suite/ecdsasecp256k1signature2019/public_key_verifier_test.go new file mode 100644 index 0000000000..0c1d4eaca8 --- /dev/null +++ b/pkg/doc/signature/suite/ecdsasecp256k1signature2019/public_key_verifier_test.go @@ -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)...) +} diff --git a/pkg/doc/signature/suite/ecdsasecp256k1signature2019/suite.go b/pkg/doc/signature/suite/ecdsasecp256k1signature2019/suite.go new file mode 100644 index 0000000000..9682f23b94 --- /dev/null +++ b/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 +} diff --git a/pkg/doc/signature/suite/ecdsasecp256k1signature2019/suite_test.go b/pkg/doc/signature/suite/ecdsasecp256k1signature2019/suite_test.go new file mode 100644 index 0000000000..27ba0983d4 --- /dev/null +++ b/pkg/doc/signature/suite/ecdsasecp256k1signature2019/suite_test.go @@ -0,0 +1,70 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package ecdsasecp256k1signature2019 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSignatureSuite_GetCanonicalDocument(t *testing.T) { + doc, err := New().GetCanonicalDocument(getDefaultDoc()) + require.NoError(t, err) + require.NotEmpty(t, doc) + require.Equal(t, test28Result, string(doc)) +} + +func TestSignatureSuite_GetDigest(t *testing.T) { + digest := New().GetDigest([]byte("test doc")) + require.NotNil(t, digest) +} + +func TestSignatureSuite_Accept(t *testing.T) { + ss := New() + accepted := ss.Accept("EcdsaSecp256k1Signature2019") + require.True(t, accepted) + + accepted = ss.Accept("RsaSignature2018") + require.False(t, accepted) +} + +func getDefaultDoc() map[string]interface{} { + // this JSON-LD document was taken from http://json-ld.org/test-suite/tests/toRdf-0028-in.jsonld + doc := map[string]interface{}{ + "@context": map[string]interface{}{ + "sec": "http://purl.org/security#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "dc": "http://purl.org/dc/terms/", + "sec:signer": map[string]interface{}{"@type": "@id"}, + "dc:created": map[string]interface{}{"@type": "xsd:dateTime"}, + }, + "@id": "http://example.org/sig1", + "@type": []interface{}{"rdf:Graph", "sec:SignedGraph"}, + "dc:created": "2011-09-23T20:21:34Z", + "sec:signer": "http://payswarm.example.com/i/john/keys/5", + "sec:signatureValue": "OGQzNGVkMzVm4NTIyZTkZDYMmMzQzNmExMgoYzI43Q3ODIyOWM32NjI=", + "@graph": map[string]interface{}{ + "@id": "http://example.org/fact1", + "dc:title": "Hello World!", + }, + } + + return doc +} + +// taken from test 28 report https://json-ld.org/test-suite/reports/#test_30bc80ba056257df8a196e8f65c097fc + +// nolint +const test28Result = ` "Hello World!" . + "2011-09-23T20:21:34Z"^^ . + "OGQzNGVkMzVm4NTIyZTkZDYMmMzQzNmExMgoYzI43Q3ODIyOWM32NjI=" . + . + . + . +` diff --git a/pkg/doc/verifiable/credential_ldp_test.go b/pkg/doc/verifiable/credential_ldp_test.go index 387f756a0c..71ba4fb17a 100644 --- a/pkg/doc/verifiable/credential_ldp_test.go +++ b/pkg/doc/verifiable/credential_ldp_test.go @@ -15,6 +15,7 @@ import ( "fmt" "testing" + "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcutil/base58" gojose "github.com/square/go-jose/v3" "github.com/stretchr/testify/require" @@ -23,6 +24,7 @@ import ( "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto" "github.com/hyperledger/aries-framework-go/pkg/doc/jose" "github.com/hyperledger/aries-framework-go/pkg/doc/signature/suite" + "github.com/hyperledger/aries-framework-go/pkg/doc/signature/suite/ecdsasecp256k1signature2019" "github.com/hyperledger/aries-framework-go/pkg/doc/signature/suite/ed25519signature2018" "github.com/hyperledger/aries-framework-go/pkg/doc/signature/suite/jsonwebsignature2020" sigverifier "github.com/hyperledger/aries-framework-go/pkg/doc/signature/verifier" @@ -195,6 +197,53 @@ func TestNewCredentialFromLinkedDataProof_JsonWebSignature2020_ecdsaP256(t *test r.Equal(vc, vcWithLdp) } +func TestNewCredentialFromLinkedDataProof_EcdsaSecp256k1Signature2019(t *testing.T) { + r := require.New(t) + + privateKey, err := ecdsa.GenerateKey(btcec.S256(), rand.Reader) + require.NoError(t, err) + + sigSuite := ecdsasecp256k1signature2019.New( + suite.WithSigner(getEcdsaSecp256k1RS256TestSigner(privateKey)), + // TODO use suite.NewCryptoVerifier(createLocalCrypto()) verifier + suite.WithVerifier(&ecdsasecp256k1signature2019.PublicKeyVerifier{})) + + ldpContext := &LinkedDataProofContext{ + SignatureType: "EcdsaSecp256k1Signature2019", + SignatureRepresentation: SignatureJWS, + Suite: sigSuite, + VerificationMethod: "did:example:123456#key1", + } + + vc, _, err := NewCredential([]byte(validCredential)) + r.NoError(err) + + err = vc.AddLinkedDataProof(ldpContext) + r.NoError(err) + + vcBytes, err := json.Marshal(vc) + r.NoError(err) + + pubKeyBytes := elliptic.Marshal(privateKey.Curve, privateKey.X, privateKey.Y) + vcWithLdp, _, err := NewCredential(vcBytes, + WithEmbeddedSignatureSuite(sigSuite), + WithPublicKeyFetcher(func(issuerID, keyID string) (*sigverifier.PublicKey, error) { + return &sigverifier.PublicKey{ + Type: "EcdsaSecp256k1VerificationKey2019", + Value: pubKeyBytes, + JWK: &jose.JWK{ + JSONWebKey: gojose.JSONWebKey{ + Algorithm: "ES256K", + }, + Crv: "secp256k1", + Kty: "EC", + }, + }, nil + })) + r.NoError(err) + r.Equal(vc, vcWithLdp) +} + func createLocalCrypto() crypto.Crypto { lKMS := createKMS() diff --git a/pkg/doc/verifiable/embedded_proof.go b/pkg/doc/verifiable/embedded_proof.go index c90325bc2a..51d4d0fd19 100644 --- a/pkg/doc/verifiable/embedded_proof.go +++ b/pkg/doc/verifiable/embedded_proof.go @@ -18,7 +18,9 @@ func mustBeLinkedDataProof(proofMap map[string]interface{}) error { } proofTypeStr := safeStringValue(proofType) - if proofTypeStr != "Ed25519Signature2018" && proofTypeStr != "JsonWebSignature2020" { + if proofTypeStr != "Ed25519Signature2018" && + proofTypeStr != "JsonWebSignature2020" && + proofTypeStr != "EcdsaSecp256k1Signature2019" { return fmt.Errorf("unsupported proof type: %s", proofType) } diff --git a/pkg/doc/verifiable/support_test.go b/pkg/doc/verifiable/support_test.go index a5bdc1c9ce..1721012d41 100644 --- a/pkg/doc/verifiable/support_test.go +++ b/pkg/doc/verifiable/support_test.go @@ -188,16 +188,25 @@ func (s *ed25519TestSigner) Sign(doc []byte) ([]byte, error) { return ed25519.Sign(s.privateKey, doc), nil } -func getEcdsaP256TestSigner(privKey *ecdsa.PrivateKey) *ecdsaP256TestSigner { - return &ecdsaP256TestSigner{privateKey: privKey} +func getEcdsaP256TestSigner(privKey *ecdsa.PrivateKey) *ecdsaTestSigner { + return &ecdsaTestSigner{privateKey: privKey, hash: crypto.SHA256} } -type ecdsaP256TestSigner struct { +func getEcdsaSecp256k1RS256TestSigner(privKey *ecdsa.PrivateKey) *ecdsaTestSigner { + return &ecdsaTestSigner{privateKey: privKey, hash: crypto.SHA256} +} + +type ecdsaTestSigner struct { privateKey *ecdsa.PrivateKey + hash crypto.Hash +} + +func (es *ecdsaTestSigner) Sign(doc []byte) ([]byte, error) { + return signEcdsa(doc, es.privateKey, es.hash) } -func (es *ecdsaP256TestSigner) Sign(doc []byte) ([]byte, error) { - hasher := crypto.SHA256.New() +func signEcdsa(doc []byte, privateKey *ecdsa.PrivateKey, hash crypto.Hash) ([]byte, error) { + hasher := hash.New() _, err := hasher.Write(doc) if err != nil { @@ -206,12 +215,12 @@ func (es *ecdsaP256TestSigner) Sign(doc []byte) ([]byte, error) { hashed := hasher.Sum(nil) - r, s, err := ecdsa.Sign(rand.Reader, es.privateKey, hashed) + r, s, err := ecdsa.Sign(rand.Reader, privateKey, hashed) if err != nil { panic(err) } - curveBits := es.privateKey.Curve.Params().BitSize + curveBits := privateKey.Curve.Params().BitSize keyBytes := curveBits / 8 if curveBits%8 > 0 {