GRPC(9)- SSL使用

最后更新:2020-06-09

公司的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
Edgar

Edgar
一个略懂Java的小菜比