trezor 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,25 @@
1
+ require 'net/ssh/authentication/methods/publickey'
2
+
3
+ require 'trezor/key_manager'
4
+
5
+ module Net
6
+ module SSH
7
+ module Authentication
8
+ module Methods
9
+
10
+ # Usage:
11
+ #
12
+ # Net::SSH.start("host", "user", auth_methods: %w[publickey-trezor], keys: %w[~/.ssh/config]) do |ssh|
13
+ # result = ssh.exec!("ls -l")
14
+ # puts result
15
+ # end
16
+ class PublickeyTrezor < Publickey
17
+ def initialize(session, options = {})
18
+ super
19
+ @key_manager = Trezor::KeyManager.new(@key_manager.options[:keys])
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
data/lib/trezor.rb ADDED
@@ -0,0 +1,2 @@
1
+ module Trezor
2
+ end
@@ -0,0 +1,88 @@
1
+ require 'base64'
2
+ require 'socket'
3
+
4
+ require 'net/ssh/authentication/agent'
5
+
6
+ require 'trezor/utils'
7
+ require 'trezor/key_manager'
8
+
9
+ module Trezor
10
+ class Agent
11
+ include Utils
12
+
13
+ MAPPING = {
14
+ Net::SSH::Authentication::Agent::SSH2_AGENT_REQUEST_VERSION => :send_version,
15
+ Net::SSH::Authentication::Agent::SSH2_AGENT_REQUEST_IDENTITIES => :list_identities,
16
+ Net::SSH::Authentication::Agent::SSH2_AGENT_SIGN_REQUEST => :sign_challenge
17
+ }
18
+
19
+ def initialize(socket_path:, key_manager:)
20
+ @socket_path = socket_path
21
+ @key_manager = key_manager
22
+ end
23
+
24
+ def server
25
+ @server ||= UNIXServer.new(@socket_path)
26
+ end
27
+
28
+ def on_request
29
+ socket = server.accept
30
+ Thread.new do
31
+ yield(socket)
32
+ socket.close
33
+ end
34
+ end
35
+
36
+ def run
37
+ while true
38
+ on_request do |socket|
39
+ while true
40
+ type, body = read_packet(socket)
41
+ break unless method_name = MAPPING[type]
42
+ response = send(method_name, body)
43
+ socket.write(Buffer.from(:string, response.to_s))
44
+ end
45
+ end
46
+ end
47
+ ensure
48
+ File.unlink(@socket_path) if File.exists?(@socket_path)
49
+ end
50
+
51
+ def send_version(body)
52
+ # Net::SSH::Authentication::Agent::SSH2_AGENT_VERSION_RESPONSE
53
+ buffer = body.read
54
+ Buffer.from(:byte, 2, :long, 0)
55
+ end
56
+
57
+ def list_identities(body)
58
+ Buffer.from(
59
+ :byte, Net::SSH::Authentication::Agent::SSH2_AGENT_IDENTITIES_ANSWER,
60
+ :long, @key_manager.identities.count,
61
+ :string,
62
+ @key_manager.map { |i| [i.key.to_blob, i.key_name] }.flatten
63
+ )
64
+ end
65
+
66
+ def failure_message
67
+ Buffer.from(:byte, Net::SSH::Authentication::Agent::SSH_AGENT_FAILURE)
68
+ end
69
+
70
+ def sign_challenge(body)
71
+ server_key = body.read_buffer.read_key
72
+ blob = body.read_buffer
73
+ return failure_message unless signature = @key_manager.sign(server_key, blob)
74
+ Buffer.from(
75
+ :byte, Net::SSH::Authentication::Agent::SSH2_AGENT_SIGN_RESPONSE,
76
+ :string, signature
77
+ )
78
+ end
79
+
80
+ def read_packet(socket)
81
+ buffer = Buffer.new(socket.read(4))
82
+ buffer.append(socket.read(buffer.read_long))
83
+ type = buffer.read_byte
84
+ #debug { "received agent packet #{type} len #{buffer.length - 4}" }
85
+ return type, buffer
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,141 @@
1
+ require 'io/console'
2
+
3
+ require 'http'
4
+
5
+ require 'trezor/utils'
6
+ require 'trezor/protobuf'
7
+
8
+ module Trezor
9
+ class Device
10
+ include Utils
11
+
12
+ HOST = 'http://127.0.0.1:21325'
13
+ HEADERS = { origin: 'https://python.trezor.io' }
14
+
15
+ def initialize(device_info)
16
+ @session_counter = 0
17
+ @device_info = device_info
18
+ @connection = HTTP.persistent(HOST)
19
+ @connection.default_options.headers.merge!(HEADERS)
20
+ end
21
+
22
+ def start_session
23
+ acquire! if @session_counter == 0
24
+ @session_counter += 1
25
+ end
26
+
27
+ def end_session
28
+ @session_counter -= 1
29
+ release! if @session_counter == 0
30
+ end
31
+
32
+ def acquire!
33
+ response = @connection.post("/acquire/#{@device_info['path']}/null")
34
+ body = response.to_s
35
+ raise body unless response.status.ok?
36
+ @session = JSON.parse(body)['session']
37
+ end
38
+
39
+ def release!
40
+ return unless @session
41
+ response = @connection.post("/release/#{@session}")
42
+ body = response.to_s
43
+ raise body unless response.status.ok?
44
+ @session = nil
45
+ end
46
+
47
+ def call(message)
48
+ response = post(message)
49
+ handler = "callback_#{response.class.name.demodulize}"
50
+ unless respond_to?(handler)
51
+ #puts "could not find handler for #{response.class.name}"
52
+ return response
53
+ end
54
+ call(send(handler, response))
55
+ end
56
+
57
+ def post(message)
58
+ serialized = Protobuf.serialize(message)
59
+ response = @connection.post("/call/#{@session}", body: serialized)
60
+ raise response.to_s unless response.status.ok?
61
+ Protobuf.decode(response.to_s)
62
+ end
63
+
64
+ def method_missing(method_name, *args, &block)
65
+ return super unless respond_to?(method_name)
66
+ call(Protobuf[method_name].new(*args), &block)
67
+ end
68
+
69
+ def respond_to_missing?(method_name, include_private = false)
70
+ super || Protobuf[method_name].present?
71
+ end
72
+
73
+ def callback_ButtonRequest(msg)
74
+ Protobuf::ButtonAck.new
75
+ end
76
+
77
+ def callback_PassphraseRequest(msg)
78
+ return Protobuf::PassphraseAck.new if msg.on_device
79
+ passphrase = ask('Enter passphrase:', false).unicode_normalize(:nfkd)
80
+ Protobuf::PassphraseAck.new(passphrase: passphrase)
81
+ end
82
+
83
+ def callback_PassphraseStateRequest(msg)
84
+ Protobuf::PassphraseStateAck.new
85
+ end
86
+
87
+ def callback_PinMatrixRequest(msg)
88
+ prompt =
89
+ case msg.type
90
+ when Protobuf::PinMatrixRequestType::PinMatrixRequestType_Current
91
+ 'current PIN'
92
+ when Protobuf::PinMatrixRequestType::PinMatrixRequestType_NewFirst
93
+ 'new PIN'
94
+ when Protobuf::PinMatrixRequestType::PinMatrixRequestType_NewSecond
95
+ 'new PIN again'
96
+ else
97
+ 'PIN'
98
+ end
99
+ Protobuf::PinMatrixAck.new(pin: ask("Enter #{prompt}:", false))
100
+ end
101
+
102
+ def self.enumerate
103
+ JSON.parse(HTTP[HEADERS].post("#{HOST}/enumerate").to_s).map do |device_info|
104
+ new(device_info)
105
+ end
106
+ end
107
+
108
+ MUTEX = Mutex.new
109
+ def self.with_session
110
+ MUTEX.lock
111
+ device = enumerate.first
112
+ unless device
113
+ # log debug message 'Trezor not found'
114
+ return
115
+ end
116
+ device.start_session
117
+ yield(device)
118
+ ensure
119
+ device.end_session if device
120
+ MUTEX.unlock
121
+ end
122
+
123
+ def self.prompter(&block)
124
+ return @default_prompter = lambda(&block) if block_given?
125
+ prompter do |prompt, echo|
126
+ STDOUT.print(prompt)
127
+ if echo
128
+ STDIN.gets
129
+ else
130
+ STDIN.noecho(&:gets)
131
+ end.chomp.tap { STDOUT.print("\n") }
132
+ end
133
+ end
134
+
135
+ private
136
+
137
+ def ask(prompt, echo = true)
138
+ self.class.prompter.call(prompt, echo)
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,102 @@
1
+ require 'uri'
2
+
3
+ require 'openssl'
4
+
5
+ require 'trezor/device'
6
+ require 'trezor/utils'
7
+
8
+ module Trezor
9
+ class Identity
10
+ extend Forwardable
11
+ include Utils
12
+
13
+ # Supported ECDSA curves for SSH (GPG not implemented)
14
+ CURVE_NIST256 = 'nist256p1'
15
+ CURVE_ED25519 = 'ed25519'
16
+
17
+ # Supported ECDH curves for GPG (not implemented)
18
+ ECDH_NIST256 = 'nist256p1'
19
+ ECDH_CURVE25519 = 'curve25519'
20
+
21
+ SSH_NIST256_KEY_PREFIX = 'ecdsa-sha2-'
22
+ SSH_NIST256_CURVE_NAME = 'nistp256'
23
+ SSH_NIST256_KEY_TYPE = SSH_NIST256_KEY_PREFIX + SSH_NIST256_CURVE_NAME
24
+ SSH_ED25519_KEY_TYPE = 'ssh-ed25519'
25
+
26
+ delegate [:user, :host, :port, :path] => :@uri
27
+ def_delegator :@uri, :scheme, :proto
28
+ attr_accessor :curve_name
29
+
30
+ def initialize(identity_string, curve_name = CURVE_NIST256, key = nil)
31
+ @uri = URI.parse(identity_string)
32
+ @uri = URI.parse('ssh://' + identity_string) unless @uri.scheme
33
+ @curve_name = curve_name
34
+ @key = key
35
+ end
36
+
37
+ def key
38
+ @key ||= load_public_key
39
+ end
40
+
41
+ def key_name
42
+ "<#{@uri.to_s}|#{@curve_name}>"
43
+ end
44
+
45
+ def export_public_key
46
+ "#{key.ssh_type} #{Base64.strict_encode64(key.to_blob).strip} #{key_name}"
47
+ end
48
+
49
+ def sign(blob)
50
+ response = Device.with_session do |device|
51
+ device.sign_identity(
52
+ challenge_hidden: blob.to_s, ecdsa_curve_name: @curve_name,
53
+ identity: { user: user, host: host, proto: proto }
54
+ )
55
+ end
56
+ return if response.is_a?(Protobuf::Failure)
57
+ signature = response.signature[1..-1]
58
+ if key.ssh_type == SSH_NIST256_KEY_TYPE
59
+ parts = [signature[0..31], signature[32..-1]]
60
+ signature = Buffer.from(:string, parts.map { |p| "\x00" + p }).to_s
61
+ end
62
+ Buffer.from(:string, [key.ssh_type, signature]).to_s
63
+ end
64
+
65
+ private
66
+
67
+ # Compute BIP32 derivation address according to SLIP-0013/0017
68
+ def get_bip32_address(ecdh = false)
69
+ identity_string = [0].pack('<L') + @uri.to_s
70
+ digest = OpenSSL::Digest.digest('SHA256', identity_string)
71
+ hardened = 0x80000000
72
+ addr_0 = ecdh ? 17 : 13
73
+ address_n = [addr_0] + digest.unpack('<L4')
74
+ address_n.map { |n| hardened | n }
75
+ end
76
+
77
+ def raw_public_key
78
+ response = Device.with_session do |device|
79
+ device.get_public_key(address_n: get_bip32_address, ecdsa_curve_name: @curve_name)
80
+ end
81
+ return if response.is_a?(Protobuf::Failure)
82
+ public_key = response.node.public_key
83
+ return public_key[1..-1] unless @curve_name == CURVE_NIST256
84
+ public_key
85
+ end
86
+
87
+ def key_type
88
+ {
89
+ CURVE_NIST256 => SSH_NIST256_KEY_TYPE,
90
+ CURVE_ED25519 => SSH_ED25519_KEY_TYPE,
91
+ ECDH_CURVE25519 => SSH_ED25519_KEY_TYPE
92
+ }[@curve_name]
93
+ end
94
+
95
+ def load_public_key
96
+ parts = [key_type]
97
+ parts << SSH_NIST256_CURVE_NAME if @curve_name == CURVE_NIST256
98
+ parts << raw_public_key
99
+ Buffer.from(:string, parts).read_key
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,77 @@
1
+ require 'trezor/identity'
2
+ require 'trezor/utils'
3
+
4
+ module Trezor
5
+ class KeyManager
6
+ include Enumerable
7
+ include Utils
8
+
9
+ def initialize(identities_file_or_string)
10
+ @path = identities_file_or_string
11
+ end
12
+
13
+ def identities
14
+ @identities ||= load_identities
15
+ end
16
+
17
+ def each
18
+ identities.each { |i| yield i }
19
+ end
20
+
21
+ # For compatibility with Net::SSH::Authentication::KeyManager
22
+ def each_identity
23
+ each { |i| yield i.key }
24
+ end
25
+
26
+ def sign(ssh_identity, blob)
27
+ nonce = blob.read_buffer.to_s
28
+ type = blob.read_byte # SSH2_MSG_USERAUTH_REQUEST == 50 (from ssh2.h, line 108)
29
+ user = blob.read_buffer.to_s
30
+ conn = blob.read_buffer.to_s
31
+ auth = blob.read_buffer.to_s
32
+ have_sig = blob.read_byte # have_sig == 1 (from sshconnect2.c, line 1056)
33
+ key_type = blob.read_buffer.to_s
34
+ public_key = blob.read_buffer.to_s
35
+ identity = identities.find { |i| i.key.fingerprint == ssh_identity.fingerprint }
36
+ return unless identity
37
+ identity.sign(blob)
38
+ end
39
+
40
+ private
41
+
42
+ def load_identities
43
+ if @path.is_a?(Array)
44
+ @path.reduce([]) do |identities, file_or_string|
45
+ identities.concat(self.class.load_from_file_or_string(file_or_string))
46
+ end
47
+ else
48
+ self.class.load_from_file_or_string(@path)
49
+ end
50
+ end
51
+
52
+ private_class_method
53
+
54
+ def self.load_from_file_or_string(file_or_string)
55
+ path = File.expand_path(file_or_string)
56
+ return load_from_file(path) if File.exist?(path)
57
+ [Trezor::Identity.new(file_or_string)]
58
+ end
59
+
60
+ def self.load_from_file(path)
61
+ identity_curve_regex = /\<(.*?)\|(.*?)\>/
62
+
63
+ if path.end_with?('.pub')
64
+ File.foreach(path).map do |line|
65
+ ssh_type, blob, key_name = line.split
66
+ identity_string, curve_name = key_name.scan(identity_curve_regex).first
67
+ key = Buffer.new(Base64.strict_decode64(blob)).read_key
68
+ Trezor::Identity.new(identity_string, curve_name, key)
69
+ end
70
+ else
71
+ File.read(path).scan(identity_curve_regex).map do |identity_string, curve_name|
72
+ Trezor::Identity.new(identity_string, curve_name)
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,38 @@
1
+ require 'active_support/core_ext/string/inflections'
2
+
3
+ require 'trezor/utils/buffer'
4
+ require 'trezor/protobuf/messages.pb'
5
+
6
+ module Trezor
7
+ module Protobuf
8
+ def self.serialize(message)
9
+ msg_type = MessageType.for(message)
10
+ serialized = message.serialize
11
+ header = Utils::Buffer.from(:short, msg_type.to_i, :long, serialized.length)
12
+ payload = Utils::Buffer.from(:bth, [header.to_s, serialized])
13
+ payload.to_s
14
+ end
15
+
16
+ def self.decode(blob)
17
+ buffer = Utils::Buffer.from(:htb, blob)
18
+ MessageType[buffer.read_short].decode(buffer.read_string)
19
+ end
20
+
21
+ def self.[](class_name)
22
+ const_get(class_name.to_s.camelize)
23
+ rescue NameError
24
+ # log debug message
25
+ end
26
+
27
+ class MessageType
28
+ def self.[](tag)
29
+ message_type_name = fetch(tag).name.to_s.split('_').last
30
+ Protobuf[message_type_name]
31
+ end
32
+
33
+ def self.for(message)
34
+ fetch("MessageType_#{message.class.name.demodulize}")
35
+ end
36
+ end
37
+ end
38
+ end