Module: Smartcard::Gp::GpCardMixin

Includes:
Iso::IsoCardMixin
Defined in:
lib/smartcard/gp/gp_card_mixin.rb

Overview

Module intended to be mixed into transport implementations to add commands for talking to GlobalPlatform smart-cards.

The module talks to the card exclusively via methods in Smartcard::Iso::IsoCardMixin, so the transport requirements are the same as for that module.

Instance Method Summary collapse

Methods included from Iso::IsoCardMixin

deserialize_response, #iso_apdu, #iso_apdu!, serialize_apdu

Instance Method Details

#applicationsObject

The GlobalPlatform applications available on the card.



263
264
265
266
267
268
269
270
# File 'lib/smartcard/gp/gp_card_mixin.rb', line 263

def applications
  select_application gp_card_manager_aid
  secure_channel
  gp_get_status :apps
  
  # TODO(costan): there should be a way to query the AIDs without asking the
  #               SD, which requires admin keys.
end

#delete_application(application_aid) ⇒ Object

Deletes a GlobalPlatform application.

Returns false if the application was not found on the card, or a true value if the application was deleted.



291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'lib/smartcard/gp/gp_card_mixin.rb', line 291

def delete_application(application_aid)
  select_application gp_card_manager_aid
  secure_channel
  
  files = gp_get_status :files_modules
  app_file_aid = nil
  files.each do |file|
    next unless modules = file[:modules]
    next unless modules.any? { |m| m[:aid] == application_aid }
    gp_delete_file file[:aid]
    app_file_aid = file[:aid]
  end    
  app_file_aid
end

#gp_card_manager_aidObject

The application ID of the GlobalPlatform card manager.



39
40
41
42
43
44
# File 'lib/smartcard/gp/gp_card_mixin.rb', line 39

def gp_card_manager_aid
  unless instance_variable_defined? :@gp_card_manager_aid
    @gp_card_manager_aid = select_application([])[:aid]
  end
  @gp_card_manager_aid
end

#gp_delete_file(aid) ⇒ Object

Issues a GlobalPlatform DELETE command targeting an executable load file.

Args:

aid:: the executable load file's AID

The return value is irrelevant.



278
279
280
281
282
283
284
285
# File 'lib/smartcard/gp/gp_card_mixin.rb', line 278

def gp_delete_file(aid)
  data = Asn1Ber.encode [{:class => :application, :primitive => true,
                          :number => 0x0F, :value => aid}]
  response = iso_apdu! :cla => 0x80, :ins => 0xE4, :p1 => 0x00, :p2 => 0x80,
                       :data => data
  delete_confirmation = response[1, response[0]]
  delete_confirmation
end

#gp_development_keysObject

Secure channel keys for development GlobalPlatform cards.

Most importantly, the JCOP cards and simulator work with these keys.



181
182
183
184
# File 'lib/smartcard/gp/gp_card_mixin.rb', line 181

def gp_development_keys
  key = (0x40..0x4F).to_a.pack('C*')
  { :senc => key, :smac => key, :dek => key }
end

#gp_get_status(scope, query_aid = []) ⇒ Object

Issues a GlobalPlatform GET STATUS command.

Args:

scope:: the information to be retrieved from the card, can be:
  :issuer_sd:: the issuer's security domain
  :apps:: applications and supplementary security domains
  :files:: executable load files
  :files_modules:: executable load files and executable modules
query_aid:: the AID to look for (empty array to get everything)

Returns an array of application information data. Each element represents an application, and is a hash with the following keys:

:aid:: the application or file's AID
:lifecycle:: the state in the application's lifecycle (symbol)
:permissions:: a Set of the application's permissions (symbols)
:modules:: array of modules in an executable load file, each array element
           is a hash with the key :aid which has the module's AID


203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/smartcard/gp/gp_card_mixin.rb', line 203

