公司的GRPC需要加入TLS支持,记录一下学习过程。
证书的生成参考这篇文章
https://edgar615.github.io/openssl.html
1. 单向认证
1.1. 生成证书
生成服务端证书
openssl req -newkey rsa:2048 -nodes -keyout domain.key -x509 -days 365 -out domain.crt -subj "/CN=localhost"
上述命令创建一个 2048 位的私钥(domain.key)和一个自签证书(domain.crt),有效期365天;CN指定为localhost
将证书转换为JAVA支持的格式
openssl pkcs8 -topk8 -nocrypt -in domain.key -out domain.pem
对于openssl的相关内容查看这篇文章(后面再补)
1.2. 服务端实现
public class HelloWorldServer {
public static void main(String[] args) throws Exception {
ServerBuilder serverBuilder = NettyServerBuilder.forPort(8443)
.sslContext(getSslContextBuilder().build());
serverBuilder.addService(new GreeterService());
Server server = serverBuilder.build();
try {
server.start();
server.awaitTermination();
} catch (Exception e) {
e.printStackTrace();
}
}
private static SslContextBuilder getSslContextBuilder() {
final String certChainFilePath = "domain.crt";
final String privateKeyFilePath = "domain.pem";
SslContextBuilder sslClientContextBuilder = SslContextBuilder.forServer(new File(certChainFilePath), new File(privateKeyFilePath));
return GrpcSslContexts.configure(sslClientContextBuilder);
}
}
主要是构建SslContext
1.3. 客户端实现
public class HelloWorldClient {
public static void main(String[] args) throws Exception {
final String trustCertCollectionFilePath = "domain.crt";
SslContext sslContext = buildSslContext(trustCertCollectionFilePath);
ManagedChannelBuilder builder = NettyChannelBuilder.forAddress("localhost", 8443)
.sslContext(sslContext);
ManagedChannel channel = builder.build();
GreeterGrpc.GreeterBlockingStub greeterBlockingStub = GreeterGrpc.newBlockingStub(channel);
HelloRequest helloRequest = HelloRequest.newBuilder().setName("Edgar").build();
HelloReply helloReply;
try {
helloReply = greeterBlockingStub.sayHello(helloRequest);
} catch (StatusRuntimeException e) {
e.printStackTrace();
return;
}
System.out.println(helloReply.getMessage());
}
private static SslContext buildSslContext(String trustCertCollectionFilePath) throws Exception {
SslContextBuilder builder = GrpcSslContexts.forClient()
.trustManager(new File(trustCertCollectionFilePath));
return builder.build();
}
}
1.4. 服务端IP
上面的例子我们使用的localhost,当我们将server部署到服务上时要生成新的证书
openssl req -newkey rsa:2048 -nodes -keyout domain.key -x509 -days 365 -out domain.crt -subj "/CN=服务器IP地址"
此时客户端调用会报错
Caused by: java.security.cert.CertificateException: No subject alternative names present
使用Openssl验证证书并无问题,
openssl s_server -accept 10001 -cert domain.crt -key domain.pem
openssl s_client -connect localhost:10001
我们可以关闭客户端的校验功能
private static SslContext buildSslContext() throws Exception {
SslContextBuilder builder = GrpcSslContexts.forClient()
.trustManager(InsecureTrustManagerFactory.INSTANCE);
return builder.build();
}
2. 双向认证
2.1.生成证书
2.1.1.生成CA根证书
# 生成CA密钥ca.key
openssl genrsa -passout pass:123456 -des3 -out ca.key 4096
# 生成CA根证书ca.crt
openssl req -passin pass:123456 -new -x509 -days 365 -key ca.key -out ca.crt -subj "/CN=IP地址"
2.1.2. 生成服务端证书
openssl genrsa -passout pass:123456 -des3 -out client.key 4096
openssl req -passin pass:123456 -new -key client.key -out client.csr -subj "/CN=IP地址"
openssl x509 -passin pass:123456 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out client.crt
2.1.3. 生成客户端证书
openssl pkcs8 -topk8 -nocrypt -in client.key -out client.pem
openssl pkcs8 -topk8 -nocrypt -in server.key -out server.pem
2.1.4. 转换成JAVA支持的证书
openssl pkcs8 -topk8 -nocrypt -``in` `client.key -out client.pem``openssl pkcs8 -topk8 -nocrypt -``in` `server.key -out server.pem
2.1.5. 验证证书
openssl s_server -accept 10001 -key server.pem -cert server.crt -CAfile ca.key -verify_ip IP地址 -Verify 5
openssl s_client -connect 172.16.116.128:10001 -cert client.crt -key client.pem
2.2. 服务端代码
public class HelloWorldServer {
public static void main(String[] args) throws Exception {
ServerBuilder serverBuilder = NettyServerBuilder.forPort(8443)
.sslContext(getSslContextBuilder().build());
serverBuilder.addService(new GreeterService());
Server server = serverBuilder.build();
try {
server.start();
server.awaitTermination();
} catch (Exception e) {
e.printStackTrace();
}
}
private static SslContextBuilder getSslContextBuilder() throws Exception {
final String certChainFilePath = "/root/workspace/grpc/server.crt";
final String privateKeyFilePath = "/root/workspace/grpc/server.pem";
final String trustCertCollectionFilePath = "/root/workspace/grpc/ca.crt";
SslContextBuilder sslClientContextBuilder = SslContextBuilder.forServer(new File(certChainFilePath), new File(privateKeyFilePath))
.trustManager(new File(trustCertCollectionFilePath))
.clientAuth(ClientAuth.REQUIRE);
return GrpcSslContexts.configure(sslClientContextBuilder);
}
}
2.3. 客户端代码
public class HelloWorldClient2 {
public static void main(String[] args) throws Exception {
final String certChainFilePath = "ca/client.crt";
final String privateKeyFilePath = "ca/client.pem";
final String trustCertCollectionFilePath = "ca/ca.crt";
SslContext sslContext = buildSslContext(trustCertCollectionFilePath, certChainFilePath, privateKeyFilePath);
ManagedChannelBuilder builder = NettyChannelBuilder.forAddress("IP", 8443)
.negotiationType(NegotiationType.TLS)
.sslContext(sslContext);
ManagedChannel channel = builder.build();
GreeterGrpc.GreeterBlockingStub greeterBlockingStub = GreeterGrpc.newBlockingStub(channel);
HelloRequest helloRequest = HelloRequest.newBuilder().setName("Edgar").build();
HelloReply helloReply;
try {
helloReply = greeterBlockingStub.sayHello(helloRequest);
} catch (StatusRuntimeException e) {
e.printStackTrace();
return;
}
System.out.println(helloReply.getMessage());
}
private static SslContext buildSslContext(String trustCertCollectionFilePath,
String clientCertChainFilePath,
String clientPrivateKeyFilePath) throws Exception {
SslContextBuilder builder = GrpcSslContexts.forClient();
if (trustCertCollectionFilePath != null) {
builder.trustManager(new File(trustCertCollectionFilePath));
}
if (clientCertChainFilePath != null && clientPrivateKeyFilePath != null) {
builder.keyManager(new File(clientCertChainFilePath), new File(clientPrivateKeyFilePath));
}
return builder.build();
}
}
localhost可以正常访问,部署到服务器上时报错
java.security.cert.CertificateException: No subject alternative names present
3. No subject alternative names present
摘自维基
主题备用名称(Subject Alternative Name,缩写SAN)是一项对X.509的扩展,它允许在安全证书中使用subjectAltName字段将多种值与证书关联[1],这些值被称为主题备用名称。名称可包括:[2]
电子邮件地址 IP地址 统一资源标志符(URI) DNS名称(通常也在主证书的主题字段中提供为公共名称RDN。) 目录名称(主题中唯一名称的备用名称) 其他通用名称,在已注册的[3]对象标识符后提供一个值。
通过Subject Alternative Name 段中列了一大串的域名、IP,一张证书能够被多个域名所使用,大的简化网站证书的管理。
为了彻底修复这个问题,我们需要在生成证书的时候指定subjectAltName字段。
下面的步骤建立在openssl-1.1.1版本上,之前在1.0.2版本上尝试很久都不成功,还不能确定是版本问题还是操作问题
3.1. 修改openssl.cnf配置
首先通过openssl version -a命令,找到openssl的配置文件
修改openssl.cnf
确保[ req ]
下存在以下2行(默认第一行是有的,第2行被注释了)
[ req ]
distinguished_name = req_distinguished_name
req_extensions = v3_req
在[ v3_req ]
段最后一行后新增内容 subjectAltName = @alt_names
(前2行默认存在)
[ v3_req ]
# Extensions to add to a certificate request
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
以下新增内容
subjectAltName = @alt_names
[ alt_names ]
IP.1 = IP地址
3.2. 生成证书
单向认证
openssl req -newkey rsa:2048 -nodes -keyout domain.key -x509 -days 365 -out domain.crt -subj "/CN=IP地址" -extensions v3_req -extfile /etc/pki/tls/openssl.cnf
或者
# openssl-1.1.1+版本才支持
openssl req -newkey rsa:2048 -nodes -keyout domain.key -x509 -days 365 -out domain.crt -subj "/CN=IP地址" -addext "subjectAltName = IP:IP地址"
双向认证
# 生成CA密钥ca.key
openssl genrsa -passout pass:123456 -des3 -out ca.key 4096
# 生成CA根证书ca.crt
openssl req -passin pass:123456 -new -x509 -days 365 -key ca.key -out ca.crt -subj "/CN=IP地址" -extensions v3_req -config /etc/pki/tls/openssl.cnf
openssl req -passin pass:123456 -new -x509 -days 365 -key ca.key -out ca.crt -subj "/CN=IP地址" -addext "subjectAltName = IP:IP地址"
# 生成密钥
openssl genrsa -passout pass:123456 -des3 -out server.key 4096
# 证书签发请求,csr是证书请求文件,用于生成证书
openssl req -passin pass:123456 -new -key server.key -out server.csr -subj "/CN=IP地址" -extensions v3_req -config /etc/pki/tls/openssl.cnf
# openssl-1.1.1+版本才支持
# openssl req -passin pass:123456 -new -key server.key -out server.csr -subj "/CN=IP地址" -addext "subjectAltName = IP:IP地址"
# 使用根证书生成生成服务端证书
openssl x509 -req -passin pass:123456 -days 365 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt -extensions v3_req -extfile /etc/pki/tls/openssl.cnf
# 客户端证书
openssl genrsa -passout pass:123456 -des3 -out client.key 4096
openssl req -passin pass:123456 -new -key client.key -out client.csr -subj "/CN=IP地址" -extensions v3_req -config /etc/pki/tls/openssl.cnf
# openssl-1.1.1+版本才支持
#openssl req -passin pass:123456 -new -key client.key -out client.csr -subj "/CN=IP地址" -addext "subjectAltName = IP:IP地址"
openssl x509 -passin pass:123456 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out client.crt -extensions v3_req -extfile /etc/pki/tls/openssl.cnf
# 转成Java支持的证书
openssl pkcs8 -topk8 -nocrypt -in client.key -out client.pem
openssl pkcs8 -topk8 -nocrypt -in server.key -out server.pem