Java Card开发指南(二)

标签(空格分隔): 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);

你可能感兴趣的:(Java Card开发指南(二))