标签: 验证

  • SSL/TLS 协议运行机制的概述

    互联网的通信安全,建立在SSL/TLS协议之上。

    本文简要介绍SSL/TLS协议的运行机制。文章的重点是设计思想和运行过程,不涉及具体的实现细节。如果想了解这方面的内容,请参阅RFC文档

    be87125057d2eba95318914303391f76

    一、作用

    不使用SSL/TLS的HTTP通信,就是不加密的通信。所有信息明文传播,带来了三大风险。

    (1) 窃听风险(eavesdropping):第三方可以获知通信内容。

    (2) 篡改风险(tampering):第三方可以修改通信内容。

    (3) 冒充风险(pretending):第三方可以冒充他人身份参与通信。

    SSL/TLS协议是为了解决这三大风险而设计的,希望达到:

    (1) 所有信息都是加密传播,第三方无法窃听。

    (2) 具有校验机制,一旦被篡改,通信双方会立刻发现。

    (3) 配备身份证书,防止身份被冒充。

    互联网是开放环境,通信双方都是未知身份,这为协议的设计带来了很大的难度。而且,协议还必须能够经受所有匪夷所思的攻击,这使得SSL/TLS协议变得异常复杂。

    二、历史

    互联网加密通信协议的历史,几乎与互联网一样长。

    1994年,NetScape公司设计了SSL协议(Secure Sockets Layer)的1.0版,但是未发布。

    1995年,NetScape公司发布SSL 2.0版,很快发现有严重漏洞。

    1996年,SSL 3.0版问世,得到大规模应用。

    1999年,互联网标准化组织ISOC接替NetScape公司,发布了SSL的升级版TLS 1.0版。

    2006年和2008年,TLS进行了两次升级,分别为TLS 1.1版和TLS 1.2版。最新的变动是2011年TLS 1.2的修订版

    目前,应用最广泛的是TLS 1.0,接下来是SSL 3.0。但是,主流浏览器都已经实现了TLS 1.2的支持。

    TLS 1.0通常被标示为SSL 3.1,TLS 1.1为SSL 3.2,TLS 1.2为SSL 3.3。

    三、基本的运行过程

    SSL/TLS协议的基本思路是采用公钥加密法,也就是说,客户端先向服务器端索要公钥,然后用公钥加密信息,服务器收到密文后,用自己的私钥解密。

    但是,这里有两个问题。

    (1)如何保证公钥不被篡改?

    解决方法:将公钥放在数字证书中。只要证书是可信的,公钥就是可信的。

    (2)公钥加密计算量太大,如何减少耗用的时间?

    解决方法:每一次对话(session),客户端和服务器端都生成一个”对话密钥”(session key),用它来加密信息。由于”对话密钥”是对称加密,所以运算速度非常快,而服务器公钥只用于加密”对话密钥”本身,这样就减少了加密运算的消耗时间。

    因此,SSL/TLS协议的基本过程是这样的:

    (1) 客户端向服务器端索要并验证公钥。

    (2) 双方协商生成”对话密钥”。

    (3) 双方采用”对话密钥”进行加密通信。

    上面过程的前两步,又称为”握手阶段”(handshake)。

    四、握手阶段的详细过程

    6c37780313051e51974a7e8e26c37e7e

    “握手阶段”涉及四次通信,我们一个个来看。需要注意的是,”握手阶段”的所有通信都是明文的。

    4.1 客户端发出请求(ClientHello)

    首先,客户端(通常是浏览器)先向服务器发出加密通信的请求,这被叫做ClientHello请求。

    在这一步,客户端主要向服务器提供以下信息。

    (1) 支持的协议版本,比如TLS 1.0版。

    (2) 一个客户端生成的随机数,稍后用于生成”对话密钥”。

    (3) 支持的加密方法,比如RSA公钥加密。

    (4) 支持的压缩方法。

    这里需要注意的是,客户端发送的信息之中不包括服务器的域名。也就是说,理论上服务器只能包含一个网站,否则会分不清应该向客户端提供哪一个网站的数字证书。这就是为什么通常一台服务器只能有一张数字证书的原因。

    对于虚拟主机的用户来说,这当然很不方便。2006年,TLS协议加入了一个Server Name Indication扩展,允许客户端向服务器提供它所请求的域名。

    4.2 服务器回应(SeverHello)

    服务器收到客户端请求后,向客户端发出回应,这叫做SeverHello。服务器的回应包含以下内容。

    (1) 确认使用的加密通信协议版本,比如TLS 1.0版本。如果浏览器与服务器支持的版本不一致,服务器关闭加密通信。

    (2) 一个服务器生成的随机数,稍后用于生成”对话密钥”。

    (3) 确认使用的加密方法,比如RSA公钥加密。

    (4) 服务器证书。

    除了上面这些信息,如果服务器需要确认客户端的身份,就会再包含一项请求,要求客户端提供”客户端证书”。比如,金融机构往往只允许认证客户连入自己的网络,就会向正式客户提供USB密钥,里面就包含了一张客户端证书。

    4.3 客户端回应

    客户端收到服务器回应以后,首先验证服务器证书。如果证书不是可信机构颁布、或者证书中的域名与实际域名不一致、或者证书已经过期,就会向访问者显示一个警告,由其选择是否还要继续通信。

    如果证书没有问题,客户端就会从证书中取出服务器的公钥。然后,向服务器发送下面三项信息。

    (1) 一个随机数。该随机数用服务器公钥加密,防止被窃听。

    (2) 编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。

    (3) 客户端握手结束通知,表示客户端的握手阶段已经结束。这一项同时也是前面发送的所有内容的hash值,用来供服务器校验。

    上面第一项的随机数,是整个握手阶段出现的第三个随机数,又称”pre-master key”。有了它以后,客户端和服务器就同时有了三个随机数,接着双方就用事先商定的加密方法,各自生成本次会话所用的同一把”会话密钥”。

    至于为什么一定要用三个随机数,来生成”会话密钥”,dog250解释得很好:

    “不管是客户端还是服务器,都需要随机数,这样生成的密钥才不会每次都一样。由于SSL协议中证书是静态的,因此十分有必要引入一种随机因素来保证协商出来的密钥的随机性。

    对于RSA密钥交换算法来说,pre-master-key本身就是一个随机数,再加上hello消息中的随机,三个随机数通过一个密钥导出器最终导出一个对称密钥。

    pre master的存在在于SSL协议不信任每个主机都能产生完全随机的随机数,如果随机数不随机,那么pre master secret就有可能被猜出来,那么仅适用pre master secret作为密钥就不合适了,因此必须引入新的随机因素,那么客户端和服务器加上pre master secret三个随机数一同生成的密钥就不容易被猜出了,一个伪随机可能完全不随机,可是是三个伪随机就十分接近随机了,每增加一个自由度,随机性增加的可不是一。”

    此外,如果前一步,服务器要求客户端证书,客户端会在这一步发送证书及相关信息。

    4.4 服务器的最后回应

    服务器收到客户端的第三个随机数pre-master key之后,计算生成本次会话所用的”会话密钥”。然后,向客户端最后发送下面信息。

    (1)编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。

    (2)服务器握手结束通知,表示服务器的握手阶段已经结束。这一项同时也是前面发送的所有内容的hash值,用来供客户端校验。

    至此,整个握手阶段全部结束。接下来,客户端与服务器进入加密通信,就完全是使用普通的HTTP协议,只不过用”会话密钥”加密内容。

    8ac24c47a63c7eb487e019394dbb2e5d

    五、参考链接

    相关文章

    转载请注明:爱开源 » SSL/TLS 协议运行机制的概述

  • Linux下使用.sig签名文件验证签名

    网上一些下载资源会同时提供下载资源名称加”.sig”为文件名的分离签名文件,用来校验下载资源的完整性。

      以grub为例,当前最新版本的grub为2.00版本,可从 [ftp://ftp.gnu.org/gnu/grub/](ftp://ftp.gnu.org/gnu/grub/) 下载,有两个文件:grub-2.00.tar.gz.sig和grub-2.00.tar.gz。
    

    验证方法:

    $ gpg --verify grub-2.00.tar.gz.sig grub-2.00.tar.gz
    

    gpg: 于 2012年06月28日 星期四 08时11分54秒 CST 创建的签名,使用 DSA,钥匙号 E82E4209
    gpg: 无法检查签名:找不到公钥

    这说明找不到对应的公钥,同时会提示当前验证的钥匙号为 E82E4209,根据这个钥匙号导入公钥:

    $ gpg --recv-keys E82E4209
    
    gpg: 下载密钥‘E82E4209’,从 hkp 服务器 keys.gnupg.net
    gpg: 密钥 E82E4209:公钥“Vladimir 'phcoder' Serbinenko <phcoder@gmail.com>”已导入
    gpg: 没有找到任何绝对信任的密钥
    gpg: 合计被处理的数量:1
    gpg: 已导入:1
    
    $ gpg --verify --verbose grub-2.00.tar.gz.sig grub-2.00.tar.gz
    

    gpg: 于 2012年06月28日 星期四 08时11分54秒 CST 创建的签名,使用 DSA,钥匙号 E82E4209
    gpg: 使用 PGP 信任模型
    gpg: 完好的签名,来自于“Vladimir ‘phcoder’ Serbinenko phcoder@gmail.com
    gpg: 警告:这把密钥未经受信任的签名认证!
    gpg: 没有证据表明这个签名属于它所声称的持有者。
    主钥指纹: E53D 497F 3FA4 2AD8 C9B4 D1E8 35A9 3B74 E82E 4209
    gpg: 二进制 签名,散列算法 SHA512

    参考:http://bbs.chinaunix.net/thread-1882309-1-1.html

    参考:http://www.aikaiyuan.com/10331.html

    相关文章

    转载请注明:爱开源 » Linux下使用.sig签名文件验证签名

  • RESTful API 设计指南

    网络应用程序,分为前端和后端两个部分。当前的发展趋势,就是前端设备层出不穷(手机、平板、桌面电脑、其他专用设备……)。

    因此,必须有一种统一的机制,方便不同的前端设备与后端进行通信。这导致API构架的流行,甚至出现“API First”的设计思想。RESTful API是目前比较成熟的一套互联网应用程序的API设计理论。我以前写过一篇《理解RESTful架构》,探讨如何理解这个概念。

    今天,我将介绍RESTful API的设计细节,探讨如何设计一套合理、好用的API。我的主要参考了两篇文章(12)。

    bg2014052201

    一、协议

    API与用户的通信协议,总是使用HTTPs协议

    二、域名

    应该尽量将API部署在专用域名之下。

    javascript
    <code class=" language-javascript">
    https<span class="token punctuation">:</span><span class="token operator">/</span><span class="token operator">/</span>api<span class="token punctuation">.</span>example<span class="token punctuation">.</span>com
    </code>

    如果确定API很简单,不会有进一步扩展,可以考虑放在主域名下。

    javascript
    <code class=" language-javascript">
    https<span class="token punctuation">:</span><span class="token operator">/</span><span class="token operator">/</span>example<span class="token punctuation">.</span>org<span class="token regex">/api/</span>
    </code>

    三、版本(Versioning)

    应该将API的版本号放入URL。

    javascript
    <code class=" language-javascript">
    https<span class="token punctuation">:</span><span class="token operator">/</span><span class="token operator">/</span>api<span class="token punctuation">.</span>example<span class="token punctuation">.</span>com<span class="token regex">/v1/</span>
    </code>

    另一种做法是,将版本号放在HTTP头信息中,但不如放入URL方便和直观。Github采用这种做法。

    四、路径(Endpoint)

    路径又称”终点”(endpoint),表示API的具体网址。

    在RESTful架构中,每个网址代表一种资源(resource),所以网址中不能有动词,只能有名词,而且所用的名词往往与数据库的表格名对应。一般来说,数据库中的表都是同种记录的”集合”(collection),所以API中的名词也应该使用复数。

    举例来说,有一个API提供动物园(zoo)的信息,还包括各种动物和雇员的信息,则它的路径应该设计成下面这样。

    • https://api.example.com/v1/zoos
    • https://api.example.com/v1/animals
    • https://api.example.com/v1/employees

    五、HTTP动词

    对于资源的具体操作类型,由HTTP动词表示。

    常用的HTTP动词有下面五个(括号里是对应的SQL命令)。

    • GET(SELECT):从服务器取出资源(一项或多项)。
    • POST(CREATE):在服务器新建一个资源。
    • PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
    • PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
    • DELETE(DELETE):从服务器删除资源。

    还有两个不常用的HTTP动词。

    • HEAD:获取资源的元数据。
    • OPTIONS:获取信息,关于资源的哪些属性是客户端可以改变的。

    下面是一些例子。

    • GET /zoos:列出所有动物园
    • POST /zoos:新建一个动物园
    • GET /zoos/ID:获取某个指定动物园的信息
    • PUT /zoos/ID:更新某个指定动物园的信息(提供该动物园的全部信息)
    • PATCH /zoos/ID:更新某个指定动物园的信息(提供该动物园的部分信息)
    • DELETE /zoos/ID:删除某个动物园
    • GET /zoos/ID/animals:列出某个指定动物园的所有动物
    • DELETE /zoos/ID/animals/ID:删除某个指定动物园的指定动物

    六、过滤信息(Filtering)

    如果记录数量很多,服务器不可能都将它们返回给用户。API应该提供参数,过滤返回结果。

    下面是一些常见的参数。

    • ?limit=10:指定返回记录的数量
    • ?offset=10:指定返回记录的开始位置。
    • ?page=2&per_page=100:指定第几页,以及每页的记录数。
    • ?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。
    • ?animal_type_id=1:指定筛选条件

    参数的设计允许存在冗余,即允许API路径和URL参数偶尔有重复。比如,GET /zoo/ID/animals 与 GET /animals?zoo_id=ID 的含义是相同的。

    七、状态码(Status Codes)

    服务器向用户返回的状态码和提示信息,常见的有以下一些(方括号中是该状态码对应的HTTP动词)。

    • 200 OK – [GET]:服务器成功返回用户请求的数据,该操作是幂等的(Idempotent)。
    • 201 CREATED – [POST/PUT/PATCH]:用户新建或修改数据成功。
    • 202 Accepted – [*]:表示一个请求已经进入后台排队(异步任务)
    • 204 NO CONTENT – [DELETE]:用户删除数据成功。
    • 400 INVALID REQUEST – [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的。
    • 401 Unauthorized – [*]:表示用户没有权限(令牌、用户名、密码错误)。
    • 403 Forbidden – [*] 表示用户得到授权(与401错误相对),但是访问是被禁止的。
    • 404 NOT FOUND – [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的。
    • 406 Not Acceptable – [GET]:用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)。
    • 410 Gone -[GET]:用户请求的资源被永久删除,且不会再得到的。
    • 422 Unprocesable entity – [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误。
    • 500 INTERNAL SERVER ERROR – [*]:服务器发生错误,用户将无法判断发出的请求是否成功。

    状态码的完全列表参见这里

    八、错误处理(Error handling)

    如果状态码是4xx,就应该向用户返回出错信息。一般来说,返回的信息中将error作为键名,出错信息作为键值即可。

    javascript
    <code class=" language-javascript">
    <span class="token punctuation">{</span>
    error<span class="token punctuation">:</span> <span class="token string">"Invalid API key"</span>
    <span class="token punctuation">}</span>
    </code>

    九、返回结果

    针对不同操作,服务器向用户返回的结果应该符合以下规范。

    • GET /collection:返回资源对象的列表(数组)
    • GET /collection/resource:返回单个资源对象
    • POST /collection:返回新生成的资源对象
    • PUT /collection/resource:返回完整的资源对象
    • PATCH /collection/resource:返回完整的资源对象
    • DELETE /collection/resource:返回一个空文档

    十、Hypermedia API

    RESTful API最好做到Hypermedia,即返回结果中提供链接,连向其他API方法,使得用户不查文档,也知道下一步应该做什么。

    比如,当用户向api.example.com的根目录发出请求,会得到这样一个文档。

    javascript
    <code class=" language-javascript">
    <span class="token punctuation">{</span><span class="token string">"link"</span><span class="token punctuation">:</span> <span class="token punctuation">{</span>
    <span class="token string">"rel"</span><span class="token punctuation">:</span> <span class="token string">"collection <a class="token url-link" href="https://www.example.com/zoos">https://www.example.com/zoos</a>"</span><span class="token punctuation">,</span>
    <span class="token string">"href"</span><span class="token punctuation">:</span> <span class="token string">"<a class="token url-link" href="https://api.example.com/zoos">https://api.example.com/zoos</a>"</span><span class="token punctuation">,</span>
    <span class="token string">"title"</span><span class="token punctuation">:</span> <span class="token string">"List of zoos"</span><span class="token punctuation">,</span>
    <span class="token string">"type"</span><span class="token punctuation">:</span> <span class="token string">"application/vnd.yourformat+json"</span>
    <span class="token punctuation">}</span><span class="token punctuation">}</span>
    </code>

    上面代码表示,文档中有一个link属性,用户读取这个属性就知道下一步该调用什么API了。rel表示这个API与当前网址的关系(collection关系,并给出该collection的网址),href表示API的路径,title表示API的标题,type表示返回类型。

    Hypermedia API的设计被称为HATEOAS。Github的API就是这种设计,访问api.github.com会得到一个所有可用API的网址列表。

    javascript
    <code class=" language-javascript">
    <span class="token punctuation">{</span>
    <span class="token string">"current_user_url"</span><span class="token punctuation">:</span> <span class="token string">"<a class="token url-link" href="https://api.github.com/user">https://api.github.com/user</a>"</span><span class="token punctuation">,</span>
    <span class="token string">"authorizations_url"</span><span class="token punctuation">:</span> <span class="token string">"<a class="token url-link" href="https://api.github.com/authorizations">https://api.github.com/authorizations</a>"</span><span class="token punctuation">,</span>
    <span class="token comment" spellcheck="true"> // ...
    </span><span class="token punctuation">}</span>
    </code>

    从上面可以看到,如果想获取当前用户的信息,应该去访问api.github.com/user,然后就得到了下面结果。

    javascript
    <code class=" language-javascript">
    <span class="token punctuation">{</span>
    <span class="token string">"message"</span><span class="token punctuation">:</span> <span class="token string">"Requires authentication"</span><span class="token punctuation">,</span>
    <span class="token string">"documentation_url"</span><span class="token punctuation">:</span> <span class="token string">"<a class="token url-link" href="https://developer.github.com/v3">https://developer.github.com/v3</a>"</span>
    <span class="token punctuation">}</span>
    </code>

    上面代码表示,服务器给出了提示信息,以及文档的网址。

    十一、其他

    (1)API的身份认证应该使用OAuth 2.0框架。

    (2)服务器返回的数据格式,应该尽量使用JSON,避免使用XML。

    转载请注明:爱开源 » RESTful API 设计指南

  • 数字证书原理

    文中首先解释了加密解密的一些基础知识和概念,然后通过一个加密通信过程的例子说明了加密算法的作用,以及数字证书的出现所起的作用。接着对数字证书做一个详细的解释,并讨论一下windows中数字证书的管理,最后演示使用makecert生成数字证书。如果发现文中有错误的地方,或者有什么地方说得不够清楚,欢迎指出!

    1、基础知识

    这部分内容主要解释一些概念和术语,最好是先理解这部分内容。

    1.1、公钥密码体制(public-key cryptography)

    公钥密码体制分为三个部分,公钥、私钥、加密解密算法,它的加密解密过程如下:

    • 加密:通过加密算法和公钥对内容(或者说明文)进行加密,得到密文。加密过程需要用到公钥。
    • 解密:通过解密算法和私钥对密文进行解密,得到明文。解密过程需要用到解密算法和私钥。注意,由公钥加密的内容,只能由私钥进行解密,也就是说,由公钥加密的内容,如果不知道私钥,是无法解密的。

    公钥密码体制的公钥和算法都是公开的(这是为什么叫公钥密码体制的原因),私钥是保密的。大家都以使用公钥进行加密,但是只有私钥的持有者才能解密。在实际的使用中,有需要的人会生成一对公钥和私钥,把公钥发布出去给别人使用,自己保留私钥。

    1.2、对称加密算法(symmetric key algorithms)

    在对称加密算法中,加密使用的密钥和解密使用的密钥是相同的。也就是说,加密和解密都是使用的同一个密钥。因此对称加密算法要保证安全性的话,密钥要做好保密,只能让使用的人知道,不能对外公开。这个和上面的公钥密码体制有所不同,公钥密码体制中加密是用公钥,解密使用私钥,而对称加密算法中,加密和解密都是使用同一个密钥,不区分公钥和私钥。

    // 密钥,一般就是一个字符串或数字,在加密或者解密时传递给加密/解密算法。前面在公钥密码体制中说到的公钥、私钥就是密钥,公钥是加密使用的密钥,私钥是解密使用的密钥。

    1.3、非对称加密算法(asymmetric key algorithms)

    在非对称加密算法中,加密使用的密钥和解密使用的密钥是不相同的。前面所说的公钥密码体制就是一种非对称加密算法,他的公钥和是私钥是不能相同的,也就是说加密使用的密钥和解密使用的密钥不同,因此它是一个非对称加密算法。

    1.4、RSA简介

    RSA是一种公钥密码体制,现在使用得很广泛。如果对RSA本身有兴趣的,后面看我有没有时间写个RSA的具体介绍。

    RSA密码体制是一种公钥密码体制,公钥公开,私钥保密,它的加密解密算法是公开的。 由公钥加密的内容可以并且只能由私钥进行解密,并且由私钥加密的内容可以并且只能由公钥进行解密。也就是说,RSA的这一对公钥、私钥都可以用来加密和解密,并且一方加密的内容可以由并且只能由对方进行解密

    1.5、签名和加密

    我们说加密,是指对某个内容加密,加密后的内容还可以通过解密进行还原。 比如我们把一封邮件进行加密,加密后的内容在网络上进行传输,接收者在收到后,通过解密可以还原邮件的真实内容。

    这里主要解释一下签名,签名就是在信息的后面再加上一段内容,可以证明信息没有被修改过,怎么样可以达到这个效果呢?一般是对信息做一个hash计算得到一个hash值,注意,这个过程是不可逆的,也就是说无法通过hash值得出原来的信息内容。在把信息发送出去时,把这个hash值加密后做为一个签名和信息一起发出去。 接收方在收到信息后,会重新计算信息的hash值,并和信息所附带的hash值(解密后)进行对比,如果一致,就说明信息的内容没有被修改过,因为这里hash计算可以保证不同的内容一定会得到不同的hash值,所以只要内容一被修改,根据信息内容计算的hash值就会变化。当然,不怀好意的人也可以修改信息内容的同时也修改hash值,从而让它们可以相匹配,为了防止这种情况,hash值一般都会加密后(也就是签名)再和信息一起发送,以保证这个hash值不被修改。至于如何让别人可以解密这个签名,这个过程涉及到数字证书等概念,我们后面在说到数字证书时再详细说明,这里您先只需先理解签名的这个概念。

    2、一个加密通信过程的演化

    我们来看一个例子,现在假设“服务器”和“客户”要在网络上通信,并且他们打算使用RSA(参看前面的RSA简介)来对通信进行加密以保证谈话内容的安全。由于是使用RSA这种公钥密码体制,“服务器”需要对外发布公钥(算法不需要公布,RSA的算法大家都知道),自己留着私钥。“客户”通过某些途径拿到了“服务器”发布的公钥,客户并不知道私钥。“客户”具体是通过什么途径获取公钥的,我们后面再来说明,下面看一下双方如何进行保密的通信:

    2.1 第一回合:

    “客户”->“服务器”:你好

    “服务器”->“客户”:你好,我是服务器

    “客户”->“服务器”:????

    因为消息是在网络上传输的,有人可以冒充自己是“服务器”来向客户发送信息。例如上面的消息可以被黑客截获如下:

    “客户”->“服务器”:你好

    “服务器”->“客户”:你好,我是服务器

    “客户”->“黑客”:你好 // 黑客在“客户”和“服务器”之间的某个路由器上截获“客户”发给服务器的信息,然后自己冒充“服务器”

    “黑客”->“客户”:你好,我是服务器

    因此“客户”在接到消息后,并不能肯定这个消息就是由“服务器”发出的,某些“黑客”也可以冒充“服务器”发出这个消息。如何确定信息是由“服务器”发过来的呢?有一个解决方法,因为只有服务器有私钥,所以如果只要能够确认对方有私钥,那么对方就是“服务器”。因此通信过程可以改进为如下:

    2.2 第二回合:

    “客户”->“服务器”:你好

    “服务器”->“客户”:你好,我是服务器

    “客户”->“服务器”:向我证明你就是服务器

    “服务器”->“客户”:你好,我是服务器 {你好,我是服务器}[私钥|RSA]

    // 意这里约定一下,{} 表示RSA加密后的内容,[ | ]表示用什么密钥和算法进行加密,后面的示例中都用这种表示方式,例如上面的 {你好,我是服务器}[私钥|RSA]就表示用私钥对“你好,我是服务器”进行加密后的结果。

    为了向“客户”证明自己是“服务器”, “服务器”把一个字符串用自己的私钥加密,把明文和加密后的密文一起发给“客户”。对于这里的例子来说,就是把字符串 “你好,我是服务器”和这个字符串用私钥加密后的内容 {你好,我是服务器}[私钥|RSA] 发给客户。

    “客户”收到信息后,她用自己持有的公钥解密密文,和明文进行对比,如果一致,说明信息的确是由服务器发过来的。也就是说“客户”把 {你好,我是服务器}[私钥|RSA] 这个内容用公钥进行解密,然后和“你好,我是服务器”对比。因为由“服务器”用私钥加密后的内容,由并且只能由公钥进行解密,私钥只有“服务器”持有,所以如果解密出来的内容是能够对得上的,那说明信息一定是从“服务器”发过来的。

    假设“黑客”想冒充“服务器”:

    “黑客”->“客户”:你好,我是服务器

    “客户”->“黑客”:向我证明你就是服务器

    “黑客”->“客户”:你好,我是服务器 {你好,我是服务器}[???|RSA] //这里黑客无法冒充,因为他不知道私钥,无法用私钥加密某个字符串后发送给客户去验证。

    “客户”->“黑客”:????

    由于“黑客”没有“服务器”的私钥,因此它发送过去的内容,“客户”是无法通过服务器的公钥解密的,因此可以认定对方是个冒牌货!

    到这里为止,“客户”就可以确认“服务器”的身份了,可以放心和“服务器”进行通信,但是这里有一个问题,通信的内容在网络上还是无法保密。为什么无法保密呢?通信过程不是可以用公钥、私钥加密吗?其实用RSA的私钥和公钥是不行的,我们来具体分析下过程,看下面的演示:

    2.3 第三回合:

    “客户”->“服务器”:你好

    “服务器”->“客户”:你好,我是服务器

    “客户”->“服务器”:向我证明你就是服务器

    “服务器”->“客户”:你好,我是服务器 {你好,我是服务器}[私钥|RSA]

    “客户”->“服务器”:{我的帐号是aaa,密码是123,把我的余额的信息发给我看看}[公钥|RSA]

    “服务器”->“客户”:{你的余额是100元}[私钥|RSA]

    注意上面的的信息 {你的余额是100元}[私钥],这个是“服务器”用私钥加密后的内容,但是我们之前说了,公钥是发布出去的,因此所有的人都知道公钥,所以除了“客户”,其它的人也可以用公钥对{你的余额是100元}[私钥]进行解密。所以如果“服务器”用私钥加密发给“客户”,这个信息是无法保密的,因为只要有公钥就可以解密这内容。然而“服务器”也不能用公钥对发送的内容进行加密,因为“客户”没有私钥,发送个“客户”也解密不了。

    这样问题就又来了,那又如何解决呢?在实际的应用过程,一般是通过引入对称加密来解决这个问题,看下面的演示:

    2.4 第四回合:

    “客户”->“服务器”:你好

    “服务器”->“客户”:你好,我是服务器

    “客户”->“服务器”:向我证明你就是服务器

    “服务器”->“客户”:你好,我是服务器 {你好,我是服务器}[私钥|RSA]

    “客户”->“服务器”:{我们后面的通信过程,用对称加密来进行,这里是对称加密算法和密钥}[公钥|RSA] //蓝色字体的部分是对称加密的算法和密钥的具体内容,客户把它们发送给服务器。

    “服务器”->“客户”:{OK,收到!}[密钥|对称加密算法]

    “客户”->“服务器”:{我的帐号是aaa,密码是123,把我的余额的信息发给我看看}[密钥|对称加密算法]

    “服务器”->“客户”:{你的余额是100元}[密钥|对称加密算法]

    在上面的通信过程中,“客户”在确认了“服务器”的身份后,“客户”自己选择一个对称加密算法和一个密钥,把这个对称加密算法和密钥一起用公钥加密后发送给“服务器”。注意,由于对称加密算法和密钥是用公钥加密的,就算这个加密后的内容被“黑客”截获了,由于没有私钥,“黑客”也无从知道对称加密算法和密钥的内容。

    由于是用公钥加密的,只有私钥能够解密,这样就可以保证只有服务器可以知道对称加密算法和密钥,而其它人不可能知道(这个对称加密算法和密钥是“客户”自己选择的,所以“客户”自己当然知道如何解密加密)。这样“服务器”和“客户”就可以用对称加密算法和密钥来加密通信的内容了。

    总结一下,RSA加密算法在这个通信过程中所起到的作用主要有两个:

    • 因为私钥只有“服务器”拥有,因此“客户”可以通过判断对方是否有私钥来判断对方是否是“服务器”。
    • 客户端通过RSA的掩护,安全的和服务器商量好一个对称加密算法和密钥来保证后面通信过程内容的安全。

    如果这里您理解了为什么不用RSA去加密通信过程,而是要再确定一个对称加密算法来保证通信过程的安全,那么就说明前面的内容您已经理解了。(如果不清楚,再看下2.3和2.4,如果还是不清楚,那应该是我们说清楚,您可以留言提问。)

    到这里,“客户”就可以确认“服务器”的身份,并且双方的通信内容可以进行加密,其他人就算截获了通信内容,也无法解密。的确,好像通信的过程是比较安全了。

    但是这里还留有一个问题,在最开始我们就说过,“服务器”要对外发布公钥,那“服务器”如何把公钥发送给“客户”呢?我们第一反应可能会想到以下的两个方法:

    a)把公钥放到互联网的某个地方的一个下载地址,事先给“客户”去下载。

    b)每次和“客户”开始通信时,“服务器”把公钥发给“客户”。

    但是这个两个方法都有一定的问题,

    对于a)方法,“客户”无法确定这个下载地址是不是“服务器”发布的,你凭什么就相信这个地址下载的东西就是“服务器”发布的而不是别人伪造的呢,万一下载到一个假的怎么办?另外要所有的“客户”都在通信前事先去下载公钥也很不现实。

    对于b)方法,也有问题,因为任何人都可以自己生成一对公钥和私钥,他只要向“客户”发送他自己的私钥就可以冒充“服务器”了。示意如下:

    “客户”->“黑客”:你好 //黑客截获“客户”发给“服务器”的消息

    “黑客”->“客户”:你好,我是服务器,这个是我的公钥 //黑客自己生成一对公钥和私钥,把公钥发给“客户”,自己保留私钥

    “客户”->“黑客”:向我证明你就是服务器

    “黑客”->“客户”:你好,我是服务器 {你好,我是服务器}[黑客自己的私钥|RSA] //客户收到“黑客”用私钥加密的信息后,是可以用“黑客”发给自己的公钥解密的,从而会误认为“黑客”是“服务器”

    因此“黑客”只需要自己生成一对公钥和私钥,然后把公钥发送给“客户”,自己保留私钥,这样由于“客户”可以用黑客的公钥解密黑客的私钥加密的内容,“客户”就会相信“黑客”是“服务器”,从而导致了安全问题。这里问题的根源就在于,大家都可以生成公钥、私钥对,无法确认公钥对到底是谁的。如果能够确定公钥到底是谁的,就不会有这个问题了。例如,如果收到“黑客”冒充“服务器”发过来的公钥,经过某种检查,如果能够发现这个公钥不是“服务器”的就好了。

    为了解决这个问题,数字证书出现了,它可以解决我们上面的问题。先大概看下什么是数字证书,一个证书包含下面的具体内容:

    • 证书的发布机构
    • 证书的有效期
    • 公钥
    • 证书所有者(Subject)
    • 签名所使用的算法
    • 指纹以及指纹算法

    证书的内容的详细解释会在后面详细解释,这里先只需要搞清楚一点,数字证书可以保证数字证书里的公钥确实是这个证书的所有者(Subject)的,或者证书可以用来确认对方的身份。也就是说,我们拿到一个数字证书,我们可以判断出这个数字证书到底是谁的。至于是如何判断的,后面会在详细讨论数字证书时详细解释。现在把前面的通信过程使用数字证书修改为如下:

    2.5 第五回合:

    “客户”->“服务器”:你好

    “服务器”->“客户”:你好,我是服务器,这里是我的数字证书 //这里用证书代替了公钥

    “客户”->“服务器”:向我证明你就是服务器

    “服务器”->“客户”:你好,我是服务器 {你好,我是服务器}[私钥|RSA]

    注意,上面第二次通信,“服务器”把自己的证书发给了“客户”,而不是发送公钥。“客户”可以根据证书校验这个证书到底是不是“服务器”的,也就是能校验这个证书的所有者是不是“服务器”,从而确认这个证书中的公钥的确是“服务器”的。后面的过程和以前是一样,“客户”让“服务器”证明自己的身份,“服务器”用私钥加密一段内容连同明文一起发给“客户”,“客户”把加密内容用数字证书中的公钥解密后和明文对比,如果一致,那么对方就确实是“服务器”,然后双方协商一个对称加密来保证通信过程的安全。到这里,整个过程就完整了,我们回顾一下:

    2.6 完整过程:

    step1: “客户”向服务端发送一个通信请求

    “客户”->“服务器”:你好

    step2: “服务器”向客户发送自己的数字证书。证书中有一个公钥用来加密信息,私钥由“服务器”持有

    “服务器”->“客户”:你好,我是服务器,这里是我的数字证书

    step3: “客户”收到“服务器”的证书后,它会去验证这个数字证书到底是不是“服务器”的,数字证书有没有什么问题,数字证书如果检查没有问题,就说明数字证书中的公钥确实是“服务器”的。检查数字证书后,“客户”会发送一个随机的字符串给“服务器”用私钥去加密,服务器把加密的结果返回给“客户”,“客户”用公钥解密这个返回结果,如果解密结果与之前生成的随机字符串一致,那说明对方确实是私钥的持有者,或者说对方确实是“服务器”。

    “客户”->“服务器”:向我证明你就是服务器,这是一个随机字符串 //前面的例子中为了方便解释,用的是“你好”等内容,实际情况下一般是随机生成的一个字符串。

    “服务器”->“客户”:{一个随机字符串}[私钥|RSA]

    step4: 验证“服务器”的身份后,“客户”生成一个对称加密算法和密钥,用于后面的通信的加密和解密。这个对称加密算法和密钥,“客户”会用公钥加密后发送给“服务器”,别人截获了也没用,因为只有“服务器”手中有可以解密的私钥。这样,后面“服务器”和“客户”就都可以用对称加密算法来加密和解密通信内容了。

    “服务器”->“客户”:{OK,已经收到你发来的对称加密算法和密钥!有什么可以帮到你的?}[密钥|对称加密算法]

    “客户”->“服务器”:{我的帐号是aaa,密码是123,把我的余额的信息发给我看看}[密钥|对称加密算法]

    “服务器”->“客户”:{你好,你的余额是100元}[密钥|对称加密算法]

    …… //继续其它的通信

    2.7 其它问题:

    上面的过程已经十分接近HTTPS的真实通信过程了,完全可以按照这个过程去理解HTTPS的工作原理。但是我为了方便解释,上面有些细节没有说到,有兴趣的人可以看下这部分的内容。可以跳过不看,无关紧要。

    【问题1】

    上面的通信过程中说到,在检查完证书后,“客户”发送一个随机的字符串给“服务器”去用私钥加密,以便判断对方是否真的持有私钥。但是有一个问题,“黑客”也可以发送一个字符串给“服务器”去加密并且得到加密后的内容,这样对于“服务器”来说是不安全的,因为黑客可以发送一些简单的有规律的字符串给“服务器”加密,从而寻找加密的规律,有可能威胁到私钥的安全。所以说,“服务器”随随便便用私钥去加密一个来路不明的字符串并把结果发送给对方是不安全的。

    〖解决方法〗

    每次收到“客户”发来的要加密的的字符串时,“服务器”并不是真正的加密这个字符串本身,而是把这个字符串进行一个hash计算,加密这个字符串的hash值(不加密原来的字符串)后发送给“客户”,“客户”收到后解密这个hash值并自己计算字符串的hash值然后进行对比是否一致。也就是说,“服务器”不直接加密收到的字符串,而是加密这个字符串的一个hash值,这样就避免了加密那些有规律的字符串,从而降低被破解的机率。“客户”自己发送的字符串,因此它自己可以计算字符串的hash值,然后再把“服务器”发送过来的加密的hash值和自己计算的进行对比,同样也能确定对方是否是“服务器”。

    【问题2】

    在双方的通信过程中,“黑客”可以截获发送的加密了的内容,虽然他无法解密这个内容,但是他可以捣乱,例如把信息原封不动的发送多次,扰乱通信过程。

    〖解决方法〗

    可以给通信的内容加上一个序号或者一个随机的值,如果“客户”或者“服务器”接收到的信息中有之前出现过的序号或者随机值,那么说明有人在通信过程中重发信息内容进行捣乱,双方会立刻停止通信。有人可能会问,如果有人一直这么捣乱怎么办?那不是无法通信了? 答案是的确是这样的,例如有人控制了你连接互联网的路由器,他的确可以针对你。但是一些重要的应用,例如军队或者政府的内部网络,它们都不使用我们平时使用的公网,因此一般人不会破坏到他们的通信。

    【问题3】

    在双方的通信过程中,“黑客”除了简单的重复发送截获的消息之外,还可以修改截获后的密文修改后再发送,因为修改的是密文,虽然不能完全控制消息解密后的内容,但是仍然会破坏解密后的密文。因此发送过程如果黑客对密文进行了修改,“客户”和“服务器”是无法判断密文是否被修改的。虽然不一定能达到目的,但是“黑客”可以一直这样碰碰运气。

    〖解决方法〗

    在每次发送信息时,先对信息的内容进行一个hash计算得出一个hash值,将信息的内容和这个hash值一起加密后发送。接收方在收到后进行解密得到明文的内容和hash值,然后接收方再自己对收到信息内容做一次hash计算,与收到的hash值进行对比看是否匹配,如果匹配就说明信息在传输过程中没有被修改过。如果不匹配说明中途有人故意对加密数据进行了修改,立刻中断通话过程后做其它处理。

    3. 证书的构成和原理
    3.1 证书的构成和原理

    之前已经大概说了一个证书由什么构成,但是没有仔细进行介绍,这里对证书的内容做一个详细的介绍。先看下一个证书到底是个什么东西,在windows下查看一个证书时,界面是这样的,我们主要关注一下Details Tab页,其中的内容比较长,我滚动内容后后抓了三个图,把完整的信息显示出来:

    b1

    里面的内容比较多——Version、Serial number、Signature algorithm 等等,挑几个重要的解释一下。

    ◆Issuer (证书的发布机构)

    指出是什么机构发布的这个证书,也就是指明这个证书是哪个公司创建的(只是创建证书,不是指证书的使用者)。对于上面的这个证书来说,就是指”SecureTrust CA”这个机构。

    ◆Valid from , Valid to (证书的有效期)

    也就是证书的有效时间,或者说证书的使用期限。 过了有效期限,证书就会作废,不能使用了。

    ◆Public key (公钥)

    这个我们在前面介绍公钥密码体制时介绍过,公钥是用来对消息进行加密的,第2章的例子中经常用到的。这个数字证书的公钥是2048位的,它的值可以在图的中间的那个对话框中看得到,是很长的一串数字。

    ◆Subject (主题)

    这个证书是发布给谁的,或者说证书的所有者,一般是某个人或者某个公司名称、机构的名称、公司网站的网址等。 对于这里的证书来说,证书的所有者是Trustwave这个公司。

    ◆Signature algorithm (签名所使用的算法)

    就是指的这个数字证书的数字签名所使用的加密算法,这样就可以使用证书发布机构的证书里面的公钥,根据这个算法对指纹进行解密。指纹的加密结果就是数字签名(第1.5节中解释过数字签名)。

    ◆Thumbprint, Thumbprint algorithm (指纹以及指纹算法)

    这个是用来保证证书的完整性的,也就是说确保证书没有被修改过,这东西的作用和2.7中说到的第3个问题类似。 其原理就是在发布证书时,发布者根据指纹算法(一个hash算法)计算整个证书的hash值(指纹)并和证书放在一起,使用者在打开证书时,自己也根据指纹算法计算一下证书的hash值(指纹),如果和刚开始的值对得上,就说明证书没有被修改过,因为证书的内容被修改后,根据证书的内容计算的出的hash值(指纹)是会变化的。 注意,这个指纹会使用”SecureTrust CA”这个证书机构的私钥用签名算法(Signature algorithm)加密后和证书放在一起。

    注意,为了保证安全,在证书的发布机构发布证书时,证书的指纹和指纹算法,都会加密后再和证书放到一起发布,以防有人修改指纹后伪造相应的数字证书。这里问题又来了,证书的指纹和指纹算法用什么加密呢?他们是用证书发布机构的私钥进行加密的。可以用证书发布机构的公钥对指纹和指纹算法解密,也就是说证书发布机构除了给别人发布证书外,他自己本身也有自己的证书。证书发布机构的证书是哪里来的呢???这个证书发布机构的数字证书(一般由他自己生成)在我们的操作系统刚安装好时(例如windows xp等操作系统),这些证书发布机构的数字证书就已经被微软(或者其它操作系统的开发机构)安装在操作系统中了,微软等公司会根据一些权威安全机构的评估选取一些信誉很好并且通过一定的安全认证的证书发布机构,把这些证书发布机构的证书默认就安装在操作系统里面了,并且设置为操作系统信任的数字证书。这些证书发布机构自己持有与他自己的数字证书对应的私钥,他会用这个私钥加密所有他发布的证书的指纹作为数字签名。

    3.2 如何向证书的发布机构去申请证书

    举个例子方便大家理解,假设我们公司”ABC Company”花了1000块钱,向一个证书发布机构”SecureTrust CA”为我们自己的公司”ABC Company”申请了一张证书,注意,这个证书发布机构”SecureTrust CA”是一个大家公认并被一些权威机构接受的证书发布机构,我们的操作系统里面已经安装了”SecureTrust CA”的证书。”SecureTrust CA”在给我们发布证书时,把Issuer,Public key,Subject,Valid from,Valid to等信息以明文的形式写到证书里面,然后用一个指纹算法计算出这些数字证书内容的一个指纹,并把指纹和指纹算法用自己的私钥进行加密,然后和证书的内容一起发布,同时”SecureTrust CA”还会给一个我们公司”ABC Company”的私钥给到我们。我们花了1000块钱买的这个证书的内容如下:

    ×××××××××××××××证书内容开始×××××××××××××××××

    Issuer : SecureTrust CA

    Subject : ABC Company

    Valid from : 某个日期

    Valid to: 某个日期

    Public Key : 一串很长的数字

    …… 其它的一些证书内容……

    {证书的指纹和计算指纹所使用的指纹算法}[SecureTrust CA的私钥|RSA] //这个就是”SecureTrust CA”对这个证书的一个数字签名,表示这个证书确实是他发布的,有什么问题他会负责(收了我们1000块,出了问题肯定要负责任的)

    ×××××××××××××××证书内容结束×××××××××××××××××

    // 记不记得前面的约定?{} 表示RSA加密后的内容,[ | ]表示用什么密钥和算法进行加密

    我们”ABC Company”申请到这个证书后,我们把证书投入使用,我们在通信过程开始时会把证书发给对方,对方如何检查这个证书的确是合法的并且是我们”ABC Company”公司的证书呢?首先应用程序(对方通信用的程序,例如IE、OUTLook等)读取证书中的Issuer(发布机构)为”SecureTrust CA” ,然后会在操作系统中受信任的发布机构的证书中去找”SecureTrust CA”的证书,如果找不到,那说明证书的发布机构是个水货发布机构,证书可能有问题,程序会给出一个错误信息。 如果在系统中找到了”SecureTrust CA”的证书,那么应用程序就会从证书中取出”SecureTrust CA”的公钥,然后对我们”ABC Company”公司的证书里面的指纹和指纹算法用这个公钥进行解密,然后使用这个指纹算法计算”ABC Company”证书的指纹,将这个计算的指纹与放在证书中的指纹对比,如果一致,说明”ABC Company”的证书肯定没有被修改过并且证书是”SecureTrust CA” 发布的,证书中的公钥肯定是”ABC Company”的。对方然后就可以放心的使用这个公钥和我们”ABC Company”进行通信了。

    ★这个部分非常重要,一定要理解,您可以重新回顾一下之前的两章“1、基础知识”和“ 2、一个加密通信过程的演化”,然后再来理解这部分的内容。如果您把这节的内容看了几遍还没有搞懂证书的工作原理,您可以留言指出我没有说清楚的内容,我好方便进行修正。

    3.3 证书的发布机构

    前面已经初步介绍了一下证书发布机构,这里再深入讨论一下。

    其实所有的公司都可以发布证书,我们自己也可以去注册一家公司来专门给别人发布证书。但是很明显,我们自己的专门发布证书的公司是不会被那些国际上的权威机构认可的,人家怎么知道你是不是个狗屁皮包公司?因此微软在它的操作系统中,并不会信任我们这个证书发布机构,当应用程序在检查证书的合法信的时候,一看证书的发布机构并不是操作系统所信任的发布机构,就会抛出错误信息。也就是说windows操作系统中不会预先安装好我们这个证书发布机构的证书,不信任我们这个发布机构。

    不受信任的证书发布机构的危害

    为什么一个证书发布机构受不受信任这么重要?我们举个例子。假设我们开了一个狗屁公司来为别人发布证书,并且我和微软有一腿,微软在他们的操作系统中把我设置为了受信任的证书发布机构。现在如果有个小公司叫Wicrosoft 花了10块钱让我为他们公司申请了一个证书,并且公司慢慢壮大,证书的应用范围也越来越广。然后有个奸商的公司JS Company想冒充Wicrosoft,于是给了我¥10000,让我为他们颁布一个证书,但是证书的名字(Subject)要写Wicrosoft,假如我为了这¥10000,真的把证书给了他们,那么他们以后就可以使用这个证书来冒充Wicrosoft了。

    如果是一个优秀的证书发布机构,比如你要向他申请一个名字叫Wicrosoft的证书,它会让你提供很多资料证明你确实可以代表Wicrosoft这个公司,也就是说他回去核实你的身份。证书发布机构是要为他发布出的证书负法律责任的。

    到这里,你可能会想,TMD,那我们自己就不能发布证书吗?就一定要花钱去申请?当然不是,我们自己也可以成立证书发布机构,但是需要通过一些安全认证等等,只是有点麻烦。另外,如果数字证书只是要在公司内部使用,公司可以自己给自己生成一个证书,在公司的所有机器上把这个证书设置为操作系统信任的证书发布机构的证书(这句话仔细看清楚,有点绕口),这样以后公司发布的证书在公司内部的所有机器上就可以通过验证了(在发布证书时,把这些证书的Issuer(发布机构)设置为我们自己的证书发布机构的证书的Subject(主题)就可以了)。但是这只限于内部应用,因为只有我们公司自己的机器上设置了信任我们自己这个所谓的证书发布机构,而其它机器上并没有事先信任我们这个证书发布机构,所以在其它机器上,我们发布的证书就无法通过安全验证。

    4. 在windows中对数字证书进行管理
    4.1 查看、删除、安装 数字证书

    我们在上一章中说到了,我们的操作系统中会预先安装好一些证书发布机构的证书,我们看下在windows中如何找到这些证书,步骤如下:

    1)开始菜单->运行,输入mmc,回车

    2)在打开的窗口中选择 File-> Add/Remove Snap-in…

    3)然后在弹出的对话框的 Standalone Tab页里面点击 Add… 按钮

    4)在弹出的对对话框中选择 certificates 后点击 Add 按钮

    具体的步骤如下图所示:

    b2

    上面的步骤结束后,会又弹出一个对话框,里面有三个单选按钮如下:

    • My user account
    • Service account
    • Computer account

    可以选择第一或者第三个选项,用来查看当前用户的证书或整个计算里面安装的证书。我们这里就默认选择第一个,平时一般安装证书的时候都会给所有用户安装,所以选择第一个和第三个选项看到的证书会差不多。我们在左边的导航树中选中受信任的证书发布机构(Trusted Root Certificate Authorities),然后点击下面的证书(Certificates),在右边的区域中就可以看到所有的受信任的证书发布机构的证书。

    b3

    注意上面的图片中,右边我们选中的这个证书发布机构”SecureTrust CA”,我们前面在第3章3.2节中举例子的时候,就是去向这个证书发布机构申请的证书,由于我们申请的证书是这个机构发布的,所以应用程序在检查我们的证书的发布机构时(会检查我们证书的签名,确认是该机构发布的证书),就会发现是可以信任的证书发布机构,从而就会相信我们证书的真实性。

    删除数字证书很简单,直接在右边的列表中右键然后删除就可以了。

    数字证书的安装也比较简单,直接双击数字证书文件,会打开数字证书,对话框下面会有一个Install Certificate按钮,点击后就可以根据向导进行安装,如下图所示:

    b4

    这个证书是我自己生成的测试证书,在证书的导入向导里面,它会让你选择导入到什么位置,如果是一个我们自己信任的证书发布机构自己的证书,只要导入到Certificate Authorities就可以了。Trusted Root Certificate Authorities, Intermediate Certification Authorities, Third-Party Root Certification Authorities 都是可以的,他们只是对证书的发布机构做了一个分类,还有一些其它的证书类型,例如Personal(个人证书)等等,具体就不介绍了。安装的时候一般来说可以用默认的选择项一直”下一步”到底。

    4.2 如何自己创建证书

    每个证书发布机构都有自己的用来创建证书的工具,当然,具体他们怎么去创建一个证书的我也不太清楚,不同类型的证书都有一定的格式和规范,我没有仔细去研究过这部分内容。 微软为我们提供了一个用来创建证书的工具makecert.exe,在安装Visual Studio的时候会安装上。如果没有安装也无所谓,可以上网去下一个,搜索makecert就可以了。可以直接从我的博客下载,这是链接

    向一些正规的证书发布机构申请证书一般是要收费的(因为别人要花时间检查你的身份,确认有没有同名的证书等等),这里我们看下如何自己创建一个证书,为后面在IIS中配置Https做准备。

    我们用到的是makecert这个工具,微软有很详细的使用帮助,我这里只做一个简单的解释,详细的各种参数和使用方法请查看MSDN的makecert的帮助。但是里面有些参数说得不够清楚,而且还有遗漏的,可以参看我后面的解释作为一个补充。

    先看下makecert最简单的使用方式:

    makecert.exe test.cer

    上面的命令会在makecert.exe所在的目录生成一个证书文件test.cer的数字证书文件。可以双击证书打开,看看证书的内容如下:

    b5

    证书的发布机构是”Root Agency”,证书的主题(证书发布给谁)是”Joe’s-Software-Emporium”,因为我们没有指定把证书发布给谁,makecert自己给我们随便生成了一个公司的名字。另外还指定了公钥、签名算法(用来解密签名)、指纹和指纹算法等。

    注意,因为这个证书是由微软的工具生成的,严格来说它没什么发布机构,所以微软虚拟了一个叫做”Root Agency”的发布机构,默认情况下,windows里面安装了这个所谓的证书发布机构的证书,但是这证书默认情况下不是受信任的,原因很简单,这样做大家都可以用makecert来制作合法的数字证书了。如果我们自己硬是要,也可以把它设置为受信任的。

    下面我们看下其它的参数,比如我们要给网站 www.jefferysun.com 生成一个证书MyCA.cer,假设我们把makecert.exe放在C:盘下,命令行如下:

    makecert -r -pe -n “CN=10.30.146.206″ -b 01/01/2000 -e 01/01/2036 -eku 1.3.6.1.5.5.7.3.1 -ss my -sr localMachine -sky exchange -sp “Microsoft RSA SChannel Cryptographic Provider” -sy 12

    C:> makecert.exe –pe -r –n “CN=www.jefferysun.com” -ss my -sr LocalMachine -a sha1 -len 2048 MyCA.cer

    解释一下makecert的常用参数的意思:

    • -n 指定主题的名字,这个是有固定的格式的, CN=主题名字 ,CN应该是Certificate Name的缩写。我这里的主题的名字就是我们的IIS所在机器的IP。这里可以指定一些主题的其它附加信息,例如 O= *** 表示组织信息等等。
    • -r 创建自签署证书,意思就是说在生成证书时,将证书的发布机构设置为自己。
    • -pe 将所生成的私钥标记为可导出。注意,服务器发送证书给客户端的时候,客户端只能从证书里面获取公钥,私钥是无法获取的。如果我们指定了这个参数,证书在安装在机器上后,我们还可以从证书中导出私钥,默认情况下是不能导出私钥的。正规的途径发布的证书,是不可能让你导出私钥的。
    • -b –e 证书的有效期
    • -ss 证书的存储名称,就是windows证书存储区的目录名,如果不存在在的话就创建一个。
    • -sr 证书的存储位置,只有currentuser(默认值)或 localmachine两个值。
    • -sv 指定保存私钥的文件,文件里面除了包含私钥外,其实也包含了证书。这个文件是需要保密的,这个文件在服务端配置时是需要用到的。
    • 这个CN=10.30.146.206要与自己的服务器相对应,要不然在配置HTTPS的时候会出现错误
    • -a 指定签名算法,必须是md5或rsa1。(还记得签名算法的作用不?可以看一下3章的第1节中关于签名算法的介绍)
    • -in 指定证书发布机构的名称
    • -len 这个参数在中文的帮助文档中好像没有提到,但是这个其实很重要,用于指定公钥的位数,越大越安全,默认值是1024,推荐2048。我试了下,这个不为1024的倍数也是可以的。

    生成证书后可以进行安装,安装过程可以参看4.1节。

    转载请注明:爱开源 » 数字证书原理

  • pureftpd安装配置详细过程

    工作中总会离不开FTP,这些年一直习惯用pureftp,很久没安装,找到以前写的文档,这次顺便把文档整到ttlsa里,以后可以参考。以前自己写文档确实很啰嗦。

    准备pureftp

    #cd /usr/local/src/
    #wget http://download.pureftpd.org/pub/pure-ftpd/releases/pure-ftpd-1.0.22.tar.gz
    #tar –xzvf pure-ftpd-1.0.22.tar.gz
    

    编译和安装

    #cd pure-ftpd-1.0.22
    #.configure
    –prefix=/usr/local/pureftpd  //pureftpd安装目录
    –with-everything  //安装几乎所有的功能,包括altlog、cookies、throttling、ratios、ftpwho、upload script、virtual users(puredb)、quotas、virtual hosts、directory aliases、external authentication、Bonjour、privilege separation本次安装只使用这个选项。
    --with-cookie  //当用户登录时显示指定的横幅
    --with-diraliases  //支持目录别名,用快捷方式代cd命令
    --with-extauth  //编译支持扩展验证的模块,大多数用户不使用这个选项
    --with-ftpwho  //支持pure-ftpwho命令,启用这个功能需要更多的额外内存
    --with-language=english  //修改服务器语言,默认是英文,如果你要做修改,请翻译‘src/messages_en.h’文件
    --with-ldap    //LADP目录支持,需要安装openldap
    --with-minimal  //FTP最小安装,最基本的功能
    --with-mysql  //MySQL支持,如果MySQL安装在自定义目录上,你需要使用命令—with-mysql=/usr/local/mysq这类
    --with-nonroot    //不需要root用户就可以启动服务
    #make
    #make install
    

    安装配置文件

    #cd /usr/local/src/pure-ftpd-1.0.22 //切换到源码目录
    #cd configuration-files        //切换到这个目录
    #chmod 755 pure-config.pl   //让用户有完全权限(因为默认没有执行权限)
    #cp pure-config-pl /usr/local/pureftpd/bin    //把执行文件复制到bin目录下
    #mkdir /usr/local/pureftpd/etc              //新建FTP的配置文件夹目录
    #cp pure-ftpd.conf /usr/local/pureftpd/etc   //复制ftp配置文件到etc中
    #cd ..     //切换到/pure-ftpd-1.0.22目录中
    #cp pureftpd-ldap.conf /usr/local/pureftpd/etc     //相关配置文件复制到etc中
    #cp pureftpd-mysql.conf /usr/local/pureftpd/etc //相关配置文件复制到etc中
    #cp pureftpd-pgsql.conf /usr/local/pureftpd/etc   //相关配置文件复制到etc中
    

    pure-ftpd.conf配置

    ChrootEveryone              yes           //锁定所有用户到家目录中
    # TrustedGID                    100 //信任组ID100,可以不锁定
    MaxClientsNumber            50           //最大的客户端数量
    MaxClientsPerIP             8        //同一个IP允许8个链接
    DisplayDotFiles             no //不显示隐藏文件
    AnonymousOnly               no   //只允许匿名用户
    NoAnonymous                 yes//不允许匿名用户
    DontResolve                 yes    //禁止反向解析
    MaxIdleTime                 10    //最大空闲10分钟
    # LDAPConfigFile                /etc/pureftpd-ldap.conf    //LDAP配置文件目录
    # MySQLConfigFile               /etc/pureftpd-mysql.conf//MySQL配置文件目录
    # PGSQLConfigFile               /etc/pureftpd-pgsql.conf //PGSQL配置文件目录
    PureDB                        /usr/local/pureftpd/etc/pureftpd.pdb //虚拟用户数据库
    # UnixAuthentication            yes //主机认证
    LimitRecursion              2000 8       //别表最大显示2000个文件,最深8个目录
    AnonymousCanCreateDirs      no     //是否允许匿名用户创建目录
    #MaxLoad                     4   //最多可下载的数量
    # PassivePortRange          30000 50000      //主动连接的端口范围
    ForcePassiveIP                192.168.0.1   //这个地址总是直到匿名目录
    # AnonymousRatio                1 10         //匿名用户上传下载速度比率
    # UserRatio                 1 10                  //用户上传下载速度比率
    # Bind                      127.0.0.1,21     //绑定IP和端口
    # AnonymousBandwidth            8             //匿名用户带宽8KB
    # UserBandwidth             8                     //用户带宽8KB
    Umask                       133:022         //文件和目录的umask
    MinUID                      1000             //用户ID至少要大于1000才能登陆
    AllowUserFXP                no           //是否允许用户使用FXP协议登陆
    AllowAnonymousFXP           no         //是否允许匿名用户使用FXP协议
    ProhibitDotFilesWrite       no                 //是否允许写入点文件
    ProhibitDotFilesRead        no                //是否允许读取点文件
    AnonymousCantUpload         yes         //不允许匿名用户上传
    #NoChmod                     yes     //不允许用户改变权限
    #KeepAllFiles                yes           //允许用户断点续传
    #Quota                       1000:10//磁盘配额
    #MaxDiskUsage               99   //磁盘的最大利用率
    #NoRename                  yes //不允许自动重命名
    IPV4Only                 yes    //只允许使用IPV4协议
    

    新建虚拟用户

    注意:新建虚拟用户之前需要创建一个组合用户(属于操作系统上的)。

    #groupadd ftpgroup        //新建系统组
    #useradd –g ftpgroup –d /dev/null –s /sbin/nologin ftpuser //新建用户加入ftpgroup中
    #cd /usr/local/pureftpd/bin     //切换到bin目录中
    #./pure-pw useradd puser –u ftpuser –d /www/ftptest –m
    //pure-pw useradd 虚拟用户名 –u 寄生到系统用户名 –d FTP目录 –m(把用户密码加入PDB数据库中,不需要重启FTP)
    #cd /www      //切换到WWW中
    #chmod –R ftpuser:ftpgroup ftptest //把FTP目录的所属用户和组改为虚拟用户所依托的系统用户和组
    

    启动测试

    #/usr/local/pureftpd/bin/pure-config.pl /usr/local/pureftpd/etc/pure-ftpd.conf
    Running: /usr/local/pureftpd/sbin/pure-ftpd -A -c50 -B -C8 -E -fftp -H -I10 -lpuredb:/usr/local/pureftpd/etc/pureftpd.pdb -L2000:8 -s -U133:022 -u1000 -i -Z -4
    

    注:如果出现running说明启动成功。

    接下来ftP连接进行测试

    转载请注明:爱开源 » pureftpd安装配置详细过程

  • CentOS 下搭建 Git WEB管理平台

    Ruby的安装
    因为CentOS源里的ruby版本太低,我们直接下载源码进行安装

    安装之前确认系统中已经安装了libyaml

    没有的话直接下载源码安装

    wget http://pyyaml.org/download/libyaml/yaml-0.1.tar.gz
    tar zxvf yaml-0.1.tar.gz
    cd yaml-0.1
    ./configure
    make && make install
    下载ruby源码

    http://ruby.taobao.org/mirrors/ruby/ruby-1.9-stable.tar.gz

    安装

    tar zxvf ruby-1.9-stable.tar.gz
    cd ruby-1.9-xxxx/
    ./configure
    make && make install
    结束

    一般情况下安装会很顺利

    Gitolite的安装
    创建专属用户

    useradd -d /home/git -m git
    然后用git身份登录

    su git
    进入到HOME目录

    cd ~
    下载源码

    直接从Github进行clone

    git clone git://github.com/sitaramc/gitolite
    进行安装

    mkdir -p $HOME/bin
    gitolite/install -to $HOME/bin
    如果提示Can’t locate Time/HiRes.pm in @INC的话执行下面的命令进行类库安装

    perl -MCPAN -e shell
    install Time::HiRes
    如果提示Can’t locate CPAN.pm in @INC的话先使用yum进行安装yum install perl-CPAN

    将bin加入到PATH变量中

    编辑~/.profile添加如下内容

    export PATH=$PATH:/home/git/bin
    GitLab的安装
    创建专属用户

    useradd -d /home/gitlab -m -g git gitlab
    切换至gitlab并进入HOME目录

    依赖的Gems (root身份安装)

    gem install resque
    gem install charlock_holmes –version ‘0.6.9’
    charlock_holmes 会依赖libicu 可以直接yum安装yum install libicu-devel

    如果提示cannot load such file — zlib需要安装zlib库

    yum install zlib-devel
    # 安装ruby自带的zlib.so
    cd ruby-1.9-xxxx/ext/zlib
    ruby ./extconf.rb
    make && make install
    下载源码

    git clone git://github.com/gitlabhq/gitlabhq server
    站点信息配置

    cp server/config/gitlab.yml.example server/config/gitlab.yml
    cp server/config/unicorn.rb.example server/config/unicorn.rb
    因为我们释放到了server目录下,需要修改unicorn.rb将第一行的默认目录修改下

    创建satellites目录

    mkdir gitlab-satellites
    数据库配置

    cp server/config/database.yml.mysql server/config/database.yml
    Redis配置(一般不需要)

    cp server/config/resque.yml.example server/config/resque.yml
    进行产品发布

    cd ~/server
    bundle install –deployment –without development test postgres
    找不到bundle的时候可以执行gem install bundler安装

    配置Git

    git config –global user.name “GitLab”
    git config –global user.email “gitlab@localhost”
    配置GitLab的Hooks

    # root 权限运行
    mkdir -p /home/git/.gitolite/hooks/common
    cp /home/gitlab/server/lib/hooks/post-receive /home/git/.gitolite/hooks/common/post-receive
    chown git:git -R /home/git/.gitolite
    创建用户(gitlab)的SSH公匙并导入到gitolite中

    执行前确定/home/git/.gitolite/logs目录存在,否则导入key时会报错

    FATAL: errors found but logfile could not be created
    FATAL: /home/git/.gitolite/logs/gitolite-2013-02.log: No such file or directory
    FATAL: cli gitolite setup -pk /home/git/gitlab.pub
    su gitlab
    ssh-keygen -t rsa
    cp ~/.ssh/id_rsa.pub /home/git/gitlab.pub
    chmod 0444 /home/git/gitlab.pub
    su git
    ~/bin/gitolite setup -pk ~/gitlab.pub
    关于传递了公钥后依然会要求输入密码的情况说明/ssh-keygen后自动验证无效

    如果你无法自动验证的话先尝试将/home/git目录权限设置为755

    如果正常了的话就不用看下面这两条了

    使用gitolite的setup命令导入key的时候会在~/.ssh/authorized_keys中添加自定义的command命令,导致/var/log/secure中查看不到ssh自动验证的错误信息(删除后再尝试即可)

    执行上面的步骤后如果在secure日志中发现Authentication refused: bad ownership or modes for directory /home/git的话需要把git目录权限设置为755

    初始化数据库

    执行下面的命令之前需要配置了正确的数据库信息并开启了Redis
    需要/home/git目录权限为770
    需要/home/git/repositories目录权限为770(没有的话新创建一个)
    执行

    cd ~/server
    bundle exec rake gitlab:setup RAILS_ENV=production
    TMD终于好了,让我们来检查下gitlab的状态吧

    cd ~/server
    # 查看环境信息
    bundle exec rake gitlab:env:info RAILS_ENV=production
    # 检查组件依赖(全绿色的话就过了)
    bundle exec rake gitlab:check RAILS_ENV=production
    添加管理脚本

    从https://raw.github.com/gitlabhq/gitlab-recipes/4-1-stable/init.d/gitlab下载脚本到/etc/init.d/gitlab

    然后chmod +x /etc/init.d/gitlab给上执行权限

    最后chkconfig –add gitlab加到service列表中,然后chkconfig gitlab on开启自启动

    安装结束
    使用service gitlab start即可启动gitlab进程了

    如何访问gitlab的web页面?

    gitlab默认是使用的unix socket访问,需要nginx进行转发,配置文件在这里https://raw.github.com/gitlabhq/gitlab-recipes/4-1-stable/nginx/gitlab

    不过我们可以修改unicorn.rb来使用TCP方式访问

    修改server/config/unicorn.rb找到listen字段将后面的内容修改成

    listen 80
    即可

    访问web后默认帐号为admin@local.host密码为5iveL!fe

    一些错误的解决方法
    如果/home/gitlab/server/log/githost.log中出现error

    ERROR -> Gitolite error -> error: cannot run hooks/post-receive: No such file or directory
    并且切换到git用户后执行env -i redis-cli报错

    解决方法

    ln -s /usr/local/bin/redis-cli /usr/bin/
    ERROR -> Gitolite error -> remote: FATAL: git config ‘core.sharedRepository’ not allowed
    解决方法

    编辑/home/git/.gitolite.rc找到GIT_CONFIG_KEYS将后面的内容修改成’.’,修改后为GIT_CONFIG_KEYS => ‘.

    转载请注明:爱开源 » CentOS 下搭建 Git WEB管理平台

  • MySQL认证协议

    • 本文是针对MySQL 5.5.9写的。MySQL协议是向老版本兼容的。老版本的MySQL Client可能不理解下面的某些字段而忽略掉。
    • 实际使用的时候,服务器的协议版本应当大于等于客户端。遗憾的是,MySQL并没有对每一次协议变动标一个数字。
    • 本文中所说的”字节”一词,英文是Byte。遵循C语言中定义,即char的大小。注意:没有规定1字节一定等于8位。所以如果你准备在1字节不等于8位的环境中使用mysql,那是给自己找事。
    • 我祈祷你的char是有符号的。

    参考http://forge.mysql.com/wiki/MySQL_Internals_ClientServer_Protocol(我也参与这个页面的编辑)

    一、客户端连接服务器的流程

    请参见sql-commonclient.c的CLI_MYSQL_REAL_CONNECT函数。

    • 使用系统的socket函数建立一个socket,然后连接服务器。
    • 使用这个socket初始化一个vio对象
      net-vio= vio_new(sock, VIO_TYPE_TCPIP, VIO_BUFFERED_READ);
    • 使用vio初始化net对象
      my_net_init(net, net-vio)
    • 并对vio设置为keep alive
      vio_keepalive(net-vio,TRUE);
    • 然后设置各种参数

    客户端接下来的流程可分为三个阶段:

    • Connection established, read and parse first packet
    • invoke the plugin to send the authentication data to the server
    • authenticated, finish the initialization of the connection

    二、服务器处理连接

    服务器启动的时候是在sql/mysqld.cc的network_init函数建立socket,然后bind。
    服务器专门有一个线程(可称为Connection Manager)处理新来的网络连接。这个线程的主函数是sql/mysqld.cc的handle_connections_sockets_thread,主要的逻辑在sql/mysqld.cc的handle_connections_sockets。handle_connections_sockets的逻辑就是典型的select()/accept()/dispatch。每个客户端连接最终会对应着一个线程以及一个THD对象。THD类不是一个通用的描述任意线程的类,它就是专门为处理客户端的的TCP连接而设计的。这个类非常大,在sql/sql_class.h中定义。

    每个worker线程的入口函数是在sql/sql_connect.cc中的handle_one_connection。sql/sql_parse.cc的do_command从网络连接上读一个command,并执行。handle_one_connection函数是以while循环的方式执行do_command。

    三、协议

    客户端发给服务器的包可分为两种:登录时的一个auth包,以及身份验证结束后的command包。
    服务器发给客户端的包可分为四种:登录时的握手包、数据包、数据流结束包、成功包(OK Packet)、错误信息包。
    所有的包都具有统一的格式,由统一的函数(sql/net_serv.cc:my_net_write(…))写入buffer等待发送。

    长度 描述
    3 包长度(单位:字节)。按低字节低址的规则存放。因为一共就3个字节,所以单个包的最大长度是(2的16次方-1)字节,约等于16MB。最小长度是0。
    1 序号。第一个是0。
    n data。

    实际上,包长等于 2的16次方-1的包也会被拆成2个包发送。因为Mysql最初没有考虑突破16M,也没有预留任何字段做标志这个包的数据不完整。所以只好把长度为2的16次方-1的包视做不完整的包,直到后面收到一个长度小于2的16次方-1的包,然后拼起来。所以最后一个包的长度有可能是0。

    登录

    服务器在每收到一个新的连接的时候,会使用sql/sql_connect.cc的login_connection函数作身份验证。先根据IP做acl,然后才进入用户名密码验证阶段。
    mysql的登录协议是经典的CHAP协议,sql/sql_acl.cc的native_password_authenticate函数的注释简单了解释了这个协议:

    • the server sends the random scramble to the client.
    • client sends the encrypted password back to the server.
    • the server checks the password.

    random scramble在4.1之前的版本中是8字节整数,在4.1以及后续版本是20字节整数。它是由password.c的create_random_string函数生的,因为它采用的是rand()%94+33这样的方式生的,所以scramble的每个字节一定是[33,127)之间的ASCII字符,在协议中发送时,是加上’’之后发的(这个后面会详细解释)。

    命令—答复

    在身份验证之后,服务器和客户端之间处于一问一答的模式。 截至到Mysql 5.5.9,mysql server一共支持30种command,sql/sql_parse.cc的 dispatch_command函数写了一个大大的switch…case来处理它们。

    • COM_SLEEP
    • COM_QUIT
    • COM_INIT_DB
    • COM_QUERY
    • COM_FIELD_LIST
    • COM_CREATE_DB
    • COM_DROP_DB
    • COM_REFRESH
    • COM_SHUTDOWN
    • COM_STATISTICS
    • COM_PROCESS_INFO
    • COM_CONNECT
    • COM_PROCESS_KILL
    • COM_DEBUG
    • COM_PING
    • COM_TIME
    • COM_DELAYED_INSERT
    • COM_CHANGE_USER
    • COM_BINLOG_DUMP
    • COM_TABLE_DUMP
    • COM_CONNECT_OUT
    • COM_REGISTER_SLAVE
    • COM_STMT_PREPARE
    • COM_STMT_EXECUTE
    • COM_STMT_SEND_LONG_DATA
    • COM_STMT_CLOSE
    • COM_STMT_RESET
    • COM_SET_OPTION
    • COM_STMT_FETCH
    • COM_DAEMON

    四、服务器的握手包

    客户端执行recv,会收到一个来自server的包,其中第一个字节是协议的版本号。
    其它的重要信息还有connection id、scramble

    41 00 00 00
    0A 35 2E 30 2E 32 30 2D 73 74 61 6E 64 61 72 64 2D 6C 6F 67 00 44 8E 4E 00 5A 66 72 2A 79 43 24 27 00 2C A2 08 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 36 7B 29 58 5E 50 56 41 21 7C 73 4C 00

    其格式如下:(来自sql_acl.cc的send_server_handshake_packet函数的注释)

    长度 说明
    1 协议的版本号 (0x0A)
    n 以0结尾的字符串。描述服务器版本
    4 thread id
    8 scramble的前8个字节
    1 0x00。也就是说让scramble看起来是一个以0结尾的字符串
    2 server capabilities的低两个字节。
    1 server character set
    2 server status
    2 server capabilities的高两个字节。
    1 scramble的总长度
    10 保留。必须以0填充。
    n(至少12) scramble的剩余部分。(不包含’’)
    1 0x00。也就是说让scramble看起来是一个以0结尾的字符串

    server capabilities表:

    名字 从右往左数第几位 说明
    CLIENT_LONG_PASSWORD 1 new more secure passwords
    CLIENT_FOUND_ROWS 2 Found instead of affected rows
    CLIENT_LONG_FLAG 3 Get all column flags
    CLIENT_CONNECT_WITH_DB 4 One can specify db on connect
    CLIENT_NO_SCHEMA 5 Don’t allow database.table.column
    CLIENT_COMPRESS 6 Can use compression protocol
    CLIENT_ODBC 7 Odbc client
    CLIENT_LOCAL_FILES 8 Can use LOAD DATA LOCAL
    CLIENT_IGNORE_SPACE 9 Ignore spaces before ‘(‘
    CLIENT_PROTOCOL_41 10 New 4.1 protocol
    CLIENT_INTERACTIVE 11 This is an interactive client
    CLIENT_SSL 12 Switch to SSL after handshake
    CLIENT_IGNORE_SIGPIPE 13 IGNORE sigpipes
    CLIENT_TRANSACTIONS 14 Client knows about transactions
    CLIENT_RESERVED 15 Old flag for 4.1 protocol
    CLIENT_SECURE_CONNECTION 16 New 4.1 authentication
    CLIENT_MULTI_STATEMENTS 17 Enable/disable multi-stmt support
    CLIENT_MULTI_RESULTS 18 Enable/disable multi-results
    CLIENT_PS_MULTI_RESULTS 19 Multi-results in PS-protocol
    CLIENT_PLUGIN_AUTH 20 Client supports plugin authentication
    CLIENT_SSL_VERIFY_SERVER_CERT 31
    CLIENT_REMEMBER_OPTIONS 32

    五、然后客户端将密码等发送过去

    客户端根据服务器发给的scramble对原始密码进行散列,然后和其它参数一起发给服务器
    发送登录数据:
    00000000 3A 00 00 01 85 A6 03 00 00 00 00 01 08 00 00 00
    00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    00000020 00 00 00 00 72 6F 6F 74 00 14 00 00 00 00 00 00
    00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00

    长度 说明
    4 client capabilities
    4 max packet size
    1 charset number
    23 保留。必须以0填充。
    n user name。以0结尾的字符串
    n hash过的密码。length (1 byte) coded
    n database name,以0结尾的字符串。只有client capabilities中有CLIENT_CONNECT_WITH_DB时,此字段才有效。
    n client auth plugin name。以0结尾的字符串。只有client capabilities中有CLIENT_PLUGIN_AUTH时,此字段才有效。如果使用mysql默认的auth机制,此处应该为mysql_native_password

    sql-common/client.c的send_client_reply_packet函数构造这个答复包然后发送。散列算法的实现在password.c的scramble(char to, const char message, const char *password)函数。

    四、再度发送scrambled password (可选)

    授权信息已经发送过去了,服务器可以会回答说OK(发回一个OK_PACKET),也有可能会要求再度发送scrambled password。
    如果要再度发送,服务器会返回一个1字节的包,如果第一个字节是0xFE且mysql.server_capabilities设置了CLIENT_SECURE_CONNECTION,那么
    就需要再度发送scrambled password
    这个似乎是为了和以前老版本兼容,这次需要使用3.23版的scramble对password进行加密然后发送。
    scramble_323(buff, mysql->scramble, passwd);
    如:
    0x8059000: 0x09 0x00 0x00 0x03 0x4d 0x45 0x46 0x4c
    0x8059008: 0x4f 0x44 0x4b 0x4b 0x00
    这个包的格式很简单,包头,然后是9个字节的scramble(其中最后一个字节必须是0x00)
    不过要注意,此处包头的第4个字节是0x03,因为这是认证过程是双方来回发送的第三个包了。

    五、命令

    0x20,0x00,0x00,0x00, 包头
    0x03 //命令的类型,COM_QUERY
    select * from xxx where xxx //arg

    ========================================================
    MYSQL认证漏洞:
    1、构造0长度的scramble绕过密码校验
    这几乎可以算是mysql目前发现的危害性最严重的安全漏洞了。

    出问题的代码:

    my_boolcheck_scramble_323(constcharscrambled,constcharmessage,ulonghash_pass){structrand_structrand_st;ulonghash_message[2];charbuff[16],to,extra;/ Big enough for check /constcharpos;hash_password(hash_message,message,SCRAMBLE_LENGTH_323);randominit(&rand_st,hash_pass[0]^hash_message[0],hash_pass[1]^hash_message[1]);to=buff;for(pos=scrambled;pos;pos++)to++=(char)(floor(my_rnd(&rand_st)31)+64);extra=(char)(floor(my_rnd(&rand_st)31));to=buff;while(scrambled){if(scrambled++!=(char)(to++^extra))return1;/ Wrong password /}return0;}

    改正后的 (多加了一句 if (pos-scrambled != SCRAMBLE_LENGTH_323) return 1)

    my_boolcheck_scramble_323(constcharscrambled,constcharmessage,ulonghash_pass){structrand_structrand_st;ulonghash_message[2];charbuff[16],to,extra;/ Big enough for check /constcharpos;hash_password(hash_message,message,SCRAMBLE_LENGTH_323);randominit(&rand_st,hash_pass[0]^hash_message[0],hash_pass[1]^hash_message[1]);to=buff;DBUG_ASSERT(sizeof(buff)>SCRAMBLE_LENGTH_323);for(pos=scrambled;pos&&to<buff+sizeof(buff);pos++)to++=(char)(floor(my_rnd(&rand_st)31)+64);if(pos-scrambled!=SCRAMBLE_LENGTH_323)return1;extra=(char)(floor(my_rnd(&rand_st)31));to=buff;while(scrambled){if(scrambled++!=(char)(to++^extra))return1;/ Wrong password /}return0;}

    2、构造一个足够长的passwd让strlen溢出。
    出问题的代码:
    uint passwd_len= thd->client_capabilities & CLIENT_SECURE_CONNECTION ?
    *passwd++ : strlen(passwd);
    db= thd->client_capabilities & CLIENT_CONNECT_WITH_DB ?
    db + passwd_len + 1 : 0;
    uint db_len= db ? strlen(db) : 0;

    修正后:
    char passwd= strend(user)+1;
    。。。。
    uint passwd_len= thd->client_capabilities & CLIENT_SECURE_CONNECTION ?
    (uchar)(
    passwd++) : strlen(passwd);
    db= thd->client_capabilities & CLIENT_CONNECT_WITH_DB ?
    db + passwd_len + 1 : 0;
    / strlen() can’t be easily deleted without changing protocol /
    uint db_len= db ? strlen(db) : 0;
    设想,客户端发来的包中,如果client_capabilities 指定 CLIENT_SECURE_CONNECTION和 CLIENT_CONNECT_WITH_DB 两个位的值
    且passwd的第一个字节大于0x80,那么*passwd作为一个char值将是负的,在其再被转为uint的时候,将会是一个很大的uint。然后db这个指针就会被指向别处,从而对一段意外的内存进行strlen。这时可能会引发内存的读错误。从而造成拒绝服务攻击。但是这个不太容易,因为passwd_len的有效范围在[-128,127]之间,所以所能造成的危害很小。
    如果passwd_len不等于-1,而是一个更大的负数,那么strlen函数至多会读到username后的那么’’就会终止。这样做的好处是可以借助前面那段填充区来设置db name以供后面用,缺点是此处无法引发内存错误。
    如果passwd_len恰好设置为-1,且passwd_len后面的那些字节(SCRAMBLE)全部用非0值填充,那么strlen就会一直朝后面读下去直到找到0。但是由于db指针指向的是堆上,而且在很多操作系统下,如Freebsd,都会把在堆上分配的内存在交给用户使用前都初始化成0,所以……想让strlen读越界也很难。

    而接下来的一个关于边界的检查
    if (passwd + passwd_len + db_len > (char *)net->read_pos + pkt_len) …
    也因为整数溢出而导致失效。
    然后程序会一直向下执行到check_user,如果编译的时候定义了NO_EMBEDDED_ACCESS_CHECKS,那么万事大吉,此时的db指针会被立即传递给mysql_change_db函数,哦……………………否则的话,将会有一处关于password_len的检查
    if (passwd_len != 0 &&
    passwd_len != SCRAMBLE_LENGTH &&
    passwd_len != SCRAMBLE_LENGTH_323)
    DBUG_RETURN(ER_HANDSHAKE_ERROR);
    server会立刻给客户端报告一个handshare error而终止连接。

    (完)

    This article is from:https://www.sunchangming.com/blog/post/4631.html

    转载请注明:爱开源 » MySQL认证协议

  • python模拟登陆登陆一:验证码与cookies的同步处理思路

    自动登陆可能是写爬虫的第一步,如果都不能登陆,很多东西爬不到的。这也不是第一次写包含验证码识别的自动登陆脚本了。这次有点被坑住了,把这次的记录下来。

    这次要自动登陆的网站地址是:2013年株洲市中小学教师全员培训 http://zhuzhou2013.feixuelixm.teacher.com.cn/IndexPage/Index.aspx

    先说下思路,好多人写那些不需要验证码识别的自动登陆脚本很容易,只要保存好cookies就可以了,但是对于需要验证码的网站就总是登陆不上去。

    对于需要验证码的网站的自动登陆脚本的步骤:(以上面我说的那个网站为例,对于python和其他语言,思路和步骤都是适用的)

    a.先打开登陆页面,获得cookies。

    b.再访问验证码的地址。验证码是动态的,每次打开都不一样。

    c.识别验证码。这里就需要你处理、识别刚才得到的验证码。自己去找验证码(captcha)识别库,python可以用 pytesser(这个库是调用PIL来处理识别的) 、openc 之类的 或者可以人工识别然后手动输入验证码。

    d.构造post请求数据(request data)和请求头部(request head) ,然后 将构造的请求 post给网站

    f.获取 响应(response)信息,并通过测试来验证登陆是否成功。

    或者直接跳过a步骤:

    b.直接访问验证码的地址。(这里之所以能跳过 a ,是因为当我们打开 验证码地址时候,已经能获得所有我们需要的cookies 和登陆的验证码 ,所以就没必要 先操作 a 步骤)

    c.识别验证码。这里就需要你处理、识别刚才得到的验证码。自己去找验证码(captcha)识别库,python可以用 pytesser(这个库是调用PIL来处理识别的) 、openc 之类的 或者可以人工识别然后手动输入验证码。

    d.构造post请求数据(request data)和请求头部(request head) ,然后 将构造的请求 post给网站

    f.获取 响应(response)信息,并通过测试来验证登陆是否成功。

    #############################################################################

    这两个步骤的区别无非是多了个访问登陆页面的步骤,对登陆没什么影响。我这里用浏览器演示下 第一种步骤(即a ,b,c,d,e,f步骤),下篇文章开始更具体的操作,获取修改cookies,post数据构造等。

    通过下面的步骤来验证我们的思路(还是刚才的网站):

    用到的工具:

    firefox浏览器中的firebug扩展 。还可以使用Fiddler,或者httpfox,wireshark 。我这用firebug方便。chrome也有firebug。

    1.访问登陆页面获取cookies (相当于a 步骤)

    先清空火狐的缓存和cookies ,打开firebug,监控所有网页。然后打开登陆页面:http://zhuzhou2013.feixuelixm.teacher.com.cn/IndexPage/Index.aspx 。我们得到了第一个验证码:2073,可以通过firebug看到所获得的cookies ,如下图

    login1

    2.访问验证码地址获得cookies与验证码(相当于 b ,c 步骤)

    先找到验证码地址。鼠标右键点击验证码,选择“复制图像地址”,得到验证码地址为:http://zhuzhou2013.feixuelixm.teacher.com.cn/GuoPeiAdmin/Login/ImageLog.aspx ,如下图:

    captcha

    测试下这个验证码地址是否有效:在不关闭刚才登陆页面的 同时用火狐打开验证码的地址,的确得到了一个验证码,也是我们获得的第二个验证码:2876,cookies 还是原来的cookies。如下图:

    captcha2

    3. 登陆网站(相当于 d ,f 步骤)

    接着,我们开始在刚才打开的登陆网页登陆,输入账号和密码,那么我问你,现在输入哪个验证码才是正确的?你肯定会回答是 2073这个。错了,这时,我们应该输入第二个获得的验证码:2876 才能正常的登陆进去。不信的话,你可以自己多试几次来验证。

    如果我们把 刚才 1——2——3 的操作顺序改为:2——1——3

    先打开验证码页面获得一个验证码:AAAA,然后打开登陆页面又得到一个验证码:BBBB,此时再登陆,输入第一个 AAAA 验证码登陆, 就是错误的,无法登陆成功。

    至于第二种步骤,我实在找不到能够模拟这个过程的,例如,用fiddler要先访问验证码地址,获取cookies ,接着用fiddler来模拟post来登陆网站。可惜,我用了n多post软件,都没法实现这些步骤,总是登陆失败。所以直接用python来模拟这个过程:

    其中需要pytesser 模块及其调用的模块和软件都要自己安装,自己去网上google教程安装吧。哥很久很久之前就装了。

    pytesser下载

    http://code.google.com/p/pytesser/

    Tesseract OCR engine下载:

    http://code.google.com/p/tesseract-ocr/

    PIL官方下载

    http://www.pythonware.com/products/pil/

    python模拟登陆代码: (feixuelixm)

    # -*- coding: utf-8 -*-
    
    import urllib2
    import cookielib
    import urllib
    import Image
    import cStringIO
    from pytesser import *
    import re
    import os
    
    #避免 UnicodeEncodeError: 'ascii' codec can't encode character.  的报错
    import sys
    reload(sys)
    sys.setdefaultencoding( "utf-8" )
    
    #下面这段是关键了,将为urlib2.urlopen绑定cookies
    #MozillaCookieJar(也可以是 LWPCookieJar ,这里模拟火狐,所以用这个了) 提供可读写操作的cookie文件,存储cookie对象
    cookiejar = cookielib.MozillaCookieJar()
    # 将一个保存cookie对象,和一个HTTP的cookie的处理器绑定
    cookieSupport= urllib2.HTTPCookieProcessor(cookiejar)
    #下面两行为了调试的
    httpHandler = urllib2.HTTPHandler(debuglevel=1)
    httpsHandler = urllib2.HTTPSHandler(debuglevel=1)
    #创建一个opener,将保存了cookie的http处理器,还有设置一个handler用于处理http的
    opener = urllib2.build_opener(cookieSupport, httpsHandler)
    #将包含了cookie、http处理器、http的handler的资源和urllib2对象绑定在一起,安装opener,此后调用urlopen()时都会使用安装过的opener对象,
    urllib2.install_opener(opener)
    
    #登陆页面
    loginpage = "http://zhuzhou2013.feixuelixm.teacher.com.cn/IndexPage/Index.aspx"
    
    #要post的url
    PostUrl   = "http://zhuzhou2013.feixuelixm.teacher.com.cn/GuoPeiAdmin/Login/Login.aspx"
    
    ##打开登陆页面, 以此来获取cookies   。  但是因为  ##打开验证码页面就可以获取全部cookies了,所以可以直接跳过这一步。算是可有可无的
    #LoginCookies = urllib2.urlopen(loginpage)
    ##打印cookies
    #print      cookiejar
    ##先打开页面获取的cookie与  后打开验证码页面的cookie不同。
    
    ##提取验证码text(手动输入验证码)
    #vrifycodeUrl = "http://zhuzhou2013.feixuelixm.teacher.com.cn/GuoPeiAdmin/Login/ImageLog.aspx"
    #file = urllib2.urlopen(vrifycodeUrl)
    #pic= file.read()
    #path = "c:\\code.jpg"
    ##img = cStringIO.StringIO(file) # constructs a StringIO holding the image  AttributeError: addinfourl instance has no attribute 'seek'
    #localpic = open(path,"wb")
    #localpic.write(pic)
    #localpic.close()
    #print "please  %s,open code.jpg"%path
    ##text =raw_input("input code :")
    #im = Image.open(path)
    #text =image_to_string(im)
    #print text
    
    #设置cookie的值,因为post request head  需要 返回 cookie (不是cookies ,是将cookies的格式处理后的值)
    cookies = ''
    #这里要从
    for index, cookie in enumerate(cookiejar):
            #print '[',index, ']';
            #print cookie.name;
            #print cookie.value;
            #print "###########################"
            cookies = cookies+cookie.name+"="+cookie.value+";";
    print "###########################"
    cookie = cookies[:-1]
    print "cookies:",cookie
    
    #用户名,密码
    #当然,我这里登顶要处理掉密码和用户名,可不能告诉你真密码账号了
    username = "7879954564555664"
    password = "12313164"
    
    #请求数据包
    postData = {
        '__EVENTTARGET':'',
        '__EVENTARGUMENT':'',
        '__VIEWSTATE': '/wEPDwUKLTcyMzEyMTY2Nw8WAh4LTG9naW5lZFBhZ2UFEExvZ2luZWRQYWdlLmFzcHgWAmYPZBYCZg8PZBYGHgV0aXRsZQUg55So5oi35ZCNL+WtpuS5oOeggS/ouqvku73or4Hlj7ceB29uZm9jdXMFEGNoZWNrSW5wdXQodGhpcykeBm9uYmx1cgUNcmVzdG9yZSh0aGlzKWQYAQUeX19Db250cm9sc1JlcXVpcmVQb3N0QmFja0tleV9fFgEFC0ltZ2J0bkxvZ2luckJjpNhrusWhtPuT33UJ1dBUkvw=',
        'txtUserName':username,
        'txtPassWord':password,
        'txtCode':text,
        'ImgbtnLogin.x':44 ,
        'ImgbtnLogin.y':14,
        'ClientScreenWidth':1180
    }
    
    #post请求头部
    headers = {
    
        'Accept' :  'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
        'Accept-Language': 'zh-cn,en-us;q=0.8,zh;q=0.5,en;q=0.3',
        'Accept-Encoding': 'gzip, deflate',
    
        'Host':    'zhuzhou2013.feixuelixm.teacher.com.cn',
        'Cookie':cookies,
        'User-Agent' : 'Mozilla/5.0 (Windows NT 5.1; rv:29.0) Gecko/20100101 Firefox/29.0',
        'Referer' : 'http://zhuzhou2013.feixuelixm.teacher.com.cn/GuoPeiAdmin/Login/Login.aspx',
        #'Content-Type':    'application/x-www-form-urlencoded',
        #'Content-Length' :474,
        'Connection' : 'Keep-Alive'
    
    }
    
    #合成post数据
    data = urllib.urlencode(postData)
    print "data:###############"
    print  data
    #创建request
    #构造request请求
    request = urllib2.Request(  PostUrl,data,headers  )
    try:
        #访问页面
        response = urllib2.urlopen(request)
        #cur_url =  response.geturl()
        #print "cur_url:",cur_url
        status = response.getcode()
        print status
    except  urllib2.HTTPError, e:
         print e.code
    
    #将响应的网页打印到文件中,方便自己排查错误
    #必须对网页进行解码处理
    f= response.read().decode("utf8")
    outfile =open("rel_ip.txt","w")
    print >> outfile , "%s"   % ( f)
    
    #打印响应的信息
    info = response.info()
    print info
    
    #测试登陆是否成功,因为在testurl只有登陆后才能访问
    #还有一个原因是因为post返回得到的网页中含有iframe ,而要搜索的信息刚好在iframe ,所以要去iframe的原来的地址验证
    testurl = "http://zhuzhou2013.feixuelixm.teacher.com.cn/GuoPeiAdmin/Login/LoginedPage.aspx"
    
    try:
        response = urllib2.urlopen(testurl)
    except  urllib2.HTTPError, e:
         print e.code
    
    #因为后面要从网页查找字符来验证登陆成功与否,所以要保证查找的字符与网页编码相同,否则无非得到正确的结论。建议用英文查找,如css中的 id, name 之类的。
    f= response.read().decode("utf8").encode("utf8")
    outfile =open("out_ip.txt","w")
    print >> outfile , "%s"   % ( f)
    
    #在返回的网页中,查找“你好” 两个字符,因为只有登陆成功后才有两个字,找到了即表示登陆成功。建议用英文
    tag = '你好'.encode("utf8")
    if  re.search( tag,f):
        #登陆成功
        print 'Logged in successfully!'
    else:
        #登陆失败
        print 'Logged in failed, check result.html file for details'
    
    response.close()
    
    #这个代码很随意,但是容易看,需要的活,可以写成函数。还有就是urlopen()在大量登陆及检验过程中,可能read(0因为网络阻塞而timeout(超时) ,需要设置urlopen() 的超时时间,或者多次发送请求
    

    python处理cookies与验证码同步,附带验证码访问网页,而urlopen()不能处理cookies,所以它需要安装一个opener容器来处理cookies这些,见下面的关键代码(可以直接用这open()来访问登陆,这里就不细说了):

    cookiejar = cookielib.MozillaCookieJar()
    cookieSupport= urllib2.HTTPCookieProcessor(cookiejar)
    httpHandler = urllib2.HTTPHandler(debuglevel=1)
    httpsHandler = urllib2.HTTPSHandler(debuglevel=1)
    opener = urllib2.build_opener(cookieSupport, httpsHandler)
    urllib2.install_opener(opener)
    

    总结:在需要输入验证码的网页写自动登陆登陆脚本时候。关键是保证cookies 和 验证码 是同步的。如果直接打开验证码地址就能获得 登陆所需的全部cookies (此时验证码和cookies必定是同步到的),那么就不必 先打开登陆页面来获取cookies,后打开验证码地址获取 验证码。何必多此一举呢?

    转载请注明:爱开源 » python模拟登陆登陆一:验证码与cookies的同步处理思路

  • 一个用户SQL慢查询分析,原因及优化

    问题描述

    一个用户反映先线一个SQL语句执行时间慢得无法接受。SQL语句看上去很简单(本文描述中修改了表名和字段名):

    SELECT count(*) FROM a JOIN b ON a.S = b.S WHERE a.L > ’2014-03-30 00:55:00′ AND a.L < ’2014-03-30 01:00:00′ ;

    且查询需要的字段都建了索引,表结构如下:

    CREATE TABLE a (

    L timestamp NOT NULL DEFAULT ’2000-01-01 00:00:00′,

    I varchar(32) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,

    A varchar(32) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,

    S varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,

    F tinyint(4) DEFAULT NULL,

    V varchar(256) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT ”,

    N varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,

    KEY IX_L (L),

    KEY IX_I (I),

    KEY IX_S (S)

    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

    CREATE TABLE b (

    R timestamp NOT NULL DEFAULT ’2000-01-01 00:00:00′,

    V varchar(32) DEFAULT NULL,

    U varchar(32) DEFAULT NULL,

    C varchar(16) DEFAULT NULL,

    S varchar(64) DEFAULT NULL,

    I varchar(64) DEFAULT NULL,

    E bigint(32) DEFAULT NULL,

    ES varchar(128) DEFAULT NULL,

    KEY IX_R (R),

    KEY IX_C (C),

    KEY IX_S (S)

    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

    从语句看,这个查询计划很自然的,就应该是先用a作为驱动表,先后使用 a.L和b.S这两个索引。而实际上explain的结果却是:

    +—-+————-+——-+——-+—————+——+———+———-+———+————-+

    | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |

    +—-+————-+——-+——-+—————+——+———+———-+———+————-+

    | 1 | SIMPLE | b | index | IX_S | IX_S | 195 | NULL | 1038165 | Using index |

    | 1 | SIMPLE | a | ref | IX_L,IX_S | IX_S | 195 | test.b.S | 1 | Using where |

    +—-+————-+——-+——-+—————+——+———+———-+———+————-+

    分析

    从explain的结果看,查询用了b作为驱动表。

    上一篇文章我们介绍到,MySQL选择jion顺序是分别分析各种join顺序的代价后,选择最小代价的方法。

    这个join只涉及到两个表,自然也与optimizer_search_depth无关。于是我们的问题就是,我们预期的那个join顺序的为什么没有被选中?

    MySQL Tips: MySQL提供straight_join语法,强制设定连接顺序。

    explain SELECT count(*) FROM a straight_join b ON a.S = b.S WHERE a.L > ’2014-03-30 00:55:00′ AND a.L < ’2014-03-30 01:00:00′ ;

    +—-+————-+——-+——-+—————+——+———+——+———+———————————————+

    | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |

    +—-+————-+——-+——-+—————+——+———+——+———+———————————————+

    | 1 | SIMPLE | a | range | IX_L,IX_S | IX_L | 4 | NULL | 63 | Using where |

    | 1 | SIMPLE | b | index | IX_S | IX_S | 195 | NULL | 1038165 | Using where; Using index; Using join buffer |

    +—-+————-+——-+——-+—————+——+———+——+———+———————————————+

    MySQL Tips: explain结果中,join的查询代价可以用依次连乘rows估算。

    join顺序对了,简单的分析查询代价:普通join是10381651, straight_join是 631038165. 貌似MySQL没有错。但一定哪里不对!

    发现异常

    回到我们最初的设想。我们预计表a作为驱动表,是因为认为表b能够用上IX_S索引,而实际上staight_join的时候确实用上了,但这个结果与我们预期的又不同。

    我们知道,索引的过滤性是决定了一个索引在查询中是否会被选中的重要因素,那么是不是b.S的过滤性不好呢?

    MySQL Tips: show index from tbname返回结果中Cardinality的值可以表明一个索引的过滤性。

    show index的结果太多,也可以从information_schema表中取。

    mysql> select * from information_schema.STATISTICS where table_name=’b’ and index_name=’IX_S’G

    ***** 1. row *******

    TABLE_CATALOG: def

    TABLE_SCHEMA: test

    TABLE_NAME: b

    NON_UNIQUE: 1

    INDEX_SCHEMA: test

    INDEX_NAME: IX_S

    SEQ_IN_INDEX: 1

    COLUMN_NAME: S

    COLLATION: A

    CARDINALITY: 1038165

    SUB_PART: NULL

    PACKED: NULL

    NULLABLE: YES

    INDEX_TYPE: BTREE

    COMMENT:

    INDEX_COMMENT:

    可以这个索引的CARDINALITY: 1038165,已经很大了。那这个表的估算行是多少呢。

    show table status like ‘b’G

    ***** 1. row *******

    Name: b

    Engine: InnoDB

    Version: 10

    Row_format: Compact

    Rows: 1038165

    Avg_row_length: 114

    Data_length: 119160832

    Max_data_length: 0

    Index_length: 109953024

    Data_free: 5242880

    Auto_increment: NULL

    Create_time: 2014-05-23 00:24:25

    Update_time: NULL

    Check_time: NULL

    Collation: utf8_general_ci

    Checksum: NULL

    Create_options:

    Comment:

    1 row in set (0.00 sec)

    从Rows: 1038165看出,IX_S这个索引的区分度被认为非常好,已经近似于唯一索引。

    MySQL Tips: 在show table status结果中看到的Rows用于表示表的当前行数。对于MyISAM表这是一个精确值,但对InnoDB这是个估算值。

    虽然是估算值,但优化器是以此为指导的,也就是说,上面的某个explain里面的数据完全不符合期望:staight_join结果中第二行的rows。

    阶段结论

    我们发现整个错误的逻辑是这样的:以a为驱动表的执行计划,由于索引b.S的rows估计为1038165导致优化器认为代价大于以b为驱动表。而实际上这个索引的区分度为1.(当然对explan结果比较熟悉的同学会发现,第二行的type字段和Extra字段一起诡异了)

    也就是说,straight_join得到的每一行去b中查询的时候,都走了全表扫描。在MySQL里面出现这种情况的最常见的是类型转换。比如一个字符串字段,虽然包含的是全数字,但查询的时候传入的不是字符串格式。

    在这个case里面,两个都是字符串。因此,就是字符集相关了。

    回到两个表结构,发现S字段的声明差别在于 COLLATE utf8_bin — 这个就是本case的根本原因了:a表得到的S值是utf8_bin,优化器认为类型不同,无法直接用上索引b.IX_S过滤。

    至于为什么还会用上索引,这个是因为覆盖索引带来“误解”。

    MySQL Tips:若查询的所有结果能够从某个索引完全得到,则会优先用遍历索引替代遍历数据。

    作为验证,

    mysql> explain SELECT * FROM a straight_JOIN b ON binary a.S = b.S WHERE a.L > ’2014-03-30 00:55:00′ AND a.L < ’2014-03-30 01:00:00′ ;

    +—-+————-+——-+——-+—————+——+———+——+———+————————————————+

    | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |

    +—-+————-+——-+——-+—————+——+———+——+———+————————————————+

    | 1 | SIMPLE | a | range | IX_L | IX_L | 4 | NULL | 63 | Using where |

    | 1 | SIMPLE | b | ALL | IX_S | NULL | NULL | NULL | 1038165 | Range checked for each record (index map: 0×4) |

    +—-+————-+——-+——-+—————+——+———+——+———+————————————————+

    由于结果是select *, 无法使用覆盖索引,因此第二行的key就显示为NULL. (笔者泪:要是早出这个结果查起来可方便多了)

    优化

    当然最直接的想法就是修改两个表的S字段的定义,改成相同即可。这个方法可以避免修改业务代码,但DDL代价略大。这里提供两种在SQL语句方面的优化。

    1、select count(*) from b join (select s from a WHERE a.L > ’2014-03-30 00:55:00′ AND a.L < ’2014-03-30 01:00:00′) ta on b.S=ta.s;

    这个写法比较直观,需要注意最后b.S和ta.S的顺序

    2、SELECT count(*) FROM a JOIN b ON binary a.S = b.S WHERE a.L > ’2014-03-30 00:55:00′ AND a.L < ’2014-03-30 01:00:00′ ;

    从前面的分析知道是由于b.S定义为utf8_bin.

    MySQL Tips: MySQL中字符集命名规则中, XXX_bin与XXX的区别为大小写是否敏感。

    这里我们将A.s全部增加binary限定,先转为小写,就是将临时结果集转成utf8_bin,之后使用b.S匹配时就能够直接利用索引。

    其实两个改写方法的本质相同,区别是写法1是隐式转换。理论上说写法2速度更快些。

    小结

    做join的字段尽量设计为类型完全相同。

    转载请注明:爱开源 » 一个用户SQL慢查询分析,原因及优化

  • 新型任意文件读取漏洞的研究

    0x00 前言


    早前发现boooom在乌云上发了很多个任意文件读取的漏洞,都是形如

    http://target/../../../../etc/passwd
    

    这样。当时感觉很新奇,因为正常情况下,通常的服务器中间件是不允许直接读取web目录以外的文件的,为什么这样的漏洞却出现在了很多案例中。

    后来在lijiejie的文章给出了解释:http://www.lijiejie.com/python-django-directory-traversal/ ,原来是python这种新型web开发方式造成的问题。然后翻了下我自己以前用web.py、tornado开发的一些应用,果然也存在这样的问题。

    这个问题就像lijiejie说的那样,一方面是低版本django框架自身的一些漏洞,另一方面,就是开发者自身的疏忽造成的问题。

    这不得不提到现今开发框架与以前的一些区别。不管是python还是node、ruby的框架,都是一个可以自定义URL分配的框架,不再是像php或asp中那样根据目录结构来请求文件。所有的请求由用户定义规则,而框架内核部分解析、配发、执行。比如我们请求的“/login/”这个URL,很可能是被配发给一个LoginHandler类去处理了,而不是请求到/login/index.php上。

    这时候造成了一个问题,如果我们就是想去请求一个真实的文件,比如css、js等静态文件,怎么办?

    一般也会有一些区分,一些要求比较高的应用,多是采用了CDN缓存或负载均衡,nginx作为负载分配的处理器。当发现我们请求的url是一个静态文件的话,就直接由CDN或nginx返回相应文件。如下图:

    2015030401212143740

    那么这之中也存在这一个定义问题,什么请求才说明是要请求“静态文件”?只要以.css、.js结尾就可以吗?当然这也是一种方法,但一般应用会定义一个目录,如/static/,所有请求匹配“/static/(.*)”的会被认为是静态文件,所以开发者一般将静态文件放在这个目录下,我们用户就能够直接请求到他了。

    如果不存在CDN、nginx等平台,其实类似web.py、tornado这样的框架自己也定义了静态目录,在web.py下,默认的静态目录都是/static/,也就是在这个目录下的请求是不会经过URLPath的。如web.py文档中说到的:

    http://webpy.org/cookbook/staticfiles.zh-cn

    这时候,我就会有这个思考,框架内部如果是以/static/(.*)来匹配请求的话,如果我们求

    /static/../../../../../etc/passwd
    

    是不是就可以读取到/etcs/passwd文件?

    0x01 web.py下可能的任意文件读取漏洞研究


    我们先来看看web.py是怎样处理这种请求的:

    /static/../../../../../etc/passwd
    

    我们在web/httpserver.py中可以看到这样的代码:

    def do_GET(self):
        if self.path.startswith('/static/'):
            SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)
        else:
            self.run_wsgi_app()
    

    当请求以/static/开头的话,就直接交给SimpleHTTPServer处理了。SimpleHTTPServer是python自带的一个简单的HTTP Server,我们在任意一个目录下执行

    python -m SimpleHTTPServer
    

    都会启动一个web服务器,可以直接通过HTTP协议访问目录下的文件。

    web.py的Server其实就是对SimpleHTTPServer的一个继承与重写,这里它简单的把这个请求交给父类SimpleHTTPServer处理,而这个HTTP Server当然不会允许请求到web目录(也就是./static/)以外的地方去,所以得到的回复是404:

    2015030401212152015

    框架本身保证了静态文件不会造成任意文件读取。但复杂的逻辑关系应用中,开发者往往不满足于/static/一种静态目录。比如,网站允许用户上传、下载文件,可能我们会新建一个uploadfile目录,按日期、时间专门保存上传的文件。

    那么,开发者为了让/uploadfile目录下的文件也能被直接访问,往往会这样写:

    #!/usr/bin/python
    import web
    
    urls = (
        '/uploadfile/(.*)', 'download',
        '/', 'hello',
    )
    app = web.application(urls, globals())
    
    class hello:
        def GET(self, name):
            if not name:
                name = 'World'
            return 'Hello, ' + name + '!'
    
    class download:
        def GET(self, filepath):
            try:
                with open("./uploadfile/%s" % filepath, "rb") as f:
                    content = f.read()
                return content
            except:
                return web.notfound("Sorry, the file you were looking for was not found.")
    
    if __name__ == "__main__":
        app.run()
    

    有个download类专门解析这类请求,直接在GET方法中读取文件,并作为response写入HTTP数据包。

    我们请求一个正常的文件/uploadfile/01.txt,是可以得到它的内容的:

    2015030401212283650

    但我们请求一个非uploadfile目录下的文件,却发现也能读取,导致了一个任意文件读取漏洞:

    2015030401212244694

    这就是由于开发者的失误,并没有检查我们传入的path是否合法而导致,与框架无关。

    0x02 Tornado下可能的任意文件读取漏洞研究


    tornado是一个全异步的web框架,它允许我们在配置中定义静态目录static_path。在tornado中,专门给出了一个方法来验证我们的请求是否在允许的目录内:

    def validate_absolute_path(self, root, absolute_path):
        """Validate and return the absolute path.
    
        ``root`` is the configured path for the `StaticFileHandler`,
        and ``path`` is the result of `get_absolute_path`
    
        This is an instance method called during request processing,
        so it may raise `HTTPError` or use methods like
        `RequestHandler.redirect` (return None after redirecting to
        halt further processing).  This is where 404 errors for missing files
        are generated.
    
        This method may modify the path before returning it, but note that
        any such modifications will not be understood by `make_static_url`.
    
        In instance methods, this method's result is available as
        ``self.absolute_path``.
    
        .. versionadded:: 3.1
        """
        root = os.path.abspath(root)
        # os.path.abspath strips a trailing /
        # it needs to be temporarily added back for requests to root/
        if not (absolute_path + os.path.sep).startswith(root):
            raise HTTPError(403, "%s is not in root static directory",
                            self.path)
        if (os.path.isdir(absolute_path) and
                self.default_filename is not None):
            # need to look at the request.path here for when path is empty
            # but there is some prefix to the path that was already
            # trimmed by the routing
            if not self.request.path.endswith("/"):
                self.redirect(self.request.path + "/", permanent=True)
                return
            absolute_path = os.path.join(absolute_path, self.default_filename)
        if not os.path.exists(absolute_path):
            raise HTTPError(404)
        if not os.path.isfile(absolute_path):
            raise HTTPError(403, "%s is not a file", self.path)
        return absolute_path
    

    这样,一旦请求不在我们定义的静态目录下,就会抛出“is not in root static directory”错误:

    2015030401212211169

    那么如果tornado中,也需要定义一个”/uploadfile/“作为用户上传目录,那么我们怎么做?

    文档中也提到了,我们只需要自定义一个URLPath即可,tornado内部有专门处理静态文件的控制器web.StaticFileHandler:

    application = web.Application([
    
        (r"/uploadfile/(.*)", web.StaticFileHandler, {"path": "/var/www"}),
    
    ])
    

    就算不考虑安全问题,作为一个异步的框架,如果我们还用同步的read、write这些IO函数自己去处理静态文件,也是不可取的。

    不过,在后面的研究中,我也发现了tornado的处理方式并不算完美,更多详情可以等这个洞公开后查看:

    WooYun: Python开源框架Tornado某缺陷可能造成文件读取漏洞

    0x03 Django中的问题


    Django低版本自身存在的漏洞导致的任意文件读取,实际上就是犯了我之前说的静态文件未检查的问题,如这个09年的BUG:

    https://bugzilla.redhat.com/show_bug.cgi?id=CVE-2009-2659

    2015030401212396741

    正如我在web.py中说到的,如果django也单纯使用open、read来读取文件,而不检查PATH的合法性,同样能够造出任意文件读取。

    比如我们django的view如此写:

    from django.shortcuts import render
    from django.http import HttpResponse
    
    # Create your views here.
    
    def index(request, path):
        with open(path, "rb") as f:
            content = f.read()
        return HttpResponse(content)
    

    没有验证path的合法性,依旧可以出现任意文件读取的现象:

    2015030401212353218

    如上图,直接读取了sqlite数据库内容,拿下管理员账号密码。

    那么我们怎么在这样的应用中防御任意文件读取?我们简单修改django的view防御这个漏洞:

    from django.shortcuts import render
    from django.http import HttpResponse
    from django.http import Http404
    import os
    
    # Create your views here.
    
    def index(request, path):
        static = "uploadfile"
        root = os.path.join(os.getcwd(), static) + os.path.sep
        path = os.path.abspath(root + path)
        if not (path).startswith(root):
            raise Http404
        else:
            with open(path, "rb") as f:
                content = f.read()
            return HttpResponse(content)
    

    其实有些框架都有自己检查静态文件的方式,django自身应该也有的。像web.py这样的轻型框架没有自带函数检查的情况下,可以考虑用我上面写的这个方法来剔除不合法的静态文件路径。

    0x04 PHP等语言会不会出现这个问题?


    这个问题其实是有思考价值的。

    最开始我就提到了,现代的python/node/ruby等web开发框架与老式的php、asp等语言的区别,也是造成这个漏洞的原因之一就是因为URL的分配导致静态文件不能被直接访问到,所以需要自定义静态文件的访问方式。但一旦访问参数未检查,就造成任意文件读取问题。

    但传统php应用就是一个以目录形式访问的,静态文件访问应该不会经过php的,这确实是一个很大的区别。

    先不论我们的请求会不会经过php,看到zblog最新版中一个实际的案例。

    /zb_system/function/c_system_event.php
    

    大概415行:

    if (isset($_SERVER['SERVER_SOFTWARE']) && (strpos($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS') !== false) && (isset($_GET['rewrite']) == true)){
        //iis+httpd.ini下如果存在真实文件
        $realurl = $zbp->path . urldecode($url);
        if(is_readable($realurl)&&is_file($realurl)){
            die(file_get_contents($realurl));
        }
        unset($realurl);
    }
    

    这里判断了

    $_SERVER['SERVER_SOFTWARE']
    

    是否包含Microsoft-IIS,当服务器中间件是IIS,而且

    $_GET['rewrite']
    

    的话,就进入这个if语句。再判断$url指向的文件是否存在,存在就把它使用file_get_contents读取并显示出来。

    实际上这段代码所做的工作和我们之前看到的python代码是一样的。为什么php也需要这样的工作,我们url请求的文件不应该就直接由webserver返回给用户了吗?

    实际上,这里开发者也考虑了url重写造成的问题。zblog重写的规则是将所有请求都指向index.php去处理,最后由index.php去处理,有可能我们的静态文件就被rewrite到index.php去了,这里的工作就是把被重写到php里的这个静态文件直接显示出来。

    可惜我还是才疏学浅,不知道怎么写rewrite规则才能让静态文件请求被重写到index.php里,所以也做不到任意文件读取了。

    但我们这里很明显的可以发现,zblog的开发者也没有检查这个$url是否在网站目录内,或是否在静态文件目录内,也是直接读取显示了。导致我们请求http://10.211.55.3/zblog/index.php?action=&rewrite=1是可以读取index.php的源码的(因为我没有IIS环境,我手工将SERVER_SOFTWARE改成IIS了~):

    2015030401212379345

    所以,传统PHP应用下是否可能存在这样的安全漏洞,这个问题还是有待继续研究的。理论上,我们如果写出一个这样的rewrite规则:将所有请求都交给index.php处理。那么,index.php的功能实际上就和之前的python框架主文件功能类似了。

    0x05 结语


    最后,自己也只是浅显得研究和讨论了几种python框架、php某个特殊情况的漏洞,但现代的开发技术,包括企业级的一些环境我并不熟悉也没怎么接触过,所以有什么欠考虑和不完善的地方,也需要各位去补充与纠正。

    转载请注明:爱开源 » 新型任意文件读取漏洞的研究