Android的证书验证过程

说明:本文分析源码基于Android_8.1
(还在看代码和修改文章的阶段,有点乱,慢慢来~)

0x01 系统证书

看源码可以发现,/system/ca-certificates/files 目录下存放了系统证书
系统证书命名规则为 <hash>.<N>,其中 hash 通过命令生成,N 为从 0 开始的一个整数标记

openssl x509 -subject_hash_old -in filename

对应在Android 系统中,证书存放路径为 /system/etc/security/cacerts 目录
ca-certificates文件均为 PEM 格式的 X509 证书格式,包含明文base64编码公钥,证书信息,哈希等

0x02 证书管理类

Android系统提供系列类用于根证书管理,位于 /frameworks/base/core/java/android/security/net/config 目录

CertificateSource
CertificateSource 是接口类,定义了对根证书可执行的获取和查询操作

public interface CertificateSource {
   
    Set<X509Certificate> getCertificates();
    X509Certificate findBySubjectAndPublicKey(X509Certificate cert);
    X509Certificate findByIssuerAndSignature(X509Certificate cert);
    Set<X509Certificate> findAllByIssuerAndSignature(X509Certificate cert);
    void handleTrustStorageUpdate();
}

他有三个实现类,分别是:

KeyStoreCertificateSource 从 mKeyStore 中获取证书

Set<X509Certificate> certificates = new ArraySet<>(mKeyStore.size());
for (Enumeration<String> en = mKeyStore.aliases(); en.hasMoreElements();) {
   
		String alias = en.nextElement();
		X509Certificate cert = (X509Certificate) mKeyStore.getCertificate(alias);
		if (cert != null) {
   
				certificates.add(cert);
				localIndex.index(cert);
		}
}

ResourceCertificateSource 基于 mResourceId 从资源目录读取文件并构造证书

Set<X509Certificate> certificates = new ArraySet<X509Certificate>();
Collection<? extends Certificate> certs;
InputStream in = null;
try {
   
		CertificateFactory factory = CertificateFactory.getInstance("X.509");
		in = mContext.getResources().openRawResource(mResourceId);
		certs = factory.generateCertificates(in);
} catch (CertificateException e) {
   
		throw new RuntimeException("Failed to load trust anchors from id " + mResourceId, e);
} finally {
   
		IoUtils.closeQuietly(in);
}
TrustedCertificateIndex indexLocal = new TrustedCertificateIndex();
for (Certificate cert : certs) {
   
		certificates.add((X509Certificate) cert);
		indexLocal.index((X509Certificate) cert);
}

DirectoryCertificateSource 则遍历指定的目录 mDir 读取证书

Set<X509Certificate> certs = new ArraySet<X509Certificate>();
if (mDir.isDirectory()) {
   
		for (String caFile : mDir.list()) {
   
				if (isCertMarkedAsRemoved(caFile)) {
   
						continue;
				}
				X509Certificate cert = readCertificate(caFile);
				if (cert != null) {
   
						certs.add(cert);
				}
		}
}

另外这是一个抽象类,还提供了一个抽象方法 isCertMarkedAsRemoved() 用于判断证书是否被移除,SystemCertificateSourceUserCertificateSource 分别定义了系统和用户根证书库的路径,并实现抽象方法

SystemCertificateSource() 构造函数定义了系统证书查询路径,同时还指定了另一个文件地址存放在变量 mUserRemovedCaDir 中

private SystemCertificateSource() {
   
  	// /system/etc/security/cacerts
		super(new File(System.getenv("ANDROID_ROOT") + "/etc/security/cacerts"));
  	// /data/misc/user/0/cacerts-removed
		File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
		mUserRemovedCaDir = new File(configDir, "cacerts-removed");
}    

判断证书是否移除就是直接判断证书文件是否存在于指定的目录

protected boolean isCertMarkedAsRemoved(String caFile) {
   
		return new File(mUserRemovedCaDir, caFile).exists();
}

同理,对于用户证书指定查询路径,而对于是否移除的判断总是返回 false

private UserCertificateSource() {
   
		// /data/misc/user/0/cacerts-added
		super(new File(Environment.getUserConfigDirectory(UserHandle.myUserId()), "cacerts-added"));
}
protected boolean isCertMarkedAsRemoved(String caFile) {
   
  	return false;
}
0x03 证书验证流程

对于证书验证流程,分为以下几个步骤来逐步分析,首先是根据之前分析的网络框架看系统如何建立安全的连接,接着我们重点看其中与安全相关的类,最后分析握手和证书验证过程具体是如何进行的。

3.1 建立安全的连接

之前分析 Android 的网络框架时了解到,Android 8.1 使用的网络框架是 HttpURLConnection,那么先从 HttpsURLConnection 类看起。
首先给出网络相关类的继承关系图:
HttpURLConnection
HttpsURLConnection 类继承自 HttpURLConnection​,在基本的网络通信功能之外,新增了两个内部成员类 HostnameVerifier​ 和 SSLSocketFactory​ 并给出默认的实现实例,以支持 https 的相关特性。

// HostnameVerifier
defaultHostnameVerifier = (HostnameVerifier)
                        Class.forName("com.android.okhttp.internal.tls.OkHostnameVerifier")
                        .getField("INSTANCE").get(null);

// SSLSocketFactory (具体实现后面会提到)
defaultSSLSocketFactory =
                (SSLSocketFactory)SSLSocketFactory.getDefault();

