fabric-gateway 0.0.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.editorconfig +3 -0
  3. data/.github/workflows/codeql-analysis.yml +71 -0
  4. data/.github/workflows/rspec.yml +37 -0
  5. data/.github/workflows/rubocop.yml +28 -0
  6. data/.github/workflows/todo.yml +10 -0
  7. data/.github/workflows/yardoc.yml +28 -0
  8. data/.gitignore +1 -0
  9. data/.rubocop.yml +23 -0
  10. data/.ruby-version +1 -0
  11. data/.vscode/settings.json +7 -0
  12. data/.yardopts +8 -0
  13. data/CODE_OF_CONDUCT.md +105 -46
  14. data/Gemfile +5 -3
  15. data/LICENSE.txt +1 -1
  16. data/README.md +123 -12
  17. data/Rakefile +6 -3
  18. data/bin/console +4 -3
  19. data/bin/regenerate +1 -0
  20. data/bin/release +5 -0
  21. data/fabric-gateway.gemspec +31 -17
  22. data/lib/fabric/accessors/contract.rb +51 -0
  23. data/lib/fabric/accessors/gateway.rb +33 -0
  24. data/lib/fabric/accessors/network.rb +40 -0
  25. data/lib/fabric/client.rb +199 -0
  26. data/lib/fabric/constants.rb +8 -0
  27. data/lib/fabric/contract.rb +178 -0
  28. data/lib/fabric/ec_crypto_suite.rb +199 -0
  29. data/lib/fabric/entities/chaincode_events_requests.rb +166 -0
  30. data/lib/fabric/entities/envelope.rb +158 -0
  31. data/lib/fabric/entities/identity.rb +87 -0
  32. data/lib/fabric/entities/proposal.rb +189 -0
  33. data/lib/fabric/entities/proposed_transaction.rb +163 -0
  34. data/lib/fabric/entities/status.rb +32 -0
  35. data/lib/fabric/entities/transaction.rb +247 -0
  36. data/lib/fabric/gateway.rb +31 -6
  37. data/lib/fabric/network.rb +70 -0
  38. data/lib/fabric/version.rb +5 -0
  39. data/lib/fabric.rb +59 -0
  40. data/lib/msp/identities_pb.rb +25 -0
  41. metadata +162 -13
  42. data/Gemfile.lock +0 -42
  43. data/lib/fabric/gateway/version.rb +0 -5
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+
5
+ # Adapted from:
6
+ # https://github.com/kirshin/hyperledger-fabric-sdk/blob/95a5a1a37001852312df25946e960a9ff149207e/lib/fabric/crypto_suite.rb
7
+ module Fabric
8
+ #
9
+ # Elliptic-curve Crypto Suite using OpenSSL
10
+ #
11
+ # @todo missing tests
12
+ class ECCryptoSuite # rubocop:disable Metrics/ClassLength
13
+ DEFAULT_KEY_SIZE = 256
14
+ DEFAULT_DIGEST_ALGORITHM = 'SHA256'
15
+ DEFAULT_AES_KEY_SIZE = 128
16
+
17
+ EC_CURVES = { 256 => 'prime256v1', 384 => 'secp384r1' }.freeze
18
+
19
+ CIPHER = 'aes-256-cbc'
20
+
21
+ attr_reader :key_size, :digest_algorithm, :digest_instance, :curve, :cipher
22
+
23
+ def initialize(opts = {})
24
+ @key_size = opts[:key_size] || DEFAULT_KEY_SIZE
25
+ @digest_algorithm = opts[:digest_algorithm] || DEFAULT_DIGEST_ALGORITHM
26
+ @digest_instance = OpenSSL::Digest.new digest_algorithm
27
+ @curve = EC_CURVES[key_size]
28
+ @cipher = opts[:cipher] || CIPHER
29
+ end
30
+
31
+ def sign(private_key, message)
32
+ digest = digest message
33
+ key = pkey_from_private_key private_key
34
+ signature = key.dsa_sign_asn1 digest
35
+ sequence = OpenSSL::ASN1.decode signature
36
+ sequence = prevent_malleability sequence, key.group.order
37
+
38
+ sequence.to_der
39
+ end
40
+
41
+ def verify(public_key, message, signature)
42
+ digest = digest message
43
+ openssl_pkey = openssl_pkey_from_public_key public_key
44
+ sequence = OpenSSL::ASN1.decode signature
45
+ return false unless check_malleability sequence, openssl_pkey.group.order
46
+
47
+ openssl_pkey.dsa_verify_asn1(digest, signature)
48
+ end
49
+
50
+ def generate_private_key
51
+ key = OpenSSL::PKey::EC.new curve
52
+ key.generate_key!
53
+
54
+ key.private_key.to_s(16).downcase
55
+ end
56
+
57
+ def generate_csr(private_key, attrs = [])
58
+ key = pkey_from_private_key private_key
59
+
60
+ req = OpenSSL::X509::Request.new
61
+ req.public_key = key
62
+ req.subject = OpenSSL::X509::Name.new attrs
63
+ req.sign key, @digest_instance
64
+
65
+ req
66
+ end
67
+
68
+ def generate_nonce(length = 24)
69
+ OpenSSL::Random.random_bytes length
70
+ end
71
+
72
+ def hexdigest(message)
73
+ @digest_instance.hexdigest message
74
+ end
75
+
76
+ def digest(message)
77
+ @digest_instance.digest message
78
+ end
79
+
80
+ def encode_hex(bytes)
81
+ bytes.unpack1('H*')
82
+ end
83
+
84
+ def decode_hex(string)
85
+ [string].pack('H*')
86
+ end
87
+
88
+ def restore_public_key(private_key)
89
+ private_bn = OpenSSL::BN.new private_key, 16
90
+ group = OpenSSL::PKey::EC::Group.new curve
91
+ public_bn = group.generator.mul(private_bn).to_bn
92
+ public_bn = OpenSSL::PKey::EC::Point.new(group, public_bn).to_bn
93
+
94
+ public_bn.to_s(16).downcase
95
+ end
96
+
97
+ def address_from_public_key(public_key)
98
+ bytes = decode_hex public_key
99
+ address_bytes = digest(bytes[1..])[-20..]
100
+
101
+ encode_hex address_bytes
102
+ end
103
+
104
+ def build_shared_key(private_key, public_key)
105
+ pkey = pkey_from_private_key private_key
106
+ public_bn = OpenSSL::BN.new public_key, 16
107
+ group = OpenSSL::PKey::EC::Group.new curve
108
+ public_point = OpenSSL::PKey::EC::Point.new group, public_bn
109
+
110
+ encode_hex pkey.dh_compute_key(public_point)
111
+ end
112
+
113
+ def encrypt(secret, data)
114
+ aes = OpenSSL::Cipher.new cipher
115
+ aes.encrypt
116
+ aes.key = decode_hex(secret)
117
+ iv = aes.random_iv
118
+ aes.iv = iv
119
+
120
+ Base64.strict_encode64(iv + aes.update(data) + aes.final)
121
+ end
122
+
123
+ def decrypt(secret, data)
124
+ return unless data
125
+
126
+ encrypted_data = Base64.strict_decode64 data
127
+ aes = OpenSSL::Cipher.new cipher
128
+ aes.decrypt
129
+ aes.key = decode_hex(secret)
130
+ aes.iv = encrypted_data[0..15]
131
+ encrypted_data = encrypted_data[16..]
132
+
133
+ aes.update(encrypted_data) + aes.final
134
+ end
135
+
136
+ def pkey_pem_from_private_key(private_key)
137
+ public_key = restore_public_key private_key
138
+ key = OpenSSL::PKey::EC.new curve
139
+ key.private_key = OpenSSL::BN.new private_key, 16
140
+ key.public_key = OpenSSL::PKey::EC::Point.new key.group,
141
+ OpenSSL::BN.new(public_key, 16)
142
+
143
+ pkey = OpenSSL::PKey::EC.new(key.public_key.group)
144
+ pkey.public_key = key.public_key
145
+
146
+ pkey.to_pem
147
+ end
148
+
149
+ def key_from_pem(pem)
150
+ key = OpenSSL::PKey::EC.new(pem)
151
+ key.private_key.to_s(16).downcase
152
+ end
153
+
154
+ def pkey_from_x509_certificate(certificate)
155
+ cert = OpenSSL::X509::Certificate.new(certificate)
156
+ cert.public_key.public_key.to_bn.to_s(16).downcase
157
+ end
158
+
159
+ def openssl_pkey_from_public_key(public_key)
160
+ pkey = OpenSSL::PKey::EC.new curve
161
+ pkey.public_key = OpenSSL::PKey::EC::Point.new(pkey.group, OpenSSL::BN.new(public_key, 16))
162
+
163
+ pkey
164
+ end
165
+
166
+ private
167
+
168
+ def pkey_from_private_key(private_key)
169
+ public_key = restore_public_key private_key
170
+ key = OpenSSL::PKey::EC.new curve
171
+ key.private_key = OpenSSL::BN.new private_key, 16
172
+ key.public_key = OpenSSL::PKey::EC::Point.new key.group,
173
+ OpenSSL::BN.new(public_key, 16)
174
+
175
+ key
176
+ end
177
+
178
+ # barely understand this code - this link provides a good explanation:
179
+ # http://coders-errand.com/malleability-ecdsa-signatures/
180
+ def prevent_malleability(sequence, order)
181
+ half_order = order >> 1
182
+
183
+ if (half_key = sequence.value[1].value) > half_order
184
+ sequence.value[1].value = order - half_key
185
+ end
186
+
187
+ sequence
188
+ end
189
+
190
+ # ported from python code, understanding extremely limited.
191
+ # from what I gather, sequence.value[0] and sequence.value[1]
192
+ # are the r and s values from the python implementation
193
+ # https://github.com/hyperledger/fabric-sdk-py/blob/25209f61518873da68d28313582607c29b5bae7d/hfc/util/crypto/crypto.py#L259
194
+ def check_malleability(sequence, order)
195
+ half_order = order >> 1
196
+ sequence.value[1].value <= half_order
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fabric
4
+ #
5
+ # Encapsulates a Chaincode Events Request protobuf message
6
+ #
7
+ class ChaincodeEventsRequest
8
+ attr_reader :contract,
9
+ :start_block
10
+
11
+ # @!parse include Fabric::Accessors::Network
12
+ # @!parse include Fabric::Accessors::Gateway
13
+ include Fabric::Accessors::Contract
14
+
15
+ #
16
+ # Creates a new ChaincodeEventsRequest
17
+ #
18
+ # @param [Fabric::Contract] contract an instance of a contract
19
+ # @param [Integer] start_block Block number at which to start reading chaincode events.
20
+ #
21
+ def initialize(contract, start_block: nil)
22
+ @contract = contract
23
+ @start_block = start_block
24
+ end
25
+
26
+ #
27
+ # Returns the signed request
28
+ #
29
+ # @return [Gateway::SignedChaincodeEventsRequest] generated signed request
30
+ #
31
+ def signed_request
32
+ @signed_request ||= ::Gateway::SignedChaincodeEventsRequest.new(request: chaincode_events_request.to_proto)
33
+ end
34
+
35
+ #
36
+ # Returns the chaincode events request
37
+ #
38
+ # @return [Gateway::ChaincodeEventsRequest] chaincode events request - controls what events are returned
39
+ # from a chaincode events request
40
+ #
41
+ def chaincode_events_request
42
+ @chaincode_events_request ||= new_chaincode_events_request
43
+ end
44
+
45
+ #
46
+ # Get the serialized chaincode events request protobuffer message.
47
+ #
48
+ # @return [String] protobuffer serialized chaincode events request
49
+ #
50
+ def request_bytes
51
+ signed_request.request
52
+ end
53
+
54
+ #
55
+ # Get the digest of the chaincode events request. This is used to generate a digital signature.
56
+ #
57
+ # @return [String] chaincode events request digest
58
+ #
59
+ def request_digest
60
+ Fabric.crypto_suite.digest(request_bytes)
61
+ end
62
+
63
+ #
64
+ # Sets the signed request signature.
65
+ #
66
+ # @param [String] signature
67
+ #
68
+ # @return [void]
69
+ #
70
+ def signature=(signature)
71
+ signed_request.signature = signature
72
+ end
73
+
74
+ #
75
+ # Returns the signed_request signature
76
+ #
77
+ # @return [String] Raw byte string signature
78
+ #
79
+ def signature
80
+ signed_request.signature
81
+ end
82
+
83
+ #
84
+ # Sign the chaincode events request; Noop if request already signed.
85
+ #
86
+ # @return [void]
87
+ #
88
+ def sign
89
+ return if signed?
90
+
91
+ self.signature = signer.sign(request_bytes)
92
+ end
93
+
94
+ #
95
+ # Checks if the signed chaincode events has been signed.
96
+ #
97
+ # @return [Boolean] true if the signed chaincode events has been signed; otherwise false.
98
+ #
99
+ def signed?
100
+ !signed_request.signature.empty?
101
+ end
102
+
103
+ #
104
+ # Get chaincode events emitted by transaction functions of a specific chaincode.
105
+ #
106
+ # @see Fabric::Client#chaincode_events Fabric::Client#chaincode_events - explanation of the different return types
107
+ # and example usage.
108
+ # @see https://www.rubydoc.info/gems/grpc/GRPC%2FClientStub:server_streamer Call options for options parameter
109
+ #
110
+ # @param [Hash] options gRPC call options (merged with default_call_options from initializer)
111
+ # @yield [chaincode_event] loops through the chaincode events
112
+ # @yieldparam [Gateway::ChaincodeEventsResponse] chaincode_event the chaincode event
113
+ #
114
+ # @return [Enumerator|GRPC::ActiveCall::Operation|nil] Dependent on parameters passed;
115
+ # please see Fabric::Client#get_chaincode_events
116
+ #
117
+ def get_events(options = {}, &block)
118
+ sign
119
+
120
+ client.chaincode_events(signed_request, options, &block)
121
+ end
122
+
123
+ private
124
+
125
+ #
126
+ # Generates a new chaincode events request
127
+ #
128
+ # @return [Gateway::ChaincodeEventsRequest] chaincode events request - controls what events are returned
129
+ #
130
+ def new_chaincode_events_request
131
+ ::Gateway::ChaincodeEventsRequest.new(
132
+ channel_id: network_name,
133
+ chaincode_id: chaincode_name,
134
+ identity: signer.to_proto,
135
+ start_position: start_position
136
+ )
137
+ end
138
+
139
+ #
140
+ # Generates the start_position for the chaincode events request or returns the cached start_position
141
+ #
142
+ # @return [Orderer::SeekPosition] start position for the chaincode events request
143
+ #
144
+ def start_position
145
+ @start_position ||= new_start_position
146
+ end
147
+
148
+ #
149
+ # Generates the start position for the chaincode events request; if no start_block is specified,
150
+ # generates a seek next commit start position, otherwise generates a start_position to the start_block
151
+ #
152
+ # @return [Orderer::SeekPosition] start position for the chaincode events request
153
+ #
154
+ def new_start_position
155
+ specified = nil
156
+ next_commit = nil
157
+
158
+ if start_block
159
+ specified = ::Orderer::SeekSpecified.new(number: start_block)
160
+ else
161
+ next_commit = ::Orderer::SeekNextCommit.new
162
+ end
163
+ Orderer::SeekPosition.new(specified: specified, next_commit: next_commit)
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fabric
4
+ #
5
+ # Encapsulates an Envelop protobuf message
6
+ #
7
+ class Envelope
8
+ # @return [Common::Envelope] transaction envelope
9
+ attr_reader :envelope
10
+
11
+ #
12
+ # Creates a new Envelope instance.
13
+ #
14
+ # @param [Common::Envelope] envelope
15
+ #
16
+ def initialize(envelope)
17
+ @envelope = envelope
18
+ end
19
+
20
+ #
21
+ # Checks if the envelope has been signed.
22
+ #
23
+ # @return [Boolean] true if the envelope has been signed; otherwise false.
24
+ #
25
+ def signed?
26
+ !envelope.signature.empty?
27
+ end
28
+
29
+ #
30
+ # The protobuffer serialized form of the envelope payload.
31
+ #
32
+ # @return [String] serialized payload
33
+ #
34
+ def payload_bytes
35
+ envelope.payload
36
+ end
37
+
38
+ #
39
+ # The digest of the payload.
40
+ #
41
+ # @return [String] payload digest
42
+ #
43
+ def payload_digest
44
+ Fabric.crypto_suite.digest(envelope.payload)
45
+ end
46
+
47
+ #
48
+ # Sets the envelope signature.
49
+ #
50
+ # @param [String] signature
51
+ #
52
+ # @return [void]
53
+ #
54
+ def signature=(signature)
55
+ envelope.signature = signature
56
+ end
57
+
58
+ #
59
+ # Returns the results from the transaction result payload.
60
+ #
61
+ # @return [Payload] transaction result payload
62
+ #
63
+ def result
64
+ @result ||= parse_result_from_payload
65
+ end
66
+
67
+ #
68
+ # Returns the deserialized payload.
69
+ #
70
+ # @return [Common::Payload] Envelope payload
71
+ #
72
+ def payload
73
+ @payload ||= Common::Payload.decode(envelope.payload)
74
+ end
75
+
76
+ #
77
+ # Returns the envelope payload header.
78
+ #
79
+ # Envelope => Payload => Header
80
+ #
81
+ # @return [Common::Header] Envelope Payload Header
82
+ #
83
+ def header
84
+ raise Fabric::Error, 'Missing header' if payload.header.nil?
85
+
86
+ @header ||= payload.header
87
+ end
88
+
89
+ #
90
+ # Returns the deserialized transaction channel header
91
+ #
92
+ # Envelope => Payload => Header => ChannelHeader
93
+ #
94
+ # @return [Common::ChannelHeader] envelop payload header channel header
95
+ #
96
+ def channel_header
97
+ @channel_header ||= Common::ChannelHeader.decode(header.channel_header)
98
+ end
99
+
100
+ #
101
+ # Grabs the channel_name frmo the depths of the envelope.
102
+ #
103
+ # @return [String] channel name
104
+ #
105
+ def channel_name
106
+ channel_header.channel_id
107
+ end
108
+
109
+ #
110
+ # Returns the deserialized transaction
111
+ #
112
+ # @return [Protos::Transaction] transaction
113
+ #
114
+ def transaction
115
+ @transaction ||= Protos::Transaction.decode(payload.data)
116
+ end
117
+
118
+ private
119
+
120
+ #
121
+ # Parse the transaction actions from the payload looking for the transaction result payload.
122
+ #
123
+ # @return [String] result payload
124
+ # @raise [Fabric::Error] if the transaction result payload is not found
125
+ #
126
+ def parse_result_from_payload
127
+ errors = []
128
+ transaction.actions.each do |action|
129
+ return parse_result_from_transaction_action(action)
130
+ rescue Fabric::Error => e
131
+ errors << e
132
+ end
133
+
134
+ raise Fabric::Error, "No proposal response found: #{errors.inspect}"
135
+ end
136
+
137
+ #
138
+ # Parse a single transaction action looking for the transaction result payload.
139
+ #
140
+ # @param [Protos::TransactionAction] transaction_action
141
+ #
142
+ # @return [Payload] transaction result payload
143
+ # @raise [Fabric::Error] if the endorsed_action is missing or the chaincode response is missing
144
+ #
145
+ def parse_result_from_transaction_action(transaction_action)
146
+ action_payload = Protos::ChaincodeActionPayload.decode(transaction_action.payload)
147
+ endorsed_action = action_payload.action
148
+ raise Fabric::Error, 'Missing endorsed action' if endorsed_action.nil?
149
+
150
+ response_payload = Protos::ProposalResponsePayload.decode(endorsed_action.proposal_response_payload)
151
+ chaincode_action = Protos::ChaincodeAction.decode(response_payload.extension)
152
+ chaincode_response = chaincode_action.response
153
+ raise Fabric::Error, 'Missing chaincode response' if chaincode_response.nil?
154
+
155
+ chaincode_response.payload
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'msp/identities_pb'
4
+ require 'base64'
5
+
6
+ # Adapted from:
7
+ # https://github.com/kirshin/hyperledger-fabric-sdk/blob/95a5a1a37001852312df25946e960a9ff149207e/lib/fabric/identity.rb
8
+
9
+ module Fabric
10
+ #
11
+ # @attr_reader [String] private_key raw private key in hex format
12
+ # @attr_reader [String] public_key raw public key in hex format
13
+ # @attr_reader [String] certificate raw certificate in pem format
14
+ # @attr_reader [String] msp_id MSP (Membership Service Provider) Identifier
15
+ #
16
+ class Identity
17
+ attr_reader :private_key,
18
+ :public_key,
19
+ :address, # TODO: possibly unnecessary
20
+ :crypto_suite
21
+
22
+ attr_accessor :certificate, :msp_id
23
+
24
+ def initialize(private_key: nil, public_key: nil, certificate: nil, msp_id: nil, crypto_suite: nil)
25
+ @crypto_suite = crypto_suite || Fabric.crypto_suite
26
+
27
+ @private_key = private_key || @crypto_suite.generate_private_key
28
+ @public_key = public_key || @crypto_suite.restore_public_key(@private_key)
29
+ @certificate = certificate
30
+ @msp_id = msp_id
31
+
32
+ @address = @crypto_suite.address_from_public_key @public_key
33
+
34
+ return unless @certificate
35
+
36
+ raise Fabric::Error, 'Key mismatch (public_key or certificate) for identity' unless validate_key_integrity
37
+ end
38
+
39
+ #
40
+ # Validates that the private_key, public_key, and certificate are valid and match
41
+ #
42
+ # @return [boolean] true if valid, false otherwise
43
+ #
44
+ def validate_key_integrity
45
+ cert_pubkey = @crypto_suite.pkey_from_x509_certificate(certificate)
46
+ priv_pubkey = @crypto_suite.restore_public_key(@private_key)
47
+
48
+ @public_key == cert_pubkey && @public_key == priv_pubkey
49
+ end
50
+
51
+ def generate_csr(attrs = [])
52
+ @crypto_suite.generate_csr private_key, attrs
53
+ end
54
+
55
+ def sign(message)
56
+ @crypto_suite.sign(private_key, message)
57
+ end
58
+
59
+ def digest(message)
60
+ @crypto_suite.digest message
61
+ end
62
+
63
+ # TODO: Do we need this?
64
+ def shared_secret_by(public_key)
65
+ @crypto_suite.build_shared_key private_key, public_key
66
+ end
67
+
68
+ def as_proto
69
+ @as_proto ||= Msp::SerializedIdentity.new(mspid: msp_id, id_bytes: certificate)
70
+ end
71
+
72
+ def to_proto
73
+ @to_proto ||= Msp::SerializedIdentity.new(mspid: msp_id, id_bytes: certificate).to_proto
74
+ end
75
+
76
+ #
77
+ # Creates a new gateway passing in the current identity
78
+ #
79
+ # @param [Fabric::Client] client
80
+ #
81
+ # @return [Fabric::Gateway] gateway
82
+ #
83
+ def new_gateway(client)
84
+ Fabric::Gateway.new(self, client)
85
+ end
86
+ end
87
+ end