1. 什么是LTPA?
Lightweight Third-Party Authentication (LTPA)是IBM Websphere和Domino产品中使用单点登录技术。当服务器配置好LTPA认证方式,用户通过浏览器成功登录后,服务器会自动发送一个session cookie给浏览器;此cookie中包含一个LTPA Token。
2. WebSphere部分
本部分描述适用于已实施WebSphere系列产品应用和Domino平台应用,或WebSphere与Domino之间已完成单点登录。在这样的环境中与构异系统实现单点登录。
u:user\:VGOLiveRealm/CN=squallzhong,O=VGOLive Technology%1301558320666%Cy2CAeru5kEElGj0hrvYsKW2ZVsvvcu6Un573aeX55OO4G3EMYWc0e/ZbqDp1z7MS+dLzniuUH4sYWCMpnKdm7ZGabwmV+WcraBl+y+yzwcl722gHVMOnDZAW7U3jEay9Tk2yG4yXkMWU+617xndpVxke2jtS5wIyVVM3q7UDPw=
从WebSphere系统中导出ltpa的key文件,使用文本文件打开,如:
com.ibm.websphere.CreationDate=Thu Mar 31 11\:08\:09 GMT+08\:00 2011 com.ibm.websphere.ltpa.version=1.0 com.ibm.websphere.ltpa.3DESKey=7dH4i81YepbVe+gF9XVUzE4C1Ca5g6A4Q69OFobJV9g\= com.ibm.websphere.CreationHost=wasserver com.ibm.websphere.ltpa.PrivateKey=N3bnOE1IbiXNsHXxxemC98iiCnmtw3JUuQvdFjEyh9r2gu+FlQRmG8xp5RBltqc6raI4EgYFhTr+t5/tmRQrFqfNKgvujeJZODeCspohi1V4C0qit7DOoqD9xOOn9Rzdb4PIuJM3ekwuBiZZYTYu7q0TANDygc7VbmwoD3xMPCk5svyvFJ/VshPyg5f7Q+VNM8dlIitU4gK9Qp8VZEqjGoXsYYzYYTQgnwAVtR2GfZtXKlf24EPXSkgUz9j8FwTvcylcKwjS22d6eVjciyAzInnxPqxE2iMRPEFDatHZFox3flsqBswmeDQrAGv8zIiffgP1DLKdjozUyAG+50v97xx7u1RtIrB4B01ik8DuLhw\= com.ibm.websphere.ltpa.Realm=VGOLiveRealm com.ibm.websphere.ltpa.PublicKey=AM04If2+ElGSyVRF0ZEesgvC59vGw8gSIfptjfoXj8iz4C7Ip/KVAu2PDkpQi3LUN/FgVF696tmsegBThks9rmMMHzOix/vGP2721dQZKbD7plOLdWtiY2AYZChsBVkOF26DfiWJ6euxD+a+KNcrfDnu2AXRC/tKncIUJV4LbeJdAQAB
以下代码为解析从WebSphere或Domino发送过来的LTPAToken Cookie以Java为例:
01
…
02
// LTPA 3DES 密钥
03
String ltpa3DESKey =
"7dH4i81YepbVe+gF9XVUzE4C1Ca5g6A4Q69OFobJV9g="
;
04
// LTPA 密钥密码
05
String ltpaPassword =
"Passw0rd"
;
06
try
{
07
// 获得加密key
08
byte
[] secretKey = getSecretKey(ltpa3DESKey, ltpaPassword);
09
// 使用加密key解密ltpa Cookie
10
String ltpaPlaintext =
new
String(decryptLtpaToken(tokenCipher,
11
secretKey));
12
displayTokenData(ltpaPlaintext);
13
}
catch
(Exception e) {
14
System.out.println(
"Caught inner: "
+ e);
15
}
16
…
17
//获得安全Key
18
private
static
byte
[] getSecretKey(String ltpa3DESKey, String password)
19
throws
Exception {
20
// 使用SHA获得key密码的hash值
21
MessageDigest md = MessageDigest.getInstance(
"SHA"
);
22
md.update(password.getBytes());
23
byte
[] hash3DES =
new
byte
[
24
];
24
System.arraycopy(md.digest(),
0
, hash3DES,
0
,
20
);
25
// 使用0替换后4个字节
26
Arrays.fill(hash3DES,
20
,
24
, (
byte
)
0
);
27
// BASE64解码 ltpa3DESKey
28
byte
[] decode3DES = Base64.decodeBase64(ltpa3DESKey.getBytes());
29
// 使用key密码hash值解密已Base64解码的ltpa3DESKey
30
return
decrypt(decode3DES, hash3DES);
31
}
32
//解密LtpaToken
33
public
static
byte
[] decryptLtpaToken(String encryptedLtpaToken,
byte
[] key)
34
throws
Exception {
35
// Base64解码LTPAToken
36
final
byte
[] ltpaByteArray = Base64.decodeBase64(encryptedLtpaToken
37
.getBytes());
38
// 使用key解密已Base64解码的LTPAToken
39
return
decrypt(ltpaByteArray, key);
40
}
41
// DESede/ECB/PKC5Padding解方法
42
public
static
byte
[] decrypt(
byte
[] ciphertext,
byte
[] key)
43
throws
Exception {
44
final
Cipher cipher = Cipher.getInstance(
"DESede/ECB/PKCS5Padding"
);
45
final
KeySpec keySpec =
new
DESedeKeySpec(key);
46
final
Key secretKey = SecretKeyFactory.getInstance(
"TripleDES"
)
47
.generateSecret(keySpec);
48
cipher.init(Cipher.DECRYPT_MODE, secretKey);
49
return
cipher.doFinal(ciphertext);
50
}
51
…
解析出来的LTPAToken信息以%分隔
Websphere LTPA生成时的签名信息是由用户DN和一些用户其他信息组成字符串,使用私有密钥进行签名,由于不清楚这些信息的组成,故无法产生正确的LTPA。
本部分的描述仅适用于单一的Domino平台应用与构异系统实现单点登录。
Base64解码/编码所需Jar包:apache-commons-codec-1.3.jar以上
01
import
org.apache.commons.codec.binary.Base64;
02
…...
03
final
String CHARSET =
"Cp850"
;
04
byte
[] dominoSecret = Base64.decodeBase64(ltpaDominoSecret.getBytes());
05
byte
[] ltpa = Base64.decodeBase64(ltpaToken.getBytes());
06
ByteArrayInputStream stream =
new
ByteArrayInputStream(ltpa);
07
int
usernameLength = ltpa.length –
40
;
08
byte
header[] =
new
byte
[
4
];
09
byte
creation[] =
new
byte
[
8
];
10
byte
expires[] =
new
byte
[
8
];
11
byte
username[] =
new
byte
[usernameLength];
12
byte
[] sha =
new
byte
[
20
];
13
// 读取LTPAToken版本号
14
stream.read(header,
0
,
4
);
15
if
(header[
0
] !=
0
|| header[
1
] !=
1
|| header[
2
] !=
2
|| header[
3
] !=
3
)
16
throw
new
IllegalArgumentException(
"Invalid ltpaToken format"
);
17
// 读取开始时间
18
stream.read(creation,
0
,
8
);
19
// 读取到期时间
20
stream.read(expires,
0
,
8
);
21
// 读取Domino用户DN
22
stream.read(username,
0
, usernameLength);
23
// 读取SHA校验和
24
stream.read(sha,
0
,
20
);
25
// 转换用户名
26
char
characters[] =
new
char
[usernameLength];
27
try
{
28
InputStreamReader isr =
new
InputStreamReader(
29
new
ByteArrayInputStream(username),
30
CHARSET);
31
isr.read(characters);
32
}
catch
(Exception e) {
33
}
34
// 获得Domino用户DN
35
String dn =
new
String(characters);
36
// 获得创建时间
37
Date creationDate =
new
Date(
38
Long.parseLong(
new
String(creation),
16
) *
1000
);
39
// 获得到期时间
40
Date expiresDate =
new
Date(
41
Long.parseLong(
new
String(expires),
16
) *
1000
);
42
…...
43
// 创建LTPA Token
44
ByteArrayOutputStream ostream =
new
ByteArrayOutputStream();
45
try
{
46
// LTPA Token版本号
47
ostream.write(header);
48
// 创建时间
49
ostream.write(creation);
50
// 过期时间
51
ostream.write(expires);
52
// Domino用户DN,如CN=SquallZhong/O=DigiWin
53
ostream.write(username);
54
// Domino LTPA 密钥
55
ostream.write(dominoSecret);
56
ostream.close();
57
}
catch
(IOException e) {
58
throw
new
RuntimeException(e);
59
}
60
// 进行 SHA-1 校验和
61
MessageDigest md;
62
try
{
63
md = MessageDigest.getInstance(
"SHA-1"
);
64
md.reset();
65
}
catch
(NoSuchAlgorithmException e) {
66
throw
new
RuntimeException(e);
67
}
68
byte
[] digest = md.digest(ostream.toByteArray());
69
// 完成 SHA-1 校验和,digest长度为20
70
boolean
valid = MessageDigest.isEqual(digest, sha);
01
/**
02
* 为指定用户创建有效的LTPA Token.创建时间为<tt>now</tt>.
03
*
04
* @param username
05
* - 用户名,注:使用用户全称,如:CN=SquallZhong/O=VGOLive Technology
06
* @param creationTime
07
* - 创建时间
08
* @param durationMinutes
09
* - 到期时间,单位:分钟
10
@param ltpaSecretStr
11
* - Domino Ltpa 加密字符串
12
* @return - 返回已Base64编码的Ltpa Cookie.
13
* @throws NoSuchAlgorithmException
14
* @throws Base64DecodeException
15
*/
16
public
static
String createLtpaToken(String username,
17
GregorianCalendar creationTime,
int
durationMinutes,
18
String ltpaSecretStr)
throws
NoSuchAlgorithmException {
19
// Base64解码ltpaSecretStr
20
byte
[] ltpaSecret = Base64.decodeBase64(ltpaSecretStr.getBytes());
21
// 用户名字节数组
22
byte
[] usernameArray = username.getBytes();
23
byte
[] workingBuffer =
new
byte
[preUserDataLength
24
+ usernameArray.length + ltpaSecret.length];
25
26
// 设置ltpaToken版本至workingBuffer
27
System.arraycopy(ltpaTokenVersion,
0
, workingBuffer,
0
,
28
ltpaTokenVersion.length);
29
// 获得过期时间,过期时间=当前时间+到期时间(分钟)
30
GregorianCalendar expirationDate = (GregorianCalendar) creationTime
31
.clone();
32
expirationDate.add(Calendar.MINUTE, durationMinutes);
33
34
// 转换创建时间至16进制字符串
35
String hex = dateStringFiller
36
+ Integer.toHexString(
37
(
int
) (creationTime.getTimeInMillis() /
1000
))
38
.toUpperCase();
39
// 设置创建时间至workingBuffer
40
System.arraycopy(hex.getBytes(), hex.getBytes().length
41
- dateStringLength, workingBuffer, creationDatePosition,
42
dateStringLength);
43
44
// 转换过期时间至16进制字符串
45
hex = dateStringFiller
46
+ Integer.toHexString(
47
(
int
) (expirationDate.getTimeInMillis() /
1000
))
48
.toUpperCase();
49
// 设置过期时间至workingBuffer
50
System.arraycopy(hex.getBytes(), hex.getBytes().length
51
- dateStringLength, workingBuffer, expirationDatePosition,
52
dateStringLength);
53
54
// 设置用户全称至workingBuffer
55
System.arraycopy(usernameArray,
0
, workingBuffer, preUserDataLength,
56
usernameArray.length);
57
58
// 设置已Base64解码ltpaSecret至workingBuffer
59
System.arraycopy(ltpaSecret,
0
, workingBuffer, preUserDataLength
60
+ usernameArray.length, ltpaSecret.length);
61
// 创建Hash字符串
62
byte
[] hash = createHash(workingBuffer);
63
64
// ltpaToken版本+开始时间(16进制)+到期时间(16进制)+用户全名+SHA-1(ltpaToken版本+开始时间(16进制)+到期时间(16进制)+用户全名)
65
byte
[] outputBuffer =
new
byte
[preUserDataLength + usernameArray.length
66
+ hashLength];
67
System.arraycopy(workingBuffer,
0
, outputBuffer,
0
, preUserDataLength
68
+ usernameArray.length);
69
System.arraycopy(hash,
0
, outputBuffer, preUserDataLength
70
+ usernameArray.length, hashLength);
71
// 返回已Base64编码的outputBuffer
72
return
new
String(Base64.encodeBase64(outputBuffer));
73
}
74
…...
F5 iRule代码如下:
when RULE_INIT {
01
set
cookie_name
"LtpaToken"
# 不更改
02
set
ltpa_version
"\x00\x01\x02\x03"
# 不更改
03
set
ltpa_secret
"b64encodedsecretkey"
# 从Domino SSO文档获得ltpa密钥
04
set
ltpa_timeout
"1800"
# 从Domino SSO文档中获得过期时间,单位:秒
05
}
06
07
when HTTP_REQUEST {
08
#
09
# Do your usual F5 HTTP authentication here
10
#
11
# Initial values
12
set
creation_time_temp [clock seconds]
13
set
creation_time [
format
%
X $creation_time_temp]
14
set
expr_time_temp [expr { $creation_time_temp
+
$::ltpa_timeout}]
15
set
expr_time [
format
%
X $expr_time_temp]
16
set
username [HTTP::username]
17
set
ltpa_secret_decode [b64decode $::ltpa_secret]
18
# First part of token
19
set
cookie_data_raw {}
20
append cookie_data_raw $::ltpa_version
21
append cookie_data_raw $creation_time
22
append cookie_data_raw $expr_time
23
append cookie_data_raw $username
24
append cookie_data_raw $ltpa_secret_decode
25
# SHA1 of first part of token
26
set
sha_cookie_raw [sha1 $cookie_data_raw]
27
# Final not yet encoded token
28
set
ltpa_token_raw {}
29
append ltpa_token_raw $::ltpa_version
30
append ltpa_token_raw $creation_time
31
append ltpa_token_raw $expr_time
32
append ltpa_token_raw $username
33
append ltpa_token_raw $sha_cookie_raw
34
# Final Base64 encoded token
35
set
ltpa_token_final [b64encode $ltpa_token_raw]
36
# Insert the cookie
37
HTTP::cookie insert name $::cookie_name value $ltpa_token_final
38
}
39
# Remove Authorization HTTP header to avoid using basic authentication
40
if
{ [HTTP::header exists
"Authorization"
] } {
41
HTTP::header remove
"Authorization"
42
}
43
}