def gp_get_status(scope, query_aid = [])
  scope_byte = { :issuer_sd => 0x80, :apps => 0x40, :files => 0x20,
                 :files_modules => 0x10 }[scope]
  data = Asn1Ber.encode [{:class => :application, :primitive => true,
                          :number => 0x0F, :value => query_aid}]
  apps = []    
  first = true  # Set to false after the first GET STATUS is issued.
  loop do
    raw = iso_apdu :cla => 0x80, :ins => 0xF2, :p1 => scope_byte,
                   :p2 => (first ? 0 : 1), :data => [0x4F, 0x00]
    if raw[:status] != 0x9000 && raw[:status] != 0x6310 
      raise Smartcard::Iso::ApduException, raw
    end
    
    offset = 0
    loop do
      break if offset >= raw[:data].length
      aid_length, offset = raw[:data][offset], offset + 1
      app = { :aid => raw[:data][offset, aid_length] }
      offset += aid_length
      
      if scope == :issuer_sd
        lc_states = { 1 => :op_ready, 7 => :initialized, 0x0F => :secured,
                      0x7F => :card_locked, 0xFF => :terminated }
        lc_mask = 0xFF
      else
        lc_states = { 1 => :loaded, 3 => :installed, 7 => :selectable,
            0x83 => :locked, 0x87 => :locked }
        lc_mask = 0x87
      end
      app[:lifecycle] = lc_states[raw[:data][offset] & lc_mask]

      permission_bits = raw[:data][offset + 1]
      app[:permissions] = Set.new()
      [[1, :mandated_dap], [2, :cvm_management], [4, :card_reset],
       [8, :card_terminate], [0x10, :card_lock], [0x80, :security_domain],
       [0xA0, :delegate], [0xC0, :dap_verification]].each do |mask, perm|
        app[:permissions] << perm if (permission_bits & mask) == mask
      end
      offset += 2
      
      if scope == :files_modules
        num_modules, offset = raw[:data][offset], offset + 1
        app[:modules] = []
        num_modules.times do
          aid_length = raw[:data][offset]
          app[:modules] << { :aid => raw[:data][offset + 1, aid_length] }
          offset += 1 + aid_length            
        end
      end
      
      apps << app
    end
    break if raw[:status] == 0x9000
    first = false  # Need more GET STATUS commands.
  end
  apps
end

#gp_install_load(file_aid, sd_aid = nil, data_hash = [], params = {}, token = []) ⇒ Object

Issues a GlobalPlatform INSTALL command that loads an application's file.

The command should be followed by a LOAD command (see gp_load).

Args:

file_aid:: the AID of the file to be loaded
sd_aid:: the AID of the security domain handling the loading
data_hash::
params::
token:: load token (needed by some SDs)

Returns a true value if the command returns a valid install confirmation.



318
319
320
321
322
323
324
325
326
327
328
329
# File 'lib/smartcard/gp/gp_card_mixin.rb', line 318

def gp_install_load(file_aid, sd_aid = nil, data_hash = [], params = {},
                    token = [])
  ber_params = []
                    
  data = [file_aid.length, file_aid, sd_aid.length, sd_aid,
          Asn1Ber.encode_length(data_hash.length), data_hash,
          Asn1Ber.encode_length(ber_params.length), ber_params,
          Asn1Ber.encode_length(token.length), token].flatten
  response = iso_apdu! :cla => 0x80, :ins => 0xE6, :p1 => 0x02, :p2 => 0x00,
                       :data => data
  response == [0x00]
end

#gp_install_selectable(file_aid, module_aid, app_aid, privileges = [], params = {}, token = []) ⇒ Object

Issues a GlobalPlatform INSTALL command that installs an application and makes it selectable.

Args:

file_aid:: the AID of the application's executable load file
module_aid:: the AID of the application's module in the load file
app_aid:: the application's AID (application will be selectable by it)
privileges:: array of application privileges (e.g. :security_domain)
params:: application install parameters
token:: install token (needed by some SDs)

Returns a true value if the command returns a valid install confirmation.



343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
# File 'lib/smartcard/gp/gp_card_mixin.rb', line 343

def gp_install_selectable(file_aid, module_aid, app_aid, privileges = [],
                          params = {}, token = [])
  privilege_byte = 0
  privilege_bits = { :mandated_dap => 1, :cvm_management => 2,
      :card_reset => 4, :card_terminate => 8, :card_lock => 0x10,
      :security_domain => 0x80, :delegate => 0xA0, :dap_verification => 0xC0 }
  privileges.each { |privilege| privilege_byte |= privilege_bits[privilege] }
  
  param_tags = [{:class => :private, :primitive => true, :number => 9,
                 :value => params[:app] || []}]
  ber_params = Asn1Ber.encode(param_tags)
  
  data = [file_aid.length, file_aid, module_aid.length, module_aid,
          app_aid.length, app_aid, 1, privilege_byte,
          Asn1Ber.encode_length(ber_params.length), ber_params,
          Asn1Ber.encode_length(token.length), token].flatten
  response = iso_apdu! :cla => 0x80, :ins => 0xE6, :p1 => 0x0C, :p2 => 0x00,
                       :data => data
  response == [0x00]
end

#gp_load_file(file_data, max_apdu_length) ⇒ Object

