The Ethan's Blog

This moment yearning and thoughtful


  • 首页

  • 归档

Java RSA密钥文件处理

发表于 2018-11-05 | 更新于 2019-04-28

最近,在跟外部的一个服务进行接口对接,需要对调用双方的身份进行验证。采用了RSA非对称签名对方式来进行验证。主要记录一下Java读取OpenSSL生成的RSA密钥对的操作过程。

1.使用openssl生成private key

1
openssl genrsa -out private.pem 2048

生成一个新的长度为2048位RSA private key。密钥存储在private.pem文件中,密钥文件是PEM格式。PEM格式实际上就是对DER结构的Base64编码,查看该文件,可以看到文件以”BEGIN RSA PRIVATE KEY”为头,”END RSA PRIVATE KEY”为结尾。

1
2
3
4
5
head -2 private.pem; tail -1 private.pem

-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAwIEbtGxpXORlhW3SYaKzHzpKZgDOGGaJd7fD6ZJnaUesVWrr
-----END RSA PRIVATE KEY-----

2.生成public key

1
openssl rsa -in private.pem -out public.pem -pubout

生成的公钥也是PEM格式。

1
2
3
4
5
head -2 public.pem; tail -1 public.pem

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwIEbtGxpXORlhW3SYaKz
-----END PUBLIC KEY-----

3. 解析生成的Key

由于Java不能直接加载openssl生成的PEM格式,这里需要做一下转换。这里插一句关于PEM的说明,PEM实际上是DER编码然后进行Base64编码,再加上对Key进行说明的头和尾。

1
2
-inform DER|NET|PEM
This specifies the input format. The DER option uses an ASN1 DER encoded form compatible with the PKCS#1 RSAPrivateKey or SubjectPublicKeyInfo format.The PEM form is the default format: it consists of the DER format base64 encoded with additional header and footer lines. On input PKCS#8 format private keys are also accepted. The NET form is a format is described in the NOTES section.

在PKCS#1(RFC 3447)标准中定义了RSAPrivateKey和SubjectPublickeyInfo的结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
RSAPrivateKey ::= SEQUENCE {
version Version,
modulus INTEGER, -- n
publicExponent INTEGER, -- e
privateExponent INTEGER, -- d
prime1 INTEGER, -- p
prime2 INTEGER, -- q
exponent1 INTEGER, -- d mod (p-1)
exponent2 INTEGER, -- d mod (q-1)
coefficient INTEGER, -- (inverse of q) mod p
otherPrimeInfos OtherPrimeInfos OPTIONAL
}

RSAPublicKey ::= SEQUENCE {
modulus INTEGER, -- n
publicExponent INTEGER -- e
}

由于Java可以解析DER格式,所以转换的思路是去掉PEM文件中的头和尾,然后再Base64解码。

3.1 解析Public Key

代码片段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 //此处省略部分代码
private static final String ALG = "RSA";

private static final String PUBLIC_KEY_HEADER = "-----BEGIN PUBLIC KEY-----";

private static final String PUBLIC_KEY_FOOTER = "-----END PUBLIC KEY-----";

//此处省略部分代码

// 解析PublicKey
public static Optional<PublicKey> getDERPublicKeyFromPEMString(String keyString) {
String content = keyString
.replace(PUBLIC_KEY_HEADER, "")
.replace(PUBLIC_KEY_FOOTER, "")
.replaceAll("\\s", "");
byte[] contentBytes = Base64.decodeBase64(content);

try {
KeyFactory keyFactory = KeyFactory.getInstance(ALG);
return Optional.of(keyFactory.generatePublic(new X509EncodedKeySpec(contentBytes)));
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
return Optional.empty();
}
}

代码中使用X509EncodedKeySpec来加载public key,X509EncodedKeySpec是Java对SubjectPublickeyInfo的实现. Note: 代码中用到了commons-codec工具包来进行Base64解码

3.2 解析Private Key

暂时还不能采用同样的方法直接去解析openssl生成的private key文件。因为Java中能直接识别的private key编码格式是PKCS#8.这里就需先对刚才生成的private key文件进行一次转换。

1
openssl pkcs8 -in private.pem -topk8 -nocrypt -out privatekey_key.pem

转换后文件内容如下:

1
2
3
4
5
head -2 private_key.pem; tail -1 private_key.pem

