## 安全背后: 瀏覽器是如何校驗證書的
https://cjting.me/2021/03/02/how-to-validate-tls-certificate/
# 安全背后: 瀏覽器是如何校驗證書的
tls certificate https
2021.03.02
現如今的 Web,HTTPS 早已經成為標配,公開的 HTTP 網站已經和 Flash 一樣,慢慢在消亡了。
啟用 HTTPS 的核心是一個叫做**證書**的東西。不知道大家是否有留意,前幾年上 12306 的時候,瀏覽器都會提示「您的鏈接不是私密鏈接」,這其實就是因為 12306 的證書有問題。如果點擊「繼續前往」,打開 12306 網站,它會提示你下載安裝它提供的“根證書”。

那么,證書是什么?里面含有什么內容?瀏覽器為什么會不信任 12306 的證書?為什么下載 12306 提供的根證書就可以解決這個問題?根證書又是什么?
## 證書,公鑰和私鑰
我們先來簡單回顧一下 TLS 和 HTTPS。關于這個話題,我在[從零開始搭建一個 HTTPS 網站](https://cjting.me/2016/09/05/build-a-https-site-from-scratch/)這篇博客中有詳細的論述,對細節感興趣的朋友可以先去讀一下這篇博客。
HTTPS 全稱是 HTTP Over TLS,也就是使用 TLS 進行 HTTP 通信。
TLS (Transport Layer Security) 是一個安全通信協議,用于建立一個安全通道來交換數據。
看名字可以知道這是一個傳輸層協議,因此應用層協議對它是沒有感知的,HTTP, FTP, SMTP 都可以和 TLS 配合使用。
Tip: 關于網絡協議,阮一峰的這篇博客[互聯網協議入門(一)](https://www.ruanyifeng.com/blog/2012/05/internet_protocol_suite_part_i.html)寫的很好,值得一看。
TLS 創建安全鏈接的步驟如下:
* 雙方協商使用的協議版本,加密算法等細節
* 服務器發送 證書 給客戶端
* 客戶端校驗證書有效性
* 雙方根據握手的一些參數生成一個對稱秘鑰,此后所有的內容使用這個秘鑰來加密
我們先來看證書,證書是一個文件,里面含有目標網站的各種信息。
例如網站的域名,證書的有效時間,簽發機構等,其中最重要的是這兩個:
* 用于生成對稱秘鑰的公鑰
* 由上級證書簽發的簽名
證書文件的格式叫做 X.509,由[RFC5280](https://tools.ietf.org/html/rfc5280)規范詳細定義。存儲上分為兩種,一種叫做 DER,是二進制的,還有一種叫做 PEM,是基于 Base64 的。
關于 RSA 的公鑰和私鑰記住一點就行:**我們可以使用算法生成一對鑰匙,他們滿足一個性質:公鑰加密的私鑰可以解開,私鑰加密的公鑰可以解開**。
Tip: RSA 算法的具體工作原理可以參考我這篇博客[RSA 的原理與實現](https://cjting.me/2020/03/13/rsa/)。
證書,顧名思義,是用來證明自己身份的。因為發送證書的時候是明文的(這一步也沒法加密),所以證書內容是可以被中間設備篡改的。
那么要怎樣設計一套機制保證當我訪問 github.com 的時候,收到的證書確實是 github.com 的證書,而不是某個中間設備隨意發來的證書?
大家可以思考一下這個問題??。
解決辦法是采用「信任鏈」。
首先,有一批證書頒發機構(Certificate Authority,簡稱為 CA),由他們生成秘鑰對,其中私鑰保存好,公鑰以證書的格式安裝在我們的操作系統中,這就是**根證書**。
我們的手機、電腦、電視機的操作系統中都預裝了 CA 的根證書,他們是所有信任構建的基石。當然,我們也可以自己下載任意的根證書進行安裝。
接下來,只要設計一個體系,能夠證明 A 證書簽發了 B 證書即可。這樣對于收到的任何一個證書,順藤摸瓜,只要最上面的根證書在系統中存在,即可證明該證書有效。
比如說,我們收到了服務器發過來的 C 證書,我們驗證了 C 是由 B 簽發的,然后又驗證了 B 是由 A 簽發的,而 A 在我們的系統中存在,那也就證明了 C 這個證書的有效性。
這其中,A 是根證書,B 是中間證書,C 是葉證書(類似樹中的葉節點)。中間證書可以有很多個,信任的鏈條可以任意長,只要最終能到根證書即可。
得益于 RSA 的非對稱性質,驗證 A 是否簽發了 B 證書很簡單:
* 計算 B 的 hash 值(算法隨便,比如 SHA1)
* 使用 A 的**私鑰**對該 hash 進行加密,加密以后的內容叫做「簽名(Signature)」
* 將該「簽名」附在 B 證書中
A 使用自己的私鑰給 B 生成簽名的過程也就是「簽發證書」,其中 A 叫做 Issuer,B 叫做 Subject。
這樣,當我們收到 B 證書時,首先使用 A 證書的公鑰(公鑰存儲在證書中)解開簽名獲得 hash,然后計算 B 的 hash,如果兩個 hash 匹配,說明 B 確實是由 A 簽發的。
重復上面的過程,直到根證書,就可以驗證某個證書的有效性。
接下來我們來問幾個問題。
### 為什么需要中間證書?
為什么要設計中間證書這個環節?直接使用根證書進行簽發不好嗎?
這是因為根證書的私鑰安全性至關重要,一旦被泄露,將引起巨大的安全問題。
所以,根證書的私鑰都是被保存在離線的計算機中,有嚴格的操作規章,每次需要使用時,會有專人將數據通過 USB 拷貝過去,操作完了以后,再將數據帶出來。
在這套流程下,直接將根證書用于簽發普通證書是不現實的。想想這個世界上有多少網站,每天對證書的需求量都是巨大的,根證書的操作效率無法滿足要求,因為不能批量和自動化。
同時,對根證書私鑰的操作越多,泄露的風險也就越大,因此,人們就發明了中間證書。
使用根證書簽發一些中間證書,這些中間證書就可以用來簽發大量的葉證書,這個過程完全可以是自動化的,就像 Let’s Encrypt 那樣。
同時,即便中間證書的私鑰泄露了也不要緊,可以使用根證書把它們撤銷掉,具體怎么撤銷是另外一個話題了,這里不再展開。
通過使用中間證書,我們就可以做到既方便,又安全。
### 瀏覽器如何獲取中間證書?
一般來說,服務器會將中間證書一并發送過來。也就是說,當我們訪問某個網站時,收到的不是一個證書,而是一系列證書。
當然,這不是強制要求,在服務器不發送的情況下,瀏覽器也會使用一些方法去定位中間證書,比如
* 緩存之前下載過的證書
* 證書文件中的 Authority Information Access (AIA) Extension 里面含有上級證書的相關信息,瀏覽器可以使用這個信息去下載上級證書
### 根證書有時效嗎?過期了怎么辦?
每個證書都有有效時間,根證書自然也不例外。
在 Mac 上,打開 Keychain Access,選擇側邊的`System Roots`就可以看到我們系統中安裝的所有根證書。

其中很明顯地顯示了一欄叫做「Expires」,過期時間。
那么,根證書過期了怎么辦?
答案是不怎么辦,在過期前 CA 會生成新的根證書,并和各大產商合作,在操作系統升級的時候安裝到我們的設備上。
老的根證書過期以后只是無法再作為信任錨點為其他的證書提供信任而已。
根證書的生命周期一般是 20 年,Let’s Encrypt 之前使用的根證書是`DST Root CA X3`,2021 年九月就要過期了,這個證書是 2000 年創建的。
Let’s Encrypt 在 2015 年創建了一個新的根證書`ISRG Root X1`,在 2018 年被完全認可并逐步安裝到各種設備上。
安裝新的根證書的唯一方式是通過系統更新,但是這其實是不可控的事情,因為總有一些設備不會更新。
我們可以想象一下,如果一個設備停止更新,同時里面內置的所有根證書都過期了,那么這個設備就無法再進行 HTTPS 通信了。
### 根證書有什么特征?根證書的簽名是什么?
每個證書中都含有當前證書對象 Subject 的信息以及該證書的簽發者 Issuer 的信息。
根證書的特征是 Subject 和 Issuer 的信息是一致的,也就是所謂的「自簽名」(Self-Signed)。

因為證書的簽名是由上級證書的私鑰來簽的,根證書沒有上級證書,所有根證書的簽名是用自己的私鑰簽的。
我們可以來驗證一下,以 GitHub 使用的根證書 DigiCert High Assurance EV Root CA 為例。
~~~bash
# 首先,打開 Keychain Access,找到上述證書,拖拽出來
# 重命名為 root.cer
# 轉換 DER 到 PEM 格式
$ openssl x509 -inform der -in root.cer -out root.pem
# 查看證書的簽名,可以看到簽名所使用的的 hash 算法是 sha1
$ openssl x509 -in root.pem -text -noout -certopt ca_default -certopt no_validity -certopt no_serial -certopt no_subject -certopt no_extensions -certopt no_signame
Signature Algorithm: sha1WithRSAEncryption
1c:1a:06:97:dc:d7:9c:9f:3c:88:66:06:08:57:21:db:21:47:
f8:2a:67:aa:bf:18:32:76:40:10:57:c1:8a:f3:7a:d9:11:65:
8e:35:fa:9e:fc:45:b5:9e:d9:4c:31:4b:b8:91:e8:43:2c:8e:
b3:78:ce:db:e3:53:79:71:d6:e5:21:94:01:da:55:87:9a:24:
64:f6:8a:66:cc:de:9c:37:cd:a8:34:b1:69:9b:23:c8:9e:78:
22:2b:70:43:e3:55:47:31:61:19:ef:58:c5:85:2f:4e:30:f6:
a0:31:16:23:c8:e7:e2:65:16:33:cb:bf:1a:1b:a0:3d:f8:ca:
5e:8b:31:8b:60:08:89:2d:0c:06:5c:52:b7:c4:f9:0a:98:d1:
15:5f:9f:12:be:7c:36:63:38:bd:44:a4:7f:e4:26:2b:0a:c4:
97:69:0d:e9:8c:e2:c0:10:57:b8:c8:76:12:91:55:f2:48:69:
d8:bc:2a:02:5b:0f:44:d4:20:31:db:f4:ba:70:26:5d:90:60:
9e:bc:4b:17:09:2f:b4:cb:1e:43:68:c9:07:27:c1:d2:5c:f7:
ea:21:b9:68:12:9c:3c:9c:bf:9e:fc:80:5c:9b:63:cd:ec:47:
aa:25:27:67:a0:37:f3:00:82:7d:54:d7:a9:f8:e9:2e:13:a3:
77:e8:1f:4a
# 提取簽名內容到文件中
$ openssl x509 -in root.pem -text -noout -certopt ca_default -certopt no_validity -certopt no_serial -certopt no_subject -certopt no_extensions -certopt no_signame | grep -v 'Signature Algorithm' | tr -d '[:space:]:' | xxd -r -p > root-signature.bin
# 提取根證書中含有的公鑰
$ openssl x509 -in root.pem -noout -pubkey > root-pub.pem
# 使用公鑰解密簽名
$ openssl rsautl -verify -inkey root-pub.pem -in root-signature.bin -pubin > root-signature-decrypted.bin
# 查看解密后的內容
# 可以看到,簽名中存儲的 hash 值為 E35E...13A8
$ openssl asn1parse -inform DER -in root-signature-decrypted.bin
0:d=0 hl=2 l= 33 cons: SEQUENCE
2:d=1 hl=2 l= 9 cons: SEQUENCE
4:d=2 hl=2 l= 5 prim: OBJECT :sha1
11:d=2 hl=2 l= 0 prim: NULL
13:d=1 hl=2 l= 20 prim: OCTET STRING [HEX DUMP]:E35EF08D884F0A0ADE2F75E96301CE6230F213A8
# 接下來我們計算證書的 hash 值
# 首先提取證書的 body
# 因為證書中含有簽名,簽名是不包含在 hash 值計算中的
# 所以不能簡單地對整個證書文件進行 hash 運算
$ openssl asn1parse -in root.pem -strparse 4 -out root-body.bin &> /dev/null
# 計算 sha1 哈希值
$ openssl dgst -sha1 root-body.bin
SHA1(root-body.bin)= e35ef08d884f0a0ade2f75e96301ce6230f213a8
~~~
hash 值匹配,這也就說明根證書確實是自簽名的,用自己的私鑰給自己簽名。
## 紙上得來終覺淺
理論知識我們已經全部具備了,接下來我們來完整走一遍流程,以 github.com 為例,校驗一下它的證書是否有效。
~~~bash
# 新建一個文件夾 github 保存所有的文件
$ mkdir github && cd github
# 首先,我們下載 github.com 發送的證書
$ openssl s_client -connect github.com:443 -showcerts 2>/dev/null </dev/null | sed -n '/-----BEGIN/,/-----END/p' > github.com.crt
# github.com.crt 是 PEM 格式的文本文件
# 打開可以發現里面有兩段 -----BEGIN CERTIFICATE----
# 這說明有兩個證書,也就是 github.com 把中間證書也一并發過來了
# 接下來我們把兩個證書提取出來
$ awk '/BEGIN/,/END/{ if(/BEGIN/){a++}; out="cert"a".tmpcrt"; print >out}' < github.com.crt && for cert in *.tmpcrt; do newname=$(openssl x509 -noout -subject -in $cert | sed -n 's/^.*CN=\(.*\)$/\1/; s/[ ,.*]/_/g; s/__/_/g; s/^_//g;p').pem; mv $cert $newname; done
# 我們得到了兩個證書文件
# github_com.pem 和 DigiCert_SHA2_High_Assurance_Server_CA.pem
# 首先,驗證 github_com.pem 證書確實
# 是由 DigiCert_SHA2_High_Assurance_Server_CA.pem 簽發的
# 提取 DigiCert_SHA2_High_Assurance_Server_CA 的公鑰
# 命名為 issuer-pub.pem
$ openssl x509 -in DigiCert_SHA2_High_Assurance_Server_CA.pem -noout -pubkey > issuer-pub.pem
# 查看 github_com.pem 的簽名
# 可以看到 hash 算法是 sha256
$ openssl x509 -in github_com.pem -text -noout -certopt ca_default -certopt no_validity -certopt no_serial -certopt no_subject -certopt no_extensions -certopt no_signame
Signature Algorithm: sha256WithRSAEncryption
86:32:8f:9c:15:b8:af:e8:d1:de:08:3a:44:0e:71:20:24:d6:
fc:0e:58:31:cc:aa:b4:ad:1c:d5:0c:c5:af:c4:bb:fe:5f:ac:
90:6a:42:c8:21:eb:25:f1:6b:2c:37:b2:2a:a8:1a:6e:f2:d1:
4f:a6:2f:bc:cf:3a:d8:c1:9f:30:c0:ec:93:eb:0a:5a:dc:cb:
6c:32:1c:60:6e:ec:6e:f8:86:a5:4f:a0:b4:6d:6a:07:4a:21:
58:d0:29:7d:65:8a:c8:da:6a:ba:ab:f0:75:21:33:00:40:6f:
85:c5:13:e6:27:73:6c:ae:ea:e3:96:d0:53:db:c1:21:68:10:
cf:e3:d8:50:b0:14:ec:a9:98:cf:b8:ce:61:5d:3d:a3:6d:93:
34:c4:13:fa:11:66:a3:dd:be:10:19:70:49:e2:04:4d:81:2c:
1f:2e:59:c6:2c:53:45:3b:ee:f6:13:f4:d0:2c:84:6e:28:6d:
e4:e4:ca:e4:48:89:1b:ab:ec:22:1f:ee:12:d4:6c:75:e9:cc:
0b:15:74:e9:6d:9f:db:40:1f:e2:24:85:a3:4b:a4:e9:cd:6b:
c8:77:9f:87:4f:05:73:00:38:a5:23:54:68:fc:a2:3d:bf:18:
19:0e:a8:fd:b9:5e:8c:5c:e8:fc:e4:a2:52:70:ee:79:a7:d2:
27:4a:7a:49
# 提取簽名到文件中
$ openssl x509 -in github_com.pem -text -noout -certopt ca_default -certopt no_validity -certopt no_serial -certopt no_subject -certopt no_extensions -certopt no_signame | grep -v 'Signature Algorithm' | tr -d '[:space:]:' | xxd -r -p > github_com-signature.bin
# 使用上級證書的公鑰解密簽名
$ openssl rsautl -verify -inkey issuer-pub.pem -in github_com-signature.bin -pubin > github_com-signature-decrypted.bin
# 查看解密后的信息
$ openssl asn1parse -inform DER -in github_com-signature-decrypted.bin
0:d=0 hl=2 l= 49 cons: SEQUENCE
2:d=1 hl=2 l= 13 cons: SEQUENCE
4:d=2 hl=2 l= 9 prim: OBJECT :sha256
15:d=2 hl=2 l= 0 prim: NULL
17:d=1 hl=2 l= 32 prim: OCTET STRING [HEX DUMP]:A8AA3F746FE780B1E2E5451CE4383A9633C4399E89AA3637252F38F324DFFD5F
# 可以發現,hash 值是 A8AA...FD5F
# 接下來計算 github_com.pem 的 hash 值
# 提取證書的 body 部分
$ openssl asn1parse -in github_com.pem -strparse 4 -out github_com-body.bin &> /dev/null
# 計算 hash 值
$ openssl dgst -sha256 github_com-body.bin
SHA256(github_com-body.bin)= a8aa3f746fe780b1e2e5451ce4383a9633c4399e89aa3637252f38f324dffd5f
~~~
hash 值匹配,我們成功校驗了 github.pem 這個證書確實是由 DigiCert\_SHA2\_High\_Assurance\_Server\_CA.pem 這個證書來簽發的。
上面的流程比較繁瑣,其實也可以直接讓 openssl 來幫我們驗證。
~~~bash
$ openssl dgst -sha256 -verify issuer-pub.pem -signature github_com-signature.bin github_com-body.bin
Verified OK
~~~
接下來的過程是上面一樣,首先,我們獲取上級證書的信息。
~~~bash
# 獲取上級證書的名字
$ openssl x509 -in DigiCert_SHA2_High_Assurance_Server_CA.pem -text -noout | grep Issuer:
Issuer: C=US, O=DigiCert Inc, OU=www.digicert.com, CN=DigiCert High Assurance EV Root CA
~~~
然后就是校驗 DigiCert\_SHA2\_High\_Assurance\_Server\_CA 是由 DigiCert High Assurance EV Root CA 來簽發的,同時這個證書存在于我們系統中。
這一步我就不再重復了,留給大家作為練習~
## 自簽名
最后,我們來看看怎么自己簽發證書用于本地測試。
我們可以使用 openssl 這樣的底層工具來完成這個任務,但是會很繁瑣:生成根證書、生成下級證書,使用根證書簽發下級證書、安裝根證書。
程序員首先要解放的就是自己的生產力,這里我們使用一個工具[mkcert](https://github.com/FiloSottile/mkcert)。
`brew intall mkcert`進行安裝,然后使用`mkcert -install`將它的根證書安裝到我們的系統中。
接下來我們就可以任意生成我們想要的證書了,以 localhost 為例,運行`mkcert localhost`,我們得到了兩個文件,其中`localhost-key.pem`是私鑰,`localhost.pem`是證書,用于發送給客戶端。
使用一個 HTTP Server 來驗證一下,這里我們使用[local-web-server](https://github.com/lwsjs/local-web-server)。
`yarn global add local-web-server`安裝完畢以后,使用`ws --key localhost-key.pem --cert localhost.pem`啟動我們的服務器,打開 https://localhost:8000,可以看到地址欄有了一把可愛的小鎖??。
- 開始
- 公益
- 更好的使用看云
- 推薦書單
- 優秀資源整理
- 技術文章寫作規范
- SublimeText - 編碼利器
- PSR-0/PSR-4命名標準
- php的多進程實驗分析
- 高級PHP
- 進程
- 信號
- 事件
- IO模型
- 同步、異步
- socket
- Swoole
- PHP擴展
- Composer
- easyswoole
- php多線程
- 守護程序
- 文件鎖
- s-socket
- aphp
- 隊列&并發
- 隊列
- 講個故事
- 如何最大效率的問題
- 訪問式的web服務(一)
- 訪問式的web服務(二)
- 請求
- 瀏覽器訪問阻塞問題
- Swoole
- 你必須理解的計算機核心概念 - 碼農翻身
- CPU阿甘 - 碼農翻身
- 異步通知,那我要怎么通知你啊?
- 實時操作系統
- 深入實時 Linux
- Redis 實現隊列
- redis與隊列
- 定時-時鐘-阻塞
- 計算機的生命
- 多進程/多線程
- 進程通信
- 拜占庭將軍問題深入探討
- JAVA CAS原理深度分析
- 隊列的思考
- 走進并發的世界
- 鎖
- 事務筆記
- 并發問題帶來的后果
- 為什么說樂觀鎖是安全的
- 內存鎖與內存事務 - 劉小兵2014
- 加鎖還是不加鎖,這是一個問題 - 碼農翻身
- 編程世界的那把鎖 - 碼農翻身
- 如何保證萬無一失
- 傳統事務與柔性事務
- 大白話搞懂什么是同步/異步/阻塞/非阻塞
- redis實現鎖
- 淺談mysql事務
- PHP異常
- php錯誤
- 文件加載
- 路由與偽靜態
- URL模式之分析
- 字符串處理
- 正則表達式
- 數組合并與+
- 文件上傳
- 常用驗證與過濾
- 記錄
- 趣圖
- foreach需要注意的問題
- Discuz!筆記
- 程序設計思維
- 抽象與具體
- 配置
- 關于如何學習的思考
- 編程思維
- 談編程
- 如何安全的修改對象
- 臨時
- 臨時筆記
- 透過問題看本質
- 程序后門
- 邊界檢查
- session
- 安全
- 王垠
- 第三方數據接口
- 驗證碼問題
- 還是少不了虛擬機
- 程序員如何談戀愛
- 程序員為什么要一直改BUG,為什么不能一次性把代碼寫好?
- 碎碎念
- 算法
- 實用代碼
- 相對私密與絕對私密
- 學習目標
- 隨記
- 編程小知識
- foo
- 落盤
- URL編碼的思考
- 字符編碼
- Elasticsearch
- TCP-IP協議
- 碎碎念2
- Grafana
- EFK、ELK
- RPC
- 依賴注入
- 科目一
- 開發筆記
- 經緯度格式轉換
- php時區問題
- 解決本地開發時調用遠程AIP跨域問題
- 后期靜態綁定
- 談tp的跳轉提示頁面
- 無限分類問題
- 生成微縮圖
- MVC名詞
- MVC架構
- 也許模塊不是唯一的答案
- 哈希算法
- 開發后臺
- 軟件設計架構
- mysql表字段設計
- 上傳表如何設計
- 二開心得
- awesomes-tables
- 安全的代碼部署
- 微信開發筆記
- 賬戶授權相關
- 小程序獲取是否關注其公眾號
- 支付相關
- 提交訂單
- 微信支付筆記
- 支付接口筆記
- 支付中心開發
- 下單與支付
- 支付流程設計
- 訂單與支付設計
- 敏感操作驗證
- 排序設計
- 代碼的運行環境
- 搜索關鍵字的顯示處理
- 接口異步更新ip信息
- 圖片處理
- 項目搭建
- 閱讀文檔的新方式
- mysql_insert_id并發問題思考
- 行鎖注意事項
- 細節注意
- 如何處理用戶的輸入
- 不可見的字符
- 抽獎
- 時間處理
- 應用開發實戰
- python 學習記錄
- Scrapy 教程
- Playwright 教程
- stealth.min.js
- Selenium 教程
- requests 教程
- pyautogui 教程
- Flask 教程
- PyInstaller 教程
- 蜘蛛
- python 文檔相似度驗證
- thinkphp5.0數據庫與模型的研究
- workerman進程管理
- workerman網絡分析
- java學習記錄
- docker
- 筆記
- kubernetes
- Kubernetes
- PaddlePaddle
- composer
- oneinstack
- 人工智能 AI
- 京東
- pc_detailpage_wareBusiness
- doc
- 電商網站設計
- iwebshop
- 商品規格分析
- 商品屬性分析
- tpshop
- 商品規格分析
- 商品屬性分析
- 電商表設計
- 設計記錄
- 優惠券
- 生成唯一訂單號
- 購物車技術
- 分類與類型
- 微信登錄與綁定
- 京東到家庫存系統架構設計
- crmeb
- 命名規范
- Nginx https配置
- 關于人工智能
- 從人的思考方式到二叉樹
- 架構
- 今日有感
- 文章保存
- 安全背后: 瀏覽器是如何校驗證書的
- 避不開的分布式事務
- devops自動化運維、部署、測試的最后一公里 —— ApiFox 云時代的接口管理工具
- 找到自己今生要做的事
- 自動化生活
- 開源與漿果
- Apifox: API 接口自動化測試指南