okhttp 2way tls

okhttp双向认证

双向认证和普通https的连接的主要区别:

  1. 需要提前分享客户端的证书到服务器
  2. 在建立连接的时候需要发送自己证书的信息
  3. 服务器会使用客户端证书对客户端证书验证

主要流程

  1. 加载客户端证书的证书和私钥到KeyStore
  2. 加载服务端证书到TrustStore
  3. 使用KeyStore和TrustStore创建SSLContext
  4. 使用SSLContext创建okhttp连接

示例代码

public class App 
{
    public static void main(String[] args)
    {
        try {
            KeyManager[] keyStoreManager = loadKeyStore();
            TrustManager[] trustManager = loadTrustStore();
            SSLSocketFactory socketFactory = getSslSocketFactory(keyStoreManager, trustManager);

            OkHttpClient client = new OkHttpClient.Builder()
                    .sslSocketFactory(socketFactory, (X509TrustManager) trustManager[0])
                    .hostnameVerifier((hostname, session) -> true)
                    .build();

            Call call = client.newCall(new Request.Builder()
                    .url("https://2way.example.com")
                    .build());

           Response response = call.execute();

            if (response.isSuccessful()) {
                System.out.printf(response.body().string());
            }
        } catch (Exception e) {
            System.out.printf(e.toString());
        }
    }

    private static KeyManager[] loadKeyStore() throws KeyStoreException, CertificateException, IOException, NoSuchAlgorithmException, UnrecoverableKeyException, InvalidKeySpecException {
        // 这里是采用从原始crt和pem中加载证书和密钥,根据情况也很容易改从jks中加载
        // create keyStore from client privateKey and cert
        KeyStore clientKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        // load client cert from file
        // 提前准备client.crt文件在resource目录
        Certificate clientCert = getCertFromFile("/client.crt");
        clientKeyStore.load(null);
        // 提前准备client.key.pem文件在resource目录
        clientKeyStore.setKeyEntry("client-cert", readPrivateKeyFromPem("/client.key.pem"), null, new Certificate[] { clientCert } );

        // crate KeyManger from keyStore
        KeyManagerFactory keyManagerFactory_Client = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        keyManagerFactory_Client.init(clientKeyStore, null);

        return keyManagerFactory_Client.getKeyManagers();
    }

    private static TrustManager[] loadTrustStore() throws NoSuchAlgorithmException, CertificateException, IOException, KeyStoreException {
        // load trust store from file
        // 提前准备truststore.jks文件在resource目录,jks需要密码保护
        KeyStore trustStore_Client = KeyStore.getInstance(KeyStore.getDefaultType());
        try (InputStream inputStreamTruststore = App.class.getResourceAsStream("/truststore.jks")) {
            trustStore_Client.load(inputStreamTruststore, "<password_here>".toCharArray());
        }

        // TrustManagerFactory from keyStore
        TrustManagerFactory trustManagerFactory_Client = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory_Client.init(trustStore_Client);
        return trustManagerFactory_Client.getTrustManagers();
    }

    private static SSLSocketFactory getSslSocketFactory(KeyManager[] keyManagers_Client, TrustManager[] trustManagers_Client) throws NoSuchAlgorithmException, KeyManagementException {
        SSLContext sslContext_Client = SSLContext.getInstance("TLSv1.2");
        sslContext_Client.init(keyManagers_Client, trustManagers_Client, new SecureRandom());
        return sslContext_Client.getSocketFactory();
    }

    private static Certificate getCertFromFile(String file) throws CertificateException {
        InputStream inputStream = App.class.getResourceAsStream(file);

        CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");

        Certificate certificate = certificateFactory.generateCertificate(inputStream);

        return certificate;
    }

    private static PrivateKey readPrivateKeyFromPem(String file) throws NoSuchAlgorithmException, InvalidKeySpecException {
        String key = new BufferedReader(
                new InputStreamReader(App.class.getResourceAsStream(file), StandardCharsets.UTF_8))
                .lines()
                .collect(Collectors.joining(System.lineSeparator()));

        String privateKeyPEM = key
                .replace("-----BEGIN PRIVATE KEY-----", "")
                .replaceAll(System.lineSeparator(), "")
                .replace("-----END PRIVATE KEY-----", "");

        byte[] encoded = Base64.getDecoder().decode(privateKeyPEM);

        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded);
        return keyFactory.generatePrivate(keySpec);
    }
}

主要步骤还是很明显,但是涉及到客户端证书,密钥和服务器证书都需要提前准备好。跟据相关文件的存放方式,加载的方法也要对应的做出修改。

测试

okhttp同提供了MockWebServer来帮助编写单元测试。

主要步骤:

  1. 服务器端证书,密钥和客户端证书
  2. 使用相关证书、密钥创建mockwebserver
  3. 客户端加载客户端证书,密钥和服务器端证书
  4. 客户端使用server.url()生成的连接访问mockwebserver

测试会用到大量的自签证书,如果不熟悉的话请先看第 准备自签证书 章节

// server,加载服务器端证书
Certificate serverCert = getCertFromFile("/server.crt");

KeyStore serverKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
serverKeyStore.load(null);
// 加载服务器端密钥
serverKeyStore.setKeyEntry("server-cert", readPrivateKeyFromPem("/server.key.pem"), null, new Certificate[] { serverCert } );

// crate KeyManger from keyStore
KeyManagerFactory keyManagerFactory_Server = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory_Server.init(serverKeyStore, null);
KeyManager[] keyManagers_Server = keyManagerFactory_Server.getKeyManagers();

//加载客户端证书
Certificate clientCaCert = getCertFromFile("/client.crt");
KeyStore trustKeyStore_Server = KeyStore.getInstance(KeyStore.getDefaultType());
trustKeyStore_Server.load(null);
trustKeyStore_Server.setCertificateEntry("client-cert", clientCaCert);

TrustManagerFactory trustManagerFactory_Server = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory_Server.init(trustKeyStore_Server);
TrustManager[] trustManagers_Server = trustManagerFactory_Server.getTrustManagers();

SSLContext sslContext_Server = SSLContext.getInstance("TLSv1.2");
sslContext_Server.init(keyManagers_Server, trustManagers_Server, new SecureRandom());

//创建MockWebServer
MockWebServer server = new MockWebServer();
//加载sslcontext
server.useHttps(sslContext_Server.getSocketFactory(), false);
server.requireClientAuth();
server.enqueue(new MockResponse());

// make call
// 使用server.url来替换要访问的url
Call call = client.newCall(new Request.Builder()
        .url(server.url("/"))
        .build());
Response response = call.execute();

//查看证书信息
System.out.println(response.handshake().peerPrincipal());
RecordedRequest recordedRequest = server.takeRequest();
System.out.println(recordedRequest.getHandshake().peerPrincipal());

准备自签证书

TODO openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 365