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 CHANGED
@@ -1,3 +1,5 @@
1
+ v0.4.2. GlobalPlatform applet instalation.
2
+
1
3
  v0.4.1. Workaround ATR resend bug in JCOP simulator 3.2.7.
2
4
 
3
5
  v0.4.0. High-level functionality for ISO7816 cards.
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
- lib/smartcard.rb
26
- LICENSE
27
- Manifest
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 = !Platform.windows?
14
- p.need_zip = !Platform.windows?
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 Platform.windows?
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
@@ -12,4 +12,7 @@ require 'smartcard/iso/transport.rb'
12
12
  require 'smartcard/iso/auto_configurator.rb'
13
13
 
14
14
 
15
+ require 'smartcard/gp/asn1_ber.rb'
16
+ require 'smartcard/gp/cap_loader.rb'
17
+ require 'smartcard/gp/des.rb'
15
18
  require 'smartcard/gp/gp_card_mixin.rb'
@@ -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
- # This would be really, really nice to have. Sadly, it's a far away TBD right
22
- # now.
23
- def install_applet(cap_contents)
24
- raise "Not implemeted; it'd be nice though, right?"
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