bitjwt 0.0.2

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 03c5f720f7f4793aa960a681c94d591b741df0b0
4
+ data.tar.gz: 41b4999bae9bc4b134b1fab2133ce52010239612
5
+ SHA512:
6
+ metadata.gz: bdf041a9d5b60a6f0fc006a4fe07bc1aee9c5acedf47c21c3a8d7c5087d9f92aadb569f8a5efb4553bdc3f39f96d015cc75f28174e7a373e66eb2068fe3dc515
7
+ data.tar.gz: e7a02f1391dd54e6a2ff1775aa040664404a213f75f5d0b4214a4f8401a1ad1ed57c350c058a23a8e8de3dfcb2220578221f7b294c43ca404012e5049b5bae27
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ bin/*
2
+ vendor
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,49 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ bitjwt (0.0.1)
5
+ bitcoin-ruby (= 0.0.8)
6
+ excon (~> 0.49)
7
+ ffi (~> 1.9)
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ addressable (2.4.0)
13
+ bitcoin-ruby (0.0.8)
14
+ crack (0.4.3)
15
+ safe_yaml (~> 1.0.0)
16
+ diff-lcs (1.2.5)
17
+ excon (0.49.0)
18
+ ffi (1.9.10)
19
+ hashdiff (0.3.0)
20
+ rspec (3.4.0)
21
+ rspec-core (~> 3.4.0)
22
+ rspec-expectations (~> 3.4.0)
23
+ rspec-mocks (~> 3.4.0)
24
+ rspec-core (3.4.4)
25
+ rspec-support (~> 3.4.0)
26
+ rspec-expectations (3.4.0)
27
+ diff-lcs (>= 1.2.0, < 2.0)
28
+ rspec-support (~> 3.4.0)
29
+ rspec-mocks (3.4.1)
30
+ diff-lcs (>= 1.2.0, < 2.0)
31
+ rspec-support (~> 3.4.0)
32
+ rspec-support (3.4.1)
33
+ safe_yaml (1.0.4)
34
+ webmock (2.1.0)
35
+ addressable (>= 2.3.6)
36
+ crack (>= 0.3.2)
37
+ hashdiff
38
+
39
+ PLATFORMS
40
+ ruby
41
+
42
+ DEPENDENCIES
43
+ bitjwt!
44
+ rspec (~> 3.4)
45
+ rspec-mocks (~> 3.4)
46
+ webmock (~> 2.1)
47
+
48
+ BUNDLED WITH
49
+ 1.12.5
data/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # BitJWT
2
+
3
+ Simple JWT Ruby implementation based on Bitcoin secp256k1
4
+ inspired by bitjws (https://github.com/deginner/bitjws).
5
+
6
+ JWT protocol header built on a custom bitcoin algorithm:
7
+ ```ruby
8
+ header = {
9
+ 'alg' => 'CUSTOM-BITCOIN-SIGN',
10
+ 'kid' => '<bitcoin public address>',
11
+ 'typ' => 'JWT'
12
+ }
13
+ ```
14
+
15
+
16
+ ## Installation
17
+
18
+ Add this line to your application's Gemfile:
19
+
20
+ gem 'bitjwt'
21
+
22
+ And then execute:
23
+
24
+ $ bundle
25
+
26
+ Or install it yourself as:
27
+
28
+ $ gem install bitjwt
29
+
30
+ ## Install bitcoin library
31
+
32
+ Compile and install libsecp256k1 required by bitcoin-ruby:
33
+ (https://github.com/bitcoin/bitcoin/tree/v0.11.0/src/secp256k1)
34
+
35
+ tag: v0.11.0
36
+
37
+ commit: d26f951802c762de04fb68e1a112d611929920ba
38
+
39
+ and place it under your vendor/bitcoin/src/secp256k1/.libs local path
40
+
41
+ ## Usage
42
+
43
+ ```ruby
44
+ # import your WIF private key
45
+ wif_key = 'L2mvkaTyiZMVZ8kPjBkov6FHbp2AVo8DeuXEZVvH7P19KrtpsJtj'
46
+
47
+ # or generate a new one using Bitcoin library
48
+ wif_key = Bitcoin::Key.generate.to_base58
49
+
50
+ # instantiate BitJWT crypto object with your private key
51
+ crypto = BitJWT::Crypto.new(wif_key)
52
+
53
+ # define your payload to send
54
+ payload =
55
+ {
56
+ 'aud' => '/api/audience',
57
+ 'data' => {
58
+ 'key1' => 'value1',
59
+ 'key2' => 0
60
+ }
61
+ }
62
+ # 'aud' relative URL path of your request
63
+ # 'data' data to send
64
+
65
+ #build BitJWT protocol request
66
+ request = BitJWT::Protocol.build_request(crypto, payload)
67
+
68
+ # send request to base URL
69
+ begin
70
+ response = request.send('http://service_base_url', 'POST')
71
+ # check returned data contains a valid signature
72
+ if response.verify
73
+ # get your decoded response
74
+ payload = response.payload_to_h
75
+ end
76
+ rescue BitJWT::ProtocolError => e
77
+ # e.status http error status code
78
+ # e.body application returned error
79
+ end
80
+ ```
data/bitjwt.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ $:.push File.expand_path("../lib", __FILE__)
2
+ require 'bitjwt/version'
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'bitjwt'
6
+ s.version = BitJWT::VERSION
7
+ s.date = '2016-06-16'
8
+ s.summary = 'Bitcoin JWT implementation'
9
+ s.description = 'JWT protocol implementation using Bitcoin secp256k1'
10
+ s.authors = ['Federico Barbazza']
11
+ s.email = 'federico.barbazza@gmail.com'
12
+ s.files = ['lib/bitjwt.rb']
13
+ s.homepage =
14
+ 'http://rubygems.org/gems/bitjwt'
15
+ s.license = 'MIT'
16
+ s.files = `git ls-files -z`.split("\x0")
17
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
18
+ s.require_paths = ['lib']
19
+
20
+ s.add_dependency 'ffi', '~> 1.9'
21
+ s.add_dependency 'bitcoin-ruby', '0.0.8'
22
+ s.add_dependency 'excon', '~> 0.49'
23
+ s.add_development_dependency 'rspec', '~> 3.4'
24
+ s.add_development_dependency 'rspec-mocks', '~> 3.4'
25
+ s.add_development_dependency 'webmock', '~> 2.1'
26
+ end
data/lib/bitjwt.rb ADDED
@@ -0,0 +1,8 @@
1
+ require 'bitjwt/version'
2
+ require 'bitjwt/util'
3
+ require 'bitjwt/crypto'
4
+ require 'bitjwt/protocol_error'
5
+ require 'bitjwt/protocol'
6
+
7
+ module BitJWT
8
+ end
@@ -0,0 +1,24 @@
1
+ require 'bitcoin'
2
+
3
+ module BitJWT
4
+ class Crypto
5
+ def initialize(private_key)
6
+ @key = Bitcoin::Key.from_base58(private_key)
7
+ end
8
+
9
+ def bitcoin_address
10
+ @key.addr
11
+ end
12
+
13
+ def sign(data)
14
+ bsm = Bitcoin.bitcoin_signed_message_hash(data)
15
+ signature = Bitcoin::Secp256k1.sign_compact(bsm, Util.hex_to_bin(@key.priv))
16
+ Base64.strict_encode64(signature)
17
+ end
18
+
19
+ def self.verify(data, signature_base64, pub_address)
20
+ pubkey = Bitcoin::Key.recover_compact_signature_to_key(data, signature_base64)
21
+ pubkey.addr == pub_address
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,93 @@
1
+ module BitJWT
2
+ class Protocol
3
+ attr_reader :header, :payload, :signature
4
+
5
+ def initialize(header, payload, signature = nil)
6
+ @header = header
7
+ @payload = payload
8
+ @signature = signature
9
+ end
10
+
11
+ def header_to_h
12
+ JSON.parse(header)
13
+ end
14
+
15
+ def payload_to_h
16
+ JSON.parse(payload)
17
+ end
18
+
19
+ def header_encoded
20
+ Util.base64url_encode(header)
21
+ end
22
+
23
+ def payload_encoded
24
+ Util.base64url_encode(payload)
25
+ end
26
+
27
+ def header_payload_encoded
28
+ "#{header_encoded}.#{payload_encoded}"
29
+ end
30
+
31
+ def signature_encoded
32
+ Util.base64url_encode(signature)
33
+ end
34
+
35
+ def build_signature(crypto)
36
+ @signature ||= crypto.sign(header_payload_encoded)
37
+ end
38
+
39
+ def send(url, method)
40
+ connection = Excon.new(url, omit_default_port: true)
41
+ response = connection.request(path: payload_to_h['aud'],
42
+ method: method,
43
+ headers: {
44
+ 'Content-Type' => 'application/jose',
45
+ 'User-Agent' => 'bitjwt_client'
46
+ },
47
+ body: "#{header_payload_encoded}.#{signature_encoded}")
48
+ raise ProtocolError.new(response.status, response.body) unless (200..299).cover?(response.status)
49
+ build_response(response.body)
50
+ end
51
+
52
+ def build_response(response)
53
+ header, payload, signature = response.split('.')
54
+ header_decoded = Base64.decode64(header)
55
+ payload_decoded = Base64.decode64(payload)
56
+ signature_decoded = Base64.decode64(signature)
57
+ self.class.new(header_decoded, payload_decoded, signature_decoded)
58
+ end
59
+
60
+ def verify
61
+ Crypto.verify(header_payload_encoded, signature, header_to_h['kid'])
62
+ end
63
+
64
+ def self.build_request(crypto, payload = {})
65
+ header = default_header.merge({ 'kid' => crypto.bitcoin_address })
66
+ payload = default_payload.merge(payload)
67
+ bitjws = new(header.to_json, payload.to_json)
68
+ bitjws.build_signature(crypto)
69
+ bitjws
70
+ end
71
+
72
+ private
73
+
74
+ class << self
75
+ def default_header
76
+ {
77
+ 'alg' => 'CUSTOM-BITCOIN-SIGN',
78
+ 'kid' => '',
79
+ 'typ' => 'JWT'
80
+ }
81
+ end
82
+
83
+ def default_payload
84
+ {
85
+ 'aud' => '',
86
+ 'data' => {},
87
+ 'exp' => Time.now.to_f + 3600,
88
+ 'iat' => Time.now.to_f
89
+ }
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,10 @@
1
+ module BitJWT
2
+ class ProtocolError < StandardError
3
+ attr_reader :status, :body
4
+
5
+ def initialize(status, body)
6
+ @status = status
7
+ @body = body
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,17 @@
1
+ module BitJWT
2
+ class Util
3
+ class << self
4
+ def hex_to_bin(hex)
5
+ [hex].pack('H*')
6
+ end
7
+
8
+ def bin_to_hex(bin)
9
+ bin.unpack('H*')[0]
10
+ end
11
+
12
+ def base64url_encode(str)
13
+ Base64.encode64(str).tr('+/', '-_').gsub(/[\n=]/, '')
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ module BitJWT
2
+ VERSION = '0.0.2'
3
+ end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+ require 'json'
3
+ require 'base64'
4
+
5
+ module BitJWT
6
+ describe BitJWT::Crypto do
7
+ let(:wif_key) { 'L2mvkaTyiZMVZ8kPjBkov6FHbp2AVo8DeuXEZVvH7P19KrtpsJtj' }
8
+ let(:public_key) { '1AyG7DLm14sWEAK1By4seHP33KUQakP3Tc' }
9
+ let(:crypto) { Crypto.new(wif_key) }
10
+ let(:data) { JSON.generate(data: 'test') }
11
+
12
+ it 'import a valid WIF key' do
13
+ expect(crypto.bitcoin_address).to eql public_key
14
+ end
15
+
16
+ it 'generate a valid Base64 strict signature' do
17
+ signature = crypto.sign(data)
18
+ expect(signature).not_to include('\n')
19
+ end
20
+
21
+ it 'verify signature to recover right public key' do
22
+ signature = crypto.sign(data)
23
+ expect(Crypto.verify(data, signature, public_key)).to be_truthy
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,79 @@
1
+ require 'spec_helper'
2
+ require 'excon'
3
+
4
+ module BitJWT
5
+ describe BitJWT::Protocol do
6
+ let(:wif_key) { 'L2mvkaTyiZMVZ8kPjBkov6FHbp2AVo8DeuXEZVvH7P19KrtpsJtj' }
7
+ let(:public_key) { '1AyG7DLm14sWEAK1By4seHP33KUQakP3Tc' }
8
+ let(:crypto) { Crypto.new(wif_key) }
9
+ let(:user_payload) {
10
+ {
11
+ 'aud' => '/endpoint',
12
+ 'data' => 'test'
13
+ }
14
+ }
15
+
16
+ context 'initialization' do
17
+ it 'build a valid JWT request' do
18
+ request = Protocol.build_request(crypto, user_payload)
19
+ expect(request.verify).to be_truthy
20
+ header = request.header_to_h
21
+ payload = request.payload_to_h
22
+ # check JWT header
23
+ expect(header).to have_key('alg')
24
+ expect(header).to have_key('kid')
25
+ expect(header).to have_key('typ')
26
+ expect(header['alg']).to eql 'CUSTOM-BITCOIN-SIGN'
27
+ expect(header['kid']).to eql public_key
28
+ expect(header['typ']).to eql 'JWT'
29
+ # check JWT payload
30
+ expect(payload).to have_key('aud')
31
+ expect(payload).to have_key('data')
32
+ expect(payload).to have_key('exp')
33
+ expect(payload).to have_key('iat')
34
+ expect(payload['aud']).to eql user_payload['aud']
35
+ expect(payload['data']).to eql user_payload['data']
36
+ end
37
+ end
38
+
39
+ context 'send request' do
40
+ let(:url) { 'http://localhost' }
41
+ let(:request) { Protocol.build_request(crypto, user_payload) }
42
+ let(:peer_wif_key) { 'L13zZXQnhgAHuQ8n5GkSasMitEoHFWxV3MC3xJ4U66NJWt2uyNA2' }
43
+ let(:peer_public_key) { '1CAnfBknvhbrpoRmGjRtZ8WsXjMg22wgLf' }
44
+ let(:jwt_stub) { JWTStub.new(peer_wif_key, url, user_payload['aud']) }
45
+
46
+ it 'receive a valid response' do
47
+ jwt_stub.valid_response
48
+ response = request.send(url, 'POST')
49
+ header = response.header_to_h
50
+ payload = response.payload_to_h
51
+ expect(response.verify).to be_truthy
52
+ expect(header['kid']).to eql peer_public_key
53
+ expect(payload['aud']).to eql JWTStub::PAYLOAD['aud']
54
+ expect(payload['data']).to eql JWTStub::PAYLOAD['data']
55
+ end
56
+
57
+ it 'receive an invalid response (tampered payload)' do
58
+ jwt_stub.invalid_response
59
+ response = request.send(url, 'POST')
60
+ expect(response.verify).to be_falsey
61
+ end
62
+
63
+ it 'receive a protocol error' do
64
+ jwt_stub.protocol_error
65
+ expect{request.send(url, 'POST')}.to raise_error(BitJWT::ProtocolError)
66
+ end
67
+
68
+ it 'get explict error from exception' do
69
+ jwt_stub.protocol_error
70
+ begin
71
+ request.send(url, 'POST')
72
+ rescue BitJWT::ProtocolError => e
73
+ expect(e.status).to eql JWTStub::STATUS_ERROR
74
+ expect(e.body).to eql JWTStub::PAYLOAD_ERROR.to_json
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,15 @@
1
+ require 'webmock/rspec'
2
+ require File.expand_path('../lib/bitjwt.rb', File.dirname(__FILE__))
3
+ require 'stubs/jwt_stub'
4
+
5
+ RSpec.configure do |config|
6
+ config.order = 'random'
7
+ config.filter_run focus: true
8
+ config.run_all_when_everything_filtered = true
9
+ config.mock_with :rspec do |mocks|
10
+ # Prevents you from mocking or stubbing a method that does not exist on
11
+ # a real object. This is generally recommended, and will default to
12
+ # `true` in RSpec 4.
13
+ mocks.verify_partial_doubles = true
14
+ end
15
+ end
@@ -0,0 +1,42 @@
1
+ module BitJWT
2
+ class JWTStub
3
+ attr_reader :crypto, :base_url
4
+
5
+ PAYLOAD = {
6
+ 'aud' => '/response',
7
+ 'data' => {
8
+ 'field1' => 'text1',
9
+ 'field2' => 'text2'
10
+ }
11
+ }.freeze
12
+
13
+ STATUS_ERROR = 400.freeze
14
+ PAYLOAD_ERROR = {
15
+ 'error' => 'data invalid'
16
+ }.freeze
17
+
18
+ def initialize(private_key, url, endpoint)
19
+ @crypto = BitJWT::Crypto.new(private_key)
20
+ @base_url = url + endpoint
21
+ end
22
+
23
+ def valid_response
24
+ protocol = BitJWT::Protocol.build_request(crypto, JWTStub::PAYLOAD)
25
+ returned_payload = "#{protocol.header_payload_encoded}.#{protocol.signature_encoded}"
26
+ WebMock.stub_request(:post, base_url)
27
+ .to_return(status: 200, body: returned_payload, headers: {})
28
+ end
29
+
30
+ def invalid_response
31
+ protocol = BitJWT::Protocol.build_request(crypto, JWTStub::PAYLOAD)
32
+ returned_payload = "#{protocol.header_payload_encoded}X11111.#{protocol.signature_encoded}"
33
+ WebMock.stub_request(:post, base_url)
34
+ .to_return(status: 200, body: returned_payload, headers: {})
35
+ end
36
+
37
+ def protocol_error
38
+ WebMock.stub_request(:post, base_url)
39
+ .to_return(status: STATUS_ERROR, body: PAYLOAD_ERROR.to_json, headers: {})
40
+ end
41
+ end
42
+ end
metadata ADDED
@@ -0,0 +1,146 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bitjwt
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Federico Barbazza
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-06-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ffi
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.9'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.9'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bitcoin-ruby
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '='
32
+ - !ruby/object:Gem::Version
33
+ version: 0.0.8
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '='
39
+ - !ruby/object:Gem::Version
40
+ version: 0.0.8
41
+ - !ruby/object:Gem::Dependency
42
+ name: excon
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.49'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.49'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.4'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.4'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec-mocks
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.4'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.4'
83
+ - !ruby/object:Gem::Dependency
84
+ name: webmock
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '2.1'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '2.1'
97
+ description: JWT protocol implementation using Bitcoin secp256k1
98
+ email: federico.barbazza@gmail.com
99
+ executables: []
100
+ extensions: []
101
+ extra_rdoc_files: []
102
+ files:
103
+ - ".gitignore"
104
+ - Gemfile
105
+ - Gemfile.lock
106
+ - README.md
107
+ - bitjwt.gemspec
108
+ - lib/bitjwt.rb
109
+ - lib/bitjwt/crypto.rb
110
+ - lib/bitjwt/protocol.rb
111
+ - lib/bitjwt/protocol_error.rb
112
+ - lib/bitjwt/util.rb
113
+ - lib/bitjwt/version.rb
114
+ - spec/lib/crypto_spec.rb
115
+ - spec/lib/protocol_spec.rb
116
+ - spec/spec_helper.rb
117
+ - spec/stubs/jwt_stub.rb
118
+ homepage: http://rubygems.org/gems/bitjwt
119
+ licenses:
120
+ - MIT
121
+ metadata: {}
122
+ post_install_message:
123
+ rdoc_options: []
124
+ require_paths:
125
+ - lib
126
+ required_ruby_version: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ required_rubygems_version: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - ">="
134
+ - !ruby/object:Gem::Version
135
+ version: '0'
136
+ requirements: []
137
+ rubyforge_project:
138
+ rubygems_version: 2.4.5.1
139
+ signing_key:
140
+ specification_version: 4
141
+ summary: Bitcoin JWT implementation
142
+ test_files:
143
+ - spec/lib/crypto_spec.rb
144
+ - spec/lib/protocol_spec.rb
145
+ - spec/spec_helper.rb
146
+ - spec/stubs/jwt_stub.rb