Java Card 应用程序的元素
请记住,Java Card 应用程序并不是独立的,而是端到端应用程序的一部分:
图 1. Java Card 应用程序的典型组件
Java Card 应用程序通常由以下部分组成:
提供后台服务(例如保存在数据库中的安全或者电子支付信息)访问的 后台应用程序。如何开发后台应用程序超出了本文的范围。
主机应用程序 位于卡片外部的卡片终端,它可以使用许多接口(如 Java Card RMI、OpenCard Framework API 或安全性和信任服务应用编程接口 [SATSA])访问智能卡上的 applet。
卡片读取器、卡片终端 或者 卡片接入设备 ,提供了主机应用程序和卡片内部 applet 之间的物理接口。
卡片内部的物理接口是 Java Card applet 和 Java Card 框架。请注意,在访问 applet 之前,主机应用程序必须提供证书并进行自我身份验证。
编写主机应用程序 —— 访问 Applet
位于客户端的主机应用程序处理用户、Java Card applet 和提供器的后端应用程序之间的通信。主机程序访问由 applet 所提供的服务。它存储在终端或卡片接入设备上,例如工作站、销售终端点( POS )、手机或者机顶盒。回想一下,主机和 applet 使用 ISO-7816 APDU 命令通过卡片读取器或终端进行交互。
通常,读取器端应用程序使用 C 语言编写,但是主机程序用 Java 编程语言或其他语言编写,只要该语言准备了与 applet 交换的有效 ISO-7816 APDU 命令就没有问题。
目前,大多数经过部署的手机集成了智能卡读取器,以便访问与该读取器捆绑的 SIM 卡片。使用即将引入的 JSR 177, 用于 J2ME 的安全性和信任服务应用编程接口(SATSA)、J2ME 设备的广泛采用,我们能够预计各种主机应用程序将使用移动设备上的 Java 技术编写。SATSA 的意图就是启用 Java Card 主机应用程序来运行基于 J2ME 的设备。目前,JSR 177 处于 JCP 团体审查阶段。
当编写客户端应用程序时,有三个主要的 API 可以使用:OpenCard Framework、Java Card RMI Client API 和用于 J2ME 的安全性和信任服务应用编程接口(SATSA)。我们将逐一研究每一个应用程序编程接口。
OpenCard Framework 简介
智能卡供应商通常不仅提供开发工具箱,还提供 API 以便支持读取器端应用程序以及 Java Card applet。供应商提供的许多产品都支持 OpenCard Framework ( OCF )和基于 Java 的 API 集合,该集合隐藏了与不同供应商提供的卡片读取器交互的一些细节。
OpenCard 联盟 是一群推动定义和采用 OpenCard Framework( 当前版本为1.2)的公司。OCF 的目标是向主机端应用程序的开发者提供跨不同的卡片读取器供应商工作的 API。
为了实现厂商无关性,OCF 定义了两个软件层:
CardTerminal 层提供了卡片读取器抽象,例如 CardTerminal ( 表示物理卡片读取器 ) APDU、CommandADPU 和 ResponseAPDU。
OCF 为您定义了许多标准卡片服务。其中两个是 FileAccessCardService 和 SignatureCardService。一个特殊的类型是 ApplicationManagerCardService,提供生命周期管理方法以便安装、注册和删除卡片内部的 applet。
当编写主机端基于 OCF 应用程序时,您基本上要将该应用程序分为两个部分:
主要的应用程序对象,它与终端或读取器进行交互(初始化 OCF、等待卡片插入、中断 OCF ),并公开高级卡片访问方法,例如 getBalance()。
Applet 代理,它实现实际的低级通道管理和 APDU I/O。当把来自应用程序的 APDU 细节隐藏起来的时候,该代理设计模式允许公开面向对象的接口。
图 2. OCF 应用程序的结构
总而言之,OCF 应用程序有一个或多个 main 对象,它们在主机上或者在执行的线程上创建。这些 main 应用程序对象公开特定于应用程序的高级调用,最终将它们委托给 applet 代理。这些对象利用 OCF 应用程序入口点的 SmartCard 对象,启用应用程序初始化并中断 OCF,并等待卡片被插入。main 对象可以实现即将看到的 CTListener,这个侦听器提供卡片插入和拔出等类似事件的异步通知。
您可以使用同步或异步模型编写应用程序。
在同步模型中,主机应用程序初始化 OCF,然后等待卡片被插入。然后它执行主机应用程序逻辑,当操作完成时,中断 OCF:
... try { // Initialize OCF SmartCard.start(); // Wait for a smart card CardRequest cr = new CardRequest(CardRequest.NEWCARD, null, OCFCardAccessor.class); SmartCard myCard = SmartCard.waitForCard(cr); // Main client work is done here... ... } catch (Exception e){ // Handle exception } finally { try { // Shut down OCF SmartCard.shutdown(); } catch (Exception e) { e.printStackTrace(); } } ... |
清单 1. 同步 OCF 应用程序的典型结构
如果您喜欢使用异步方法,您选定的类必须实现 CTListener 接口,且在初始化阶段,进行自我注册来通知类似插入和拔出这样的卡片终端事件通知。下列应用程序主干通过初始化 OCF 并注册侦听器开始,然后为重要事件定义回调方法。
|
清单 3. 基于侦听器的放大 OCF 应用程序。
接下来是一个来自 applet 代理的引用。OCF 应用程序将服务调用委托给实现( 复杂 ) APDU 管理片断的 applet 代理:
public class MyCardProxy extends AppletProxy { // My APDU definitions. final static byte MyAPPLET_CLA = (byte)0x80; final static byte VERIFY_INS = (byte)0x20; final static byte GET_BALANCE_INS = (byte) 0x30; final static short GET_BALANCE_RESPONSE_SZ = 2; protected final static int OK = 0x9000; final static short SW_PINVERIFY_FAILED = (short)0x6900; /** * Reusable command APDU for getting an information * entry field. */ private CommandAPDU getBalanceAPDU = new CommandAPDU(14); ... /** Application identifier of the BusinessCard applet */ private static final ApplicationID MY_CARD_AID = new ApplicationID(new byte[] { (byte)0xD4, (byte)0x55, (byte)0x00, (byte)0x00, (byte)0x22, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0xFF}); /** * Create a MyCardProxy instance. * * @param scheduler The Scheduler from which channels * have to be obtained. * @param card The SmartCard object to which this * service belongs. * @param blocking Currently not used. * * @throws opencard.core.service.CardServiceException * Thrown when instantiation fails. */ protected void initialize(CardServiceScheduler scheduler, SmartCard card, boolean blocking) throws CardServiceException { super.initialize(MY_CARD_AID, scheduler, card, blocking); try { // Allocate the card channel. This gives us // exclusive access to the card until we release the // channel. allocateCardChannel(); // Get the Card State. ... } finally { releaseCardChannel(); } } /** * Gets the balance. * @return The balance. */ public String getBalance() throws CardServiceInvalidCredentialException, CardServiceOperationFailedException, CardServiceInvalidParameterException, CardServiceUnexpectedResponseException, CardServiceException, CardTerminalException { try { allocateCardChannel(); // Set up the command APDU and send it to the card. getBalanceAPDU.setLength(0); getBalanceAPDU.append(MyAPPLET_CLA); // Class getBalanceAPDU.append(GET_BALANCE_INS); // Instr'n getBalanceAPDU.append((byte) 0x00); // P1 getBalanceAPDU.append((byte) 0x00); // P2 getBalanceAPDU.append((byte) 0x00); // Lc getBalanceAPDU.append((byte) 0x00); // Le // Send command APDU and check the response. ResponseAPDU response = sendCommandAPDU(getCardChannel(), MY_CARD_AID, getBalanceAPDU); switch (response.sw() & 0xFFFF) { case OK : return new String(response.data()); default : throw new CardServiceUnexpectedResponseException ("RC=" + response.sw()); } } finally { releaseCardChannel(); } } ... } |
清单 4. Applet 代理示例
在 OpenCard Framework 1.2 编程人员指南 中您会找到关于 OCF 使用的更多信息。还可以参考 OpenCard Framework 参考实现 中的示例文件。
本文中未涉及,但是值得一提的是主机端 API,称作 Global Platform。Global Platform Card Committee 提供了补充 Java Card API 的卡片 API 集合,除其他内容以外,还提供了卡片管理功能。这些规范都在2.1.1版本中。
Java Card RMI 客户机 API
前面你已经学到了如何编写一个基于 JCRMI 的 applet。如果您的 Java Card applet 基于 JCRMI,您可以使用 Java Card RMI Client API编写一个主机应用程序,访问智能卡中保存的 apple 对象。
对于智能卡管理和访问,JCRMI Client API 需要一个卡片终端和诸如刚刚描述的 OpenCard Framework 这样的服务 API。
当我们把这两个 API 放在一起时,我们得到一个非常简单、非常完整的面向对象编程模型,有以下几个优点:
不必知道智能卡和卡片读取器的细节
不必知道低级的 APDU 通信
便于设计和维护的代码,从而缩短开发时间
JCRMI Client API 在下面的软件包中定义:
com.sun.javacard.javax.smartcard.rmiclient 包含核心 JCRMI Client API。它定义了以下内容:
JCRMI 桩模块用来访问智能卡的 CardAccessor 接口
CardObjectFactory 类,用于 JCRMI 桩模块生成实现的基类。这个类的实例与一个 Java Card applet 选择会话有关。
客户机应用程序使用的 JavaCardRMIConnect 类,用于初始化一个 JCRMI 会话并且获得初始的远程引用。
许多 Java Card 的异常子类,例如 APDUExceptionSubclass、CardExceptionSubclass、CardRuntimeExceptionSubclass、CryptoExceptionSubclass、ISOExceptionSubclass、PINExceptionSubclass、PINException、ServiceExceptionSubclass、SystemExceptionSubclass、TransactionExceptionSubclass 和 UserExceptionSubclass。
javacard.framework 定义了许多客户机上的许多可以被再次抛出的 Java Card 异常:APDUException、CardException、CardRuntimeException、ISOException、PINException、SystemException、TransactionException 和 UserException。
javacard.framework.service 定义了 ServiceException,表示与服务框架有关的异常。
javacard.security 定义了 CryptoException,用来表示一个有关加密的异常。
生成 RMI 客户机桩模块
您选择使用标准的 Java RMI 编译程序(rmic)生成客户机桩模块。您必须使用下面格式的命令运行 rmic,用于你的 applet 中的每个远程类:
rmic -v1.2 -classpath path -d output_dir class_name |
......其中:
-v1.2 是一个 Java Card RMI 客户机框架所需要的标记。
-classpath path 确定到远程类的路径。
output_dir 是存放结果桩模块的目录
class_name 是远程类的名称。
然而,推荐生成 RMI 客户机桩模块的方法使用 J2SE SDK 1.3 中的 动态代理 生成机制。当您选择 JCRMI applet 的时候,如果使用 CardObjectFactory 子类型 JCCardProxyFactory 的话,JavaCard RMI Client API 的 2.2 版本将为你自动生成桩模块,你不必再生成任何桩模块!这个方法在清单 5 中加以说明。
用法限制
因为 Java Card 是一个有限制的运行时环境,我们可以发现关于 JCRMI 实际支持的限制。Java Card 不支持序列化和 JCRMI 参数,并且返回值也有限制:
每个到远程方法的参数必须是 Java Card 支持的类型之一,不包括 char, double, float, long 或者多维数组。对 int 的支持为可选的。
任何远程方法的返回值必须是受支持的类型之一,或者 void,或者一个远程接口类型。
JCRMI 客户机应用程序
因为 JCRMI Client API 依靠 OCF 用于卡片管理和通信,所以 JCRMI 客户机应用程序类似您前面看到的 OCF 主机应用程序。
下面的代码片断首先初始化 OCF,并且等待智能卡被插入。然后它创建一个 OCFCardAccessor 实现,用于把我们的 JCRMI 连接到卡片上,如有必要,客户机桩模块将动态生成,applet 被选中,我们取得了远程引用,最后我们实现到 getBalance() 的远程调用:
... try { // Initialize OCF SmartCard.start(); // Wait for a smart card CardRequest cr = new CardRequest(CardRequest.NEWCARD, null, OCFCardAccessor.class); SmartCard myCard = SmartCard.waitForCard(cr); // Get an OCFCardAccessor for Java Card RMI CardAccessor ca = (CardAccessor) myCard.getCardService(OCFCardAccessor.class, true); // Create a Java Card RMI instance JavaCardRMIConnect jcRMI = new JavaCardRMIConnect(ca); // Create a Java Card Proxy Factory that is used for dynamic // proxy generation. CardObjectFactory factory = new JCCardProxyFactory(ca); // select the Java Card applet jcRMI.selectApplet(MY_APPLET_AID, factory); // Get the initial reference MyRemoteInterface myRemoteInterface = (MyRemoteInterface) jcRMI.getInitialReference(); if(myRemoteInterface == null) { throw new Exception("Received null instead of the initial ref"); } // Invoke the remote getBalance() method short balance = myRemoteInterface.getBalance(); } catch(UserException e) { // Handle exception ... } catch (Exception e){ // Handle exception ... } finally { // Clean up try{ SmartCard.shutdown(); }catch (Exception e){ System.out.println(e); } } |
清单 5. 示例 JCRMI 客户机
如您所见,您必须要做的是减少代码并大幅度简化代码。
用于 J2ME 的安全性和信任服务应用编程接口
SATSA 是一套用于 J2ME 的新的可选软件包,定义一个客户端 API 来访问 安全元素:例如智能卡这样的设备。在本节中,我将仅仅介绍 SATSA 的通信部分;本文中将不介绍 SATSA PKI 和加密 API。
SATSA 通信 API 被分解成下面几部分:
SATSA-APDU 定义一个 API,用于和遵循 ISO-7816-4 的智能卡进行通信。这个可选软件包由单独的 javax.microedition.io.APDUConnection 软件包组成。
SATSA-JCRMI 定义了 Java Card RMI 客户机 API。这个可选软件包由下面的 Java 软件包组成:
javax.microedition.io.JavaCardRMIConnection
javax.microedition.jcrmi.RemoteRef
javax.microedition.jcrmi.RemoteStub
java.rmi.Remote
java.rmi.RemoteException
javacard.framework.service.ServiceException
javacard.framework.CardRuntimeException
javacard.framework.ISOException
javacard.framework.APDUException
javacard.framework.CardException
javacard.framework.PINException
javacard.framework.SystemException
javacard.framework.TransactionException
javacard.framework.UserException
javacard.security.CryptoException
SATSA 将 J2ME 和 Java Card 平台紧密地结合在一起。SATSA 使用 CLDC 1.0 Generic Connection Framework(GCF)用于基于 J2ME 的设备和智能卡之间的通信,如下所示:
图 3. 通用连接框架和 SATSA 连接
因为 SATSA 基于 GCF,开发使用手机上的智能卡的 MIDlet 相对不容易,但是对于 J2ME 开发人员来说很熟悉。
SATSA 考虑到用于 Java Card 应用程序的两个总体编程模型:APDU-消息传递模型和 Java Card RMI 面向对象分布式模型。SATSA 为了每一个模型定义了一个新的 GCF 连接类型:
APDUConnection 允许一个 J2ME 应用程序使用 ISO-7816 APDU 协议以便与智能卡应用程序交换 APDU。
JavaCardRMIConnection 允许一个 J2ME 应用程序使用 Java Card RMI 来调用智能卡上的远程方法。
指定 SATSA 的连接类型
所有的 GCF 连接都使用 Connector.open() 方法创建。Connector.open() 的一个参数是指示要创建的连接类型的 URL。CLDC GCF 使用下面的格式定义这个 URL 为一个字符串:
scheme:[target][params] |
......其中:
scheme 是要创建的连接类型(和要使用的协议)。
target 一般是某种网络地址。
params 是可选参数,表达形式是名称=值,通过分号隔开。
对于 SATSA,URL 的格式是:
protocol:[slotID]; AID |
......其中:
protocol 要么是 apdu,用于基于 APDU 的连接,要么是 jcrmi,用于基于 JCRMI 的连接。
slotID 是指示卡片插入的插槽。slotID 字段是可选的;默认值为 0。
AID 是用于智能卡应用程序的应用程序标识符。AID 是一个由句号分隔开的 5 到 16 个十六进制字节值的字符串;例如,“ A0.0.0.67.4.7.1F.3.2C.3”。
使用 APDUConnection
APDUConnection 定义允许我们使用 GCF 与遵循 ISO-7816 的卡片进行通信的各种方法。它定义了三个方法是:
enterPIN() 提示用户输入一个个人识别号码。
exchangeAPDU() 与智能卡应用程序交换 APDU。这个调用直到一个响应从智能卡返回的时候(或者处理被中断)才会阻断。
getATR() 返回智能卡发送的 Answer To Reset(ATR) 消息,作为重置操作的响应。
下面的代码片断显示如何打开一个 APDUConnection,如何关闭它,以及如何交换一个命令 APDU 并且接收一个 APDU 响应:
... try { // Create an APDUConnection String url = "apdu:0;AID=A1.0.0.67.4.7.1F.3.2C.5"; APDUConnection ac = (APDUConnection) Connector.open(url); // Send a command APDU and receive a response APDU byte[] responseAPDU = ac.exchangeAPDU(commandAPDU); ... // Close connection. ac.close(); } catch(IOException e){ ... } ... |
清单6. 使用 SATSA-APDU
SATSA 使 APDU 通信简单化。请注意,这个 APDU 命令和响应的格式和您在本系列文章的第 2 部分的“编写客户端 JavaCard Applet”一节中看到的相同,告诉您如何编写一个基于 APDU 消息传递的 Java Card Applet。
使用 JavaCardRMIConnection
JavaCardRMIConnection 定义允许我们使用 Java Card RMI 程序设计模型的方法。JavaCardRMIConnection 定义了方法 getInitialReference(),为初始远程引用返回桩模块对象。
... try { // Create a JavaCardRMIConnection String url = "jcrmi:0;AID=A0.0.0.67.4.7.1F.3.2C.3"; JavaCardRMIConnection jc = (JavaCardRMIConnection) Connector.open(url); MyRemoteObject robj = (MyRemoteObject) jc.getInitialReference(); ... short balance = robj.getBalance(); ... // Close connection jc.close(); } catch (Exception e) { ... } ... |
清单7. 使用 SATSA-JCRMI
采用上面所说的 SATSA-JCRMI 应用编程接口,允许我们调用本系列文章的第 2 部分中所定义的 getBalance() 方法,向你说明如何编写一个基于 RMI 的 JavaCard applet。
清单 2. 异步 OCF 应用程序的典型结构
当卡片被插入时,运行时调用 cardInserted() 方法,当卡片被拔出时,运行时调用 cardRemoved() 方法。在更为充实的下列清单中,插入卡片将初始化 applet 代理的创建,拔出卡片将触发 applet 代理的清除。清单还显示了用于信用卡余额的请求被委托给代理。
import opencard.core.event.CTListener; import opencard.core.event.CardTerminalEvent; import opencard.core.service.SmartCard; import opencard.core.service.CardService; ... public class MyHostSideApp implements CTListener { public void MyHostSideApp() { try { // Initialize the framework SmartCard.start (); // Register this as a Card Terminal Event Listener CardTerminalRegistry.getRegistry().addCTListener(this); } catch (Exception e) { // Handle error. ... } } /** * Card insertion event. Get new card and card service * @param ctEvent The card insertion event. */ public void cardInserted(CardTerminalEvent ctEvent) { try { // Get a SmartCard object card = SmartCard.getSmartCard(ctEvent); // Get the card proxy instance. myCardProxy = (MyCardProxy) card.getCardService(MyCardProxy.class, true); } catch (Exception e) { e.printStackTrace(); } } /** * Card removal event. Invalidate card and card service. * @param ctEvent The card removal event. */ public synchronized void cardRemoved(CardTerminalEvent ctEvent) { card = null; myCardProxy = null; // Initialize the framework SmartCard.shutdown(); } /** * Get balance from the smart card. */ public int getBalance() { try { // Get mutex to prevent other Card Services from modifying // data. Delegate the call to the applet proxy. card.beginMutex(); return Integer.parseInt(myCardProxy.getBalance()); } catch (Throwable e) { return 0; } finally { // End mutual exclusion card.endMutex(); } } ... } |