回顾本系列的 第一篇文章,它介绍了移动银行 MIDlet 应用程序,并解释了 Kerberos 是如何满足这种应用程序的安全要求的。文章还描述了 Kerberos 用来提供安全性的数据格式。
本系列的 第二篇 文章展示了如何在 J2ME 中生成 ASN.1 数据类型。介绍了如何用 Bouncy Castle 加密库进行 DES 加密,并用用户的密码生成 Kerberos 密钥。最后将这些内容放到一起并生成一个 Kerberos 票据请求。
在本系列文章中开发的 Kerberos 客户不要求某个特定的 Kerberos 服务器,它可以使用所有 KDC 实现。参考资料部分包含了一些可以被 Kerberos 客户机所使用的 KDC 服务器的链接。
不管所选的是什么 KDC 服务器,必须告诉服务器移动银行 MIDlet 的用户在对
TGT的请求中不需要发送预认证数据(
padata,
本系列第一篇文章的图
2中显示的
KDC-REQ结构的第三个字段)。
根据 Kerberos 规范,发送
padata字段是可以选择的。因此,KDC 服务器通常允许配置特定的用户,使得对于所配置的用户不需要
padata字段就可以接受
TGT请求。为了尽量减少 Kerberos 客户机上的负荷,必须告诉 KDC 服务器接受电子银行移动用户的不带
padata的
TGT请求。
在这个例子中,我使用了 Microsoft 的 KDC 服务器以试验基于 J2ME 的移动银行应用程序。在本文
源代码下载中的 readme.txt 文件包含了如何设置 KDC 服务器、以及如何告诉它接受不带
padata字段的
TGT请求的指导。(在我的“用单点登录简化企业 Java 认证”一文中,我使用了同一个 KDC 服务器展示单点登录。有关链接请参阅
参考资料。)
设置了 KDC 服务器后,就向它发送
TGT请求。看一下
清单
1中的
getTicketResponse()方法。它与
本系列第二篇文章中的清单
12中的
getTicketResponse()方法是相同的,只有一处不同:这个方法现在包括向 KDC
服务器发送
TGT请求的 J2ME 代码。在
清单
1中标出了新的代码,所以您可以观察在
清单
12中没有的新增代码。
在
清单
1的
NEW CODE部分中,我以一个现有的
DatagramConnection
对象(
dc)为基础创建了一个新的 Datagram 对象(
dg)。注意在本文的最后一节中,移动银行
MIDlet 创建了我在这里用来创建
Datagram对象的
dc对象。
创建了
dg对象后,
getTicketResponse()方法调用了它的
send()方法,向 KDC 服务器发送票据请求。
在向服务器发送了
TGT请求之后,
清单
1的
getTicketResponse()方法接收服务器的
TGT响应。收到响应后,它将响应返回给调用应用程序。
清单 1. getTicketResponse() 方法
public byte[] getTicketResponse( )
{
byte ticketRequest[];
byte msg_type[];
byte pvno[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
1, getIntegerBytes(5));
msg_type = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
2, getIntegerBytes(10));
byte kdc_options[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
0, getBitStringBytes(new byte[5]));
byte generalStringSequence[] = getSequenceBytes (
getGeneralStringBytes (userName));
byte name_string[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
1, generalStringSequence);
byte name_type[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
0, getIntegerBytes(ASN1DataTypes.NT_PRINCIPAL));
byte principalNameSequence [] = getSequenceBytes(
concatenateBytes (name_type, name_string));
byte cname[] = getTagAndLengthBytes (ASN1DataTypes.CONTEXT_SPECIFIC,
1, principalNameSequence);
byte realm[] = getTagAndLengthBytes (ASN1DataTypes.CONTEXT_SPECIFIC,
2, getGeneralStringBytes (realmName));
byte sgeneralStringSequence[] =
concatenateBytes(getGeneralStringBytes(kdcServiceName),
getGeneralStringBytes (realmName));
byte sname_string[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
1, getSequenceBytes(sgeneralStringSequence));
byte sname_type[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
0, getIntegerBytes(ASN1DataTypes.NT_UNKNOWN));
byte sprincipalNameSequence [] = getSequenceBytes
(concatenateBytes (sname_type, sname_string));
byte sname[] = getTagAndLengthBytes (ASN1DataTypes.CONTEXT_SPECIFIC,
3, sprincipalNameSequence);
byte till[] = getTagAndLengthBytes (
ASN1DataTypes.CONTEXT_SPECIFIC,
5,
getGeneralizedTimeBytes (
new String("19700101000000Z").getBytes()));
byte nonce[] = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
7,
getIntegerBytes (getRandomNumber()));
byte etype[] = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
8,
getSequenceBytes(getIntegerBytes(3)));
byte req_body[] = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
4,
getSequenceBytes(
concatenateBytes(
kdc_options,
concatenateBytes(
cname,
concatenateBytes(
realm,
concatenateBytes(
sname,
concatenateBytes(
till,
concatenateBytes
(nonce, etype)
)
)
)
)
)
)
);
ticketRequest = getTagAndLengthBytes(
ASN1DataTypes.APPLICATION_TYPE,
10,
getSequenceBytes(
concatenateBytes(
pvno,
concatenateBytes
(msg_type, req_body)
)
)
);
/****** NEW CODE BEGINS ******/
try {
Datagram dg = dc.newDatagram(ticketRequest, ticketRequest.length);
dc.send(dg);
} catch (IllegalArgumentException il) {
il.printStackTrace();
}
catch (Exception io) {
io.printStackTrace();
}
byte ticketResponse[] = null;
try
{
Datagram dg = dc.newDatagram(700);
dc.receive(dg);
if (dg.getLength() > 0) {
ticketResponse = new byte[dg.getLength()];
System.arraycopy(dg.getData(), 0, ticketResponse, 0, dg.getLength());
} else
return null;
} catch (IOException ie){
ie.printStackTrace();
}
/****** NEW CODE ENDS ******/
return ticketResponse;
}//getTicketResponse
|
既然已经收到了来自
KDC的
TGT响应,现在该对响应进行处理以便从响应中提取
票据和
会话密钥。
自然,响应处理包括一些低层 ASN.1 处理(就像在本系列第二篇文章中生成票据请求时遇到的低层 ASN.1 生成方法一样)。所以在展示如何使用低层处理方法从票据响应中提取
票据和
会话密钥之前,我将实现并解释一些低层 ASN.1 处理方法以及一些低层加密支持方法。
像以前一样,低层 ASN1 处理方法放在
ASN1DataTypes类中。下面的方法在本文的
源代码下载中的 ASN1DataTypes.java 文件中:
- isSequence()
- getIntegerValue()
- isASN1Structure()
- getNumberOfLengthBytes()
- getLength()
- getASN1Structure()
- getContents()
下面是上面列出的每一个低层 ASN.1 处理方法的说明。
清单 2中显示的
isSequence()方法取单个
字节作为参数,并检查这个
字节是否是一个 ASN.1
SEQUENCE字节。如果
字节值表示一个
SEQUENCE,那么它就返回 true,否则它返回 false。
清单 2. isSequence() 方法
public boolean isSequence(byte tagByte)
{
if (tagByte == (byte)0x30)
return true;
else
return false;
}//isSequence
|
清单 3中显示的
getIntegerValue()方法只取一个输入参数,它是表示一个 ASN.1
INTEGER数据类型的内容的
字节数组。它将输入
字节数组转换为 J2ME
int数据类型,并返回 J2ME
int。在从 ASN.1
INTEGER中提取了内容字节,并且希望知道它所表示的是什么
integer值时就需要这个方法。还要用这个方法将长度字节转换为 J2ME
int。
注意,
getIntegerValue()方法设计为只处理正的
integer值。
ASN.1 以最高有效位优先(most-significant-byte-first)的序列存储一个正的
INTEGER。例如,用 ASN.1 表示的十进制
511就是
0x01 0xFF。可以写出十进制值的完整位表示(对于
511,它是
1 11111111),然后对每一个字节写出
十六进制值(对于
511,它是
0x01, 0xFF),最后以最高有效位优先的顺序写出
十六进制值。
另一方面,在 J2ME 中一个
int总是四字节长,并且最低有效
字节占据了最右边的位置。在正
integer值中空出的位置上填入零。例如,对于
511,J2ME
int的写法是
0x00 0x00 0x01 0xFF。
这意味着在将 ASN.1
INTEGER转换为一个 J2ME
int时,必须将输入数组的每一个
字节正确地放到输出 J2ME
int中的相应位置上。
例如,如果输入字节数组包含两个字节的数据
(0x01, 0xFF),那么必须像下面这样将这些字节放到输出
int中:
- 必须在输出
int的最左边或者最高有效位置写入0x00。 - 类似地,必须在与输出
int的最高有效字节相邻的位置上写入0x00。 - 输入数组的第一个字节
(0x01)放入输出int中与最低有效位置相邻的位置。 - 输出数组的第二个字节
(0xFF)放到输出int的最低有效或者最右边的位置。
getIntegerValue()方法中的
for循环计算每一个
字节的正确位置,再将这个
字节拷贝到其相应的位置上。
还要注意因为 J2ME
int总是有四个字节,
getIntegerValue()
方法只能处理最多四
字节 integer值。能力有限的、基于 J2ME 的 Kerberos 客户不需要处理更大的值。
清单 3. getIntegerValue() 方法
public int getIntegerValue(byte[] intValueAsBytes)
{
int intValue = 0;
int i = intValueAsBytes.length;
for (int y = 0; y < i; y++)
intValue |= ((int)intValueAsBytes[y] & 0xff) << ((i-(y+1)) * 8);
return intValue;
}//getIntegerValue()
|
清单 4中显示的
isASN1Structure()方法分析一个输入字节是否表示具有特定标签号的特定类型的
ASN.1 结构(即,
特定于上下文的 (context specific)、应用程序级 (application level) 或者通用类型 (universal
type))的标签字节(第一个字节)。
这个方法取三个参数。第一个参数(
tagByte)是要分析的输入
字节。第二和第三个参数
(
tagType和
tagNumber)分别表示所要查找的标签类型和标签号。
为了检查
tagByte是否具有所需要的标签号的标签类型,
isASN1Structure()
方法首先用
tagType和
tagNumber参数构建一个新的临时标签字节(
tempTagByte)。然后比较
tempTagByte与
tagByte。如果它们是相同的,那么方法就返回 true,如果不相同它就返回
false。
清单 4. isASN1Structure() 方法
public boolean isASN1Structure (byte tagByte, int tagType, int tagNumber)
{
byte tempTagByte = (byte) (tagType + tagNumber);
if (tagByte == tempTagByte)
return true;
else
return false;
}//isASN1Structure
|
清单 5显示的
getNumberOfLengthBytes()方法取一个参数(
firstLengthByte)。
firstLengthByte参数是 ASN.1 结构的第一个长度字节。
getNumberOfLengthBytes()方法处理第一个长度字节,以计算 ASN.1 结构中长度字节的字节数。这是一个工具方法,
ASN1DataTypes类中的其他方法在需要知道一个 ASN.1 结构的长度字节的字节数时就使用它。
清单 5中的
getNumberOfLengthBytes()方法的实现策略如下:
- 检查
firstLengthByte的最高有效位(第 8 位)是否为零。 清单 5中的if ( (firstLengthByte)& (1<<8)==0)这一行完成这一任务。
- 如果最高有效位为零,那么长度字节就遵循
单字节长度表示法。在 本系列的第 1 部分我们说过有两种长度表示法 ――单字节和多字节。在单字节长度表示法中总是有一个长度字节。因此,如果最高有效位为零,那么只需返回 1 作为长度字节的字节数。
- 如果
firstLengthByte的最高有效位是 1,这意味着长度字节遵循多字节长度表示法。在这时, 清单 5中的else块取得控制。
在
多字节长度格式中,
firstLengthByte的最高有效位指定后面有多少长度字节。例如,如果
firstLengthByte的值是
1000 0010,那么最左边的 1(最高有效位)说明后面的长度字节使用
多字节长度表示法。其他 7 位(
000 0010)说明还有两个长度字节。因此,在这里
getNumberOfLengthBytes()方法应当返回 3(
firstLengthBytes加上另外两个长度字节)。
清单 5中
else块的第一行(
firstLengthByte &= (byte)0x7f;)删除
firstLengthByte的最高有效位。
else块中的第二行(
return (int)firstLengthByte + 1;)将
firstLengthByte强制转换为
integer,在得到的
integer值中加 1,并返回这个
integer。
清单 5. getNumberOfLengthBytes() 方法
public int getNumberOfLengthBytes (byte firstLengthByte) {
if ( (firstLengthByte & 1<<8) == 0 )
return 1;
else {
firstLengthByte &= (byte)0x7f;
return (int)firstLengthByte + 1;
}
}//getNumberOfLengthBytes
|
这个方法的目的是检查一个特定的 AS1 结构有多少个字节。处理应用程序通常有一个由多个 ASN.1 结构构成的嵌入层次所组成的字节数组。
getLength()方法计算特定结构中的字节数。
这个方法取两个参数。第一个参数(
ASN1Structure)是一个字节数组,它应当包含至少一个完整的 ASN.1
结构,这个结构本身包含标签字节、长度字节和内容字节。第二个参数(
offset)是一个在
ASN1Structure字节数组中的偏移值。这个参数指定在
ASN1Structure字节数组中包含的 ASN.1 结构的开始位置。
getLength()方法返回一个等于从
offset字节处开始的 ASN.1
结构中的字节总数。
看一下
清单 6,它显示了
getLength()方法的一个实现:
- 第一步是向
getNumberOfLengthBytes()方法传 ASN.1 结构的第二个字节。这个 ASN.1 结构从offset字节开始,所以可以预计 offset 字节实际上就是标签字节。因为所有 Kerberos 结构只包含一个标签字节,所以第二个字节(在 offset 字节后面的那个字节)是第一个长度字节。第一个长度字节说明长度字节的总字节数,getNumberOfLengthBytes()方法返回长度字节数。int numberOfLengthBytes = getNumberOfLengthBytes(ASN1Structure [offset+1]);这一行执行这项任务 。
- 如果
getNumberOfLengthBytes()方法返回一个大于 1 的值,那么必须处理多字节长度表示法。在这种情况下,将从offset + 2( 让过标签字节和第一个长度字节 ) 开始的长度字节读到一个名为lengthValueAsBytes的变量中。然后用getIntegerValue()方法将长度值从 ASN.1 字节转换为 J2MEint。最后,将结果加 1(以补偿不包含在长度值中的标签字节),再将长度值返回给调用应用程序。
- 如果
getNumberOfLengthBytes()方法返回 1,则要处理单字节长度表示法。在这种情况下,只要将第一个(也是惟一的一个)长度字节转换为 J2MEint,对它加 1(以补偿不包含在长度值中的标签字节),并将得到的值返回给调用应用程序。
清单 6 getLength() 方法
public int getLength (byte[] ASN1Structure, int offset) {
int structureLength;
int numberOfLengthBytes = getNumberOfLengthBytes(ASN1Structure[offset + 1]);
byte[] lengthValueAsBytes = new byte[numberOfLengthBytes - 1];
if (numberOfLengthBytes > 1)
{
for (int i=0; i < numberOfLengthBytes-1 ; i++)
lengthValueAsBytes[i]= ASN1Structure [offset + i + 2];
structureLength = getIntegerValue(lengthValueAsBytes);
}
else
structureLength = (int) (ASN1Structure[offset+1]);
structureLength += numberOfLengthBytes + 1;
return structureLength;
}//getLength()
|
清单 7中的
getASN1Structure()方法从一个包含一系列 ASN.1 结构的字节数组中找出并提取特定 ASN.1 结构。这个方法有三个参数。第一个参数(
inputByteArray)是输入字节数组,需要从这个字节数组中找到所需要的 ASN.1 结构。第二个参数是一个
int,它指定要查找的标签的类型。第三个参数指定标签号。
看一下
清单 7中的
getASN1Strucute()方法实现。它将 offset 值初始化为零并进入
do-while循环。
在
do-while循环中,将字节数组中第一个字节读入名为
tagByte的字节中。然后用
isASN1Structure()方法检查输入数组的第一个字节是否是所需要的 ASN.1 结构。
如果第一个字节代表所需要的结构,那么就用
getLength()方法找到要返回的所需数量的字节。然后将所需要的字节拷贝到名为
outputBytes的字节数组中、并将这些字节返回到调用应用程序。
如果第一个字节不代表所需要的结构,那么就要跳到下一个结构。为此,将 offset 值设置为下一个结构的开始位置。
do-while循环在下一个循环中检查下一个结构,并以此方式检查整个输入数组。如果没有找到所需要的结构,那么
do-while循环就会退出并返回 null。
清单 7. getASN1Structure() 方法
public byte[] getASN1Structure (byte[] inputByteArray, int tagType, int tagNumber)
{
byte tagByte;
int offset = 0;
do {
tagByte = inputByteArray[offset];
if (isASN1Structure(tagByte, tagType, tagNumber)) {
int lengthOfStructure = getLength(inputByteArray, offset);
byte[] outputBytes = new byte[lengthOfStructure];
for (int x =0; x < lengthOfStructure; x++)
outputBytes[x]= inputByteArray [x + offset];
return outputBytes;
}
else
offset += getLength(inputByteArray, offset);
} while (offset < inputByteArray.length);
return null;
}//getASN1Structure
|
清单 8中显示的
getContents()方法取
ASN1Structure字节数组并返回一个包含
ASN1Structure内容的字节数组。
getContents()方法假定所提供的字节数组是一个有效的 ASN1 结构,所以它忽略结构中表示标签字节的第一个字节。它将第二个字节(即第一个长度字节)传递给
getNumberOfLengthBytes()方法,这个方法返回 ASN1Structure
输入字节数组中的长度字节数。
然后它构建一个名为
contentBytes的新字节数组,并将 ASN1Structure 的内容拷贝到
contentBytes数组中(去掉标签和长度字节)。
清单 8. getContents() 方法
public byte[] getContents (byte[] ASN1Structure)
{
int numberOfLengthBytes = getNumberOfLengthBytes(ASN1Structure [1]);
byte[] contentBytes = new byte[ASN1Structure.length - (numberOfLengthBytes + 1)];
for (int x =0; x < contentBytes.length; x++)
contentBytes[x]= ASN1Structure [x + numberOfLengthBytes + 1];
return contentBytes;
}//getContents
|
除了前面描述的低层处理方法,还需要一些低层加密支持方法以处理一个票据响应。这就是为什么在解释票据响应的处理之前,我要讨论以下这些为 Kerberos 客户机提供加密支持的方法:
- encrypt()
- decrypt()
- getMD5DigestValue()
- decryptAndVerifyDigest()
这些方法是
KerberosClient类的组成部分,可以在 KerberosClient.java 文件中找到它们,本文的
源代码下载中可以找到这个文件。下面是对这几个方法的说明:
清单 9中显示的
encrypt()方法处理低层加密并加密一个输入字节数组。
这个方法取三个字节数组参数,即一个用于加密的密码(
keyBytes)、要加密的纯文本数据(
plainData)和一个初始向量或者 IV(
ivBytes)。它用密钥和 IV 加密纯文本数据,并返回加密后的纯文本数据。
注意在
清单 9中的
encrypt()方法中,我使用了
DESEngine、
CBCBlockCipher、
KeyParameter和
ParametersWithIV类以加密这个纯文本数据。这些类属于在讨论
第二篇文章中的清单
11中的
getFinalKey()方法时介绍的 Bouncy Castle 加密库。回头看一下并比较
清单
9中的
encrypt()方法与第二篇文章中
清单
11中的
getFinalKey()方法。注意以下几点:
-
getFinalKey()方法使用一个包装了初始向量的ParametersWithIV类。Kerberos 规范要求在生成加密密钥时,用加密密钥作为 IV。因此,方法中的加密算法用加密密钥作为 IV。因此,getFinalKey()方法中的算法使用这个加密密钥作为一个 IV。
另一方面,
encrypt()方法设计为可以使用或者不使用 IV 值。更高级别的应用程序逻辑使用 encrypt() 方法时可以提供一个 IV 值或者忽略它。如果应用程序要求一个没有 IV 值的数据加密,那么它将传递 null 作为第三个参数。
如果有 IV,那么encrypt()方法用一个 ParametersWithIV 实例初始化 CBCBlockCipher。注意在 清单 9的if (ivBytes != null)块中,我传递了一个 ParametersWithIV 实例作为给cbcCipher.init()方法调用的第二个参数。
如果第三个参数为 null,那么encrypt()方法就用一个 KeyParameter 对象实始化 CBCBlockCipher 对象。注意在 清单 9中的 else 块中,我传递了一个KeyParameter实例作为cbcCipher.init()方法调用的第二个参数。 -
第二篇文章的清单 11中的
getFinalKey()方法返回输入数据最后一块的处理结果。另一方面,encrypt()方法将纯文本处理的每一步的结果串接在一起、并返回串接在一起的所有处理过的(加密的)字节。
清单 9. encrypt() 方法
public byte[] encrypt(byte[] keyBytes, byte[] plainData, byte[] ivBytes)
{
byte[] encryptedData = new byte[plainData.length];
CBCBlockCipher cbcCipher = new CBCBlockCipher(new DESEngine());
KeyParameter keyParameter = new KeyParameter(keyBytes);
if (ivBytes != null) {
ParametersWithIV kpWithIV = new ParametersWithIV (keyParameter, ivBytes);
cbcCipher.init(true, kpWithIV);
} else
cbcCipher.init(true, keyParameter);
int offset = 0;
int processedBytesLength = 0;
while (offset < encryptedData.length) {
try {
processedBytesLength = cbcCipher.processBlock( plainData,
offset,
encryptedData,
offset
);
offset += processedBytesLength;
} catch (Exception e) {
e.printStackTrace();
}//catch
}
return encryptedData;
}
|
(
清单 10显示的)
decrypt()方法与
encrypt()方法的工作方式完全相同,只不过解密时,
cbcCipher.init()方法的第一个参数是
false(加密时它是
true)。
清单 10. decrypt() 方法
public byte[] decrypt(byte[] keyBytes, byte[] encryptedData, byte[] ivBytes)
{
byte[] plainData = new byte[encryptedData.length];
CBCBlockCipher cbcCipher = new CBCBlockCipher(new DESEngine());
KeyParameter keyParameter = new KeyParameter(keyBytes);
if (ivBytes != null) {
ParametersWithIV kpWithIV = new ParametersWithIV (keyParameter, ivBytes);
cbcCipher.init(false, kpWithIV);
} else
cbcCipher.init(false, keyParameter);
int offset = 0;
int processedBytesLength = 0;
while (offset < encryptedData.length) {
try {
processedBytesLength = cbcCipher.processBlock( encryptedData,
offset,
plainData,
offset
);
offset += processedBytesLength;
} catch (Exception e) {
e.printStackTrace();
}//catch
}
return plainData;
}//decrypt()
|
清单 11中显示的
getMD5DigestValue()方法取一个输入数据字节数组,并返回一个用输入数据计算的
MD5 摘要值。
Bouncy Castle 加密库在一个名为
MD5Digest的类中包含 MD5 摘要支持。使用
MD5Digest
类进行摘要计算需要四步:
- 首先,实例化一个
MD5Digest对象。 - 然后,调用
MD5Digest对象的update()方法,在调用同时传递要摘要的数据。 - 然后,实例化一个用来包含 MD5 摘要值输出字节数组。
- 最后,调用
MD5Digest对象的doFinal()方法,同时传递输出字节数组。doFinal()方法计算摘要值并将它放到输出字节数组中。
清单 11. getMD5DigestValue() 方法
public byte[] getMD5DigestValue (byte[] data)
{
MD5Digest digest = new MD5Digest();
digest.update (data, 0, data.length);
byte digestValue[] = new byte[digest.getDigestSize()];
digest.doFinal(digestValue, 0);
return digestValue;
}
|
回想一下在
第一篇文章图 3 和清单 2 中,KDC 服务器的票据响应包含一个名为
enc-part的字段,它包装了一个名为
EncryptedData
的加密的数据结构。就像在第一篇文章的
图 3的说明中描述的那样,
EncryptedData结构由三个字段组成。
清单 12中显示的
decryptAndVerifyDigest()方法取一个
EncryptedData结构(实质上就是
enc-part字段的内容)和一个解密密钥作为参数,并返回
EncryptedData结构的纯文本表示。加密过程步骤如下:
第
1 步:注意在
第一篇文章的清单
2中,
EncryptedData结构实际上是
etype、kvno和
cipher字段的一个
SEQUENCE。因此,第一步是检查输入字节数组是否是一个
SEQUENCE。为此调用
isSequence()方法。
第
2 步:如果输入字节数组是一个
SEQUENCE,那么需要解析这个
SEQUENCE
并提取出其内容。调用
getContents()方法以提取出
SEQUENCE内容。
在
SEQUENCE内容中,感兴趣的是第一个字段(
etype,特定于上下文的标签号
0),它表明了加密类型。使用了
getASN1Structure()方法调用以从
SEQUENCE
内容中提取
etype字段。
第
3 步:调用
getContents()方法以提取
etype字段的内容,这是一个
ASN.1
INTEGER。再次调用
getContents()方法以提取
INTEGER的内容。然后将
INTEGER内容传递给
getIntegerValue()
方法,这个方法返回 J2ME
int格式的
INETGER内容。将 J2ME int
值存储为一个名为
eTypeValue的变量。
eTypeValueint 指定在生成
EncryptedData结构时使用的加密类型。
第
4 步:回想一下 Kerberos 客户机只支持一种加密类型 ―― DES-CBC ―― 它的标识号为 3。因此,我检查
eTypeValue是否为 3。如果它不是 3(即服务器使用了非 DES-CBC 的加密算法), 那么 Kerberos
客户机就不能处理这一过程。
第
5 步:下一步是从
EncryptedData
SEQUENCE内容中提取第三个字段(
cipher,特定于上下文的标签号 2)。调用
getASN1Structure()方法以完成这项任务。
第
6 步:下一步,调用
getContents()方法提取 cipher 字段的内容。cipher 字段的内容是一个
ASN.1
OCTET STRING。还需要再调用
getContents()方法,以提取
OCTET STRING的内容 。
第
7 步:
OCTET STRING内容是加密的,因此需要用前面讨论的
decrypt()
方法解密。
第
8 步:解密的数据字节数组由三部分组成。第一部分由前八位组成,它包含一个称为
confounder的随机数。confounder 字节没有意义,它们只是帮助增加黑客的攻击的难度。
解密的数据的第 9 到第 24 个字节构成了第二部分,它包含一个 16 字节的 MD5 摘要值。这个摘要值是对整个解密的数据 ―― 其中 16 个摘要字节(第二部分)是用零填充的 ―― 计算的。
第三部分是要得到实际纯文本数据。
因为第八步进行完整性检查,所以必须将解密的数据的第 9 到第 24 个字节用零填充,对整个数据计算一个 MD5 摘要值,并将摘要值与第二部分(第 9 到第 24 个字节)进行匹配。如果两个摘要值匹配,那么消息的完整性就得到验证。
第 9 步:如果通过了完整性检查,那么就返回解密的数据的第三部分(第 25 个字节到结束)。
清单 12. decryptAndVerifyDigest() 方法
public byte[] decryptAndVerifyDigest (byte[] encryptedData, byte[] decryptionKey)
{
/****** Step 1: ******/
if (isSequence(encryptedData[0])) {
/****** Step 2: ******/
byte[] eType = getASN1Structure(getContents(encryptedData),
CONTEXT_SPECIFIC, 0);
if (eType != null) {
/****** Step 3: ******/
int eTypeValue = getIntegerValue(getContents(getContents(eType)));
/****** Step 4: ******/
if ( eTypeValue == 3) {
/****** Step 5: ******/
byte[] cipher = getASN1Structure(getContents(encryptedData),
CONTEXT_SPECIFIC, 2);
/****** Step 6: ******/
byte[] cipherText = getContents(getContents(cipher));
if (cipherText != null) {
/****** Step 7: ******/
byte[] plainData = decrypt(decryptionKey,
cipherText, null);
/****** Step 8: ******/
int data_offset = 24;
byte[] cipherCksum = new byte [16];
for (int i=8; i < data_offset; i++)
cipherCksum[i-8] = plainData[i];
for (int j=8; j < data_offset; j++)
plainData[j] = (byte) 0x00;
byte[] digestBytes = getMD5DigestValue(plainData);
for (int x =0; x < cipherCksum.length; x++) {
if (!(cipherCksum[x] == digestBytes[x]))
return null;
}
byte[] decryptedAndVerifiedData =
new byte[plainData.length - data_offset];
/****** Step 9: ******/
for (int i=0; i < decryptedAndVerifiedData.length; i++)
decryptedAndVerifiedData[i] = plainData[i+data_offset];
return decryptedAndVerifiedData;
} else
return null;
} else
return null;
} else
return null;
} else
return null;
}//decryptAndVerifyDigest
|
我们已经讨论了低层 ASN.1 处理以及低层加密支持方法,现在可以讨论如何用这些方法处理在前面用
清单 1中的
getTicketResponse()方法提取的票据响应了。
看一下
清单 13中显示的
getTicketAndKey()方法(它属于
KerberosClient类)。这个方法取票据响应字节数组和一个解密密钥字节数组作为参数。这个方法从票据响应中提取票据和密钥。
getTicketAndKey()方法返回一个名为
TicketAndKey的类的实例(这是一个要从票据响应中提取的密钥和票据的包装器)。我在
清单 14中已经展示了
TicketAndKey类。这个类只有四个方法:两个子 setter 方法和两个 getter 方法。
setKey()和
getKey()方法分别设置和获得密钥字节。
setTicket()和
getTicket()方法分别设置和获得票据字节。
现在看一看在
清单 13的
getTicketAndKey()方法中所发生的过程。回想在对
第一篇文章的图 4 和清单 2的讨论中,介绍了 Kerberos 密钥和票据是如何存储在票据响应中的。从票据响应中提取密钥是一个漫长的过程,包括以下步骤:
1.
首先,检查
ticketResponse字节数组是否真的包含了票据响应。为此,我使用了
isASN1Structure()方法。如果
isASN1Structure()方法返回 false,那么它表明输入
ticketResponse字节数组不是有效的票据响应。在这种情况下,不进行任何进行一步的处理并返回 null。
注意在
清单 13中,我调用了两次
isASN1Structure()方法。第一调用
isASN1Structure()方法时用“11”作为第三个参数的值,而第二次调用
isASN1Structure()方法时,用“13”作为第三个参数的值。这是因为“11”是
TGT响应的特定于应用程序的标签号(本系列的
第一篇文章的清单 2),而“13”是服务票据响应的特定于应用程序的标签号(本系列的
第一篇文章的清单 4)。如果
ticketResponse字节数组是一个
TGT响应或者服务票据响应,那么这两次方法调用之一会返回 true,就可以进行进一步的处理。如果这两个方法调用都不返回 true,那么表明
ticketResponse字节数组不是一个票据响应,就要返回 null 并且不做任何进一步的处理。
2.
第二步是提取票据响应结构的内容。为此,我使用了
getContents()方法调用。
3.
票据响应的内容应当是一个 ASN.1
SEQUENCE,可以调用
isSequence()
方法对此进行检查。
4.
接下来,我调用
getContents()方法提取
SEQUENCE的内容。
5.
SEQUENCE的内容是票据响应的七个结构(如图 3 和
第一篇文章的清单
2所示)。在这七个结构之外,只需要两个:ticket 和 enc-part。
因此,第五步是从
SEQUENCE内容中提取 ticket 字段(调用
getASN1Structure()
方法),提取 ticket 字段(调用
getContents()方法)的内容,并将内容存储到在前面创建的
TicketAndKey对象中。注意 ticket 字段是特定于上下文的标签号 5,而这个字段的内容是实际的票据,它以一个应用程序级别的标签号 1 开始,如
第一篇文章的清单 3 和图 9所示。
6.
下面,必须从在第 4 步中得到的
SEQUENCE内容中提取密钥。这个键在在
SEQUENCE
内容的 enc-part 字段中。因此,在第 6 步,我调用
getASN1Structure()方法从
SEQUENCE
内容中捕捉
enc-part字段。
7.
得到了
enc-part字段后,就要调用
getContents()方法得到其内容
。
enc-part字段的内容构成了一个
EncryptedData结构。
8.
可以向
decryptAndVerifyDigest()方法传递
EncryptedData
结构,这个方法解密
EncryptedData结构并对
EncryptedData进行一个摘要验证检查。
9.
如果成功进行了解密和摘要验证过程,那么
decryptAndVerifyDigest()方法就从已解密的密文数据中提取了
ASN.1 数据。ASN.1 数据应当符合我在
第一篇文章的图 4中展示的结构。注意所需要的密钥是
第一篇文章的图 4中显示的结构中的第一个字段。一个应用程序级别的标签号“25”或者“26”包装纯文本数据。这个结构称为
EncKDCRepPart(加密的
KDC回复部分)。
这样,下一步就是检查由
decryptAndVerifyDigest()方法返回的数据是否是一个应用程序级别的标签号 25 或者 26。
10.
下一步是提取
EncKDCRepPart结构的内容 。调用
getContents()
方法提取所需要的内容。
EncKDCRepPart内容是一个
SEQUENCE,所以还必须提取
SEQUENCE内容 。再一次调用
getContents()方法以提取
SEQUENCE
内容。
11.
SEQUENCE内容的第一个字段(称为 key,具有上下文特定的标签号 0)包含 key 字段。可以调用
getASN1Structure()方法以从
SEQUENCE内容中提取第一个字段。
12.
下面,提取 key 字段的内容。调用
getConents()方法可以返回这些内容。
key 字段的内容构成另一个名为
EncryptionKey的 ASN.1 结构,它是一个两字段 ―― 即
keytype和
keyvalue―― 的
SEQUENCE。再一次调用
getContents()方法提取
SEQUENCE的内容。
13.
所需要的会话密钥在
SEQUENCE内容的第二个字段中(
keyvalue)。因此,必须调用
getASN1Structure()方法以从
SEQUENCE内容中提取
keyvalue
字段(特定于上下文的标签号 1)。
14.
现在已经有了
keyvalue字段。必须调用
getContents()方法提取它的内容。
keyvalue
内容是一个
OCTET STRING,所以必须再次调用
getContents()方法以提取
OCTET STRING的内容,它就是所要找的那个密钥。
所以只要将这个密钥字节包装在
KeyAndTicket对象中(通过调用其
setKey()
方法)并返回
KeyAndTicket对象。
清单 13. getTicketAndKey() 方法
public TicketAndKey getTicketAndKey( byte[] ticketResponse, byte[] decryptionKey)
{
TicketAndKey ticketAndKey = new TicketAndKey();
int offset = 0;
/***** Step 1:*****/
if ((isASN1Structure(ticketResponse[0], APPLICATION_TYPE, 11)) ||
(isASN1Structure(ticketResponse[0], APPLICATION_TYPE, 13))) {
try {
/***** Step 2:*****/
byte[] kdc_rep_sequence = getContents(ticketResponse);
/***** Step 3:*****/
if (isSequence(kdc_rep_sequence[0])) {
/***** Step 4:*****/
byte[] kdc_rep_sequenceContent = getContents(kdc_rep_sequence);
/***** Step 5:*****/
byte[] ticket = getContents(getASN1Structure(kdc_rep_sequenceContent,
CONTEXT_SPECIFIC, 5));
ticketAndKey.setTicket(ticket);
/***** Step 6:*****/
byte[] enc_part = getASN1Structure(kdc_rep_sequenceContent,
CONTEXT_SPECIFIC, 6);
if (enc_part!=null) {
/***** Step 7:*****/
byte[] enc_data_sequence = getContents(enc_part);
/***** Step 8:*****/
byte[] plainText = decryptAndVerifyDigest(enc_data_sequence,
decryptionKey);
if (plainText != null){
/***** Step 9:*****/
if ((isASN1Structure(plainText[0],APPLICATION_TYPE, 25)) ||
(isASN1Structure(plainText[0], APPLICATION_TYPE, 26))) {
/***** Step 10:*****/
byte[] enc_rep_part_content
= getContents(getContents(plainText));
/***** Step 11:*****/
byte[] enc_key_structure
= getASN1Structure(enc_rep_part_content,
CONTEXT_SPECIFIC, 0);
/***** Step 12:*****/
byte[] enc_key_sequence
= getContents(getContents(enc_key_structure));
/***** Step 13:*****/
byte[] enc_key_val = getASN1Structure(enc_key_sequence,
CONTEXT_SPECIFIC, 1);
/***** Step 14:*****/
byte[] enc_key = getContents(getContents(enc_key_val));
ticketAndKey.setKey(enc_key);
return ticketAndKey;
} else
return null;
} else
return null;
} else
return null;
} else
return null;
} catch (Exception e) {
e.printStackTrace();
}
return null;
} else
return null;
}//getTicketAndKey()
|
清单 14. TicketAndKey 类
public class TicketAndKey
{
private byte[] key;
private byte[] ticket;
public void setKey(byte[] key)
{
this.key = key;
}//setKey()
public byte[] getKey()
{
return key;
}//getKey
public void setTicket(byte[] ticket)
{
this.ticket = ticket;
}//setTicket
public byte[] getTicket()
{
return ticket;
}//getTicket
} |
已经处理了
TGT响应并提取了
TGT和会话密钥。现在可以使用这个
TGT和会话密钥向 KDC 服务器请求一个服务票据。对服务票据的请求类似于对我在清单 1 中生成的对
TGT的请求。我在
TGT请求中省略的可选
padata字段在服务票据请求中不再是可选的了。因此,需要在服务票据请求中加上
padata字段。
padata字段是包含两个字段 ――
padata-type和
padata-value
―― 的
SEQUENCE。
padata-value字段带有几种类型的数据,因此相应的
padata-type字段指定了
padata-value字段所带的数据的类型。
在
本系列的第一篇文章的图 5中我介绍了服务票据中的
padata字段的结构。在那里说过服务票据请求中的
padata
字段包装了一个认证头(一个
KRB_AP_REQ结构),它又包装了
TGT以及其他数据。
所以,在可以开始生成票据请求之前,必须生成一个认证头。下面是分析了生成认证头的过程。
我在
KerberosClient类中加入了以下方法以生成一个认证头:
- getMD5DigestValue()
- getChceksumBytes()
- authorDigestAndEncrypt()
- getAuthenticationHeader()
这四个方法都是 helper 方法。第五个方法(
getAuthenticationHeader())使用 helper 方法并生成认证头。
清单
15显示的
authorDigestAndEncrypt()方法取一个纯文本数据字节数组和一个加密密钥。这个方法对纯文本数据计算一个摘要值、加密纯文本数据、并返回一个
EncryptedData结构,这个结构与我作为输入传递给
清单
12的
decryptAndVerifyDigest()方法的结构完全匹配。
可以说
清单
15的
authorDigestAndEncrypt()方法与前面讨论的
decryptAndVerifyDigest()
方法正好相反。
authorDigestAndEncrypt()方法取
decryptAndVerifyDigest()
方法返回的纯文本数据作为输入。与此类似,
authorDigestAndEncrypt()方法返回的
EncryptedData结构就是我作为输入传递给
decryptAndVerifyDigest()方法的结构。
authorDigestAndEncrypt() 方法实现了以下策略:
- 首先,生成八个随机字节,它们构成了 confounder。
- 然后,声明一个名为
zeroedChecksum的字节数组,它有十六个字节并初始化为零。这个有十六个零的数组作为一个全为零的摘要值。 - 第三,用其他的字节填入输入数据字节数组,以使数组中的字节数成为八的倍感数。编写了一个名为
getPaddedData()的方法(如 清单 16所示),它取一个字节数组并在填充后返回这个数组。下面,链接(第 1 步得到的)confounder、(第 2 步得到的)全为零的摘要以及填充后的纯文本字节数组。 - 第四步是对第 3 步串接的字节数组计算 MD5 摘要值。
- 第五步是将摘要字节放到它们相应的位置上。第 5 的结果与第 3 步一样,只不过全为零的摘要现在换成了真正的摘要值。
- 现在调用
encrypt()方法以加密第 5 步得到的字节数组。 - 然后,生成
etype字段(特定于上下文的标签号 0)。 - 然后,调用
getOctetStringBytes()方法将第 6 步得到的加密字节数组包装到OCTET STRING中。然后将OCTET STRING包装到cipher字段中(一个特定于上下文的标签号 2)。 - 最后,链接
etype和cipher字段,将这个字符串包装到一个SEQUENCE中,并返回这个SEQUENCE。
清单 15. authorDigestAndEncrypt() 方法
public byte[] authorDigestAndEncrypt(byte[] key, byte[] data)
{
/****** Step 1: ******/
byte[] conFounder = concatenateBytes (getRandomNumber(), getRandomNumber());
/****** Step 2: ******/
byte[] zeroedChecksum = new byte[16];
/****** Step 3: ******/
byte[] paddedDataBytes = concatenateBytes (conFounder,
concatenateBytes(zeroedChecksum,
getPaddedData(data)
)
);
/****** Step 4: ******/
byte[] checksumBytes = getMD5DigestValue(paddedDataBytes);
/****** Step 5: ******/
for (int i=8; i < 24; i++)
paddedDataBytes[i] = checksumBytes[i-8];
/****** Step 6: ******/
byte[] encryptedData = encrypt(key, paddedDataBytes, null);
/****** Step 7: ******/
byte[] etype = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
0, getIntegerBytes(3)
);
/****** Step 8: ******/
byte[] cipher = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
2, getOctetStringBytes(encryptedData)
);
/****** Step 9: ******/
byte[] ASN1_encryptedData = getSequenceBytes (
concatenateBytes(etype,cipher)
);
return ASN1_encryptedData;
}//authorDigestAndEncrypt
|
清单 16. getPaddedData() 方法
public byte[] getPaddedData(byte[] data) {
int numberToPad = 8 - ( data.length % 8 );
if (numberToPad > 0 && numberToPad != 8)
{
byte[] bytesPad = new byte[numberToPad];
for (int x = 0; x < numberToPad; x++)
bytesPad [x] = (byte)numberToPad;
return concatenateBytes(data, bytesPad);
}
else
return data;
}//getPaddedData()
|
getChecksumBytes()方法生成一个称为
Checksum的结构,如
清单
17所示。Checksum 结构包含两个字段:
cksumtype和
checksum。
清单 17. Checksum 结构
Checksum ::= SEQUENCE {
cksumtype[0] INTEGER,
checksum[1] OCTET STRING
}+
|
有两个地方需要 Checksum 结构 ―― 第一个是生成服务票据响应时,然后是生成安全上下文建立请求时。Checksum 结构的作用在这两种情况下是不同的,需要在生成服务票据和上下文建立请求时说明 (elaborate)。
清单
18所示的
getChecksumBytes()方法取两个字节数组参数。第一个参数带有
checksum
字段,而第二个参数带有
cksumtype字段。
getChecksumBytes()方法将
cksumtype字段包装到一个特定于上下文的标签号
0(它表示
cksumtype字段,如
清单
17所示),而将
checksum字段包装到一个特定于上下文的标签号 1(它表示 checksum
字段,同样如
清单
17所示)。然后它链接这两个字段,将这个数组包装到一个
SEQUENCE中,并返回这个
SEQUENCE。
清单 18. getChecksumBytes() 方法
public byte[] getChecksumBytes(byte[] cksumData, byte[] cksumType){
byte[] cksumBytes = getTagAndLengthBytes (
ASN1DataTypes.CONTEXT_SPECIFIC, 3,
getSequenceBytes (
concatenateBytes (
getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
0,
cksumType
),
getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC, 1,
getOctetStringBytes(cksumData)
)
)
)
);
return cksumBytes;
}//getChecksumBytes()
|
在
本系列的第一篇文章中的“服务票据请求”一节中,介绍过
KRB-AP-REQ结构(也称为认证头)包装了 Kerberos 票据。此外,认证头还包装了 authenticator 字段,它表明客户机是否掌握了
会话或者
子会话 密钥。
如
第一篇文章的图
5所示,认证头由五个字段组成,即
pvno、msg-type、ap-options、ticket和
authenticator。
清单
19的
getAuthenticationHeader()方法逐一生成这五个字段,然后以正确的顺序将各个字段串接起来以形成一个完整的认证头。
清单 19. getAuthenticationHeader() 方法
public byte[] getAuthenticationHeader( byte[] ticketContent,
String clientRealm,
String clientName,
byte[] checksumBytes,
byte[] encryptionKey,
int sequenceNumber
)
{
byte[] authenticator = null;
byte[] vno = getTagAndLengthBytes (
ASN1DataTypes.CONTEXT_SPECIFIC,
0, getIntegerBytes(5)
);
byte[] ap_req_msg_type = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
1, getIntegerBytes(14)
);
byte[] ap_options = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
2, getBitStringBytes(new byte[5])
);
byte[] ticket = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
3, ticketContent
);
byte[] realmName = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
1, getGeneralStringBytes(clientRealm)
);
byte[] generalStringSequence = getSequenceBytes(
getGeneralStringBytes (clientName)
);
byte[] name_string = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
1, generalStringSequence
);
byte[] name_type = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
0, getIntegerBytes(ASN1DataTypes.NT_PRINCIPAL)
);
byte[] clientNameSequence = getSequenceBytes(
concatenateBytes (name_type, name_string)
);
byte[] cName = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
2, clientNameSequence);
byte[] cusec = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
4, getIntegerBytes(0)
);
byte[] ctime = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
5, getGeneralizedTimeBytes (
getUTCTimeString(System.currentTimeMillis()).getBytes()
)
);
if (sequenceNumber !=0 ) {
byte[] etype = getTagAndLengthBytes (
ASN1DataTypes.CONTEXT_SPECIFIC,
0, getIntegerBytes(3)
);
byte[] eKey = getTagAndLengthBytes (
ASN1DataTypes.CONTEXT_SPECIFIC,
1, getOctetStringBytes(encryptionKey)
);
byte[] subKey_sequence = getSequenceBytes (concatenateBytes(etype, eKey));
byte[] subKey = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
6, subKey_sequence
);
byte[] sequenceNumberBytes = {
(byte)0xff,
(byte)0xff,
(byte)0xff,
(byte)0xff
};
sequenceNumberBytes[3] = (byte)sequenceNumber;
byte[] seqNumber = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
7, getIntegerBytes(sequenceNumberBytes)
);
authenticator = getTagAndLengthBytes(ASN1DataTypes.APPLICATION_TYPE,
2, getSequenceBytes(
concatenateBytes(vno,
concatenateBytes(realmName,
concatenateBytes(cName,
concatenateBytes(checksumBytes,
concatenateBytes(cusec,
concatenateBytes(ctime,
concatenateBytes(subKey,seqNumber)
)
)
)
)
)
)
)
);
} else {
authenticator = getTagAndLengthBytes(ASN1DataTypes.APPLICATION_TYPE,
2, getSequenceBytes(
concatenateBytes(vno,
concatenateBytes(realmName,
concatenateBytes(cName,
concatenateBytes(checksumBytes,
concatenateBytes(cusec,ctime)
)
)
)
)
)
);
}//if (sequenceNumber !=null)
byte[] enc_authenticator = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
4, authorDigestAndEncrypt(encryptionKey, authenticator)
);
byte[] ap_req = getTagAndLengthBytes (
ASN1DataTypes.APPLICATION_TYPE,
14, getSequenceBytes(
concatenateBytes (vno,
concatenateBytes(ap_req_msg_type,
concatenateBytes(ap_options,
concatenateBytes(ticket, enc_authenticator)
)
)
)
)
);
return ap_req;
}//getAuthenticationHeader
|
getAuthenticationHeader()方法有几个输入参数:
- 名为
ticketContent的字节数组,它包含由getAuthenticationHeader()方法包装到认证头的 Kerberos 票据(TGT)。 - 名为
clientRealm的字符串类型参数,它指定(生成这个请求的)Kerberos 客户机所注册的域(realm )的名字。 - 名为
clientName的字符串类型参数指定生成这个请求的 Kerberos 客户机的名字。 -
checksumBytes字节数组携带一个 Checksum 结构以及getChecksumBytes()方法。 -
encryptionKey字节数组携带用于生成认证头的加密部分的加密密钥。 - 名为
sequenceNumber的参数是一个integer值,它标识发送者的请求号。
在 第一篇文章的图 5介绍过,认证头包含以下字段:
- pvno
- msg-type
- ap-options
- ticket
- authenticator
现在让我们看看
清单 19中的
getAuthenticationHeader()方法实现是如何生成认证头的各个字段的(
KRB-AP-REQ结构):
首先要生成
pvno字段,它有特定于上下文的标签号 0,并包装一个值为 5 的 ASN1
INTEGER。调用
getTagAndLengthBytes()方法执行这项任务。我将
pvno字段存储 在一个名为
vno的字节数组中。
类似地,两次调用
getTagAndLengthBytes()方法生成
msg-type(特定于上下文的标签号 1)和
ap-options字段(特定于上下文的标签号 2)。
下一行(
byte[] ticket = getTagAndLengthBytes(ASN1DataTypes.Context_Specific,
3, ticketContent))将票据结构包装到特定于上下文的标签号 3 中,这是认证头的第四个字段。
然后,必须生成认证头的第五个字段(名为
authenticator,它有特定于上下文的标签号 4)。authenticator 字段是一个
EncryptedData结构。authenticator 字段的纯文本格式是一个
Authenticator结构。因此,首先生成纯文本格式的完整
Authenticator结构,将这个纯文本
Authenticator传递给
authorDigestAndEncrypt()方法,这个方法返回
Authenticator
的完整
EncryptedData表示。
注意在
第一篇文章中的清单
3 和图 5中,纯文本格式的
Authenticator结构由以下字段组成(省略最后一个字段,它是不需要的):
- authenticator-vno
- creal
- cname
- cksum
- cusec
- ctime
- subkey
- seq-number
在解释 第一篇文章的图 5时,我已经介绍了每一个字段的意义。
authenticator-vno字段与
pvno字段完全相同(本节前面讨论了
vno字节数组,它包含特定于上下文的标签号 0 且带值为 5 的
INTEGER)。所以我重用了在
authenticator_vno字段中使用的同一个字节数组。
现在该生成
crealm字段了,它类似于我在
第二篇
文章“生成请求正文”一节中介绍的 realm 字段。同样,在那一节也介绍了
PrincipalName类型的
cname字段。在这里我就不介绍
crealm和
cname字段的生成细节了。
下一项任务是生成
cksum字段,它是
Checksum类型。服务票据请求中的
cksum 字段的作用是加密结合 authenticator 与一些应用程序数据。注意以下三点:
- authenticator 结构包含
cksum字段。 -
cksum字段包含一些应用程序数据的加密哈希值。 - 整个 authenticator 结构(包括
cksum字段)是用一个密钥加密的。
只要在 authenticator 中的
cksum字段与对应用程序数据的加密 checksum 相匹配,就证明生成 authenticator 和应用程序数据的客户机拥有密钥。
调用
getAuthenticationHeader()方法的应用程序(通过调用
getChecksumBytes()
方法)生成
Checksum结构,并将
Checksum字节数组作为
checksumBytes
参数的值传递给
getAuthenticationHeader()方法。
结果,
checksumBytes参数中就有了
Checksum结构。只需要将
checksumBytes
包装到特定于上下文的标签号 3 中(这是 authenticator 结构中
cksum字段的标签号)。
现在生成
cusec字段,它表示客户机时间的微秒部分。这个字段的取值范围为 0 到 999999。这意味着可以在这个字段提供的最大值为 999999 微秒。不过,MIDP 不包含任何可以提供比一毫秒更精确的时间值的方法。因此,不能指定客户机的微秒部分。只是对这个字段传递一个零值。
在 Authenticator 结构中,还要生成两个字段 ――
subkey和
seq-number。在为票据请求而生成的
Authenticator中不一定要包含这两个字段,但是后面在用同一个
getAuthenticationHeader()
方法生成上下文建立请求时需要它们。
现在,只需知道只要检查
sequenceNumber参数是否为零。对于服务票据请求它为零,对于上下文建立请求它为非零。
如果
sequenceNumber参数为非零,那么就生成
subkey和
seq-number字段,然后链接
authenticator-vno、 realm、cname、cksum、cusec、ctime、subkey
和
seq-number字段以构成一个字节数组,将这个字节数组包装到一个
SEQUENCE
中,然后将
SEQUENCE包装到
Authenticator中(应用程序级别标签号
2)。
如果
sequenceNumber参数为零,那么可以忽略
subkey和
seq-number字段,链接
authenticator-vno、crealm、cname、cksum、cusec
和
ctime字段以构成串接的字节数组,将这个字节数组包装到一个
SEQUENCE中,然后将这个
SEQUENCE包装到
Authenticator中(应用程序级别标签号 2)。
下面,需要取完整的
Authenticator结构并将它传递
authorDigestAndEncrypt()
方法,这个方法返回纯文本 Authenticator 的完整
EncryptedData表示。
下一个任务是串接认证头或者
KRB-AP-REQ结构的五个字段(
pvno、msg-type、ap-options、ticket、authenticator)为一个字节数组,将这个字节数组包装为一个
SEQUECNE,最后将这个
SEQUENCE
包装到应用程序级别的标签号 14。
现在已经完成认证头,可以将它返回给给调用应用程序了。
我讨论了生成服务票据请求需要的所有低层方法。将使用
清单
1中请求
TGT时所使用的同一个
getTicketResponse()方法生成服务票据请求,只需要对
清单
1稍加修改以使它可以同时用于
TGT和服务票据请求。让我们看一下这个过程。
看一下
清单
20,其中可以看到修改过的清单 1 中的
getTicketRespone()方法。与
清单
1相比,修改过的版本增加了一些代码:
清单 20. getTicketResponse() 方法
public byte[] getTicketResponse( String userName,
String serverName,
String realmName,
byte[] kerberosTicket,
byte[] key
)
{
byte ticketRequest[];
byte msg_type[];
byte pvno[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
1, getIntegerBytes(5));
msg_type = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
2, getIntegerBytes(10));
byte kdc_options[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
0, getBitStringBytes(new byte[5]));
byte generalStringSequence[] = getSequenceBytes (
getGeneralStringBytes (userName));
byte name_string[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
1, generalStringSequence);
byte name_type[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
0, getIntegerBytes(ASN1DataTypes.NT_PRINCIPAL));
byte principalNameSequence [] = getSequenceBytes(
concatenateBytes (name_type, name_string));
byte cname[] = getTagAndLengthBytes (ASN1DataTypes.CONTEXT_SPECIFIC,
1, principalNameSequence);
byte realm[] = getTagAndLengthBytes (ASN1DataTypes.CONTEXT_SPECIFIC,
2, getGeneralStringBytes (realmName));
byte sgeneralStringSequence[] = concatenateBytes(getGeneralStringBytes(serverName),
getGeneralStringBytes (realmName));
byte sname_string[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
1, getSequenceBytes(sgeneralStringSequence));
byte sname_type[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
0, getIntegerBytes(ASN1DataTypes.NT_UNKNOWN));
byte sprincipalNameSequence [] = getSequenceBytes(
concatenateBytes (sname_type, sname_string)
);
byte sname[] = getTagAndLengthBytes (ASN1DataTypes.CONTEXT_SPECIFIC,
3, sprincipalNameSequence);
byte till[] = getTagAndLengthBytes (
ASN1DataTypes.CONTEXT_SPECIFIC,
5,
getGeneralizedTimeBytes (
new String("19700101000000Z").getBytes())
);
byte nonce[] = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
7,
getIntegerBytes (getRandomNumber())
);
byte etype[] = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
8,
getSequenceBytes(getIntegerBytes(3))
);
byte req_body[] = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
4,
getSequenceBytes(
concatenateBytes(kdc_options,
concatenateBytes(cname,
concatenateBytes(realm,
concatenateBytes(sname,
concatenateBytes(till,
concatenateBytes(nonce, etype)
)
)
)
)
)
)
);
if (kerberosTicket != null) {
msg_type = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
2, getIntegerBytes(12));
sname_string = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
1, getSequenceBytes(getGeneralStringBytes(serverName)));
sname_type = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
0, getIntegerBytes(ASN1DataTypes.NT_UNKNOWN));
sprincipalNameSequence = getSequenceBytes(
concatenateBytes (sname_type, sname_string)
);
sname = getTagAndLengthBytes (
ASN1DataTypes.CONTEXT_SPECIFIC,
3, sprincipalNameSequence
);
byte[] req_body_sequence = getSequenceBytes(
concatenateBytes(kdc_options,
concatenateBytes(realm,
concatenateBytes(sname,
concatenateBytes(till,
concatenateBytes(nonce, etype)
)
)
)
)
);
req_body = getTagAndLengthBytes (
ASN1DataTypes.CONTEXT_SPECIFIC,
4, req_body_sequence
);
byte[] cksum = getChecksumBytes(
getMD5DigestValue(req_body_sequence),
getIntegerBytes(7)
);
byte[] authenticationHeader = getAuthenticationHeader(
kerberosTicket,
realmName,
userName,
cksum,
key,
0
);
byte[] padata_sequence = getSequenceBytes(concatenateBytes(
getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
1,getIntegerBytes(1)),
getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
2, getOctetStringBytes(authenticationHeader)
)
)
);
byte[] padata_sequences = getSequenceBytes(padata_sequence);
byte[] padata = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
3, padata_sequences
);
ticketRequest = getTagAndLengthBytes(
ASN1DataTypes.APPLICATION_TYPE,
12, getSequenceBytes(
concatenateBytes(pvno,
concatenateBytes(msg_type,
concatenateBytes(padata, req_body)
)
)
)
);
} else {
ticketRequest = getTagAndLengthBytes(
ASN1DataTypes.APPLICATION_TYPE,
10, getSequenceBytes(
concatenateBytes(pvno,
concatenateBytes(msg_type, req_body)
)
)
);
}
try {
Datagram dg = dc.newDatagram(ticketRequest, ticketRequest.length);
dc.send(dg);
} catch (IllegalArgumentException il) {
il.printStackTrace();
}
catch (Exception io) {
io.printStackTrace();
}
byte ticketResponse[] = null;
try {
Datagram dg = dc.newDatagram(700);
dc.receive(dg);
if (dg.getLength() > 0) {
ticketResponse = new byte[dg.getLength()];
System.arraycopy(dg.getData(), 0, ticketResponse, 0, dg.getLength());
} else
return null;
} catch (IOException ie){
ie.printStackTrace();
}
return ticketResponse;
}//getTicketResponse
|
清单
20中显示的新的
getTicketResponse()方法有五个参数:
userName、serverName、realmName、kerberosTicket
和
key。要请求一个服务票据,需要传递
kerberosTicket字节数组的
TGT。另一方面,在请求
TGT时,不必传递一个票据,因此对于
kerberosTicket字节数组传递“null”。
TGT请求与服务票据请求的主要区别是
padata字段。在本系列
第一篇文章
中的“请求服务票据”一节中讨论服务票据请求的
padata字段时已经介绍过。
在
getTicketResponse()的最后,我加入了一个
if (kerberosTicket!=null)
块。只有在
kerberosTicket参数不为 null 时才进入这个块(在所有
TGT
请求中它都是 null)。
在
if (kerberosTicket!=null)块中,我生成了
padata
字段。正如
第一篇文章的图
5中描述的,这个
padata字段包装一个可由
getAuthenticationHeader()
方法生成的认证头。
在
getAuthenticationHeader()方法中还可了解到,为了生成一个认证头,需要一个可由
getChecksumBytes()
方法生成的
Checksum结构。
现在,回想一下在讨论
getChecksumBytes()方法时说过,为了生成一个
Checksum
结构,需要用于
cksumtype和
checksum字段的数据。
因此,生成一个认证头需要三步:
- 生成
cksumtype和checksum字段的数据。如果是服务票据请求,那么checksum字段的数据只是对包含服务票据请求的req-body字段的所有子字段的SEQUENCE计算的 MD5 摘要值(注意在 第一篇文章的图 5,req-body是服务票据请求的第四个字段,就在服务票据请求的第三个字段padata字段后面)。cksumtype字段的数据是integer7 的 ASN1 表示。这个值指定 checksum 的类型。 - 调用
getChecksumBytes()方法并传递cksumtype和checksum字段的数据。getChecksumBytes()方法生成完整的Checksum结构。 - 调用
getAuthenticationHeader()方法,同时传递Checksum结构。getAuthenticationHeader()返回认证头。
生成了认证头后,必须将它包装到一个
padata字段中。为此,有几件事要做:
- 调用我在
第二篇文章的清单
5中描述的
getOctetStringBytes()方法将认证头包装到一个OCTET STRING中。 - 将这个
OCTET STRING包装到padata-value字段中(特定于上下文的标签号 2),调用getTagAndLengthBytes()方法以完成这项任务。 - 再次调用
getTagAndLengthBytes()方法生成对应于第 2 步生成的padata-value的padata-type字段。 - 现在,链接
padata-type和padata-value字段。 - 将第 4 步链接的字节数组放入一个
SEQUENCE中。这个SEQUENCE表示一个PADATA结构(如 第一篇文章的图 5 和清单 3所示)。 -
第一篇文章的图
5 和清单 3中显示的
padata字段是PADATA结构的一个SEQUENCE。这意味着一个padata字段可以包含几个PADATA结构。不过,只有一个PADATA结构要包装到padata字段中,这意味着只要将第 5 步得到的SEQUENCE包装到另一个外部或者更高层的SEQUENCE中。 - 第 6 步的更高层
SEQUENCE表示PADATA结构的SEQUENCE,现在可以将它包装到padata字段中(一个特定于上下文的标签号 3)。
在
清单
20的结尾处的
if (kerberosTicket!=null)块中可以找到
getTicketResponse()
方法中增加的所有新代码。
到此就结束了对于修改现有的
getTicketResponse()方法以使它可同时用于
TGT
和服务票据请求的讨论。
getTicketResponse()方法生成一个服务票据请求、将这个请求发送给
KDC、接收服务票据响应、并将响应返回给调用应用程序。
服务票据响应类似于
TGT响应。在
清单 13中的
getTicketAndKey()方法解析一个
TGT响应以提取
TGT和会话密钥。同一个方法也解析服务票据响应以从服务票据响应中提取服务票据和子会话密钥。所以,不必编写任何提取服务票据和子会话密钥的代码。
现在有了与电子银行的业务逻辑服务器建立安全通信上下文所需要的两项内容:子会话密钥和服务票据。这时 Kerberos 客户机必须生成针对电子银行的业务逻辑服务器的上下文建立请求。
参见
第一篇文章的图
7 和清单 5,它们描述了客户机为建立安全上下文而发送给电子银行服务器的消息。
清单
21中显示的
createKerberosSession()方法处理与电子银行的业务逻辑服务器建立安全通信上下文的所有方面(包括生成上下文建立请求、向服务器发送请求、从服务器中获得响应、解析响应以检查远程服务器是否同意上下文建立请求,并将这些工作的结果返回给调用应用程序)。
看一下
清单
21中的
createKerberosSession()方法,它有以下参数:
-
ticketContent字节数组带有准备用于建立安全上下文的服务票据。 -
clientRealm字符串包装了请求客户机所属的域realm的名字。 -
clientName字符串指定了请求客户机的名字。 -
sequenceNumber参数是一个表示这个消息序号(sequence number)的integer。 - encryptionKey:子会话密钥。
-
inStream和outStream是createKerberosSession()方法用来与电子银行的服务器通信的输入输出流。
正如在第一篇文章中介绍的,要使用 Java-GSS 实现电子银行的服务器端逻辑。GSS-Kerberos 机制规定服务票据包装在一个认证头中,而这个认证头本身又包装在
第一篇文章的图
7 和清单 5中显示的
InitialContextToken包装器中。
可以使用
清单
19的
getAuthenticationHeader()方法包装服务票据。回想一下在
清单
20的
getTicketResponse()方法中我使用了
getAuthenticationHeader()
方法包装一个
TGT。
为了生成认证头,需要一个
Checksum。回想在讨论
清单
19的
getAuthenticationHeader()方法时说过,
Checksum
的目的是加密绑定认证头与一些应用程序数据。但是,与票据请求认证头不一样,上下文建立认证头不带有应用程序数据。
GSS-Kerberos 机制出于不同的目的使用
Checksum结构。除了将认证头绑定到一些应用程序数据外,GSS-Kerberos 机制使用一个
Checksum结构用物理网络地址(即客户机可以用来与服务器进行安全通信的网络地址)绑定安全上下文。如果使用这种功能,那么只能从它所绑定的网络地址上使用安全上下文。
不过,我不作准备在这个示例移动银行应用程序中使用这种功能。这就是为什么我在
Checksum结构中指定安全上下文没有任何绑定的缘故。为此,我编写了一个名为
getNoNetworkBindings()的方法,如
清单
22所示。
getNoNetworkBindings()方法非常简单。它只是生成一个硬编码的字节数组,表明不需要任何网络绑定。然后它调用
getChecksumBytes()方法以将硬编码的数组包装到
cksum字段中。
得到了无网络绑定的
Checksum的字节数组后,可以将这个数组传递给
getAuthenticationHeader()
方法,这个方法返回完整的认证头。
生成了认证头后,
清单
21的
createKerberosSession()方法将认证头字节数组与一个名为
gssHeaderComponents
的硬编码的字节数组相链接。
gssHeaderComponents字节数组包含一个 GSS 头的字节表示,这个 GSS
头在上下文建立请求中将伴随一个认证头。
最后,将串接的 GSS 头和认证头包装到一个应用程序级别的标签号 0 中。GSS 要求所有上下文建立请求都包装到应用程序级别的标签号 0 中。
现在完成了上下文建立请求。下一项任务就是通过一个输出流(
outStream对象)发送这个请求。发送了请求后,监听并接收
inStream对象上的响应。
当
createKerberosSession()方法收到响应后,它就检查响应是确认创建一个新的上下文还是显示一个错误消息。要进行这种检查,必须知道消息开始标签字节后面的长度字节的字节数。
GSS头字节(紧接着长度字节)提供了答案。
不用解析响应以进行任何进一步的处理。只是要知道电子银行的服务器是创建了一个新会话还是拒绝会话。如果电子银行的服务器确认创建新会话,那么
createKerberosSession()方法就返回
true,如果不是,它就返回
false。
清单 21. createKerberosSession() 方法
public boolean createKerberosSession (
byte[] ticketContent,
String clientRealm,
String clientName,
int sequenceNumber,
byte[] encryptionKey,
DataInputStream inStream,
DataOutputStream outStream
)
{
byte[] cksum = getNoNetworkBindings();
if (sequenceNumber == 0)
sequenceNumber++;
byte[] authenticationHeader = getAuthenticationHeader(
ticketContent,
clientRealm,
clientName,
cksum,
encryptionKey,
sequenceNumber
);
byte[] gssHeaderComponents = {
(byte)0x6,
(byte)0x9,
(byte)0x2a,
(byte)0xffffff86,
(byte)0x48,
(byte)0xffffff86,
(byte)0xfffffff7,
(byte)0x12,
(byte)0x1,
(byte)0x2,
(byte)0x2,
(byte)0x1,
(byte)0x0
};
byte[] contextRequest = getTagAndLengthBytes(
ASN1DataTypes.APPLICATION_TYPE,
0, concatenateBytes (
gssHeaderComponents, authenticationHeader
)
);
try {
outStream.writeInt(contextRequest.length);
outStream.write(contextRequest );
outStream.flush();
byte[] ebankMessage = new byte[inStream.readInt()];
inStream.readFully(ebankMessage);
int respTokenNumber = getNumberOfLengthBytes (ebankMessage[1]);
respTokenNumber += 12;
byte KRB_AP_REP = (byte)0x02;
if (ebankMessage[respTokenNumber] == KRB_AP_REP){
return true;
} else
return false;
} catch (Exception io) {
io.printStackTrace();
}
return false;
}//createKerberosSession
|
清单 22. getNoNetworkBindings() 方法
public byte[] getNoNetworkBindings() {
byte[] bindingLength = { (byte)0x10, (byte)0x0, (byte)0x0, (byte)0x0};
byte[] bindingContent = new byte[16];
byte[] contextFlags_bytes = {
(byte)0x3e,
(byte)0x00,
(byte)0x00,
(byte)0x00
};
byte[] cksumBytes = concatenateBytes (
concatenateBytes(bindingLength,bindingContent),
contextFlags_bytes);
byte[] cksumType = {
(byte)0x2,
(byte)0x3,
(byte)0x0,
(byte)0x80,
(byte)0x3
};
byte[] cksum = getChecksumBytes(cksumBytes, cksumType);
return cksum;
}//getNoNetWorkBindings()
|
如果
createKerberosSession()方法返回
true,就知道成功地与远程
Kerberos 服务器建立了一个安全会话。就可以开始与 Kerveros 服务器交换消息了。
看一下
清单
23的
sendSecureMessage()方法。这个方法取一个纯文本消息、一个加密密钥、一个序号(它惟一地标识了发送的消息)和与服务器交换数据所用的输入输出流对象作为参数。
endSecureMessage()
方法生成一个安全消息、通过输出流将这个消息发送给服务器、监听服务器的响应,并返回服务器的响应。
发送给服务器的消息是用子会话密钥保护的。这意味着只有特定的接收者(拥有子会话密钥的电子银行业务逻辑服务器)可以解密并理解这个消息。而且,安全消息包含消息完整性数据,所以电子银行的服务器可以验证来自客户机的消息的完整性。
让我们看一下
sendSecureMessage()方法是如何用一个纯文本消息生成一个安全 GSS 消息的。
一个 GSS 安全消息采用 token(token 格式的字节数组)的形式。token 格式由以下部分组成:
- 一个 GSS 头,类似于我在讨论
createKerberosSession()方法时介绍的头。 - 一个八字节 token 头。在 GSS-Kerveros 规范中有几个不同类型的 token,每一种 token 类型都由一个惟一的头所标识。其中只有要在
sendSecureMessage()方法中生成的安全消息头是我们感兴趣的。一个安全消息 token 是由具有值0x02、0x01、0x00、0x00、0x00、0x00、0xff和0xff的头所标识的。 - 一个加密的序号,它有助于检测回复攻击。例如,如果有恶意的黑客想要重现(即重复)一个转账指令,他是无法生成加密形式的正确序号的(当然,除非他知道
子会话密钥)。 - 消息的加密摘要值。
- 加密后的消息。
将上面列出的五个字段以正确的顺序链接在一起,然后包装到一个 ASN.1 应用程序级别的标签号 0 中。这就构成了完整的 GSS-Kerberos 安全消息 token,如 图 1所示。
图 1.
为了生成如 图 1所示的完整安全 token,必须生成所有五个字段。
前两个字段没有动态内容,它们在所有安全消息中都是相同的,所以我在
清单
23中硬编码了它们的值。另外三个字段必须根据以下算法动态计算:
1.
在纯文本消息中添加额外的字节以使消息中的字节数是八的倍数。
2.
生成一个名为
confounder的八字节随机数。链接 confounder 与第 1 步中填充的消息。
3.
串接 token 头(
图
1中的第二个字段)和第 2 步的结果。然后对链接的结果计算 16 字节 MD5 摘要值。
4.
用
子会话密钥加密第 3 步得到的 16 字节摘要值。加密算法是使用零 IV 的 DES-CBC。加密的数据的最后八个字节(放弃前八个字节)构成了
图 1第四个字段(加密的摘要值)。
5.
现在必须生成一个加密的 8 字节序号(
图
1的第三个字段)。这个序号是用
子会话密钥和第 4 步使用 IV 的加密摘要值的后八个字节加密的。
6.
现在取第 2 步的结果(链接在一起的 confounder 和填充的消息)并用 DES-CBC 加密它。要进行这种加密,使用一个用
0xF0
对
子会话密钥的所有字节执行 OR 操作生成的密钥。这种加密得到的结果构成了
图
1的第五个字段,也就是加密的消息。
生成了各个字段后,将它们链接为一个字节数组,最后,调用
getTagAndLengthBytes()方法以在链接的字节数组前面附加一个应用程序级别的标签号 0。
可以观察
清单
23的
sendSecureMessage()方法中的这些步骤。生成了安全消息后,通过输出流将消息发送给服务器、监听服务器的响应并将响应返回给接收者。
清单 23. sendSecureMessage() 方法
public byte[] sendSecureMessage( String message, byte[] sub_sessionKey,
int seqNumber,
DataInputStream inStream,
DataOutputStream outStream
)
{
byte[] gssHeaderComponents = {
(byte)0x6,
(byte)0x9,
(byte)0x2a,
(byte)0x86,
(byte)0x48,
(byte)0x86,
(byte)0xf7,
(byte)0x12,
(byte)0x01,
(byte)0x02,
(byte)0x02
};
byte[] tokenHeader = {
(byte)0x02,
(byte)0x01,
(byte)0x00,
(byte)0x00,
(byte)0x00,
(byte)0x00,
(byte)0xff,
(byte)0xff
};
try {
/***** Step 1: *****/
byte[] paddedDataBytes = getPaddedData (message.getBytes());
/***** Step 2: *****/
byte[] confounder = concatenateBytes (getRandomNumber(),
getRandomNumber());
/***** Step 3: *****/
byte[] messageBytes = concatenateBytes(confounder, paddedDataBytes);
byte[] digestBytes = getMD5DigestValue(
concatenateBytes (tokenHeader,messageBytes));
CBCBlockCipher cipher = new CBCBlockCipher(new DESEngine());
KeyParameter kp = new KeyParameter(sub_sessionKey);
ParametersWithIV iv = new ParametersWithIV (kp, new byte[8]);
cipher.init(true, iv);
byte processedBlock[] = new byte[digestBytes.length];
byte message_cksum[] = new byte[8];
for(int x = 0; x < digestBytes.length/8; x ++) {
cipher.processBlock(digestBytes, x*8, processedBlock, x*8);
System.arraycopy(processedBlock, x*8, message_cksum, 0, 8);
iv = new ParametersWithIV (kp, message_cksum);
cipher.init (true, iv);
}
/***** Step 4: *****/
byte[] sequenceNumber = {
(byte)0xff,
(byte)0xff,
(byte)0xff,
(byte)0xff,
(byte)0x00,
(byte)0x00,
(byte)0x00,
(byte)0x00
};
sequenceNumber[0] = (byte)seqNumber;
/***** Step 5: *****/
byte[] encryptedSeqNumber = encrypt(sub_sessionKey, sequenceNumber,
message_cksum);
/***** Step 6: *****/
byte[] encryptedMessage = encrypt(getContextKey(sub_sessionKey),
messageBytes, new byte[8]);
byte[] messageToken = getTagAndLengthBytes (
ASN1DataTypes.APPLICATION_TYPE,
0,
concatenateBytes (
gssHeaderComponents, concatenateBytes (
tokenHeader, concatenateBytes (
encryptedSeqNumber, concatenateBytes (
message_cksum, encryptedMessage
)
)
)
)
);
/***** Step 7: *****/
outStream.writeInt(messageToken.length);
outStream.write(messageToken);
outStream.flush();
/***** Step 8: *****/
byte[] responseToken = new byte[inStream.readInt()];
inStream.readFully(responseToken);
return responseToken;
} catch(IOException ie){
ie.printStackTrace();
} catch(Exception e){
e.printStackTrace();
}
return null;
}//sendSecureMessage
public byte[] getContextKey(byte keyValue[])
{
for (int i =0; i < keyValue.length; i++)
keyValue[i] ^= 0xf0;
return keyValue;
}//getContextKey
|
就像生成并发送给服务器的消息一样,
sendSecureMessage()方法返回的服务器消息是安全的。它遵循
图
1所示的同样的 token 格式,这意味着只有拥有
子会话密钥的客户机才能解密这个消息。
我编写了一个名为
decodeSecureMessage()的方法(如
清单
24所示),它以一个安全消息和解密密钥为参数,解密这个消息并返回纯文本格式的消息。解码算法如下:
- 第一步是将消息的加密部分(图 24 所示的第五个字段)与 token 头分离。token 头的长度是固定的,所以只有长度字节的数目是随消息的总长度而变化的。因此,只要读取长度字节数并相应地将消息的加密部分拷贝到一个单独的字节数组中。
- 第二步是读取消息 checksum( 图 1的第四个字段)。
- 现在用解密密钥解密加密的消息。
- 然后,取 token 头( 图 1的第二个字段),将它与解密的消息链接,然后取链接的字节数组的 MD5 摘要值。
- 现在加密 MD5 摘要值。
- 需要比较第 2 步的八字节消息 checksum 与第 5 步的 MD5 摘要值的后八个字节。如果它们相匹配,那么完整性检查就得到验证。
- 经过验证后,删除 cofounder(解密的消息的前八个字节)并返回消息的其余部分(它就是所需要的纯文本消息)。
清单 24. decodeSecureMessage() 方法
public String decodeSecureMessage (byte[] message, byte[] decryptionKey){
int msg_tagAndHeaderLength = 36;
int msg_lengthBytes = getNumberOfLengthBytes (message[1]);
int encryptedMsg_offset = msg_tagAndHeaderLength + msg_lengthBytes;
byte[] encryptedMessage = new byte[message.length - encryptedMsg_offset];
System.arraycopy(message, encryptedMsg_offset,
encryptedMessage, 0,
encryptedMessage.length);
byte[] msg_checksum = new byte[8];
System.arraycopy(message, (encryptedMsg_offset-8),
msg_checksum, 0,
msg_checksum.length);
byte[] decryptedMsg = decrypt (decryptionKey, encryptedMessage, new byte[8]);
byte[] tokenHeader = {
(byte)0x2,
(byte)0x1,
(byte)0x0,
(byte)0x0,
(byte)0x0,
(byte)0x0,
(byte)0xff,
(byte)0xff
};
byte[] msg_digest = getMD5DigestValue (concatenateBytes(tokenHeader,decryptedMsg));
byte[] decMsg_checksum = new byte[8];
try {
CBCBlockCipher cipher = new CBCBlockCipher(new DESEngine());
KeyParameter kp = new KeyParameter(getContextKey(decryptionKey));
ParametersWithIV iv = new ParametersWithIV (kp, decMsg_checksum);
cipher.init(true, iv);
byte[] processedBlock = new byte[msg_digest.length];
for(int x = 0; x < msg_digest.length/8; x ++) {
cipher.processBlock(msg_digest, x*8, processedBlock, x*8);
System.arraycopy(processedBlock, x*8, decMsg_checksum, 0, 8);
iv = new ParametersWithIV (kp, decMsg_checksum);
cipher.init (true, iv);
}
} catch(java.lang.IllegalArgumentException il){
il.printStackTrace();
}
for (int x = 0; x < msg_checksum.length; x++) {
if (!(msg_checksum[x] == decMsg_checksum[x]))
return null;
}
return new String (decryptedMsg,
msg_checksum.length,
decryptedMsg.length - msg_checksum.length);
}//decodeSecureMessage()
public byte[] getContextKey(byte keyValue[])
{
for (int i =0; i < keyValue.length; i++)
keyValue[i] ^= 0xf0;
return keyValue;
}//getContextKey
|
已经完成了示例移动银行应用程序所需要的安全 Kerberos 通信的所有阶段。现在可以讨论移动银行 MIDlet 如何使用 Kerberos 客户机功能并与电子银行的服务器通信了。
清单 25显示了一个简单的 MIDlet,它模拟了示例移动银行应用程序。
清单 25. 一个示例移动银行 MIDlet
import java.io.*;
import java.util.*;
import javax.microedition.lcdui.*;
import javax.microedition.midlet.*;
import javax.microedition.io.*;
public class J2MEClientMIDlet extends MIDlet implements CommandListener, Runnable {
private Command OKCommand = null;
private Command exitCommand = null;
private Command sendMoneyCommand = null;
private Display display = null;
private Form transForm;
private Form transResForm;
private Form progressForm;
private TextField txt_userName;
private TextField txt_password;
private TextField txt_amount;
private TextField txt_sendTo;
private StringItem si_message;
private TextField txt_label;
private SocketConnection sc;
private DataInputStream is;
private DataOutputStream os;
private DatagramConnection dc;
private KerberosClient kc;
private TicketAndKey tk;
private String realmName = "EBANK.LOCAL";
private String kdcServerName = "krbtgt";
private String kdcAddress = "localhost";
private int kdcPort = 8080;
private String e_bankName = "ebankserver";
private String e_bankAddress = "localhost";
private int e_bankPort = 8000;
private int i =0;
private byte[] response;
public J2MEClientMIDlet() {
exitCommand = new Command("Exit", Command.EXIT, 0);
sendMoneyCommand = new Command("Pay", Command.SCREEN, 1);
OKCommand = new Command("Back", Command.EXIT, 2);
display = Display.getDisplay(this);
transactionForm();
}
public void startApp() {
Thread t = new Thread(this);
t.start();
}//startApp()
public void pauseApp() {
}//pauseApp()
public void destroyApp(boolean unconditional) {
}//destroyApp
public void commandAction(Command c, Displayable s) {
if (c == exitCommand) {
destroyApp(false);
notifyDestroyed();
} else if(c == sendMoneyCommand) {
sendMoney();
} else if (c == OKCommand) {
transactionForm();
} else if (c == exitCommand) {
destroyApp(true);
}
}//commandaction
public void sendMoney() {
System.out.println("MIDlet... SendMoney() Starts");
String userName = txt_userName.getString();
String password = txt_password.getString();
kc.setParameters(userName, password, realmName);
System.out.println("MIDlet... Getting TGT Ticket");
response = kc.getTicketResponse (
userName,
kdcServerName,
realmName,
null,
null
);
System.out.println ("MIDLet...Getting Session Key from TGT Response");
tk = new TicketAndKey();
tk = kc.getTicketAndKey(response, kc.getSecretKey());
System.out.println ("MIDLet...Getting Service Ticket (TGS)");
response = kc.getTicketResponse (
userName,
e_bankName,
realmName,
tk.getTicket(),
tk.getKey()
);
System.out.println ("MIDLet...Getting Sub-Session Key from TGS Response");
tk = kc.getTicketAndKey( response, tk.getKey());
i++;
System.out.println ("MIDLet...Establishing Secure context with E-Bank");
boolean isEstablished = kc.createKerberosSession (
tk.getTicket(),
realmName,
userName,
i,
tk.getKey(),
is,
os
);
if (isEstablished) {
System.out.println ("MIDLet...Sending transactoin message " +
"over secure context");
byte[] rspMessage = kc.sendSecureMessage(
"Transaction of Amount:"+txt_amount.getString()
+ " From: "+userName
+" To: "+txt_sendTo.getString(),
tk.getKey(),
i,
is,
os
);
String decodedMessage = kc.decodeSecureMessage(rspMessage, tk.getKey());
if (decodedMessage!=null)
showTransResult(" OK", decodedMessage);
else
showTransResult(" Error!", "Transaction failed..");
} else
System.out.println ("MIDlet...Context establishment failed..");
}//sendMoney()
public synchronized void run() {
try {
dc = (DatagramConnection)Connector.open("datagram://"+kdcAddress+":"+
kdcPort);
kc = new KerberosClient(dc);
sc = (SocketConnection)Connector.open("socket://"+e_bankAddress+":"+
e_bankPort);
sc.setSocketOption(SocketConnection.KEEPALIVE, 1);
is = sc.openDataInputStream();
os = sc.openDataOutputStream();
} catch (ConnectionNotFoundException ce) {
System.out.println("Socket connection to server not found....");
} catch (IOException ie) {
ie.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}//run
public void transactionForm(){
transForm = new Form("EBANK Transaction Form");
txt_userName = new TextField("Username", "", 10, TextField.ANY);
txt_password = new TextField("Password", "", 10, TextField.PASSWORD);
txt_amount = new TextField("Amount", "", 4, TextField.NUMERIC);
txt_sendTo = new TextField("Pay to", "", 10, TextField.ANY);
transForm.append(txt_userName);
transForm.append(txt_password);
transForm.append(txt_amount);
transForm.append(txt_sendTo);
transForm.addCommand(sendMoneyCommand);
transForm.addCommand(exitCommand);
transForm.setCommandListener(this);
display.setCurrent(transForm);
}
public void showTransResult(String info, String message) {
transResForm = new Form("Transaction Result");
si_message = new StringItem("Status:" , info);
txt_label = new TextField("Result:", message, 150, TextField.ANY);
transResForm.append(si_message);
transResForm.append(txt_label);
transResForm.addCommand(exitCommand);
transResForm.addCommand(OKCommand);
transResForm.setCommandListener(this);
display.setCurrent(transResForm);
}
}//J2MEClientMIDlet
|
运行这个 MIDlet 会得到如 图 2所示的屏幕。
图 2.
图 2显示为这个示例移动银行应用程序开发了一个非常简单的 GUI。 图 2还显示了四个数据输入字段:
- “
Username”字段取要使用移动银行 MIDlet 的金融服务的人的用户名。 - “
Password”字段取用户的密码。 - “
Amount”字段允许输入要支付给一个收款人的金额。 - “
Pay to”字段包含收款人的用户名。
输入完数据后,按 Pay 按钮。Pay 按钮的事件处理器(
清单
25中的
sendMoney()方法)执行 Kerveros 通信的所有七个阶段:
- 生成一个
TGT请求、将请求发送给出服务器、并接收TGT响应。 - 从
TGT响应中提取TGT和会话密钥。 - 生成一个服务票据请求、将请求发送给
KDC、并接收服务票据响应。 - 从服务票据响应中提取服务票据和子会话密钥。
- 生成上下文建立请求并发送给电子银行的业务逻辑服务器、接收响应、并解析它以确定服务器同意建立一个新的安全上下文。
- 生成一个安全消息、将这个消息发送给服务器、并接收服务器的响应。
- 解码来自服务器的消息。
清单 25的 MIDlet 代码相当简单,不需要很多解释。只要注意以下几点:
- 我使用了不同的线程(
清单 25中的
run()方法) 创建Datagram连接 (dc) 和Socket连接上的数据输入和输出流。这是因为 MIDP 2.0 不允许在 J2ME MIDlet 的主执行线程中创建Datagram和Socket连接。 - 在
清单 25的 J2ME MIDlet 中,我硬编码了 KDC 服务器的域、服务器名、地址和端口号以及电子银行服务器的名字和地址。注意 MIDlet 中的硬编码只是出于展示目的。另一方面,
KerberosClient是完全可重用的。 - 为了试验这个应用程序,需要一个作为电子银行服务器运行的 GSS 服务器。本文的 源代码下载 包含一个服务器端应用程序和一个 readme.txt 文件,它描述了如何运行这个服务器。
- 最后,注意我没有设计电子银行通信框架,我只是设计了基于 Kerberos 的安全框架。可以设计自己的通信框架,并用 KerberosClient 提供安全支持。例如,可以使用 XML 格式定义不同的消息作为转账指令。
在这个三部分的系列文章中,我展示了 J2ME 应用程序中的安全 Kerberos 通信。介绍了进行一系列加密密钥交换的 Kerveros 通信。还介绍了 J2ME 应用程序是如何使用密钥与远程电子银行服务器建立通信上下文并安全地交换消息的。我还提供了展示文章中讨论的所有概念的 J2ME 代码。
- 您可以参阅本文在 developerWorks 全球站点上的
英文原文.
- 阅读本系列的
第一篇
和
第二篇文章。
-
下载下载本文附带的源代码。
- 阅读 Kerberos(第 5 版)的官方
RFC 1510。
- 下载
Bouncy Castle 的加密库。本文的代码是用 Bouncy Castle 的 1.19 版测试的。
- 阅读官方
DES
和
DES
Modes of Operation(包括 CBC 模式)规范。
- 访问 IETF 网站上的
Kerberos
工作组页面。
- 在“
Simplify
enterprise Java authentication with single sign-on”一文中(
developerWorks,2003 年 9 月),展示了使用 Kerberos 和 Java GSS API 的单点登录(SSO)。
- 在这里可以找到
有关 Kerberos 的很好的一组链接。
-
下载
完整的 ASN.1 文档和编码规则。
- 阅读 Jason Garman 的
Kerberos:
The Definitive Guide
(O'Reilly Associates,2003 年),以学习 Kerberos
的使用。
- 访问
GSS
页。
- 看看 MIT 的这个
KDC
实现。
-
CSG
组和
Heimdal
都提供了免费的 Kerberos 实现。
- 在
开发者书店
上寻找有关 Kerberos 所有内容的更多信息。
Faheem Khan 是一个独立软件顾问,专长是企业应用集成 (EAI) 和 B2B 解决方案。读者可以通过 fkhan872@yahoo.com与 Faheem 联系。