smartcard 0.4.1 → 0.4.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.
- 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
|
|