-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDAgRu0bGlc5GWF
-----END PRIVATE KEY-----

注意,在这里没有对进行加密。然后再进行解析,代码片段如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

// 省略部分代码

private static final String UNENCRYPTED_PRIVATE_KEY_HEADER = "-----BEGIN PRIVATE KEY-----";

private static final String UNENCRYPTED_PRIVATE_KEY_FOOTER = "-----END PRIVATE KEY-----";

private static final String ENCRYPTED_PRIVATE_KEY_HEADER = "-----BEGIN ENCRYPTED PRIVATE KEY-----";

private static final String ENCRYPTED_PRIVATE_KEY_FOOTER = "-----END ENCRYPTED PRIVATE KEY-----";

// 省略部分代码

// 解析PrivateKey
public static Optional<PrivateKey> getDERPrivateKeyFromPEMString(String keyString, String password) {
try {
KeyFactory keyFactory = KeyFactory.getInstance(ALG);
if (isEncrypted(keyString)) {
return Optional.ofNullable(keyFactory.generatePrivate(getEncryptedPrivateKeySpec(keyString, password)));
}
return Optional.ofNullable(keyFactory.generatePrivate(getUnencryptedPrivateKeySpec(keyString)));
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
return Optional.empty();
}
}

// 未加密的PrivateKey
public static PKCS8EncodedKeySpec getUnencryptedPrivateKeySpec(String keyString) {
String content = keyString
.replace(UNENCRYPTED_PRIVATE_KEY_HEADER, "")
.replace(UNENCRYPTED_PRIVATE_KEY_FOOTER, "")
.replaceAll("\\s", "");
byte[] contentBytes = Base64.decodeBase64(content);

return new PKCS8EncodedKeySpec(contentBytes);
}

// 加密的PrivateKey
public static PKCS8EncodedKeySpec getEncryptedPrivateKeySpec(String keyString, String password) {
String content = keyString
.replace(ENCRYPTED_PRIVATE_KEY_HEADER, "")
.replace(ENCRYPTED_PRIVATE_KEY_FOOTER, "")
.replaceAll("\\s", "");
byte[] contentBytes = Base64.decodeBase64(content);

try {
EncryptedPrivateKeyInfo epkInfo = new EncryptedPrivateKeyInfo(contentBytes);
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName());
PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray());
SecretKey secretKey = keyFactory.generateSecret(pbeKeySpec);

Cipher cipher = Cipher.getInstance(epkInfo.getAlgName());
cipher.init(Cipher.DECRYPT_MODE, secretKey, epkInfo.getAlgParameters());

return epkInfo.getKeySpec(cipher);

} catch (IOException | NoSuchAlgorithmException
| InvalidKeySpecException | NoSuchPaddingException
| InvalidKeyException | InvalidAlgorithmParameterException e) {
return null;
}
}

private static boolean isEncrypted(String keyString) {
return keyString.contains(ENCRYPTED_PRIVATE_KEY_HEADER);
}

实践总结(1) Kubernetes使用总结

发表于 2018-11-01 | 更新于 2018-11-02

公司测试环境和预发布环境使用k8s进行容器编排和集群管理已经有一段时间了,期间遇到的问题还挺多的。特在这里进行一下总结。

安全问题

某天,AWS上一个由k8s管理的集群,突然变得不稳定。该集群是用来部署一些测试服务的,现在测试服务基本不能运行。

集群由1个Master和3个Node组成。检查发现基本上每个Node的CPU占用都100%。使用top命令查看,发现命名为gcc的进程占用了大量CPU。
仔细分析后发现,该进程是一个挖矿程序,指向了一个门罗币矿池,所挖的矿都转到了一个钱包地址。查了这个钱包地址的状态,发现这个地址有大量Hash Rate而且转入转出到额度都挺惊人,看来有很多服务器都被埋下了这个挖矿程序。

