# 如何讓 cURL 可以連接自簽憑證提供的 TLS/SSL 加密連線網站

當我在 Linux 環境下開發或測試 ASP.NET Core 應用程式時，經常會需要用 [curl](https://curl.haxx.se/) 命令快速測試或取得網站內容，這時如果你連上 `https://` 加密連線網址，就會立刻遇到 `curl: (60) SSL certificate problem: unable to get local issuer certificate` 的問題，關於這個問題我打算用這篇文章徹底搞定它。

### 建立測試環境

請先安裝 [.NET Core SDK](https://dotnet.microsoft.com/download/dotnet-core/3.1) 到你的主機，然後執行以下命令建立並啟動 ASP.NET Core 網站：

```bash
dotnet new mvc -n mvc1
cd mvc1
dotnet run
```

上述三個命令就會快速啟動 ASP.NET Core 內建的 Kestrel 伺服器，並監聽 Port 5001 為加密連線：

```bash
$ dotnet run
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /home/will/build/mvc1
```

### 建立測試憑證

[.NET Core SDK](https://dotnet.microsoft.com/download/dotnet-core/3.1) 有內建一組 .NET CLI 工具，透過 `dotnet dev-certs https` 命令就可以快速產生一組以 `localhost` 為主的自簽憑證，但是在不同作業系統有不同的行為，需要特別注意！

-   Windows
    
    ```bash
    dotnet dev-certs https --trust
    ```
    
    如果想要將憑證註冊到「**受信任的根憑證授權單位**」(Trusted Root CA)，讓整台電腦預設信任這張自簽憑證，只要加上 `--trust` 或 `-t` 參數即可。如果沒有加上 `--trust` 參數，一樣也可以產生憑證，但只會註冊到「**個人**」憑證儲存區。
    
-   macOS
    
    ```bash
    dotnet dev-certs https --trust
    ```
    
    新版 .NET Core 3.1 SDK 也開始支援 `--trust` 參數，可以讓整台電腦預設信任這張自簽憑證。
    
-   Linux
    
    ```bash
    dotnet dev-certs https
    ```
    
    由於 .NET Core 3.1 SDK 在 Linux 並沒有支援 `--trust` 參數，所以這個命令只會幫你產生憑證到 `~/.dotnet/corefx/cryptography/x509stores/my` 目錄下，並不會自動幫你註冊到系統中。
    
    ```bash
    $ ls ~/.dotnet/corefx/cryptography/x509stores/my/
    6F30C86687D3A9B2503A17DDA64D66E79EE539C5.pfx
    ```
    

無論你在哪個作業系統下使用 `dotnet dev-certs https` 命令，都可以透過以下命令，將 PFX 憑證儲存到指定路徑：

```bash
dotnet dev-certs https -ep localhost.pfx -p 123123
```

由於透過 `dotnet dev-certs https` 所建立的憑證都是以 PKCS#12 的格式儲存的 (`*.pfx`)，一般在 Linux 都是用 PEM 格式為主，所以需要做一次格式轉換。透過以下命令可以將憑證的 **公開金鑰** (Public Key) 改用 PEM 格式儲存成 `localhost.crt` 檔案：

```bash
openssl pkcs12 -in localhost.pfx -out localhost.crt -nokeys -nodes
```

> 注意：由於所有 PKCS#12 格式的 PFX 憑證，都要求一定要輸入密碼，當你看到 `Enter Import Password:` 的時候，只要直接按下 Enter 即可！

### 使用 cURL 連接由自簽憑證加密的 TLS/SSL 加密連線

你只要用 [curl](https://curl.haxx.se/) 連接 Kestrel 提供的加密連線網站，就會立刻發現以下錯誤：

```bash
$ curl https://localhost:5001/
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.haxx.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
```

這個錯誤主要是告訴你，它找不到 local issuer certificate (本地發行者憑證)，所以無法「驗證」遠端加密連線的安全性，這是一種「信任」問題，並不是真的連不上，最簡單的解法，就是忽略「驗證」步驟，直接加上 `--insecure` 參數即可：

```bash
curl https://localhost:5001/ --insecure
```

無論你想用 [curl](https://curl.haxx.se/) 連接哪個 TLS/SSL 網站，當這個「信任」問題發生時，最好的解決方法還是用「信任」的方式來解決較為妥當！

如果你已經拿到 `localhost.crt` 憑證檔，就可以透過 curl 的 `--cacert` 參數直接指定該憑證，這個就可以用來驗證伺服器所使用的 TLS/SSL 加密連線：

```bash
curl https://localhost:5001/ --cacert localhost.crt
```

如果你已經拿到該網站的 **根授權憑證** (Root CA) 的 **公開金鑰** (Public Key) 檔案，也可以直接加入到 [curl](https://curl.haxx.se/) 正在使用的 **憑證授權單位**(CA) 的憑證清單中，如此一來，未來所有從 [curl](https://curl.haxx.se/) 發出的加密連線請求，就會預設通過「信任」驗證，以下是不同作業系統平台可以使用的方法：

-   Linux
    
    由於 Linux 作業系統的 Distro 版本繁多，特定檔案的儲存路徑雖然大同小異，但是遇到不同的時候，也可能會讓你找很久。
    
    我個人是利用 [strace](https://man7.org/linux/man-pages/man1/strace.1.html) 工具，分析 [curl](https://curl.haxx.se/) 執行時到底使用了哪一個 `ca-certificates.crt` 檔案，找出確切的檔案路徑，然後手動將新的憑證加入即可！
    
    如下範例，這台電腦的 CA 憑證清單就放在 `/etc/ssl/certs/ca-certificates.crt`：
    
    ```bash
    $ strace curl https://localhost:5001 2>&1 | grep ca-certificates.crt
    openat(AT_FDCWD, "/etc/ssl/certs/ca-certificates.crt", O_RDONLY) = 6
    ```
    
    除了用 [strace](https://man7.org/linux/man-pages/man1/strace.1.html) 之外，也可以在執行 [curl](https://curl.haxx.se/) 的時候，隨便指定一個不存在的 CA 檔案，讓 [curl](https://curl.haxx.se/) 出現錯誤，並顯示預設的 `CApath` 路徑，在該路徑下，就會有預設的 `ca-certificates.crt` 檔案：
    
    ```bash
    $ curl --cacert xxxxxx https://localhost:5001/
    curl: (77) error setting certificate verify locations:
      CAfile: xxxxxx
      CApath: /etc/ssl/certs
    ```
    
    不過，在 **Ubuntu 20.04 LTS** 作業系統中，更新系統管理的 CA 憑證清單有標準作法，建議透過以下命令進行更新：
    
    ```bash
    sudo cp localhost.crt /usr/local/share/ca-certificates/
    sudo update-ca-certificates
    ```
    
-   Windows (使用 PowerShell 執行)
    
    由於 Windows 10 已經內建 `curl.exe` 命令列工具，這個工具是 Microsoft 微調過的版本，大部分參數都完全一樣，唯獨驗證憑證的方式跟其他平台不同。Windows 的 `curl` 不會有自己的 `ca-certificates.crt` 檔案，而是會自動讀取 Windows 作業系統中的註冊的憑證資訊。因此，只要把你拿到的憑證匯入到 `Cert:\CurrentUser\Root\` 即可：
    
    ```powershell
    Import-Certificate -FilePath .\localhost.crt -CertStoreLocation Cert:\CurrentUser\Root\
    ```
    
    > 上述命令不需要使用系統管理者權限執行！
    
-   macOS
    
    ```bash
    sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain localhost.crt
    ```
    

### 使用 OpenSSL 快速取得遠端網站的憑證

有時候我們想連接的網站，並不是我們自己建立的憑證，當該網站的憑證不受 [curl](https://curl.haxx.se/) 信任的時候，就需要把對方的憑證抓回來，加入到 [curl](https://curl.haxx.se/) 正在使用的 **憑證授權單位**(CA) 的憑證清單中，以下我示範幾個可以方便用來抓取憑證的命令：

-   顯示該網站正在使用的所有憑證鏈完整資訊
    
    ```bash
    echo QUIT | openssl s_client -connect example.com:443 -servername example.com -showcerts
    ```
    
-   僅取得憑證內容
    
    使用 `sed` 取得憑證內容
    
    ```bash
    openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null | sed -n '/^-----BEGIN CERT/,/^-----END CERT/p'
    ```
    
    使用 `awk` 取得憑證內容
    
    ```bash
    openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null | awk '/^-----BEGIN CERT/,/^-----END CERT/'
    ```
    
-   將憑證儲存到檔案中
    
    ```bash
    openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null | sed -n '/^-----BEGIN CERT/,/^-----END CERT/p' > example.crt
    ```
    

### 相關連結

-   [curl - SSL CA Certificates](https://curl.haxx.se/docs/sslcerts.html)
-   [Default CA Cert Bundle Location - Server Fault](https://serverfault.com/questions/485597/default-ca-cert-bundle-location)
-   [How to save a remote server SSL certificate locally as a file](https://superuser.com/questions/97201/how-to-save-a-remote-server-ssl-certificate-locally-as-a-file)
-   [Add new certificate (cert) from local/internal intranet to your mac](https://gist.github.com/Artistan/5219484efb2fe51cd064175b3d0d5971)
-   [Import certificates using command line on Windows](https://superuser.com/questions/1506440/import-certificates-using-command-line-on-windows)
-   [How to run 'dotnet dev-certs https --trust'? - Stack Overflow](https://stackoverflow.com/questions/55485511/how-to-run-dotnet-dev-certs-https-trust)