在进行网络连接前,系统会首先根据不同的协议返回不同的客户端网络连接代理类实例。如果是 https,则返回 HttpsURLConnectionImpl​ 对象,这个类继承自 ​DlegatingHttpsURLConnection​,而 DlegatingHttpsURLConnection​ 又正好是 HttpsURLConnection 的派生抽象类,对连接的相关方法做了一个代理转发,重点还定义了握手的抽象类对象 Handshake,就是在这个方法中,完成了 Https 协议中的握手操作

protected abstract Handshake handshake();

下面看 HttpsURLConnectionImpl 这个类,handshake()的实现为

// Handshake
return delegate.httpEngine.hasResponse()
  ? delegate.httpEngine.getResponse().handshake()
  : delegate.handshake;

注意,HttpsURLConnectionImpl​ 类聚合了 HttpURLConnectionImpl,因此上面方法中的 delegate 也还是 HttpURLConnectionImpl 类型对象,只不过这个对象包含了 SSL 相关属性,例如

@Override public void setHostnameVerifier(HostnameVerifier hostnameVerifier) {
  delegate.client.setHostnameVerifier(hostnameVerifier);
}

@Override public void setSSLSocketFactory(SSLSocketFactory sslSocketFactory) {
  delegate.client.setSslSocketFactory(sslSocketFactory);
}

对象创建完成之后,就开始建立连接。HttpURLConnectionImpl 实现了 getInputStream() 方法,实际上是调用 getResponse() 返回类型为响应消息的 HttpEngine 对象。而在 getResponse() 方法中,首先调用 initHttpEngine() 初始化 HttpEngine​ 对象,接着循环调用 execute(true) 直到建立连接

private boolean execute(boolean readResponse) throws IOException {
   
    boolean releaseConnection = true;
    ......
    try {
   
      httpEngine.sendRequest();
      Connection connection = httpEngine.getConnection();
      if (connection != null) {
   
        route = connection.getRoute();
        handshake = connection.getHandshake();
      } else {
   
        route = null;
        handshake = null;
      }
   ....
}

HttpEngine​sendRequest() 会真正调用 connect() 进行连接,这里会传入若干超时的参数,都是之前通过 OkHttpClient​ 设置的。

private HttpStream connect() throws RouteException, RequestException, IOException {
   
  boolean doExtensiveHealthChecks = !networkRequest.method().equals("GET");
  return streamAllocation.newStream(client.getConnectTimeout(),
                                    client.getReadTimeout(), client.getWriteTimeout(),
                                    client.getRetryOnConnectionFailure(),
                                    doExtensiveHealthChecks);
}

这里的 streamAllocationStreamAllocation​ 类型对象,newStream() 方法中去建立真正的连接,返回 HttpStream​ 类型对象。

public HttpStream newStream(int connectTimeout, int readTimeout, int writeTimeout,
      boolean connectionRetryEnabled, boolean doExtensiveHealthChecks)
      throws RouteException, IOException {
   
    try {
   
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, connectionRetryEnabled, doExtensiveHealthChecks);
      ......
    }
    ......
}

这里是循环判断连接是否 healthy

private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
                                             int writeTimeout, boolean connectionRetryEnabled,
                                             boolean doExtensiveHealthChecks)
  throws IOException, RouteException {
   
  while (true) {
   
    RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
                                              connectionRetryEnabled);
    // If this is a brand new connection, we can skip the extensive health checks.
    synchronized (connectionPool) {
   
      if (candidate.streamCount == 0) {
   
        return candidate;
      }
    }
    // Otherwise do a potentially-slow check to confirm that the pooled connection is still good.
    if (candidate.isHealthy(doExtensiveHealthChecks)) {
   
      return candidate;
    }
    connectionFailed();
  }
}

findConnection() 才是真正获取连接的逻辑,如果成员变量 connection 不为空直接返回,否则从 connectionPool 里面获取对应 address 的连接,如果还为空,那么就新建一个连接并放到 connectionPool 里面,最后终于走到连接的封装类 RealConnection 去调用 connect()

  private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
      boolean connectionRetryEnabled) throws IOException, RouteException {
   
    synchronized (connectionPool) {
   
      ......
      RealConnection allocatedConnection = this.connection;
      if (allocatedConnection != null && !allocatedConnection.noNewStreams) {
   
        return allocatedConnection;
      }

      // Attempt to get a connection from the pool.
      RealConnection pooledConnection = Internal.instance.get(connectionPool, address, this);
      if (pooledConnection != null) {
   
        this.connection = pooledConnection;
        return pooledConnection;
      }

      // Attempt to create a connection.
      if (routeSelector == null) {
   
        routeSelector = new RouteSelector(address, routeDatabase());
      }
    }

    Route route = routeSelector.next();
    RealConnection newConnection = new RealConnection(route);
    acquire(newConnection);

    synchronized (connectionPool) {
   
      Internal.instance.put(connectionPool, newConnection);
      this.connection = newConnection;
      if (canceled) throw new IOException("Canceled");
    }

    newConnection.connect(connectTimeout, readTimeout, writeTimeout, address.getConnectionSpecs(),
        connectionRetryEnabled);
    routeDatabase().connected(newConnection.getRoute());

    return newConnection;
  }

connect() 会调用 connectSocket(),然后判断如果设置了 SSLSocketFactory,就会继续调用 connectTls() 建立安全的连接。

public void connect(int connectTimeout, int readTimeout, int writeTimeout,
      List<ConnectionSpec> connectionSpecs
  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值