杀死该进程,并做了清理工作,服务都恢复了正常。那么问题来了,挖矿程序是怎样植入k8s Node的呢?公司能登录管理这批服务器的人就只有两个,而且相应的安全措施都有。除了这个k8s集群,其他服务器上并没有发现挖坑的程序。问题应该跟k8s有关,查看了这三个Node开放出去的端口,发现10250这个端口被映射到公网。这个端口属于kubelet,kubelet作为node agent运行在每个Node上,负责管理和监控Node上容器的创建。kubelet会运行一个HTTP服务,提供REST API来管理和控制Node。于是检查kubelet日志,发现了有一条比较奇怪的日志,该日志显示用curl下载并执行了一个脚本。而该脚本的用途就是下载挖矿程序和配置文件并启动挖矿。Google了一下,kubelet 10250的发现一篇文章刚好讲的就是这个Analysis of a Kubernetes hack — Backdooring through kubelet。

归根结底这个问题跟k8s配置有关。Kubelet authentication里面有说明。重新配置k8s,并禁止了10250端口开放。

使用k8s,完整阅读和理解官方文档很重要。这不由得让我想起了,k8s API Server的安全问题,API Server默认会开启两个端口: 8080和6443,见官方文档。
直接访问8080端口会返回API列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
{
"paths": [
"/api",
"/api/v1",
"/apis",
"/apis/apps",
"/apis/apps/v1beta1",
"/apis/authentication.k8s.io",
"/apis/authentication.k8s.io/v1beta1",
"/apis/authorization.k8s.io",
"/apis/authorization.k8s.io/v1beta1",
"/apis/autoscaling",
"/apis/autoscaling/v1",
"/apis/batch",
"/apis/batch/v1",
"/apis/batch/v2alpha1",
"/apis/certificates.k8s.io",
"/apis/certificates.k8s.io/v1alpha1",
"/apis/extensions",
"/apis/extensions/v1beta1",
"/apis/policy",
"/apis/policy/v1beta1",
"/apis/rbac.authorization.k8s.io",
"/apis/rbac.authorization.k8s.io/v1alpha1",
"/apis/storage.k8s.io",
"/apis/storage.k8s.io/v1beta1",
"/healthz",
"/healthz/ping",
"/healthz/poststarthook/bootstrap-controller",
"/healthz/poststarthook/extensions/third-party-resources",
"/healthz/poststarthook/rbac/bootstrap-roles",
"/logs",
"/metrics",
"/swaggerapi/",
"/ui/",
"/version"
]
}

按照官方文档8080端口应该仅用于测试,不能暴露到公网。但实际上也是有人没有注意这一点的。用ZoomEye搜索:metrics healthz +port:"8080"可以看到有相当多的目标。
使用Kubernetes官方提供的命令行工具可以直接操作相关的node和pod。还可以在容器中执行命令。

查看Pods

ihipCT.png

进入容器

ih8wbq.png

利用这一点,可以有很多操作的空间,具体我就不再叙述。

日常吹水(1) 十月最后的一天

发表于 2018-10-31 | 更新于 2018-11-01

总会有些莫名其妙的感情,在不经意间流露。

一晚上加一上午的单曲循环《你离开了南京,从此没有人和我说话》。这首简短且没有词的歌曲在今天似乎格外悠长。我也已经没有太多话去述说,就这样吧。。。

工作杂记(1) 一次HTTP请求超时

发表于 2018-10-25 | 更新于 2018-11-01

有两台服务器,一台上运行着PHP服务,另一台运行Java服务。两台服务器之间通过HTTP进行接口调用。查看Java服务端的日志发现,有很多往PHP服务的HTTP请求都超时。
于是在PHP服务器端用 netstat 命令查看了一下网络套接字连接情况。

