From a1e5a4faba4db47552d6e7ebb970251ca7561323 Mon Sep 17 00:00:00 2001 From: Victor Nosov Date: Tue, 25 Jun 2024 17:21:49 +0300 Subject: [PATCH] signer / crypto abstractions (#39) enveloper verifier --- .github/workflows/golangci-lint.yml | 4 +- extensions/envelope/crypto/crypto.go | 25 ++++ extensions/envelope/crypto/crypto_test.go | 40 ++++++ extensions/envelope/crypto/ed25519.go | 48 +++++++ extensions/envelope/envelope.pb.go | 28 ++-- extensions/envelope/envelope.proto | 1 + .../cc_envelope.go => envelope_cc_test.go} | 18 ++- extensions/envelope/envelope_test.go | 135 ++++++------------ extensions/envelope/middleware.go | 46 +++--- extensions/envelope/sign.go | 43 ++++++ extensions/envelope/signature.go | 56 -------- extensions/envelope/verifier.go | 27 ++++ 12 files changed, 286 insertions(+), 185 deletions(-) create mode 100644 extensions/envelope/crypto/crypto.go create mode 100644 extensions/envelope/crypto/crypto_test.go create mode 100644 extensions/envelope/crypto/ed25519.go rename extensions/envelope/{testdata/cc_envelope.go => envelope_cc_test.go} (52%) create mode 100644 extensions/envelope/sign.go delete mode 100644 extensions/envelope/signature.go create mode 100644 extensions/envelope/verifier.go diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 1dfbaf5..440f096 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -20,7 +20,7 @@ jobs: uses: golangci/golangci-lint-action@v3 with: # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. - version: v1.52 + version: v1.59.1 # Optional: working directory, useful for monorepos # working-directory: somedir @@ -29,7 +29,7 @@ jobs: args: --exclude SA1019 # Optional: show only new issues if it's a pull request. The default value is `false`. - # only-new-issues: true + only-new-issues: true # Optional: if set to true then the action will use pre-installed Go # skip-go-installation: true diff --git a/extensions/envelope/crypto/crypto.go b/extensions/envelope/crypto/crypto.go new file mode 100644 index 0000000..370ac15 --- /dev/null +++ b/extensions/envelope/crypto/crypto.go @@ -0,0 +1,25 @@ +package crypto + +type ( + Crypto interface { + Signer + Hasher + Verifier + + GenerateKey() (publicKey, privateKey []byte, err error) + PublicKey(privateKey []byte) ([]byte, error) + } + + Signer interface { + Sign(privateKey, hash []byte) ([]byte, error) + } + + Hasher interface { + Hash([]byte) []byte + } + + Verifier interface { + Verify(publicKey, hash, signature []byte) error + Hasher + } +) diff --git a/extensions/envelope/crypto/crypto_test.go b/extensions/envelope/crypto/crypto_test.go new file mode 100644 index 0000000..58e6805 --- /dev/null +++ b/extensions/envelope/crypto/crypto_test.go @@ -0,0 +1,40 @@ +package crypto_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/hyperledger-labs/cckit/extensions/envelope/crypto" +) + +const ( + Ed25519PublicKeyLen = 32 + Ed25519PrivateKeyLen = 64 + Ed25519SignatureLen = 64 +) + +func TestCrypto(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Crypto suite") +} + +var _ = Describe(`Ed25519 crypto`, func() { + + ed25519 := crypto.NewEd25519() + + It("Allow to create keys", func() { + publicKey, privateKey, err := ed25519.GenerateKey() + Expect(err).NotTo(HaveOccurred()) + Expect(len(publicKey)).To(Equal(Ed25519PublicKeyLen)) + Expect(len(privateKey)).To(Equal(Ed25519PrivateKeyLen)) + }) + + It("Allow to create signature", func() { + _, privateKey, _ := ed25519.GenerateKey() + sig, err := ed25519.Sign(privateKey, []byte(`anything`)) + Expect(err).NotTo(HaveOccurred()) + Expect(len(sig)).To(Equal(Ed25519SignatureLen)) + }) +}) diff --git a/extensions/envelope/crypto/ed25519.go b/extensions/envelope/crypto/ed25519.go new file mode 100644 index 0000000..00c71cf --- /dev/null +++ b/extensions/envelope/crypto/ed25519.go @@ -0,0 +1,48 @@ +package crypto + +import ( + "crypto/ed25519" + "crypto/rand" + "crypto/sha256" + "errors" + "fmt" +) + +func NewEd25519() *Ed25519 { + return &Ed25519{} +} + +type Ed25519 struct{} + +func (ed *Ed25519) GenerateKey() (publicKey, privateKey []byte, err error) { + publicKey, privateKey, err = ed25519.GenerateKey(rand.Reader) + if err != nil { + return nil, nil, err + } + return publicKey, privateKey, nil +} + +func (ed *Ed25519) Sign(privateKey, hash []byte) (signature []byte, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("sign: %v", r) + } + }() + return ed25519.Sign(privateKey, hash), nil +} + +func (ed *Ed25519) Hash(msg []byte) []byte { + h := sha256.Sum256(msg) + return h[:] +} + +func (ed *Ed25519) Verify(publicKey, hash, signature []byte) error { + if !ed25519.Verify(publicKey, hash, signature) { + return errors.New(`invalid signature`) + } + return nil +} + +func (ed *Ed25519) PublicKey(privateKey []byte) ([]byte, error) { + return ed25519.PrivateKey(privateKey).Public().(ed25519.PublicKey), nil +} diff --git a/extensions/envelope/envelope.pb.go b/extensions/envelope/envelope.pb.go index b8551cd..837a5fc 100644 --- a/extensions/envelope/envelope.pb.go +++ b/extensions/envelope/envelope.pb.go @@ -35,9 +35,10 @@ type Envelope struct { HashFunc string `protobuf:"bytes,5,opt,name=hash_func,json=hashFunc,proto3" json:"hash_func,omitempty"` // function used for hashing Deadline *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=deadline,proto3" json:"deadline,omitempty"` // signature is not valid after deadline (EIP-2612) // channel + chaincode + method are used as domain separator to prevent replay attack from other domains (EIP-2612) - Channel string `protobuf:"bytes,7,opt,name=channel,proto3" json:"channel,omitempty"` - Chaincode string `protobuf:"bytes,8,opt,name=chaincode,proto3" json:"chaincode,omitempty"` - Method string `protobuf:"bytes,9,opt,name=method,proto3" json:"method,omitempty"` + Channel string `protobuf:"bytes,7,opt,name=channel,proto3" json:"channel,omitempty"` + Chaincode string `protobuf:"bytes,8,opt,name=chaincode,proto3" json:"chaincode,omitempty"` + Method string `protobuf:"bytes,9,opt,name=method,proto3" json:"method,omitempty"` + SignatureAlg string `protobuf:"bytes,10,opt,name=signature_alg,json=signatureAlg,proto3" json:"signature_alg,omitempty"` } func (x *Envelope) Reset() { @@ -135,6 +136,13 @@ func (x *Envelope) GetMethod() string { return "" } +func (x *Envelope) GetSignatureAlg() string { + if x != nil { + return x.SignatureAlg + } + return "" +} + var File_envelope_envelope_proto protoreflect.FileDescriptor var file_envelope_envelope_proto_rawDesc = []byte{ @@ -146,7 +154,7 @@ var file_envelope_envelope_proto_rawDesc = []byte{ 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, - 0xa4, 0x02, 0x0a, 0x08, 0x45, 0x6e, 0x76, 0x65, 0x6c, 0x6f, 0x70, 0x65, 0x12, 0x1d, 0x0a, 0x0a, + 0xc9, 0x02, 0x0a, 0x08, 0x45, 0x6e, 0x76, 0x65, 0x6c, 0x6f, 0x70, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, @@ -164,11 +172,13 @@ var file_envelope_envelope_proto_rawDesc = []byte{ 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, - 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x79, 0x70, 0x65, 0x72, 0x6c, 0x65, 0x64, 0x67, 0x65, 0x72, - 0x2d, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x63, 0x63, 0x6b, 0x69, 0x74, 0x2f, 0x65, 0x78, 0x74, 0x65, - 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x65, 0x6e, 0x76, 0x65, 0x6c, 0x6f, 0x70, 0x65, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, + 0x75, 0x72, 0x65, 0x5f, 0x61, 0x6c, 0x67, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x73, + 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x41, 0x6c, 0x67, 0x42, 0x37, 0x5a, 0x35, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x79, 0x70, 0x65, 0x72, 0x6c, + 0x65, 0x64, 0x67, 0x65, 0x72, 0x2d, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x63, 0x63, 0x6b, 0x69, 0x74, + 0x2f, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x65, 0x6e, 0x76, 0x65, + 0x6c, 0x6f, 0x70, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/extensions/envelope/envelope.proto b/extensions/envelope/envelope.proto index c0a8cc3..382d6a2 100644 --- a/extensions/envelope/envelope.proto +++ b/extensions/envelope/envelope.proto @@ -18,4 +18,5 @@ message Envelope { string channel = 7; string chaincode = 8; string method = 9; + string signature_alg = 10; } \ No newline at end of file diff --git a/extensions/envelope/testdata/cc_envelope.go b/extensions/envelope/envelope_cc_test.go similarity index 52% rename from extensions/envelope/testdata/cc_envelope.go rename to extensions/envelope/envelope_cc_test.go index f7d5930..87495a5 100755 --- a/extensions/envelope/testdata/cc_envelope.go +++ b/extensions/envelope/envelope_cc_test.go @@ -1,17 +1,29 @@ -package testdata +package envelope_test import ( "github.com/hyperledger-labs/cckit/extensions/envelope" "github.com/hyperledger-labs/cckit/router" "github.com/hyperledger-labs/cckit/router/param" "github.com/hyperledger-labs/cckit/serialize" + testcc "github.com/hyperledger-labs/cckit/testing" ) type EnvelopCC struct { } -func NewEnvelopCC(chaincodeName string) *router.Chaincode { - r := router.New(chaincodeName, router.WithSerializer(serialize.PreferJSONSerializer)).Use(envelope.Verify()) +const ( + chaincode = "envelope-chaincode" + channel = "envelope-channel" + methodInvoke = "invokeWithEnvelope" + methodQuery = "queryWithoutEnvelope" +) + +func NewNewEnvelopCCMock(verifier envelope.Verifier) *testcc.MockStub { + return testcc.NewMockStub(chaincode, NewEnvelopCC(verifier, chaincode)).WithChannel(channel) +} + +func NewEnvelopCC(verifier envelope.Verifier, chaincodeName string) *router.Chaincode { + r := router.New(chaincodeName, router.WithSerializer(serialize.PreferJSONSerializer)).Use(envelope.Verify(verifier)) r.Invoke("invokeWithEnvelope", func(c router.Context) (interface{}, error) { return nil, nil diff --git a/extensions/envelope/envelope_test.go b/extensions/envelope/envelope_test.go index 43d11f7..aab4286 100644 --- a/extensions/envelope/envelope_test.go +++ b/extensions/envelope/envelope_test.go @@ -10,13 +10,11 @@ import ( "github.com/btcsuite/btcutil/base58" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" - timestamppb "google.golang.org/protobuf/types/known/timestamppb" + "google.golang.org/protobuf/types/known/timestamppb" e "github.com/hyperledger-labs/cckit/extensions/envelope" - "github.com/hyperledger-labs/cckit/extensions/envelope/testdata" - identitytestdata "github.com/hyperledger-labs/cckit/identity/testdata" + "github.com/hyperledger-labs/cckit/extensions/envelope/crypto" "github.com/hyperledger-labs/cckit/serialize" - testcc "github.com/hyperledger-labs/cckit/testing" ) func TestEnvelop(t *testing.T) { @@ -25,68 +23,39 @@ func TestEnvelop(t *testing.T) { } var ( - Owner = identitytestdata.Certificates[0].MustIdentity(`SOME_MSP`) - - envelopCC *testcc.MockStub - - chaincode = "envelope-chaincode" - channel = "envelope-channel" - methodInvoke = "invokeWithEnvelope" - methodQuery = "queryWithoutEnvelope" - deadline = timestamppb.New(time.Now().AddDate(0, 0, 2)) - - payload = []byte(`{"symbol":"GLD","decimals":"8","name":"Gold digital asset","type":"DM","underlying_asset":"gold","issuer_id":"GLDINC"}`) + deadline = timestamppb.New(time.Now().AddDate(0, 0, 2)) + payload = []byte(`{"symbol":"GLD","decimals":"8","name":"Gold digital asset","type":"DM","underlying_asset":"gold","issuer_id":"GLDINC"}`) ) -var _ = Describe(`Envelop`, func() { - - Describe("Signature methods", func() { - - It("Allow to create keys", func() { - publicKey, privateKey, err := e.CreateKeys() - Expect(err).NotTo(HaveOccurred()) - Expect(len(publicKey)).To(Equal(32)) - Expect(len(privateKey)).To(Equal(64)) - }) - - It("Allow to create nonces", func() { - nonce1 := e.CreateNonce() - nonce2 := e.CreateNonce() - - Expect(nonce1).NotTo(BeEmpty()) - Expect(nonce2).NotTo(BeEmpty()) - // todo: test nonces equivalence - }) +var _ = Describe(`Envelope`, func() { - It("Allow to create signature", func() { - _, privateKey, _ := e.CreateKeys() - _, sig := e.CreateSig(payload, e.CreateNonce(), channel, chaincode, methodInvoke, deadline.String(), privateKey) - Expect(len(sig)).To(Equal(64)) - }) + c := crypto.NewEd25519() + verifier := e.NewVerifier(c) - It("Allow to check valid signature", func() { + Describe("Verifier", func() { + It("Allow to verify valid signature", func() { nonce := e.CreateNonce() - publicKey, privateKey, _ := e.CreateKeys() - _, sig := e.CreateSig(payload, nonce, channel, chaincode, methodInvoke, deadline.String(), privateKey) - err := e.CheckSig(payload, nonce, channel, chaincode, methodInvoke, deadline.String(), publicKey, sig) + publicKey, privateKey, _ := c.GenerateKey() + sig, err := e.Sign(c, payload, nonce, channel, chaincode, methodInvoke, deadline.String(), privateKey) + Expect(err).NotTo(HaveOccurred()) + err = verifier.Verify(payload, nonce, channel, chaincode, methodInvoke, deadline.String(), publicKey, sig) Expect(err).NotTo(HaveOccurred()) }) - It("Disallow to check signature with invalid payload", func() { + It("Disallow to verify signature with invalid payload", func() { nonce := e.CreateNonce() - publicKey, privateKey, _ := e.CreateKeys() - _, sig := e.CreateSig(payload, nonce, channel, chaincode, methodInvoke, deadline.String(), privateKey) + publicKey, privateKey, _ := c.GenerateKey() + sig, _ := e.Sign(c, payload, nonce, channel, chaincode, methodInvoke, deadline.String(), privateKey) invalidPayload := []byte("invalid payload") - err := e.CheckSig(invalidPayload, nonce, channel, chaincode, methodInvoke, deadline.String(), publicKey, sig) + err := verifier.Verify(invalidPayload, nonce, channel, chaincode, methodInvoke, deadline.String(), publicKey, sig) Expect(err).Should(MatchError(e.ErrSignatureCheckFailed)) }) - }) Describe("Handle base64 envelop", func() { It("Allow to parse base64 envelop", func() { - _, envelope := createEnvelope(payload, channel, chaincode, methodInvoke, deadline) + _, envelope := createEnvelope(c, payload, channel, chaincode, methodInvoke, deadline) jj, _ := json.Marshal(envelope) b64 := base64.StdEncoding.EncodeToString(jj) bb, err := e.DecodeEnvelope([]byte(b64)) @@ -99,20 +68,14 @@ var _ = Describe(`Envelop`, func() { Describe("Signature verification", func() { It("Allow to verify valid signature", func() { - serializedEnvelope, _ := createEnvelope(payload, channel, chaincode, methodInvoke, deadline) - - envelopCC = testcc.NewMockStub(chaincode, testdata.NewEnvelopCC(chaincode)).WithChannel(channel) - resp := envelopCC.Invoke(methodInvoke, payload, serializedEnvelope) - + serializedEnvelope, _ := createEnvelope(c, payload, channel, chaincode, methodInvoke, deadline) + resp := NewNewEnvelopCCMock(verifier).Invoke(methodInvoke, payload, serializedEnvelope) Expect(resp.Status).To(BeNumerically("==", 200)) }) It("Allow to verify valid signature without deadline", func() { - serializedEnvelope, _ := createEnvelope(payload, channel, chaincode, methodInvoke) - - envelopCC = testcc.NewMockStub(chaincode, testdata.NewEnvelopCC(chaincode)).WithChannel(channel) - resp := envelopCC.Invoke(methodInvoke, payload, serializedEnvelope) - + serializedEnvelope, _ := createEnvelope(c, payload, channel, chaincode, methodInvoke) + resp := NewNewEnvelopCCMock(verifier).Invoke(methodInvoke, payload, serializedEnvelope) Expect(resp.Status).To(BeNumerically("==", 200)) }) @@ -122,9 +85,7 @@ var _ = Describe(`Envelop`, func() { decodedEnvelope, err := e.DecodeEnvelope([]byte(b64Envelope)) Expect(err).NotTo(HaveOccurred()) - envelopCC = testcc.NewMockStub(chaincode, testdata.NewEnvelopCC(chaincode)).WithChannel(channel) - resp := envelopCC.Invoke(methodInvoke, payload, decodedEnvelope) - + resp := NewNewEnvelopCCMock(verifier).Invoke(methodInvoke, payload, decodedEnvelope) Expect(resp.Status).To(BeNumerically("==", 200)) }) @@ -134,44 +95,34 @@ var _ = Describe(`Envelop`, func() { decodedEnvelope, err := e.DecodeEnvelope([]byte(b64Envelope)) Expect(err).NotTo(HaveOccurred()) - envelopCC = testcc.NewMockStub(chaincode, testdata.NewEnvelopCC(chaincode)).WithChannel(channel) - resp := envelopCC.Invoke(methodInvoke, payload, decodedEnvelope) - + resp := NewNewEnvelopCCMock(verifier).Invoke(methodInvoke, payload, decodedEnvelope) Expect(resp.Status).To(BeNumerically("==", 200)) }) It("Disallow to verify signature with invalid payload", func() { - serializedEnvelope, _ := createEnvelope(payload, channel, chaincode, methodInvoke, deadline) - - envelopCC = testcc.NewMockStub(chaincode, testdata.NewEnvelopCC(chaincode)).WithChannel(channel) + serializedEnvelope, _ := createEnvelope(c, payload, channel, chaincode, methodInvoke, deadline) invalidPayload := []byte("invalid payload") - resp := envelopCC.Invoke(methodInvoke, invalidPayload, serializedEnvelope) + resp := NewNewEnvelopCCMock(verifier).Invoke(methodInvoke, invalidPayload, serializedEnvelope) Expect(resp.Status).To(BeNumerically("==", 500)) }) It("Disallow to verify signature with invalid method", func() { - serializedEnvelope, _ := createEnvelope(payload, channel, chaincode, "invalid method", deadline) - - envelopCC = testcc.NewMockStub(chaincode, testdata.NewEnvelopCC(chaincode)).WithChannel(channel) - resp := envelopCC.Invoke(methodInvoke, payload, serializedEnvelope) + serializedEnvelope, _ := createEnvelope(c, payload, channel, chaincode, "invalid method", deadline) + resp := NewNewEnvelopCCMock(verifier).Invoke(methodInvoke, payload, serializedEnvelope) Expect(resp.Status).To(BeNumerically("==", 500)) }) It("Disallow to verify signature with invalid channel", func() { - serializedEnvelope, _ := createEnvelope(payload, "invalid channel", chaincode, methodInvoke, deadline) - - envelopCC = testcc.NewMockStub(chaincode, testdata.NewEnvelopCC(chaincode)).WithChannel(channel) - resp := envelopCC.Invoke(methodInvoke, payload, serializedEnvelope) + serializedEnvelope, _ := createEnvelope(c, payload, "invalid channel", chaincode, methodInvoke, deadline) + resp := NewNewEnvelopCCMock(verifier).Invoke(methodInvoke, payload, serializedEnvelope) Expect(resp.Status).To(BeNumerically("==", 500)) }) It("Don't check signature for query method", func() { - envelopCC = testcc.NewMockStub(chaincode, testdata.NewEnvelopCC(chaincode)).WithChannel(channel) - resp := envelopCC.Query(methodQuery, payload) - + resp := NewNewEnvelopCCMock(verifier).Query(methodQuery, payload) Expect(resp.Status).To(BeNumerically("==", 200)) }) @@ -179,12 +130,11 @@ var _ = Describe(`Envelop`, func() { Describe("Nonce verification (replay attack)", func() { It("Disallow to execute tx with the same parameters (nonce, payload, pubkey)", func() { - envelopCC = testcc.NewMockStub(chaincode, testdata.NewEnvelopCC(chaincode)).WithChannel(channel) - - publicKey, privateKey, _ := e.CreateKeys() + publicKey, privateKey, _ := c.GenerateKey() nonce := "thesamenonce" - hashToSign := e.Hash(payload, nonce, channel, chaincode, methodInvoke, deadline.AsTime().Format(e.TimeLayout), []byte(publicKey)) - _, sig := e.CreateSig(payload, nonce, channel, chaincode, methodInvoke, deadline.AsTime().Format(e.TimeLayout), privateKey) + + hashToSign := c.Hash(e.PrepareToHash(payload, nonce, channel, chaincode, methodInvoke, deadline.AsTime().Format(e.TimeLayout), publicKey)) + sig, _ := c.Sign(privateKey, hashToSign) envelope := &e.Envelope{ PublicKey: base58.Encode([]byte(publicKey)), Signature: base58.Encode(sig), @@ -199,18 +149,19 @@ var _ = Describe(`Envelop`, func() { serializer := serialize.PreferJSONSerializer serializedEnvelope, _ := serializer.ToBytesFrom(envelope) - resp := envelopCC.Invoke(methodInvoke, payload, serializedEnvelope) + cc := NewNewEnvelopCCMock(verifier) + resp := cc.Invoke(methodInvoke, payload, serializedEnvelope) Expect(resp.Status).To(BeNumerically("==", 200)) - resp = envelopCC.Invoke(methodInvoke, payload, serializedEnvelope) + resp = cc.Invoke(methodInvoke, payload, serializedEnvelope) Expect(errors.New(resp.Message)).To(MatchError(e.ErrTxAlreadyExecuted)) }) }) }) -func createEnvelope(payload []byte, channel, chaincode, method string, deadline ...*timestamppb.Timestamp) ([]byte, *e.Envelope) { - publicKey, privateKey, _ := e.CreateKeys() +func createEnvelope(c crypto.Crypto, payload []byte, channel, chaincode, method string, deadline ...*timestamppb.Timestamp) ([]byte, *e.Envelope) { + publicKey, privateKey, _ := crypto.NewEd25519().GenerateKey() nonce := e.CreateNonce() envelope := &e.Envelope{ @@ -226,10 +177,10 @@ func createEnvelope(payload []byte, channel, chaincode, method string, deadline envelope.Deadline = deadline[0] formatDeadline = envelope.Deadline.AsTime().Format(e.TimeLayout) } - hashToSign := e.Hash(payload, nonce, channel, chaincode, method, formatDeadline, publicKey) - envelope.HashToSign = base58.Encode(hashToSign[:]) + hashToSign := c.Hash(e.PrepareToHash(payload, nonce, channel, chaincode, method, formatDeadline, publicKey)) + envelope.HashToSign = base58.Encode(hashToSign) - _, sig := e.CreateSig(payload, nonce, channel, chaincode, method, formatDeadline, privateKey) + sig, _ := c.Sign(privateKey, hashToSign) envelope.Signature = base58.Encode(sig) serializedEnvelope, _ := serialize.PreferJSONSerializer.ToBytesFrom(envelope) diff --git a/extensions/envelope/middleware.go b/extensions/envelope/middleware.go index ba2d392..9648e64 100644 --- a/extensions/envelope/middleware.go +++ b/extensions/envelope/middleware.go @@ -6,8 +6,9 @@ import ( "time" "github.com/btcsuite/btcutil/base58" - "github.com/hyperledger-labs/cckit/router" "go.uber.org/zap" + + "github.com/hyperledger-labs/cckit/router" ) const ( @@ -26,68 +27,68 @@ const ( ) // Verify is a middleware for checking signature in envelop -func Verify() router.MiddlewareFunc { +func Verify(verifier Verifier) router.MiddlewareFunc { return func(next router.HandlerFunc, pos ...int) router.HandlerFunc { - return func(c router.Context) (interface{}, error) { - if c.Handler().Type == invokeType { - iArgs := c.GetArgs() + return func(ctx router.Context) (interface{}, error) { + if ctx.Handler().Type == invokeType { + iArgs := ctx.GetArgs() if string(iArgs[methodNamePos]) != initType { if len(iArgs) == 2 { - c.Logger().Sugar().Error(ErrSignatureNotFound) + ctx.Logger().Sugar().Error(ErrSignatureNotFound) return nil, ErrSignatureNotFound } else { var ( e *Envelope err error ) - if e, err = verifyEnvelope(c, iArgs[methodNamePos], iArgs[payloadPos], iArgs[envelopePos]); err != nil { + if e, err = verifyEnvelope(ctx, verifier, iArgs[methodNamePos], iArgs[payloadPos], iArgs[envelopePos]); err != nil { return nil, err } // store correct pubkey in context - c.SetParam(PubKey, e.PublicKey) + ctx.SetParam(PubKey, e.PublicKey) } } } - return next(c) + return next(ctx) } } } -func verifyEnvelope(c router.Context, method, payload, envlp []byte) (*Envelope, error) { +func verifyEnvelope(ctx router.Context, verifier Verifier, method, payload, envlp []byte) (*Envelope, error) { // parse json envelope format (json is original format for envelope from frontend) - data, err := c.Serializer().FromBytesTo(envlp, &Envelope{}) + data, err := ctx.Serializer().FromBytesTo(envlp, &Envelope{}) if err != nil { - c.Logger().Error(`convert from bytes failed:`, zap.Error(err)) + ctx.Logger().Error(`convert from bytes failed:`, zap.Error(err)) return nil, err } envelope := data.(*Envelope) if envelope.Deadline.AsTime().Unix() != 0 { if envelope.Deadline.AsTime().Unix() < time.Now().Unix() { - c.Logger().Sugar().Error(ErrDeadlineExpired) + ctx.Logger().Sugar().Error(ErrDeadlineExpired) return nil, ErrDeadlineExpired } } // check method and channel names because envelope can only be used once for channel+chaincode+method combination if string(method) != envelope.Method { - c.Logger().Sugar().Error(ErrInvalidMethod) + ctx.Logger().Sugar().Error(ErrInvalidMethod) return nil, ErrInvalidMethod } - if c.Stub().GetChannelID() != envelope.Channel { - c.Logger().Sugar().Error(ErrInvalidChannel) + if ctx.Stub().GetChannelID() != envelope.Channel { + ctx.Logger().Sugar().Error(ErrInvalidChannel) return nil, ErrInvalidChannel } // replay attack check txHash := txNonceKey(payload, envelope.Nonce, envelope.Channel, envelope.Chaincode, envelope.Method, envelope.PublicKey) - key, err := c.Stub().CreateCompositeKey(nonceObjectType, []string{txHash}) + key, err := ctx.Stub().CreateCompositeKey(nonceObjectType, []string{txHash}) if err != nil { return nil, err } - bb, err := c.Stub().GetState(key) + bb, err := ctx.Stub().GetState(key) if bb == nil && err == nil { - if err := c.Stub().PutState(key, []byte{'0'}); err != nil { + if err := ctx.Stub().PutState(key, []byte{'0'}); err != nil { return nil, err } // convert public key and sig from base58 @@ -98,13 +99,12 @@ func verifyEnvelope(c router.Context, method, payload, envlp []byte) (*Envelope, if envelope.Deadline != nil { deadline = envelope.Deadline.AsTime().Format(TimeLayout) } - if err := CheckSig(payload, envelope.Nonce, envelope.Channel, envelope.Chaincode, envelope.Method, deadline, pubkey, sig); err != nil { - c.Logger().Error(ErrCheckSignatureFailed.Error(), zap.String("payload", string(payload)), zap.Any("envelope", envelope)) - //c.Logger().Sugar().Error(ErrCheckSignatureFailed) + if err := verifier.Verify(payload, envelope.Nonce, envelope.Channel, envelope.Chaincode, envelope.Method, deadline, pubkey, sig); err != nil { + ctx.Logger().Error(ErrCheckSignatureFailed.Error(), zap.String("payload", string(payload)), zap.Any("envelope", envelope)) return nil, ErrCheckSignatureFailed } } else { - c.Logger().Sugar().Error(ErrTxAlreadyExecuted) + ctx.Logger().Sugar().Error(ErrTxAlreadyExecuted) return nil, ErrTxAlreadyExecuted } return envelope, nil diff --git a/extensions/envelope/sign.go b/extensions/envelope/sign.go new file mode 100644 index 0000000..912ae65 --- /dev/null +++ b/extensions/envelope/sign.go @@ -0,0 +1,43 @@ +package envelope + +import ( + "fmt" + "strconv" + "strings" + "time" + + "github.com/btcsuite/btcutil/base58" + + "github.com/hyperledger-labs/cckit/extensions/envelope/crypto" +) + +func Sign(crypto crypto.Crypto, payload []byte, nonce, channel, chaincode, method, deadline string, privateKey []byte) ([]byte, error) { + pubKey, err := crypto.PublicKey(privateKey) + if err != nil { + return nil, fmt.Errorf(`extract public key: %w`, err) + } + + return crypto.Sign(privateKey, + crypto.Hash(PrepareToHash(payload, nonce, channel, chaincode, method, deadline, pubKey))) +} + +func CreateNonce() string { + return strconv.Itoa(int(time.Now().Unix())) +} + +func PrepareToHash(payload []byte, nonce, channel, chaincode, method, deadline string, pubkey []byte) []byte { + bb := append(removeSpacesBetweenCommaAndQuotes(payload), nonce...) // resolve the unclear json serialization behavior in protojson package + bb = append(bb, channel...) + bb = append(bb, chaincode...) + bb = append(bb, method...) + bb = append(bb, deadline...) + b58Pubkey := base58.Encode(pubkey) + bb = append(bb, b58Pubkey...) + return bb +} + +func removeSpacesBetweenCommaAndQuotes(s []byte) []byte { + removed := strings.ReplaceAll(string(s), `", "`, `","`) + removed = strings.ReplaceAll(removed, `"}, {"`, `"},{"`) + return []byte(strings.ReplaceAll(removed, `], "`, `],"`)) +} diff --git a/extensions/envelope/signature.go b/extensions/envelope/signature.go deleted file mode 100644 index 4a9312a..0000000 --- a/extensions/envelope/signature.go +++ /dev/null @@ -1,56 +0,0 @@ -package envelope - -import ( - "crypto/ed25519" - "crypto/rand" - "crypto/sha256" - "strconv" - "strings" - "time" - - "github.com/btcsuite/btcutil/base58" -) - -func CreateKeys() (ed25519.PublicKey, ed25519.PrivateKey, error) { - publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader) - if err != nil { - return nil, nil, err - } - return publicKey, privateKey, nil -} - -func CreateNonce() string { - return strconv.Itoa(int(time.Now().Unix())) -} - -func Hash(payload []byte, nonce, channel, chaincode, method, deadline string, pubkey []byte) [32]byte { - bb := append(removeSpacesBetweenCommaAndQuotes(payload), nonce...) // resolve the unclear json serialization behavior in protojson package - bb = append(bb, channel...) - bb = append(bb, chaincode...) - bb = append(bb, method...) - bb = append(bb, deadline...) - b58Pubkey := base58.Encode(pubkey) - bb = append(bb, b58Pubkey...) - return sha256.Sum256(bb) -} - -func CreateSig(payload []byte, nonce, channel, chaincode, method, deadline string, privateKey []byte) ([]byte, []byte) { - pubKey := ed25519.PrivateKey(privateKey).Public() - hashed := Hash(payload, nonce, channel, chaincode, method, deadline, []byte(pubKey.(ed25519.PublicKey))) - sig := ed25519.Sign(privateKey, hashed[:]) - return pubKey.(ed25519.PublicKey), sig -} - -func CheckSig(payload []byte, nonce, channel, chaincode, method, deadline string, pubKey []byte, sig []byte) error { - hashed := Hash(payload, nonce, channel, chaincode, method, deadline, pubKey) - if !ed25519.Verify(pubKey, hashed[:], sig) { - return ErrCheckSignatureFailed - } - return nil -} - -func removeSpacesBetweenCommaAndQuotes(s []byte) []byte { - removed := strings.ReplaceAll(string(s), `", "`, `","`) - removed = strings.ReplaceAll(removed, `"}, {"`, `"},{"`) - return []byte(strings.ReplaceAll(removed, `], "`, `],"`)) -} diff --git a/extensions/envelope/verifier.go b/extensions/envelope/verifier.go new file mode 100644 index 0000000..dfa1afd --- /dev/null +++ b/extensions/envelope/verifier.go @@ -0,0 +1,27 @@ +package envelope + +import ( + "github.com/hyperledger-labs/cckit/extensions/envelope/crypto" +) + +type ( + Verifier interface { + Verify(payload []byte, nonce, channel, chaincode, method, deadline string, publicKey []byte, sig []byte) error + } + + DefaultVerifier struct { + verifier crypto.Verifier + } +) + +func NewVerifier(verifier crypto.Verifier) *DefaultVerifier { + return &DefaultVerifier{verifier: verifier} +} + +func (s *DefaultVerifier) Verify(payload []byte, nonce, channel, chaincode, method, deadline string, pubKey []byte, sig []byte) error { + if err := s.verifier.Verify(pubKey, + s.verifier.Hash(PrepareToHash(payload, nonce, channel, chaincode, method, deadline, pubKey)), sig); err != nil { + return ErrCheckSignatureFailed + } + return nil +}