标签(空格分隔): Java 智能卡
我们将从原理上理解Java Card开发中一些关键的技术信息。
名词解释
Applet:J2SE中也存在Applet这个概念。这里的Applet可以看做类似于Android中一个个APP。分为ROM Applet、事前发行和事后发行Applet。
AID:应用标识符。又区分为Package AID和Applet AID。相当于MAC地址的构造,前四个字节是IEEE分配的,后四个字节是厂商定义的内部序列号。Applet AID是Package AID的所有部分外加一些字节
CAP:Converted Applet。我们知道,J2SE中也存在Applet,这里沿袭了Applet的定义——小应用程序。由于智能卡的计算能力有限(据说是最小的计算平台),所以需要将二进制文件class进行削减,于是就构成了CAP
ATR:复位应答,卡-机交流的第一句话,包括协议之类的数据
DF:专用文件Dedicated File的缩写,DF的作用可以等同于计算机中的目录文件
MF:在一张卡片里(这里说的是卡片而不是某个应用)有且仅有一个特殊的DF,称为主文件MF,这个MF的FID默认为3F00,相当于计算机中的根目录,而且在任何时候MF都可以被选择
EF:基本信息文件Elementary File,也就是说通常情况下和应用相关的数据都会存放于EF中。
如果某个DF下没有子DF,只有若干EF,那么这个DF也被称作ADF,反之如果某个DF下除了有EF之外,还有子DF,那么这个父级的DF也被称作DDF。为了对文件进行访问,需要给文件分配一个特定的标识。无论是DF还是EF都会有对应的两个字节长ID标识,也就是所谓的FID。而DF还会有5-16个字节长的名字,也叫做AID。EF还会有一个5位长(范围从1到30)的短文件标识,就是SFI。
ADPU
CLA | INS | P1 | P2 | Lc | 数据域 |
---|---|---|---|---|---|
0x0 | 0xA4 | 0x4 | 0x0 | AID的长度 | AID 字节 |
APDU的全称是Application Protocol Data Unit,也就是在应用层实现的数据单元。其中读卡器发出的叫Command-APDU,卡回复的叫Response-APDU。
Command-APDU:
CLA | INS | P1 | P2 | Lc | 数据域 |
---|---|---|---|---|---|
0x0 | 0xA4 | 0x4 | 0x0 | AID的长度 | AID 字节 |
Le | 数据域 | SW1 | SW2 |
---|---|---|---|
0x00 | Null | 0x90 | 0x00 |
Response-APDU:
Le | 数据域 | SW1 | SW2 |
---|---|---|---|
0x00 | Null | 0x90 | 0x00 |
在卡-机通信开始之前,还有个请求和回复的过程。这时候我们可以收到一个ATR如:
ATR = 0x3b 0xf0 0x11 0x00 0xff 0x00
| TS | T0 | TA1,TB1,TC1,TD1 |T1,T2,...,T15 | TCK |
| :----: | :----: | :----: | :----: | :----: | :----: |
| 起始* | 格式*,决定接口和历史 | 接口(由T0决定:b5-TA1,b8-TD1) | 历史 | 校验 |
| 0x3B/0x3F | 0xF0 | 0x11 0x00 0xFF 0x00 | Null | Null |
Applet构成简介
我们以最简单的demo为例:
package hellojavacard;
import javacard.framework.APDU;
import javacard.framework.ISO7816;
import javacard.framework.Applet;
import javacard.framework.ISOException;
public class Appletcard extends Applet {
private Appletcard() {
}
public static void install(byte bArray[], short bOffset, byte bLength) throws ISOException {
new Appletcard().register(bArray, (short) (bOffset + 1), bArray[bOffset]);
}
public void process(APDU apdu) {
if (selectingApplet()) {
return;
}
byte[] buf = apdu.getBuffer();
switch (buf[ISO7816.OFFSET_INS]) {
case (byte) 0x00:
break;
default:
ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
}
}
}
注册Applet
这一步是通过register()函数实现的,register()函数有两种形式,一种是不带参数的,一种是带参数的,这些参数在Applet的安装期间就进行了传递:
register(byte[] bArray,short bOffset,byte bLength)
#byte[] bArray - 包含安装参数的数组
#short bOffset - 安装参数在 bArray 中的起始位移
#byte Length - bArray 中的参数数据的字节数
这些初始化值是Applet开发者定义的,通常钱包应用可以包括记录数目,钱包ID,卡初始余额等。
选择Applet
CLA | INS | P1 | P2 | Lc | 数据域 |
---|---|---|---|---|---|
0x0 | 0xA4 | 0x4 | 0x0 | AID的长度 | AID 字节 |
在未被选择之前,Applet是处于挂起状态的。这一步是通过selectingApplet()实现的,SELECT APDU 命令是在 Java 卡平台上被标准化的唯一 APDU 命令。注意,正常情况下Applet只能通过成功的SELECT APDU命令执行,但有些卡可以通过配置缺省Applet的方式省去这一步骤。
C-APDU:
CLA | INS | P1 | P2 | Lc | 数据域 |
---|---|---|---|---|---|
0x0 | 0xA4 | 0x4 | 0x0 | AID的长度 | AID 字节 |
Le | 数据域 | SW1 | SW2 |
---|---|---|---|
0x00 | Null | 0x90 | 0x00 |
R-APDU:
Le | 数据域 | SW1 | SW2 |
---|---|---|---|
0x00 | Null | 0x90 | 0x00 |
与之对应的反向操作为deselect(),相当于进程阻塞操作block。在复位和掉电的情况下,取消选择不执行deselect()。
获取APDU
为了处理APDU,我们先要将其获取并存储至字节数组buffer。APDU buffer 是一个字节数组,其长度可通过方法 apdu_buffer.length确定。
public void process(APDU apdu){
//索取APDU
buffer byte[] apdu_buffer = apdu.getBuffer();
}
在调用一个 applet 的 process 方法时,在APDU buffer 中只有命令的前 5 个字节是可用的――前 4
个字节是APDU 头〔CLA,INS,P1,P2〕而第 5 个字节(P3)是一个附加的长度域。P3 的含义是隐含的, 由命令的 case 决定:
- 对于 case1,P3=0;
- 对于 case2,P3=Le,表示输出的响应数据的长度;
- 对于 case3 和 case4,P3=Lc,表示输入的命令数据的长度。
返回状态字
- 正常状态:返回状态字0x9000。(SW1:0x90;SW2:0x0)
- 在 命 令 处 理 过 程 中 的 任 何 地 方 , applet 终 止 操 作 并 通 过 调 用 静 态 方 法
ISOException.throwIt(reason) 抛出 ISOException 例外。 - 如果由底层 Java 卡系统检测到错误, JCRE 的行为是不确定的。例如, JCRE 可能抛出一个例
外 APDUException 或 TransactionException。
智能卡安全
从上面的APDU交互过程我们可以看出,交互的过程完全通过明文传输。这就好比TCP/IP协议实现了端-端的传输,踢进了临门一脚。对于智能卡传输过程中的安全性,我们就要借助应用层协议(如SSL/TLS)。智能卡的传输过程也是一样,我们需要对一些敏感的信息进行加密(比如卡交易金额),同时对消息的可靠性也有认证的需求(还比如卡交易金额)。另外,加密可以作为一个身份认证的标示(比如SIM卡与注册网络),这样可以将其视作一个令牌(token)。
javacard.security 包
类或接口 | 描述 |
---|---|
Key | 所有密钥的基接口 |
SecretKey | 用于对称算法的密钥的接口 |
DESKey | 代表 DES 或双密钥 3DES 或 3 密钥 3DES 的 8/16/24 字节密钥 |
PrivateKey | 关于用于非对称算法的私钥的基接口 |
PublicKey | 关于用于非对称算法的公钥的基接口 |
RSAPrivateKey | 用于利用模/指数形式的 RSA 算法签名数据 |
RSAPrivateCrtKey | 用于利用中国余数定理形式的 RSA 算法签名数据 |
RSAPublicKey | 用于验证利用 RSA 算法签名数据时的签名 |
DSAKey | 关于 DSA 算法私钥和公钥实现的基接口 |
DSAPrivateKey | 用于利用 DSA 算法签名数据 |
DSAPublicKey | 用于验证利用 DSA 算法的签名 |
KeyBuilder | 生成一个密钥对象的 Factory 类 |
MessageDigest | 关于 hash 算法的抽象基类 |
Signature | 关于签名算法的抽象基类 |
RandomData | 关于随机数生成的抽象基类 |
CryptoException | 代表一个密码技术相关的例外 |
类或接口 | 描述 |
---|---|
Cipher | 提供关于加密和解密的密码技术 cipher 的功能。类 cipher 是一个抽象基类 |
KeyEncryption | 能使密钥的实现访问加密的密钥数据 |
javacardx.crypto 包
类或接口 | 描述 |
---|---|
Cipher | 提供关于加密和解密的密码技术 cipher 的功能。类 cipher 是一个抽象基类 |
KeyEncryption | 能使密钥的实现访问加密的密钥数据 |
下面将一些应用进行举例:
计算消息摘要
Public static MessageDigest GetInstance(byte algorithm,boolean externalAccess);
#第一个参数可以为ALG_SHA、ALG_MD5,和 ALG_RIPEMD160
#第二个参数设置的是Applet计算的MD结果可否被外部访问
这里以SHA-1为例,对三个字节数组m1、m2、m3计算摘要:
Public class myApp extends Applet
{
Private MessageDigest sha;
Public MyApplet()
{
sha = MessageDigest.getInstance(MessageDigest.ALG_SHA,false);
//把字节数组 m1 中的整个数据馈入消息摘要计算
sha.update(m1,(short)0,(short)(m1.length));
//再从字节数组 m2 中位移 0 起馈入 8 字节数据
sha.update(m2,(short)0,(short)8);
//将字节数组 m3 中的全部数据作为最后一批发送给消息摘要计算,并将 hash 值保存于字节数组 digest 中位移 0 开始的地方
sha.doFinal(m3,(short)0,(short)(m3.length),digest,(short)0);
}
}
产生密钥
类 KeyBuilder 定义了一个供你选择的选择参数集,使你选择密钥的类型和密钥的长度。例如,为了建 立一个长度为 64 字节(64*8 = 512 位)的 RSA 私钥,你调用 buildKey 方法如下:
RSAPrivateKey rsa_private_key;
rsa_private_key = (RSAPrivateKey)KeyBuilder.buildkey(KeyBuilder.TYPE_RSA_PRIVATE, KeyBuilder.LENGTH_RSA_512,false);#这里进行了强制类型转换是便于调用RSAPrivateKey接口中的方法setModulus和setExponent
注意,这里生成的密钥对象未被初始化,还需要设置模数Modulus(r)和指数Exponent(e)。
计算和验证签名
签名就是生成摘要和产生密钥并进行加密的复合过程。所以我们要先构造签名实例:
Signature signature;
Signature = Signature.GetInstance(Signature.ALG_DSA_SHA,false);
#第一个参数支持主流的签名算法
#第二个参数设置的是Applet计算的Signature结果可否被外部访问
因为签名要使用一个密钥,我们需要初始化这个 Signature 对象。为此,我们需要调用两个 init 方法
之一:
public void init(Key theKey,byte theMode);
public void init(Key theKey,byte theMode,byte[] bArray,short bOff,short bLen);
#第二个init方法环允许你在字节数组bArray中规定算法初始化参数。如初始向量IV
在一个非对称算法中,签名和验证不是使用同一个密钥。所以,你应在第二个参数 theMode 中指
出如何使用密钥。有两种模式,如类 Signature 中所定义:
- MODE_SIGN--指出签名模式
- MODE_VERIFY--指出验证模式
下面的代码计算来自数组 s1, s2,和 s3 的数据的签名:
Public class myApp extends Applet
{
Private Signature signature;
Public MyApplet()
{
Signature = Signature.GetInstance(Signature.ALG_DSA_SHA,false);
Signnature.init(Key theKey,byte theMode);
//或Signnature.init(Key theKey,byte theMode,byte[] bArray,short bOff,short bLen);
//输入字节数组 s1 中的数据
Signature.update(s1,(short)0,(short)(s1.length));
//输入字节数组 s2 中的数据
Signature.update(s2,(short)0,(short)(s2.length));
//作为最后一批数据输入字节数组 s3 中的数据并生成签名,放到数组 sig_buffer 中,从位移 0 起
Signature.sign(s3,(short)0,(short)(s3.length),sig_buffer,(short)0);
}
}
下面的例子表明如何验证上一例子中计算出来的签名:
Public class myApp extends Applet
{
Private Signature signature;
Public MyApplet()
{
Signature = Signature.GetInstance(Signature.ALG_DSA_SHA,false);
Signnature.init(Key theKey,byte theMode);
//或Signnature.init(Key theKey,byte theMode,byte[] bArray,short bOff,short bLen);
//输入字节数组 s1 中的数据
Signature.update(s1,(short)0,(short)(s1.length));
//输入字节数组 s2 中的数据
Signature.update(s2,(short)0,(short)(s2.length));
//作为最后一批数据输入字节数组 s3 中的数据并生成签名,放到数组 sig_buffer 中,从位移 0 起
Signature.sign(s3,(short)0,(short)(s3.length),sig_buffer,(short)0);
//输入数组 s3 中的最后一批数据并用计算出来的签名验证放在 sig_buffer 中的签名
If(Signature.verify(s2,(short)0,(short)(s2.length),sig_buffer,sig_offset,sig_length) != true){ISOException.throwIt(SW_WRONG_SIGNATURE);}
}
}
数据加密和解密
下面的例子以 CBC 模式 DES 算法建立一个 Cipher 对象。输入数据无 padding。该 Cipher 对象以一个
用于加密的 DES 密钥初始化:
Public class myApp extends Applet
{
Private Cipher cipher;
Public MyApplet()
{
Cipher = Cipher.getInstance(Cipher.ALG_DES_CBC_NO_PAD,false);
Cipher.init(des_key,Cipher.MODE_ENCRYPT);
//或Signnature.init(Key theKey,byte theMode,byte[] bArray,short bOff,short bLen);
//输入字节数组 s1 中的数据
Signature.update(s1,(short)0,(short)(s1.length));
//输入字节数组 s2 中的数据
Signature.update(s2,(short)0,(short)(s2.length));
//作为最后一批数据输入字节数组 s3 中的数据并生成签名,放到数组 sig_buffer 中,从位移 0 起
Signature.sign(s3,(short)0,(short)(s3.length),sig_buffer,(short)0);
//输入数组 s3 中的最后一批数据并用计算出来的签名验证放在 sig_buffer 中的签名
If(Signature.verify(s2,(short)0,(short)(s2.length),sig_buffer,sig_offset,sig_length) != true){ISOException.throwIt(SW_WRONG_SIGNATURE);}
}
}
接着,为了加密数据,利用 update 方法和 doFinal 方法:
public short update(byte[] inBuf,short inOffset,short inLength,byte[] outBuff,short outOffset);
public short doFinal(byte[] inBuf,short inOffset,short inLength,byte[] outBuff,short outOffset);
随机数据的生成
随机数是密码技术过程( procedures)经常需要的。为了建立一个随机数发生器,你要调用类javacard.security.randomData中的方法getInstance并指出一种算法。算法选择参数可为RandomData.ALG_PSEDO_RANDOM,它表示伪随机数生成算法实用程序;或者为RandomData.ALG_SECURE_RANDOM,它指的是密码技术上安全性很强的随机数生成算法。
为了获得一个随机数,调用generatedata方法如下:
RandomData random_data = randomdata.getInstance(RandomData.ALG_SECURE_RANDOM);
//种子提供于字节数组 seed 中
random_data.setSeed(seed,seed_offset,seed_length);
//把一个随机数写入字节数组 random_num 中
random_data.generatedata(random_num, random_num_offset, random_num_ length);