Issues a GlobalPlatform LOAD command.

Args:

file_data:: the file's data
max_apdu_length:: the maximum APDU length, returned from

Returns a true value if the loading succeeds.



372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
# File 'lib/smartcard/gp/gp_card_mixin.rb', line 372

def gp_load_file(file_data, max_apdu_length)
  data_tag = { :class => :private, :primitive => true, :number => 4,
               :value => file_data }
  ber_data = Asn1Ber.encode [data_tag]
  
  max_data_length = max_apdu_length - 5
  offset = 0
  block_number = 0
  loop do
    block_length = [max_data_length, ber_data.length - offset].min
    last_block = (offset + block_length >= ber_data.length)
    response = iso_apdu! :cla => 0x80, :ins => 0xE8,
                         :p1 => (last_block ? 0x80 : 0x00),
                         :p2 => block_number,
                         :data => ber_data[offset, block_length]
    offset += block_length
    block_number += 1
    break if last_block
  end
  true
end

#gp_lock_secure_channel(host_auth, security = []) ⇒ Object

Issues a GlobalPlatform EXTERNAL AUTHENTICATE command.

This should not be called directly. Call secure_session insteaad.

Args:

host_auth:: 8-byte host authentication value
security:: array of desired security flags (leave empty for the default
           of no security)

The return value is irrelevant. The card will fire an ISO exception if the authentication doesn't work out.



108
109
110
111
112
113
114
115
116
117
# File 'lib/smartcard/gp/gp_card_mixin.rb', line 108

def gp_lock_secure_channel(host_auth, security = [])
  security_level = 0
  security_flags = { :command_mac => 0x01, :response_mac => 0x10,
                     :command_encryption => 0x02 }
  security.each do |flag|
    security_level |= security_flags[flag]
  end
  gp_signed_apdu! :cla => 0x80, :ins => 0x82, :p1 => security_level, :p2 => 0,
                  :data => host_auth
end

#gp_setup_secure_channel(host_challenge, key_version = 0) ⇒ Object

Issues a GlobalPlatform INITIALIZE UPDATE command.

This should not be called directly. Call secure_session insteaad.

Args:

host_challenge:: 8-byte array with a unique challenge for the session
key_version:: the key in the Security domain to be used (0 = any key)

Returns a hash containing the command's parsed response. The keys are:

:key_diversification:: key diversification data
:key_version:: the key in the Security domain chosen to be used
:protocol_id:: numeric ID for the secure protocol to be used
:counter:: counter for creating session keys
:challenge:: the card's 6-byte challenge
:auth:: the card's 8-byte authentication value


61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/smartcard/gp/gp_card_mixin.rb', line 61

def gp_setup_secure_channel(host_challenge, key_version = 0)
  raw = iso_apdu! :cla => 0x80, :ins => 0x50, :p1 => key_version, :p2 => 0,
                  :data => host_challenge
  response = {
    :key_diversification => raw[0, 10],
    :key_version => raw[10], :protocol_id => raw[11], :auth => raw[20, 8]
  }
  case response[:protocol_id]
  when 1
    response[:challenge] = raw[12, 8]
  when 2
    response.merge! :counter => raw[12, 2].pack('C*').unpack('n').first,
                    :challenge => raw[14, 6] 
  else
    raise "Unimplemented Secure Channel protocol #{response[:protocol_id]}"
  end
  response
end

#gp_signed_apdu!(apdu_data) ⇒ Object

Wrapper around iso_apdu! that adds a MAC to the APDU.



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/smartcard/gp/gp_card_mixin.rb', line 81

def gp_signed_apdu!(apdu_data)    
  apdu_data = apdu_data.dup
  apdu_data[:cla] = (apdu_data[:cla] || 0) | 0x04
  apdu_data[:data] = (apdu_data[:data] || []) + [0, 0, 0, 0, 0, 0, 0, 0]
  
  apdu_bytes = Smartcard::Iso::IsoCardMixin.serialize_apdu(apdu_data)[0...-9]
  mac = Des.send @gp_secure_channel_mac,
                 @gp_secure_channel_keys[:cmac], apdu_bytes.pack('C*'),
                 @gp_secure_channel_keys[:mac_iv]
  @gp_secure_channel_keys[:mac_iv] = mac
  
  apdu_data[:data][apdu_data[:data].length - 8, 8] = mac.unpack('C*')
  apdu_data[:le] = false
  iso_apdu! apdu_data
end

#install_applet(cap_file, applet_aid = nil, package_aid = nil, install_data = []) ⇒ Object

Installs a JavaCard applet on the JavaCard.

