Android中HTTPS通信客户端单向认证校验防止中间人攻击

引入HTTPS是为了解决HTTP所带来的三个问题:

  • HTTP是明文传输,数据容易被窃取,因此要加密数据以防止数据中途窃取
  • 认证服务器身份,确保数据发送到正确的服务器
  • 维护数据的完整性,防止数据在传输中被改变,如中间人攻击

所以本章主要是讲述如何使用OkHttpHttpUrlConnection来实现自制证书的访问

设置证书校验

绕过证书访问是不安全的访问方式,容易受到中间人攻击,所以客户端需要做双向证书校验,来保证客户端的合法性

private static SSLSocketFactory setCertificates(InputStream... certificates) {
   try {
       CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
       KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
       keyStore.load(null);
       int index = 0;
       for (InputStream certificate : certificates) {
           String certificateAlias = Integer.toString(index++);
           keyStore.setCertificateEntry(certificateAlias, certificateFactory.generateCertificate(certificate));
           if (certificate != null) {
               certificate.close();
           }
       }
       SSLContext sslContext = SSLContext.getInstance("TLS");
       TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
       trustManagerFactory.init(keyStore);
       sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());
       return sslContext.getSocketFactory();
   } catch (Exception e) {
       e.printStackTrace();
       return null;
   }
}

设置域名校验

域名校验需要自己设置一个trustHosts数组,当前访问域名匹配这个数组中的一条,即可返回true,否则返回false

//获取HostnameVerifier
public static HostnameVerifier getHostnameVerifier(final JSONArray trustHosts) {
    HostnameVerifier hostnameVerifier = new HostnameVerifier() {
        @Override
        public boolean verify(String host, SSLSession sslSession) {
            if (trustHosts == null || trustHosts.length() == 0) {
                return false;
            }
            try {
                for (int i = 0; i < trustHosts.length(); i++) {
                    String trustHost = trustHosts.getString(i);
                    if (host.equalsIgnoreCase(trustHost)) {
                        return true;
                    }
                }
            } catch (JSONException e) {
                e.printStackTrace();
            }
            return false;
        }
    };
    return hostnameVerifier;
}

调用接口

