HTTP严格安全传输(HTTP Strict Transport Security, HSTS)chromuim实现源码分析(二)
HTTP strict transport security (HSTS) is defined in
http://tools.ietf.org/html/ietf-websec-strict-transport-sec
HTTP-based dynamic public key pinning (HPKP) is defined in
http://tools.ietf.org/html/ietf-websec-key-pinning.
深入理解需参考chromuim设计文档:
https://www.chromium.org/developers/design-documents
(中文版)https://ahangchen.gitbooks.io/chromium_doc_zh/content/zh//General_Architecture/Threading.html
chromuim在线源码:https://chromium.googlesource.com/chromium/src/+/58.0.3025.2/
------------------------------------------------分割线---------------------------------------------------------------------------------
通过观察文件名,发现涉及的文件主要是net目录下的:
Transport_security_persister.cc
Transport_security_persister.h
Transport_security_state.cc
Transport_security_state.h
Url_request_http_job.cc
在开始之前,需要从源码层面了解chromuim的网络栈部分,可参考《WebKit技术内幕》作者朱永盛的博文http://blog.csdn.net/milado_nju/article/details/9255563以及官网网络栈部分的文档http://www.chromium.org/developers/design-documents/network-stack。简单来说,发送网络请求基本都是通过URLRequest类,再根据不同协议选择不同的工厂,如HTTP为URLRequestHttpJob,由于HSTS针对HTTP,所以也只需关注URLRequestHttpJob;另一个重要的类是URLRequestContext,它包含其他完成URL请求的上下文信息,如cookie、主机解析、缓存以及HSTS信息等;许多URLRequest对象共享一个URLRequestContext。本文采用好理解的“自上而下”顺序来进行总结分析,但是实际中由于作者对chromuim完全没有认识,其实是从源码中搜索关键词看注释再查找引用一步一步摸索的。
类URLRequestHttpJob的Factory方法在创建实例前,调用了Url_request_http_job.cc中的MaybeInternallyRedirect()函数对是否要进行升级HSTS进行了判断,通过名字可以猜出chromuim是采用内部重定向的方式来实现HTTP到HTTPS升级的,这跟使用开发者工具调试时观察到的请求一致。
MaybeInternallyRedirect函数中,根据request->url()来判断该域名是否应该升级HSTS,若hsts->ShouldUpgradeToSSL返回false,就返回nullptr,不用重定向;否则,使用Replacements类,根据request->url()的协议,来进行替换为https或wss(即websocket),再返回307状态码的URLRequestRedirectJob类。因此,URLRequestHttpJob返回URLRequestRedirectJob而非正常的URLRequestHttpJob,进行重定向来升级协议。由此可见,hsts的ShouldUpgradeToSSL方法就至关重要了,是否升级https全看它的返回值了。
hsts是从request->context()->transport_security_state()取出,通过查看源码,即是URLRequest中的URLRequestContext指针类型变量context_中的TransportSecurityState*类型的transport_security_state变量。这句话有点绕,其实就是从取出了保存HSTS信息的变量,该变量(260行)类型为TransportSecurityState*,TransportSecurityState就是实现HSTS机制的重点了。另外,源码指针的实现也呼应了开始提到的多URLRequest对一个URLRequestContext。URLRequestContext
Transport_security_state.h中可以看到TransportSecurityState的定义,它驻留在内存中追踪哪个域名启用了HSTS和PKP,并且用SetDelegate方法注册了一个代理来存储状态到硬盘中。为了解一个类,发现从其对应的单元测试文件可以得到一个整体的感性认识,单元测试是开发者编写的一小段代码,用于检验被测代码的一个很小的、很明确的功能是否正确。从单元测试文件Transport_security_state_unittest.cc中可以看出该类的简单用法,以及作者已经考虑到的一些避免HSTS被绕过的情况。比如这里就考虑了域名末尾加“.”和不加“.”在本策略中是一样的。并且,除了subdomain选项和preload外, 还有一些极端情况,如下所述。(Google C++ Testing Framework可参考这个网址http://developer.51cto.com/art/201108/285290.htm)
(1)防止拒绝服务的产生(比如添加一个域名为“.”的情况),
(2)对大小写不敏感(Google.com与GooGLe.CoM是一样的),
(3)规则冲突覆盖以更具体的为准(example.com的subdomain为true,而foo.example.com的subdomain为false,那么sub.foo.example.com的HSTS情况应为false;这点浏览器的实现情况应引起网站管理员的注意,可测试网站有没有这样的漏洞,RFC标准里我不记得提到),
(4)若设置了subdomain,不规范子域名也应使用HSTS(如2\x01.foo.example.test返回true),
(5)域名删除的情况。
下面的这个810行的测试没看懂,为何exampl1.com一会儿期望为true,一会儿为false?猜想可能是在测试单元测试功能有没有正常工作吧。
--------------------------------------------------分割线-------------------------------------------------
由单元测试和之前的源码分析以及源码中的注释可以得到,TransportSecurityState的ShouldUpgradeToSSL是判断该URL是否应该升级HTTPS的方法。查看ShouldUpgradeToSSL方法,发现是根据STSState类型变量的ShouldUpgradeToSSL()来返回布尔值的,从名称看依次检查了动态HSTS和预置的HSTS(即preload,如www.google.com已经预置在浏览器里了)。
先看类STSState,他是一个内部类,实现非常简单,注释说明它描述了HSTS的状态,属性包括过期时间、include_subdomains标识、域名等,根据host来由GetDynamicSTSState和GetStaticDomainState更新。首先来看GetDynamicSTSState,该函数根据host查询保存的信息,来对result赋值,并且特别强调了以更具体的结果为准;通过源码,其进行了host的规范化,然后用迭代器和enabled_sts_hosts_以及hash后的host(其实本地存储的HSTS信息文件中域名只有哈希后的值,为了隐私)来进行查询,并判断了查询结果中当前时间是否超过了过期时间,对结果result进行赋值为j->second,这样STSState就有值可以判断ShouldUpgradeToSSL了。ShouldUpgradeToSSL方法仅仅是比较upgrade_mode属性是否为MODE_FORCE_HTTPS。由此可见,STSStateMap类型的enabled_sts_hosts_就是本地维护HSTS信息的变量了,由此将我们引向了STSStateMap类。其实,若要发现实现细节方面的漏洞就需要详细看实现查找的算法了(CanonicalizeHost),对发现逻辑漏洞帮助不大,没有耐心的我就先略过算法细节分析。
bool TransportSecurityState::GetDynamicSTSState(const std::string& host, STSState* result) { DCHECK(CalledOnValidThread()); const std::string canonicalized_host = CanonicalizeHost(host); if (canonicalized_host.empty()) return false; base::Time current_time(base::Time::Now()); for (size_t i = 0; canonicalized_host[i]; i += canonicalized_host[i] + 1) { std::string host_sub_chunk(&canonicalized_host[i], canonicalized_host.size() - i); STSStateMap::iterator j = enabled_sts_hosts_.find(HashHost(host_sub_chunk)); if (j == enabled_sts_hosts_.end()) continue; // If the entry is invalid, drop it. if (current_time > j->second.expiry) { enabled_sts_hosts_.erase(j); DirtyNotify(); continue; } // If this is the most specific STS match, add it to the result. Note: a STS // entry at a more specific domain overrides a less specific domain whether // or not |include_subdomains| is set. if (current_time <= j->second.expiry) { if (i == 0 || j->second.include_subdomains) { *result = j->second; result->domain = DNSDomainToString(host_sub_chunk); return true; } break; } } return false; }