命令 netstat -atp

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
...
tcp 0 0 172.19.126.49:53334 172.19.126.50:mysql TIME_WAIT -
tcp 0 0 172.19.126.49:53556 172.19.126.50:mysql TIME_WAIT -
tcp 0 0 172.19.126.49:52912 172.19.126.50:mysql TIME_WAIT -
tcp 0 0 172.19.126.49:50760 172.19.126.50:mysql TIME_WAIT -
tcp 0 0 172.19.126.49:53534 172.19.126.50:mysql TIME_WAIT -
tcp 0 0 172.19.126.49:50570 172.19.126.50:mysql TIME_WAIT -
tcp 0 0 172.19.126.49:50080 172.19.126.50:mysql TIME_WAIT -
tcp 0 0 172.19.126.49:50660 172.19.126.50:mysql TIME_WAIT -
tcp 0 0 172.19.126.49:50708 172.19.126.50:mysql TIME_WAIT -
tcp 0 0 172.19.126.49:50156 172.19.126.50:mysql TIME_WAIT -
tcp 0 0 172.19.126.49:51220 172.19.126.50:mysql TIME_WAIT -
tcp 0 0 172.19.126.49:53924 172.19.126.50:mysql TIME_WAIT -
tcp 0 0 172.19.126.49:49602 172.19.126.50:mysql TIME_WAIT -
tcp 0 0 172.19.126.49:51988 172.19.126.50:mysql TIME_WAIT -
tcp 0 0 172.19.126.49:38694 172.19.126.48:9981 TIME_WAIT -
tcp 0 0 172.19.126.49:54832 172.19.126.50:mysql TIME_WAIT -
tcp 0 0 172.19.126.49:50184 172.19.126.50:mysql TIME_WAIT -
tcp 0 0 172.19.126.49:49270 172.19.126.50:mysql TIME_WAIT -
tcp 0 0 172.19.126.49:53124 172.19.126.50:mysql TIME_WAIT -
tcp 0 0 172.19.126.49:54068 172.19.126.50:mysql TIME_WAIT -
tcp 0 0 172.19.126.49:52530 172.19.126.50:mysql TIME_WAIT -
tcp 0 0 172.19.126.49:50504 172.19.126.50:mysql TIME_WAIT -
tcp 0 0 172.19.126.49:54822 172.19.126.50:mysql TIME_WAIT -
tcp 0 0 172.19.126.49:52070 172.19.126.50:mysql TIME_WAIT -
tcp 0 0 172.19.126.49:53562 172.19.126.50:mysql TIME_WAIT -
tcp 0 0 172.19.126.49:54286 172.19.126.50:mysql TIME_WAIT -
...

发现大量的端口处于TIME_WAIT状态,而且都是与mysql连接相关.

运行命令 netstat -at | grep 'mysql' | awk '{print $6}' | sort | uniq -c。发现有差不多3000个端口处于TIME_WAIT。看来PHP服务打开的MySQL连接还真多。鉴于自己对PHP开发不了解,只是直觉上感觉打开这么多MySQL连接会影响服务性能。接下来主要想看一下HTTP调用的端口情况。用netstat检查发现端口连接不多,于是打算抓一下http端口的数据包看看。用tcpdump抓包,wireshark分析没有发现问题。然后又在Java服务端抓取双方通信的数据包,分析也没看出问题。It’s so strange!!!

既然日志是Java服务报出来的,看来应该在Java服务上去找原因。查看了Java服务的代码,发现Java服务中使用了Vertx框架中的HttpClient来进行Http请求。HttpClient配置如下:

1
2
3
4
5
6
7
{
"keepAlive": true,
"idleTimeout": 10,
"maxPoolSize": 50,
"maxWaitQueueSize": 3000,
"connectTimeout": 10000
}

当keepAlive设置为true的时候,HttpClient会重用建立的连接进行HTTP请求。idleTimeout参数控制连接空闲时间,当空闲时间达到idleTimout,该连接会被关闭。maxPoolSize指定了最大的连接数,当连接数没有达到maxPoolSize时,HttpClient会为新的请求新建连接,当达到maxPoolSize,HttpClient不再新建连接,而是把请求放进等待队列,等待队列的大小由maxWaitQueueSize来指定。connectTimeOut指定了连接超时时间。从日志看到的超时并不是连接超时。而是请求超时,请求超时是在HttpClientRequest里设置的。当一个请求达到设置的timeout且没有返回任何数据,就会一个java.util.concurrent.TimeoutException传递给异常处理方法。

通过运行命令netstat -at | grep -E '172.19.126.49' -c发现当前连接已经达到了50,也就是达到了maxPoolSize的设置。

统计Java服务发起的请求,1秒钟有差不多接近三千个请求。

综合上述信息,问题还是出现在PHP服务,PHP服务提供的HTTP接口处理速度不够快,导致Java服务端产生大量的HTTP请求堆积超时。而且从PHP服务端一开始发现的大量MySQL连接也证实了PHP服务提供的接口实现会去直接访问数据库,没有采用缓存之类的操作,导致接口更慢。看来还得找PHP研发打架去。。。

Ethan Zhang

4 日志
4 标签
GitHub
© 2017 – 2019 Ethan Zhang
由 Hexo 强力驱动 v3.8.0
|
主题 – NexT.Muse v6.4.2