//获取这个SSLSocketFactory
public static SSLSocketFactory getSSLSocketFactory(InputStream cerIn) {
    try {
        SSLSocketFactory factory = setCertificates(cerIn);
        return factory;
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

OkHttp设置信任证书

JSONArray trustHosts = new JSONArray();
jsonArray.put("192.168.1.11");
jsonArray.put("192.168.1.12");
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.sslSocketFactory(SSLSocketCert.getSSLSocketFactory(context.getAssets().open("jetty.crt")));
builder.hostnameVerifier(SSLSocketCert.getHostnameVerifier(trustHosts));

HttpURLConnection设置信任证书

JSONArray trustHosts = new JSONArray();
jsonArray.put("192.168.1.11");
jsonArray.put("192.168.1.12");
URL url = new URL(fileUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
((HttpsURLConnection) conn).setSSLSocketFactory(SSLSocketCert.getSSLSocketFactory(context.getAssets().open("jetty.crt")));
((HttpsURLConnection) conn).setHostnameVerifier(SSLSocketCert.getHostnameVerifier(trustHosts));

使用证书字符串

以上是将证书放到Android工程的assets下,我们也可以将证书的内容导出来放到程序中

private String CER_STR = "-----BEGIN CERTIFICATE-----\n" +
        "MIICmjCCAgOgAwIBAgIIbyZr5/jKH6QwDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCQ04xKTAn\n" +
        "BgNVBAoTIFNpbm9yYWlsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRTUkNBMB4X\n" +
        "DTA5MDUyNTA2NTYwMFoXDTI5MDUyMDA2NTYwMFowRzELMAkGA1UEBhMCQ04xKTAnBgNVBAoTIFNp\n" +
        "bm9yYWlsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRTUkNBMIGfMA0GCSqGSIb3\n" +
        "DQEBAQUAA4GNADCBiQKBgQDMpbNeb34p0GvLkZ6t72/OOba4mX2K/eZRWFfnuk8e5jKDH+9BgCb2\n" +
        "9bSotqPqTbxXWPxIOz8EjyUO3bfR5pQ8ovNTOlks2rS5BdMhoi4sUjCKi5ELiqtyww/XgY5iFqv6\n" +
        "D4Pw9QvOUcdRVSbPWo1DwMmH75It6pk/rARIFHEjWwIDAQABo4GOMIGLMB8GA1UdIwQYMBaAFHle\n" +
        "tne34lKDQ+3HUYhMY4UsAENYMAwGA1UdEwQFMAMBAf8wLgYDVR0fBCcwJTAjoCGgH4YdaHR0cDov\n" +
        "LzE5Mi4xNjguOS4xNDkvY3JsMS5jcmwwCwYDVR0PBAQDAgH+MB0GA1UdDgQWBBR5XrZ3t+JSg0Pt\n" +
        "x1GITGOFLABDWDANBgkqhkiG9w0BAQUFAAOBgQDGrAm2U/of1LbOnG2bnnQtgcVaBXiVJF8LKPaV\n" +
        "23XQ96HU8xfgSZMJS6U00WHAI7zp0q208RSUft9wDq9ee///VOhzR6Tebg9QfyPSohkBrhXQenvQ\n" +
        "og555S+C3eJAAVeNCTeMS3N/M5hzBRJAoffn3qoYdAO1Q8bTguOi+2849A==\n" +
        "-----END CERTIFICATE-----";

然后再调用

JSONArray trustHosts= new JSONArray();
jsonArray.put("192.168.1.11");
jsonArray.put("192.168.1.12");
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.sslSocketFactory(SSLSocketCert.getSSLSocketFactory(new Buffer().writeUtf8(CER_STR).inputStream()));
builder.hostnameVerifier(SSLSocketCert.getHostnameVerifier(trustHosts));

SSLSocketCert.java

import android.net.http.SslCertificate;
import android.os.Bundle;

import org.json.JSONArray;
import org.json.JSONException;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;

public class SSLSocketCert {

    //获取这个SSLSocketFactory
    public static SSLSocketFactory getSSLSocketFactory(InputStream cerIn) {
        try {
            SSLSocketFactory factory = setCertificates(cerIn);
            return factory;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 载入证书
     */
    private static SSLSocketFactory setCertificates(InputStream... certificates) {
        try {
            CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            keyStore.load(null);
            int index = 0;
            for (InputStream certificate : certificates) {
                String certificateAlias = Integer.toString(index++);
                keyStore.setCertificateEntry(certificateAlias, certificateFactory.generateCertificate(certificate));
                if (certificate != null) {
                    certificate.close();
                }
            }
            SSLContext sslContext = SSLContext.getInstance("TLS");
            TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            trustManagerFactory.init(keyStore);
            sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());
            return sslContext.getSocketFactory();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    //获取HostnameVerifier
    public static HostnameVerifier getHostnameVerifier(final JSONArray trustHosts) {
        HostnameVerifier hostnameVerifier = new HostnameVerifier() {
            @Override
            public boolean verify(String host, SSLSession sslSession) {
                if (trustHosts == null || trustHosts.length() == 0) {
                    return false;
                }
                try {
                    for (int i = 0; i < trustHosts.length(); i++) {
                        String trustHost = trustHosts.getString(i);
                        if (host.equalsIgnoreCase(trustHost)) {
                            return true;
                        }
                    }
                } catch (JSONException e) {
                    e.printStackTrace();
                }
                return false;
            }
        };
        return hostnameVerifier;
    }

    /**
     * SSL证书错误,手动校验https证书
     *
     * @param cert https证书
     * @return true通过,false失败
     */
    public static String getSSLCertFromServer(SslCertificate cert) {
        Bundle bundle = SslCertificate.saveState(cert);
        if (bundle != null) {
            byte[] bytes = bundle.getByteArray("x509-certificate");
            if (bytes != null) {
                try {
                    CertificateFactory cf = CertificateFactory.getInstance("X.509");
                    Certificate ca = cf.generateCertificate(new ByteArrayInputStream(bytes));
                    MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
                    byte[] key = sha256.digest(ca.getEncoded());
                    return bytesToHex(key);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }

    public static String getSSLCertSHA256FromCert(InputStream cerIn) {
        try {
            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            Certificate ca = cf.generateCertificate(cerIn);
            MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
            byte[] key = sha256.digest(ca.getEncoded());
            String sha256Str = bytesToHex(key);
            return sha256Str;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 十六进制转字节数组
     *
     * @param hex
     * @return
     */
    public static byte[] hexToBinary(String hex) {
        String string = "0123456789ABCDEF";
        char[] ch = hex.toCharArray();
        int len = ch.length / 2;
        byte byts[] = new byte[len];
        for (int i = 0; i < len; i++) {
            byts[i] = (byte) ((((byte) string.indexOf(Character
                    .toUpperCase(ch[i * 2])) & 0xFF) << 4)
                    | ((byte) string.indexOf(Character.toUpperCase(ch[i * 2 + 1])) & 0xFF));
        }
        return byts;
    }

    /**
     * 字节数组转十六进制字符串
     *
     * @param bytes
     * @return
     */
    public static String bytesToHex(byte[] bytes) {
        final char[] hexArray = {'0', '1', '2', '3', '4', '5', '6', '7', '8',
                '9', 'A', 'B', 'C', 'D', 'E', 'F'};
        char[] hexChars = new char[bytes.length * 2];
        int v;
        for (int j = 0; j < bytes.length; j++) {
            v = bytes[j] & 0xFF;
            hexChars[j * 2] = hexArray[v >>> 4];
            hexChars[j * 2 + 1] = hexArray[v & 0x0F];
        }
        return new String(hexChars);
    }
}

版权声明:
作者:Joe.Ye
链接:https://www.appblog.cn/index.php/2023/03/25/unidirectional-authentication-verification-for-https-communication-clients-in-android-to-prevent-man-in-the-middle-attacks/
来源:APP全栈技术分享
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
打赏
海报
Android中HTTPS通信客户端单向认证校验防止中间人攻击
引入HTTPS是为了解决HTTP所带来的三个问题: HTTP是明文传输,数据容易被窃取,因此要加密数据以防止数据中途窃取 认证服务器身份,确保数据发送到正确的服务……
<<上一篇
下一篇>>
文章目录
关闭
目 录