Args:

cap_file:: path to the applet's CAP file
package_aid:: the applet's package AID; if nil, the AID in the CAP's
              header is used (should work all the time)
applet_aid:: the AID used to select the applet; if nil, the first AID
             in the CAP's Applet section is used (this works pretty well)
install_data:: data to be passed to the applet at installation time


403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
# File 'lib/smartcard/gp/gp_card_mixin.rb', line 403

def install_applet(cap_file, applet_aid = nil, package_aid = nil,
                   install_data = [])
  load_data = CapLoader.cap_load_data(cap_file)
  applet_aid ||= load_data[:applets].first[:aid]
  package_aid ||= load_data[:header][:package][:aid]

  delete_application applet_aid
  
  manager_data = select_application gp_card_manager_aid
  max_apdu = manager_data[:max_apdu_length]
  secure_channel
      
  gp_install_load package_aid, gp_card_manager_aid
  gp_load_file load_data[:data], max_apdu
  gp_install_selectable package_aid, applet_aid, applet_aid, [],
                        { :app => install_data }    
end

#secure_channel(keys = gp_development_keys) ⇒ Object

Sets up a secure session with the current GlobalPlatform application.

Args:

keys:: hash containing 3 3DES encryption keys, identified by the following
       keys:
         :senc:: channel encryption key
         :smac:: channel MAC key
         :dek:: data encryption key


127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/smartcard/gp/gp_card_mixin.rb', line 127

def secure_channel(keys = gp_development_keys)
  host_challenge = Des.random_bytes 8
  card_info = gp_setup_secure_channel host_challenge.unpack('C*')
  card_challenge = card_info[:challenge].pack('C*')

  # Compute session keys.
  session_keys = {}
  case card_info[:protocol_id]
  when 1  # Secure Channel 01 (GlobalPlatform 2.0)
    derivation_data = card_challenge[4, 4] + host_challenge[0, 4] +
                      card_challenge[0, 4] + host_challenge[4, 4]
    session_keys[:cmac] = Des.crypt keys[:smac], derivation_data,
                                    nil, false, true
    session_keys[:rmac] = session_keys[:cmac]
    session_keys[:senc] = Des.crypt keys[:senc], derivation_data,
                                    nil, false, true
    session_keys[:dek] = keys[:dek].dup
    
    card_auth = Des.mac_3des session_keys[:senc],
                             host_challenge + card_challenge
    host_auth = Des.mac_3des session_keys[:senc],
                             card_challenge + host_challenge
    @gp_secure_channel_mac = :mac_3des
  when 2  # Secure Channel 02 (GlobalPlatform 2.1+)
    card_counter = [card_info[:counter]].pack('n')
    derivation_data = "\x01\x01" + card_counter + "\x00" * 12
    session_keys[:cmac] = Des.crypt keys[:smac], derivation_data    
    derivation_data[0, 2] = "\x01\x02"
    session_keys[:rmac] = Des.crypt keys[:smac], derivation_data
    derivation_data[0, 2] = "\x01\x82"
    session_keys[:senc] = Des.crypt keys[:senc], derivation_data
    derivation_data[0, 2] = "\x01\x81"
    session_keys[:dek] = Des.crypt keys[:dek], derivation_data

    card_auth = Des.mac_3des session_keys[:senc],
        host_challenge + card_counter + card_challenge
    host_auth = Des.mac_3des session_keys[:senc],
        card_counter + card_challenge + host_challenge
    @gp_secure_channel_mac = :mac_retail
  else
    raise "Unimplemented Secure Channel #{card_info[:protocol_id]}"
  end
  session_keys[:mac_iv] = "\x00" * 8
  @gp_secure_channel_keys = session_keys
      
  unless card_auth == card_info[:auth].pack('C*')
    raise 'Card authentication invalid' 
  end
  gp_lock_secure_channel host_auth.unpack('C*')
end

#select_application(app_id) ⇒ Object

Selects a GlobalPlatform application.



23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/smartcard/gp/gp_card_mixin.rb', line 23

def select_application(app_id)
  ber_data = iso_apdu! :ins => 0xA4, :p1 => 0x04, :p2 => 0x00, :data => app_id
  app_tags = Asn1Ber.decode ber_data
  app_data = {}
  Asn1Ber.visit app_tags do |path, value|
    case path
    when [0x6F, 0xA5, 0x9F65]
      app_data[:max_apdu_length] = value.inject(0) { |acc, v| (acc << 8) | v }
    when [0x6F, 0x84]
      app_data[:aid] = value
    end
  end
  app_data
end