smartcard 0.4.1 → 0.4.2
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +2 -0
- data/Manifest +13 -6
- data/Rakefile +3 -3
- data/lib/smartcard.rb +3 -0
- data/lib/smartcard/gp/asn1_ber.rb +199 -0
- data/lib/smartcard/gp/cap_loader.rb +88 -0
- data/lib/smartcard/gp/des.rb +68 -0
- data/lib/smartcard/gp/gp_card_mixin.rb +364 -5
- data/lib/smartcard/iso/iso_card_mixin.rb +13 -3
- data/lib/smartcard/iso/jcop_remote_server.rb +14 -5
- data/lib/smartcard/iso/pcsc_transport.rb +8 -1
- data/smartcard.gemspec +5 -5
- data/test/gp/asn1_ber_test.rb +116 -0
- data/test/gp/cap_loader_test.rb +24 -0
- data/test/gp/des_test.rb +39 -0
- data/test/gp/gp_card_mixin_test.rb +194 -5
- data/test/gp/hello.apdu +1 -0
- data/test/gp/hello.cap +0 -0
- data/test/iso/iso_card_mixin_test.rb +9 -8
- data/test/iso/jcop_remote_test.rb +4 -1
- metadata +25 -11
data/CHANGELOG
CHANGED
data/Manifest
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
BUILD
|
2
2
|
CHANGELOG
|
3
|
+
LICENSE
|
4
|
+
Manifest
|
5
|
+
README
|
6
|
+
Rakefile
|
3
7
|
ext/smartcard_pcsc/extconf.rb
|
4
8
|
ext/smartcard_pcsc/pcsc.h
|
5
9
|
ext/smartcard_pcsc/pcsc_card.c
|
@@ -13,6 +17,10 @@ ext/smartcard_pcsc/pcsc_namespace.c
|
|
13
17
|
ext/smartcard_pcsc/pcsc_reader_states.c
|
14
18
|
ext/smartcard_pcsc/pcsc_surrogate_reader.h
|
15
19
|
ext/smartcard_pcsc/pcsc_surrogate_wintypes.h
|
20
|
+
lib/smartcard.rb
|
21
|
+
lib/smartcard/gp/asn1_ber.rb
|
22
|
+
lib/smartcard/gp/cap_loader.rb
|
23
|
+
lib/smartcard/gp/des.rb
|
16
24
|
lib/smartcard/gp/gp_card_mixin.rb
|
17
25
|
lib/smartcard/iso/auto_configurator.rb
|
18
26
|
lib/smartcard/iso/iso_card_mixin.rb
|
@@ -22,13 +30,12 @@ lib/smartcard/iso/jcop_remote_transport.rb
|
|
22
30
|
lib/smartcard/iso/pcsc_transport.rb
|
23
31
|
lib/smartcard/iso/transport.rb
|
24
32
|
lib/smartcard/pcsc/pcsc_exception.rb
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
Rakefile
|
29
|
-
README
|
30
|
-
smartcard.gemspec
|
33
|
+
test/gp/asn1_ber_test.rb
|
34
|
+
test/gp/cap_loader_test.rb
|
35
|
+
test/gp/des_test.rb
|
31
36
|
test/gp/gp_card_mixin_test.rb
|
37
|
+
test/gp/hello.apdu
|
38
|
+
test/gp/hello.cap
|
32
39
|
test/iso/auto_configurator_test.rb
|
33
40
|
test/iso/iso_card_mixin_test.rb
|
34
41
|
test/iso/jcop_remote_test.rb
|
data/Rakefile
CHANGED
@@ -10,13 +10,13 @@ Echoe.new('smartcard') do |p|
|
|
10
10
|
p.summary = 'Interface with ISO 7816 smart cards.'
|
11
11
|
p.url = 'http://www.costan.us/smartcard'
|
12
12
|
|
13
|
-
p.need_tar_gz = !
|
14
|
-
p.need_zip = !
|
13
|
+
p.need_tar_gz = !Gem.win_platform?
|
14
|
+
p.need_zip = !Gem.win_platform?
|
15
15
|
p.clean_pattern += ['ext/**/*.manifest', 'ext/**/*_autogen.h']
|
16
16
|
p.rdoc_pattern = /^(lib|bin|tasks|ext)|^BUILD|^README|^CHANGELOG|^TODO|^LICENSE|^COPYING$/
|
17
17
|
|
18
18
|
p.eval = proc do |p|
|
19
|
-
if
|
19
|
+
if Gem.win_platform?
|
20
20
|
p.files += ['lib/smartcard/pcsc.so']
|
21
21
|
p.platform = Gem::Platform::CURRENT
|
22
22
|
|
data/lib/smartcard.rb
CHANGED
@@ -0,0 +1,199 @@
|
|
1
|
+
# Encoding and decoding of ASN.1-BER data.
|
2
|
+
#
|
3
|
+
# Author:: Victor Costan
|
4
|
+
# Copyright:: Copyright (C) 2009 Massachusetts Institute of Technology
|
5
|
+
# License:: MIT
|
6
|
+
|
7
|
+
# :nodoc: namespace
|
8
|
+
module Smartcard::Gp
|
9
|
+
|
10
|
+
|
11
|
+
# Logic for encoding and decoding ASN.1-BER data as specified in X.690-0207.
|
12
|
+
module Asn1Ber
|
13
|
+
# Decodes a TLV tag (the data type).
|
14
|
+
#
|
15
|
+
# Args:
|
16
|
+
# data:: the array to decode from
|
17
|
+
# offset:: the position of the first byte containing the tag
|
18
|
+
#
|
19
|
+
# Returns the offset of the first byte after the tag, and the tag information.
|
20
|
+
# Tag information is a hash with the following keys.
|
21
|
+
# :class:: the tag's class (symbol, named after X690-0207)
|
22
|
+
# :primitive:: if +false+, the tag's value is a sequence of TLVs
|
23
|
+
# :number:: the tag's number
|
24
|
+
def self.decode_tag(data, offset)
|
25
|
+
class_bits = data[offset] >> 6
|
26
|
+
tag_class = [:universal, :application, :context, :private][class_bits]
|
27
|
+
tag_primitive = (data[offset] & 0x20) == 0
|
28
|
+
tag_number = (data[offset] & 0x1F)
|
29
|
+
if tag_number == 0x1F
|
30
|
+
tag_number = 0
|
31
|
+
loop do
|
32
|
+
offset += 1
|
33
|
+
tag_number <<= 7
|
34
|
+
tag_number |= (data[offset] & 0x7F)
|
35
|
+
break if (data[offset] & 0x80) == 0
|
36
|
+
end
|
37
|
+
end
|
38
|
+
return (offset + 1), { :class => tag_class, :primitive => tag_primitive,
|
39
|
+
:number => tag_number }
|
40
|
+
end
|
41
|
+
|
42
|
+
# Decodes a TLV length.
|
43
|
+
#
|
44
|
+
# Args:
|
45
|
+
# data:: the array to decode from
|
46
|
+
# offset:: the position of the first byte containing the length
|
47
|
+
#
|
48
|
+
# Returns the offset of the first byte after the length, and the length. The
|
49
|
+
# returned value might be +:indefinite+ if the encoding uses the indefinite
|
50
|
+
# length.
|
51
|
+
def self.decode_length(data, offset)
|
52
|
+
return (offset + 1), data[offset] if (data[offset] & 0x80) == 0
|
53
|
+
len_bytes = (data[offset] & 0x7F)
|
54
|
+
return (offset + 1), :indefinite if len_bytes == 0
|
55
|
+
length = 0
|
56
|
+
len_bytes.times do
|
57
|
+
offset += 1
|
58
|
+
length = (length << 8) | data[offset]
|
59
|
+
end
|
60
|
+
return (offset + 1), length
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
# Decodes a TLV value.
|
65
|
+
#
|
66
|
+
# Args:
|
67
|
+
# data:: the array to decode from
|
68
|
+
# offset:: the position of the first byte containing the length
|
69
|
+
#
|
70
|
+
# Returns the offset of the first byte after the value, and the value.
|
71
|
+
def self.decode_value(data, offset, length)
|
72
|
+
return offset + length, data[offset, length] unless length == :indefinite
|
73
|
+
|
74
|
+
length = 0
|
75
|
+
loop do
|
76
|
+
raise 'Unterminated data' if offset + length + 2 > data.length
|
77
|
+
break if data[offset + length, 2] == [0, 0]
|
78
|
+
length += 1
|
79
|
+
end
|
80
|
+
return (offset + length + 2), data[offset, length]
|
81
|
+
end
|
82
|
+
|
83
|
+
# Maps a TLV value with a known tag to a Ruby data type.
|
84
|
+
def self.map_value(value, tag)
|
85
|
+
# TODO(costan): map primitive types if necessary
|
86
|
+
value
|
87
|
+
end
|
88
|
+
|
89
|
+
# Decodes a TLV (tag-length-value).
|
90
|
+
#
|
91
|
+
# Returns a hash that contains tag and value information. See decode_tag for
|
92
|
+
# the keys containing the tag information. Value information is contained in
|
93
|
+
# the :value: tag.
|
94
|
+
def self.decode_tlv(data, offset)
|
95
|
+
offset, tag = decode_tag data, offset
|
96
|
+
offset, length = decode_length data, offset
|
97
|
+
offset, value = decode_value data, offset, length
|
98
|
+
|
99
|
+
tag[:value] = tag[:primitive] ? map_value(value, tag) : decode(value)
|
100
|
+
return offset, tag
|
101
|
+
end
|
102
|
+
|
103
|
+
# Decodes a sequence of TLVs (tag-length-value).
|
104
|
+
#
|
105
|
+
# Returns an array with one element for each TLV in the sequence. See
|
106
|
+
# decode_tlv for the format of each array element.
|
107
|
+
def self.decode(data, offset = 0, length = data.length - offset)
|
108
|
+
sequence = []
|
109
|
+
loop do
|
110
|
+
break if offset >= length
|
111
|
+
offset, tlv = decode_tlv data, offset
|
112
|
+
sequence << tlv
|
113
|
+
end
|
114
|
+
sequence
|
115
|
+
end
|
116
|
+
|
117
|
+
# Encodes a TLV tag (the data type).
|
118
|
+
#
|
119
|
+
# Args:
|
120
|
+
# tag:: a hash with the keys produced by decode_tag.
|
121
|
+
#
|
122
|
+
# Returns an array of byte values.
|
123
|
+
def self.encode_tag(tag)
|
124
|
+
tag_classes = { :universal => 0, :application => 1, :context => 2,
|
125
|
+
:private => 3 }
|
126
|
+
tag_lead = (tag_classes[tag[:class]] << 6) | (tag[:primitive] ? 0x00 : 0x20)
|
127
|
+
return [tag_lead | tag[:number]] if tag[:number] < 0x1F
|
128
|
+
|
129
|
+
number_bytes, number = [], tag[:number]
|
130
|
+
first = true
|
131
|
+
while number != 0
|
132
|
+
byte = (number & 0x7F)
|
133
|
+
number >>= 7
|
134
|
+
byte |= 0x80 unless first
|
135
|
+
first = false
|
136
|
+
number_bytes << byte
|
137
|
+
end
|
138
|
+
[tag_lead | 0x1F] + number_bytes.reverse
|
139
|
+
end
|
140
|
+
|
141
|
+
# Encodes a TLV length (the length of the data).
|
142
|
+
#
|
143
|
+
# Args::
|
144
|
+
# length:: the length to be encoded (number of :indefinite)
|
145
|
+
#
|
146
|
+
# Returns an array of byte values.
|
147
|
+
def self.encode_length(length)
|
148
|
+
return [0x80] if length == :indefinite
|
149
|
+
return [length] if length < 0x80
|
150
|
+
length_bytes = []
|
151
|
+
while length > 0
|
152
|
+
length_bytes << (length & 0xFF)
|
153
|
+
length >>= 8
|
154
|
+
end
|
155
|
+
[0x80 | length_bytes.length] + length_bytes.reverse
|
156
|
+
end
|
157
|
+
|
158
|
+
# Encodes a TLV (tag-length-value).
|
159
|
+
#
|
160
|
+
# Args::
|
161
|
+
# tlv:: hash with tag and value information, to be encoeded as TLV; see
|
162
|
+
# decode_tlv for the hash keys encoding the tag and value
|
163
|
+
#
|
164
|
+
# Returns an array of byte values.
|
165
|
+
def self.encode_tlv(tlv)
|
166
|
+
value = tlv[:primitive] ? tlv[:value] : encode(tlv[:value])
|
167
|
+
[encode_tag(tlv), encode_length(value.length), value].flatten
|
168
|
+
end
|
169
|
+
|
170
|
+
# Encodes a sequence of TLVs (tag-length-value).
|
171
|
+
#
|
172
|
+
# Args::
|
173
|
+
# tlvs:: an array of hashes to be encoded as TLV
|
174
|
+
#
|
175
|
+
# Returns an array of byte values.
|
176
|
+
def self.encode(tlvs)
|
177
|
+
tlvs.map { |tlv| encode_tlv tlv }.flatten
|
178
|
+
end
|
179
|
+
|
180
|
+
# Visitor pattern for decoded TLVs.
|
181
|
+
#
|
182
|
+
# Args:
|
183
|
+
# tlvs:: the TLVs to visit
|
184
|
+
# tag_path:: internal, do not use
|
185
|
+
#
|
186
|
+
# Yields: |tag_path, value| tag_path lists the numeric tags for the current
|
187
|
+
# value's tag, and all the parents' tags.
|
188
|
+
def self.visit(tlvs, tag_path = [], &block)
|
189
|
+
tlvs.each do |tlv|
|
190
|
+
tag_number = encode_tag(tlv).inject { |acc, v| (acc << 8) | v }
|
191
|
+
new_tag_path = tag_path + [tag_number]
|
192
|
+
yield new_tag_path, tlv[:value]
|
193
|
+
next if tlv[:primitive]
|
194
|
+
visit tlv[:value], new_tag_path, &block
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end # module Smartcard::Gp::Asn1Ber
|
198
|
+
|
199
|
+
end # namespace
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# Loads JavaCard CAP files.
|
2
|
+
#
|
3
|
+
# Author:: Victor Costan
|
4
|
+
# Copyright:: Copyright (C) 2009 Massachusetts Institute of Technology
|
5
|
+
# License:: MIT
|
6
|
+
|
7
|
+
require 'zip/zip'
|
8
|
+
|
9
|
+
# :nodoc: namespace
|
10
|
+
module Smartcard::Gp
|
11
|
+
|
12
|
+
|
13
|
+
# Logic for loading JavaCard CAP files.
|
14
|
+
module CapLoader
|
15
|
+
# Loads a CAP file.
|
16
|
+
#
|
17
|
+
# Returns a hash mapping component names to component data.
|
18
|
+
def self.load_cap(cap_file)
|
19
|
+
components = {}
|
20
|
+
Zip::ZipFile.open(cap_file) do |file|
|
21
|
+
file.each do |entry|
|
22
|
+
data = entry.get_input_stream { |io| io.read }
|
23
|
+
offset = 0
|
24
|
+
while offset < data.length
|
25
|
+
tag = TAG_NAMES[data[offset, 1].unpack('C').first]
|
26
|
+
length = data[offset + 1, 2].unpack('n').first
|
27
|
+
value = data[offset + 3, length]
|
28
|
+
components[tag] = value
|
29
|
+
offset += 3 + length
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
components
|
34
|
+
end
|
35
|
+
|
36
|
+
# Serializes CAP components for on-card loading.
|
37
|
+
#
|
38
|
+
# Returns an array of bytes.
|
39
|
+
def self.serialize_components(components)
|
40
|
+
[:header, :directory, :import, :applet, :class, :method, :static_field,
|
41
|
+
:export, :constant_pool, :reference_location].map { |name|
|
42
|
+
tag = TAG_NAMES.keys.find { |k| TAG_NAMES[k] == name }
|
43
|
+
if components[name]
|
44
|
+
length = [components[name].length].pack('n').unpack('C*')
|
45
|
+
data = components[name].unpack('C*')
|
46
|
+
[tag, length, data]
|
47
|
+
else
|
48
|
+
[]
|
49
|
+
end
|
50
|
+
}.flatten
|
51
|
+
end
|
52
|
+
|
53
|
+
# Parses the Applet section in a CAP file, obtaining applet AIDs.
|
54
|
+
#
|
55
|
+
# Returns an array of hashes, one hash per applet. The hash has a key +:aid+
|
56
|
+
# that contains the applet's AID.
|
57
|
+
def self.parse_applets(components)
|
58
|
+
applets = []
|
59
|
+
return applets unless section = components[:applet]
|
60
|
+
offset = 1
|
61
|
+
section[0].times do
|
62
|
+
aid_length = section[offset]
|
63
|
+
install_method = section[offset + 1 + aid_length, 2].unpack('n').first
|
64
|
+
applets << { :aid => section[offset + 1, aid_length].unpack('C*'),
|
65
|
+
:install_method => install_method }
|
66
|
+
offset += 3 + aid_length
|
67
|
+
end
|
68
|
+
applets
|
69
|
+
end
|
70
|
+
|
71
|
+
# Loads a CAP file and serializes its components for on-card loading.
|
72
|
+
#
|
73
|
+
# Returns an array of bytes.
|
74
|
+
def self.cap_load_data(cap_file)
|
75
|
+
components = load_cap cap_file
|
76
|
+
{ :data => serialize_components(components),
|
77
|
+
:applets => parse_applets(components) }
|
78
|
+
end
|
79
|
+
|
80
|
+
# Maps numeric tags to tag names.
|
81
|
+
TAG_NAMES = {
|
82
|
+
1 => :header, 2 => :directory, 3 => :applet, 4 => :import,
|
83
|
+
5 => :constant_pool, 6 => :class, 7 => :method, 8 => :static_field,
|
84
|
+
9 => :reference_location, 10 => :export, 11 => :descriptor, 12 => :debug
|
85
|
+
}
|
86
|
+
end # module Smartcard::Gp::CapLoader
|
87
|
+
|
88
|
+
end # namespace
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# DES and 3DES encryption and MAC logic for GlobalPlatform secure channels.
|
2
|
+
#
|
3
|
+
# Author:: Victor Costan
|
4
|
+
# Copyright:: Copyright (C) 2009 Massachusetts Institute of Technology
|
5
|
+
# License:: MIT
|
6
|
+
|
7
|
+
require 'openssl'
|
8
|
+
|
9
|
+
# :nodoc: namespace
|
10
|
+
module Smartcard::Gp
|
11
|
+
|
12
|
+
|
13
|
+
# DES and 3DES encryption and MAC logic for GlobalPlatform secure channels.
|
14
|
+
module Des
|
15
|
+
# Generates random bytes for session nonces.
|
16
|
+
#
|
17
|
+
# Args:
|
18
|
+
# bytes:: how many bytes are desired
|
19
|
+
#
|
20
|
+
# Returns a string of random bytes.
|
21
|
+
def self.random_bytes(bytes)
|
22
|
+
OpenSSL::Random.random_bytes bytes
|
23
|
+
end
|
24
|
+
|
25
|
+
# Perform DES or 3DES encryption.
|
26
|
+
#
|
27
|
+
# Args:
|
28
|
+
# key:: the encryption key to be used (8-byte or 16-byte)
|
29
|
+
# data:: the data to be encrypted or decrypted
|
30
|
+
# iv:: initialization vector
|
31
|
+
# decrypt:: if +false+ performs encryption, otherwise performs decryption
|
32
|
+
#
|
33
|
+
# Returns the encrypted / decrypted data.
|
34
|
+
def self.crypt(key, data, iv = nil, decrypt = false)
|
35
|
+
cipher_name = key.length == 8 ? 'DES-CBC' : 'DES-EDE-CBC'
|
36
|
+
cipher = OpenSSL::Cipher::Cipher.new cipher_name
|
37
|
+
decrypt ? cipher.decrypt : cipher.encrypt
|
38
|
+
cipher.key = key
|
39
|
+
cipher.iv = iv || ("\x00" * 8)
|
40
|
+
cipher.padding = 0
|
41
|
+
crypted = cipher.update data
|
42
|
+
crypted += cipher.final
|
43
|
+
crypted
|
44
|
+
end
|
45
|
+
|
46
|
+
# Computes a MAC using DES mixed with 3DES.
|
47
|
+
def self.mac_retail(key, data, iv = nil)
|
48
|
+
# Output transformation: add 80, then 00 until it's block-sized.
|
49
|
+
data = data + "\x80"
|
50
|
+
data += "\x00" * (8 - data.length % 8) unless data.length % 8 == 0
|
51
|
+
|
52
|
+
# DES-encrypt everything except for the last block.
|
53
|
+
iv = crypt(key[0, 8], data[0, data.length - 8], iv)[-8, 8]
|
54
|
+
# Take the chained block and supply it to a 3DES-encryption.
|
55
|
+
crypt(key, data[-8, 8], iv)
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.mac_3des(key, data)
|
59
|
+
# Output transformation: add 80, then 00 until it's block-sized.
|
60
|
+
data = data + "\x80"
|
61
|
+
data += "\x00" * (8 - data.length % 8) unless data.length % 8 == 0
|
62
|
+
|
63
|
+
# The MAC is the last block from 3DES-encrypting the data.
|
64
|
+
crypt(key, data)[-8, 8]
|
65
|
+
end
|
66
|
+
end # module Smartcard::Gp::Des
|
67
|
+
|
68
|
+
end # namespace
|
@@ -4,24 +4,383 @@
|
|
4
4
|
# Copyright:: Copyright (C) 2009 Massachusetts Institute of Technology
|
5
5
|
# License:: MIT
|
6
6
|
|
7
|
+
require 'set'
|
8
|
+
|
7
9
|
# :nodoc: namespace
|
8
10
|
module Smartcard::Gp
|
9
11
|
|
10
12
|
|
13
|
+
# Module intended to be mixed into transport implementations to add commands for
|
14
|
+
# talking to GlobalPlatform smart-cards.
|
15
|
+
#
|
16
|
+
# The module talks to the card exclusively via methods in
|
17
|
+
# Smartcard::Iso::IsoCardMixin, so the transport requirements are the same as
|
18
|
+
# for that module.
|
11
19
|
module GpCardMixin
|
12
20
|
include Smartcard::Iso::IsoCardMixin
|
13
21
|
|
14
22
|
# Selects a GlobalPlatform application.
|
15
23
|
def select_application(app_id)
|
16
|
-
iso_apdu! :ins => 0xA4, :p1 => 0x04, :p2 => 0x00, :data => app_id
|
24
|
+
ber_data = iso_apdu! :ins => 0xA4, :p1 => 0x04, :p2 => 0x00, :data => app_id
|
25
|
+
app_tags = Asn1Ber.decode ber_data
|
26
|
+
app_data = {}
|
27
|
+
Asn1Ber.visit app_tags do |path, value|
|
28
|
+
case path
|
29
|
+
when [0x6F, 0xA5, 0x9F65]
|
30
|
+
app_data[:max_apdu_length] = value.inject(0) { |acc, v| (acc << 8) | v }
|
31
|
+
when [0x6F, 0x84]
|
32
|
+
app_data[:aid] = value
|
33
|
+
end
|
34
|
+
end
|
35
|
+
app_data
|
36
|
+
end
|
37
|
+
|
38
|
+
# The default application ID of the GlobalPlatform card manager.
|
39
|
+
def gp_card_manager_aid
|
40
|
+
[0xA0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00]
|
41
|
+
end
|
42
|
+
|
43
|
+
# Issues a GlobalPlatform INITIALIZE UPDATE command.
|
44
|
+
#
|
45
|
+
# This should not be called directly. Call secure_session insteaad.
|
46
|
+
#
|
47
|
+
# Args:
|
48
|
+
# host_challenge:: 8-byte array with a unique challenge for the session
|
49
|
+
# key_version:: the key in the Security domain to be used (0 = any key)
|
50
|
+
#
|
51
|
+
# Returns a hash containing the command's parsed response. The keys are:
|
52
|
+
# :key_diversification:: key diversification data
|
53
|
+
# :key_version:: the key in the Security domain chosen to be used
|
54
|
+
# :protocol_id:: numeric ID for the secure protocol to be used
|
55
|
+
# :counter:: counter for creating session keys
|
56
|
+
# :challenge:: the card's 6-byte challenge
|
57
|
+
# :auth:: the card's 8-byte authentication value
|
58
|
+
def gp_setup_secure_channel(host_challenge, key_version = 0)
|
59
|
+
raw = iso_apdu! :cla => 0x80, :ins => 0x50, :p1 => key_version, :p2 => 0,
|
60
|
+
:data => host_challenge
|
61
|
+
response = {
|
62
|
+
:key_diversification => raw[0, 10],
|
63
|
+
:key_version => raw[10], :protocol_id => raw[11],
|
64
|
+
:counter => raw[12, 2].pack('C*').unpack('n').first,
|
65
|
+
:challenge => raw[14, 6], :auth => raw[20, 8]
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
# Wrapper around iso_apdu! that adds a MAC to the APDU.
|
70
|
+
def gp_signed_apdu!(apdu_data)
|
71
|
+
apdu_data = apdu_data.dup
|
72
|
+
apdu_data[:cla] = (apdu_data[:cla] || 0) | 0x04
|
73
|
+
apdu_data[:data] = (apdu_data[:data] || []) + [0, 0, 0, 0, 0, 0, 0, 0]
|
74
|
+
|
75
|
+
apdu_bytes = Smartcard::Iso::IsoCardMixin.serialize_apdu(apdu_data)[0...-9]
|
76
|
+
mac = Des.mac_retail @gp_secure_channel_keys[:cmac], apdu_bytes.pack('C*'),
|
77
|
+
@gp_secure_channel_keys[:mac_iv]
|
78
|
+
@gp_secure_channel_keys[:mac_iv] = mac
|
79
|
+
|
80
|
+
apdu_data[:data][apdu_data[:data].length - 8, 8] = mac.unpack('C*')
|
81
|
+
iso_apdu! apdu_data
|
82
|
+
end
|
83
|
+
|
84
|
+
# Issues a GlobalPlatform EXTERNAL AUTHENTICATE command.
|
85
|
+
#
|
86
|
+
# This should not be called directly. Call secure_session insteaad.
|
87
|
+
#
|
88
|
+
# Args:
|
89
|
+
# host_auth:: 8-byte host authentication value
|
90
|
+
# security:: array of desired security flags (leave empty for the default
|
91
|
+
# of no security)
|
92
|
+
#
|
93
|
+
# The return value is irrelevant. The card will fire an ISO exception if the
|
94
|
+
# authentication doesn't work out.
|
95
|
+
def gp_lock_secure_channel(host_auth, security = [])
|
96
|
+
security_level = 0
|
97
|
+
security_flags = { :command_mac => 0x01, :response_mac => 0x10,
|
98
|
+
:command_encryption => 0x02 }
|
99
|
+
security.each do |flag|
|
100
|
+
security_level |= security_flags[flag]
|
101
|
+
end
|
102
|
+
gp_signed_apdu! :cla => 0x80, :ins => 0x82, :p1 => security_level, :p2 => 0,
|
103
|
+
:data => host_auth
|
104
|
+
end
|
105
|
+
|
106
|
+
# Sets up a secure session with the current GlobalPlatform application.
|
107
|
+
#
|
108
|
+
# Args:
|
109
|
+
# keys:: hash containing 3 3DES encryption keys, identified by the following
|
110
|
+
# keys:
|
111
|
+
# :senc:: channel encryption key
|
112
|
+
# :smac:: channel MAC key
|
113
|
+
# :dek:: data encryption key
|
114
|
+
def secure_channel(keys = gp_development_keys)
|
115
|
+
host_challenge = Des.random_bytes 8
|
116
|
+
card_info = gp_setup_secure_channel host_challenge.unpack('C*')
|
117
|
+
card_counter = [card_info[:counter]].pack('n')
|
118
|
+
card_challenge = card_info[:challenge].pack('C*')
|
119
|
+
|
120
|
+
# Compute session keys.
|
121
|
+
session_keys = {}
|
122
|
+
derivation_data = "\x01\x01" + card_counter + "\x00" * 12
|
123
|
+
session_keys[:cmac] = Des.crypt keys[:smac], derivation_data
|
124
|
+
derivation_data[0, 2] = "\x01\x02"
|
125
|
+
session_keys[:rmac] = Des.crypt keys[:smac], derivation_data
|
126
|
+
derivation_data[0, 2] = "\x01\x82"
|
127
|
+
session_keys[:senc] = Des.crypt keys[:senc], derivation_data
|
128
|
+
derivation_data[0, 2] = "\x01\x81"
|
129
|
+
session_keys[:dek] = Des.crypt keys[:dek], derivation_data
|
130
|
+
session_keys[:mac_iv] = "\x00" * 8
|
131
|
+
@gp_secure_channel_keys = session_keys
|
132
|
+
|
133
|
+
# Compute authentication cryptograms.
|
134
|
+
card_auth = Des.mac_3des session_keys[:senc],
|
135
|
+
host_challenge + card_counter + card_challenge
|
136
|
+
host_auth = Des.mac_3des session_keys[:senc],
|
137
|
+
card_counter + card_challenge + host_challenge
|
138
|
+
|
139
|
+
unless card_auth == card_info[:auth].pack('C*')
|
140
|
+
raise 'Card authentication invalid'
|
141
|
+
end
|
142
|
+
|
143
|
+
gp_lock_secure_channel host_auth.unpack('C*')
|
144
|
+
end
|
145
|
+
|
146
|
+
# Secure channel keys for development GlobalPlatform cards.
|
147
|
+
#
|
148
|
+
# Most importantly, the JCOP cards and simulator work with these keys.
|
149
|
+
def gp_development_keys
|
150
|
+
key = (0x40..0x4F).to_a.pack('C*')
|
151
|
+
{ :senc => key, :smac => key, :dek => key }
|
152
|
+
end
|
153
|
+
|
154
|
+
# Issues a GlobalPlatform GET STATUS command.
|
155
|
+
#
|
156
|
+
# Args:
|
157
|
+
# scope:: the information to be retrieved from the card, can be:
|
158
|
+
# :issuer_sd:: the issuer's security domain
|
159
|
+
# :apps:: applications and supplementary security domains
|
160
|
+
# :files:: executable load files
|
161
|
+
# :files_modules:: executable load files and executable modules
|
162
|
+
# query_aid:: the AID to look for (empty array to get everything)
|
163
|
+
#
|
164
|
+
# Returns an array of application information data. Each element represents an
|
165
|
+
# application, and is a hash with the following keys:
|
166
|
+
# :aid:: the application or file's AID
|
167
|
+
# :lifecycle:: the state in the application's lifecycle (symbol)
|
168
|
+
# :permissions:: a Set of the application's permissions (symbols)
|
169
|
+
# :modules:: array of modules in an executable load file, each array element
|
170
|
+
# is a hash with the key :aid which has the module's AID
|
171
|
+
def gp_get_status(scope, query_aid = [])
|
172
|
+
scope_byte = { :issuer_sd => 0x80, :apps => 0x40, :files => 0x20,
|
173
|
+
:files_modules => 0x10 }[scope]
|
174
|
+
data = Asn1Ber.encode [{:class => :application, :primitive => true,
|
175
|
+
:number => 0x0F, :value => query_aid}]
|
176
|
+
apps = []
|
177
|
+
first = true # Set to false after the first GET STATUS is issued.
|
178
|
+
loop do
|
179
|
+
raw = iso_apdu :cla => 0x80, :ins => 0xF2, :p1 => scope_byte,
|
180
|
+
:p2 => (first ? 0 : 1), :data => [0x4F, 0x00]
|
181
|
+
if raw[:status] != 0x9000 && raw[:status] != 0x6310
|
182
|
+
Smartcard::Iso::IsoCardMixin.raise_response_exception raw
|
183
|
+
end
|
184
|
+
|
185
|
+
offset = 0
|
186
|
+
loop do
|
187
|
+
break if offset >= raw[:data].length
|
188
|
+
aid_length, offset = raw[:data][offset], offset + 1
|
189
|
+
app = { :aid => raw[:data][offset, aid_length] }
|
190
|
+
offset += aid_length
|
191
|
+
|
192
|
+
if scope == :issuer_sd
|
193
|
+
lc_states = { 1 => :op_ready, 7 => :initialized, 0x0F => :secured,
|
194
|
+
0x7F => :card_locked, 0xFF => :terminated }
|
195
|
+
lc_mask = 0xFF
|
196
|
+
else
|
197
|
+
lc_states = { 1 => :loaded, 3 => :installed, 7 => :selectable,
|
198
|
+
0x83 => :locked, 0x87 => :locked }
|
199
|
+
lc_mask = 0x87
|
200
|
+
end
|
201
|
+
app[:lifecycle] = lc_states[raw[:data][offset] & lc_mask]
|
202
|
+
|
203
|
+
permission_bits = raw[:data][offset + 1]
|
204
|
+
app[:permissions] = Set.new()
|
205
|
+
[[1, :mandated_dap], [2, :cvm_management], [4, :card_reset],
|
206
|
+
[8, :card_terminate], [0x10, :card_lock], [0x80, :security_domain],
|
207
|
+
[0xA0, :delegate], [0xC0, :dap_verification]].each do |mask, perm|
|
208
|
+
app[:permissions] << perm if (permission_bits & mask) == mask
|
209
|
+
end
|
210
|
+
offset += 2
|
211
|
+
|
212
|
+
if scope == :files_modules
|
213
|
+
num_modules, offset = raw[:data][offset], offset + 1
|
214
|
+
app[:modules] = []
|
215
|
+
num_modules.times do
|
216
|
+
aid_length = raw[:data][offset]
|
217
|
+
app[:modules] << { :aid => raw[:data][offset + 1, aid_length] }
|
218
|
+
offset += 1 + aid_length
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
apps << app
|
223
|
+
end
|
224
|
+
break if raw[:status] == 0x9000
|
225
|
+
first = false # Need more GET STATUS commands.
|
226
|
+
end
|
227
|
+
apps
|
228
|
+
end
|
229
|
+
|
230
|
+
# The GlobalPlatform applications available on the card.
|
231
|
+
def applications
|
232
|
+
select_application gp_card_manager_aid
|
233
|
+
secure_channel
|
234
|
+
gp_get_status :apps
|
235
|
+
|
236
|
+
# TODO(costan): there should be a way to query the AIDs without asking the
|
237
|
+
# SD, which requires admin keys.
|
238
|
+
end
|
239
|
+
|
240
|
+
# Issues a GlobalPlatform DELETE command targeting an executable load file.
|
241
|
+
#
|
242
|
+
# Args:
|
243
|
+
# aid:: the executable load file's AID
|
244
|
+
#
|
245
|
+
# The return value is irrelevant.
|
246
|
+
def gp_delete_file(aid)
|
247
|
+
data = Asn1Ber.encode [{:class => :application, :primitive => true,
|
248
|
+
:number => 0x0F, :value => aid}]
|
249
|
+
response = iso_apdu! :cla => 0x80, :ins => 0xE4, :p1 => 0x00, :p2 => 0x80,
|
250
|
+
:data => data
|
251
|
+
delete_confirmation = response[1, response[0]]
|
252
|
+
delete_confirmation
|
253
|
+
end
|
254
|
+
|
255
|
+
# Deletes a GlobalPlatform application.
|
256
|
+
#
|
257
|
+
# Returns +false+ if the application was not found on the card, or a true
|
258
|
+
# value if the application was deleted.
|
259
|
+
def delete_application(application_aid)
|
260
|
+
select_application gp_card_manager_aid
|
261
|
+
secure_channel
|
262
|
+
|
263
|
+
files = gp_get_status :files_modules
|
264
|
+
app_file_aid = nil
|
265
|
+
files.each do |file|
|
266
|
+
next unless modules = file[:modules]
|
267
|
+
next unless modules.any? { |m| m[:aid] == application_aid }
|
268
|
+
gp_delete_file file[:aid]
|
269
|
+
app_file_aid = file[:aid]
|
270
|
+
end
|
271
|
+
app_file_aid
|
272
|
+
end
|
273
|
+
|
274
|
+
# Issues a GlobalPlatform INSTALL command that loads an application's file.
|
275
|
+
#
|
276
|
+
# The command should be followed by a LOAD command (see gp_load).
|
277
|
+
#
|
278
|
+
# Args:
|
279
|
+
# file_aid:: the AID of the file to be loaded
|
280
|
+
# sd_aid:: the AID of the security domain handling the loading
|
281
|
+
# data_hash::
|
282
|
+
# params::
|
283
|
+
# token:: load token (needed by some SDs)
|
284
|
+
#
|
285
|
+
# Returns a true value if the command returns a valid install confirmation.
|
286
|
+
def gp_install_load(file_aid, sd_aid = nil, data_hash = [], params = {},
|
287
|
+
token = [])
|
288
|
+
ber_params = []
|
289
|
+
|
290
|
+
data = [file_aid.length, file_aid, sd_aid.length, sd_aid,
|
291
|
+
Asn1Ber.encode_length(data_hash.length), data_hash,
|
292
|
+
Asn1Ber.encode_length(ber_params.length), ber_params,
|
293
|
+
Asn1Ber.encode_length(token.length), token].flatten
|
294
|
+
response = iso_apdu! :cla => 0x80, :ins => 0xE6, :p1 => 0x02, :p2 => 0x00,
|
295
|
+
:data => data
|
296
|
+
response == [0x00]
|
297
|
+
end
|
298
|
+
|
299
|
+
# Issues a GlobalPlatform INSTALL command that installs an application and
|
300
|
+
# makes it selectable.
|
301
|
+
#
|
302
|
+
# Args:
|
303
|
+
# file_aid:: the AID of the application's executable load file
|
304
|
+
# module_aid:: the AID of the application's module in the load file
|
305
|
+
# app_aid:: the application's AID (application will be selectable by it)
|
306
|
+
# privileges:: array of application privileges (e.g. :security_domain)
|
307
|
+
# params:: application install parameters
|
308
|
+
# token:: install token (needed by some SDs)
|
309
|
+
#
|
310
|
+
# Returns a true value if the command returns a valid install confirmation.
|
311
|
+
def gp_install_selectable(file_aid, module_aid, app_aid, privileges = [],
|
312
|
+
params = {}, token = [])
|
313
|
+
privilege_byte = 0
|
314
|
+
privilege_bits = { :mandated_dap => 1, :cvm_management => 2,
|
315
|
+
:card_reset => 4, :card_terminate => 8, :card_lock => 0x10,
|
316
|
+
:security_domain => 0x80, :delegate => 0xA0, :dap_verification => 0xC0 }
|
317
|
+
privileges.each { |privilege| privilege_byte |= privilege_bits[privilege] }
|
318
|
+
|
319
|
+
param_tags = [{:class => :private, :primitive => true, :number => 9,
|
320
|
+
:value => params[:app] || []}]
|
321
|
+
ber_params = Asn1Ber.encode(param_tags)
|
322
|
+
|
323
|
+
data = [file_aid.length, file_aid, module_aid.length, module_aid,
|
324
|
+
app_aid.length, app_aid, 1, privilege_byte,
|
325
|
+
Asn1Ber.encode_length(ber_params.length), ber_params,
|
326
|
+
Asn1Ber.encode_length(token.length), token].flatten
|
327
|
+
response = iso_apdu! :cla => 0x80, :ins => 0xE6, :p1 => 0x0C, :p2 => 0x00,
|
328
|
+
:data => data
|
329
|
+
response == [0x00]
|
330
|
+
end
|
331
|
+
|
332
|
+
# Issues a GlobalPlatform LOAD command.
|
333
|
+
#
|
334
|
+
# Args:
|
335
|
+
# file_data:: the file's data
|
336
|
+
# max_apdu_length:: the maximum APDU length, returned from
|
337
|
+
# select_application
|
338
|
+
#
|
339
|
+
# Returns a true value if the loading succeeds.
|
340
|
+
def gp_load_file(file_data, max_apdu_length)
|
341
|
+
data_tag = { :class => :private, :primitive => true, :number => 4,
|
342
|
+
:value => file_data }
|
343
|
+
ber_data = Asn1Ber.encode [data_tag]
|
344
|
+
|
345
|
+
max_data_length = max_apdu_length - 5
|
346
|
+
offset = 0
|
347
|
+
block_number = 0
|
348
|
+
loop do
|
349
|
+
block_length = [max_data_length, ber_data.length - offset].min
|
350
|
+
last_block = (offset + block_length >= ber_data.length)
|
351
|
+
response = iso_apdu! :cla => 0x80, :ins => 0xE8,
|
352
|
+
:p1 => (last_block ? 0x80 : 0x00),
|
353
|
+
:p2 => block_number,
|
354
|
+
:data => ber_data[offset, block_length]
|
355
|
+
offset += block_length
|
356
|
+
block_number += 1
|
357
|
+
break if last_block
|
358
|
+
end
|
359
|
+
true
|
17
360
|
end
|
18
361
|
|
19
362
|
# Installs a JavaCard applet on the JavaCard.
|
20
363
|
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
|
24
|
-
|
364
|
+
# Args:
|
365
|
+
# cap_file:: path to the applet's CAP file
|
366
|
+
# package_aid:: the applet's package AID
|
367
|
+
# applet_aid:: the AID used to select the applet; if nil, the first AID
|
368
|
+
# in the CAP's Applet section is used (this works pretty well)
|
369
|
+
# install_data:: data to be passed to the applet at installation time
|
370
|
+
def install_applet(cap_file, package_aid, applet_aid = nil, install_data = [])
|
371
|
+
load_data = CapLoader.cap_load_data(cap_file)
|
372
|
+
applet_aid ||= load_data[:applets].first[:aid]
|
373
|
+
|
374
|
+
delete_application applet_aid
|
375
|
+
|
376
|
+
manager_data = select_application gp_card_manager_aid
|
377
|
+
max_apdu = manager_data[:max_apdu_length]
|
378
|
+
secure_channel
|
379
|
+
|
380
|
+
gp_install_load package_aid, gp_card_manager_aid
|
381
|
+
gp_load_file load_data[:data], max_apdu
|
382
|
+
gp_install_selectable package_aid, applet_aid, applet_aid, [],
|
383
|
+
{ :app => install_data }
|
25
384
|
end
|
26
385
|
end # module GpCardMixin
|
27
386
|
|