自宅サーバーでできるだけセキュアな証明書を運用する上でやったこと

## 動機
自宅でサーバーを運用しています。
ローカルでは保護されたコンテキスト（Secure Context）として証明書なしでもできていたことが、リモートだとHTTPSが必須となりブラウザで弾かれる場合があります。
特にSSL_ERROR_NO_CYPHER_OVERLAPには悩まされました

<details><summary>
以下どうすれば保護されたコンテキストになるか
</summary>

### ブラウザの「保護されたコンテキスト（Secure Context）」とは？
**「保護されたコンテキスト（Secure Context）」** とは、Webブラウザが「安全」と判断した環境でのみ実行される機能のことです。  
具体的には、**HTTPS経由で提供されるページ** または **ローカル環境（`localhost` や `file://`）** で動作するページが該当します。  


### 保護されたコンテキストの条件

次のいずれかの条件を満たすと、ブラウザはそのページを **「保護されたコンテキスト」** とみなします。  

✅ **1. HTTPS で配信されている**  
  - `https://example.com` → ✅ **保護されたコンテキスト**  
  - `http://example.com` → ❌ **保護されていない**（HTTPは安全でない）  

✅ **2. `localhost` でホストされている**  
  - `http://localhost` → ✅ **例外的に許可**（開発用に特別扱い）  

✅ **3. ファイルとして開かれている（`file://`）**  
  - `file:///C:/myproject/index.html` → ✅ **ローカルファイルは許可**  

✅ **4. サンドボックス環境（拡張機能・PWAなど）**  
  - Chrome拡張機能や一部のPWA（Progressive Web Apps）は `chrome://` の内部URLで動作するため保護される。  


### **🔹 なぜ「保護されたコンテキスト」が必要なのか？**
ブラウザは **「安全でない環境（HTTPなど）」で危険な機能を使えないようにするため** に、この仕組みを導入しました。  

例えば、以下の **「セキュリティ上重要なAPI」** は、**保護されたコンテキストでのみ利用可能** です。  

| **API名** | **説明** |
|-----------|---------|
| `navigator.geolocation` | ユーザーの位置情報を取得（GPSなど） |
| `navigator.credentials` | Web Authentication API（パスキー、FIDO2など） |
| `navigator.serviceWorker` | PWAでオフライン対応をするためのService Worker |
| `window.crypto.subtle` | Web Cryptography API（鍵の生成・暗号化処理） |
| `getUserMedia` | カメラ・マイクへのアクセス（WebRTC） |
| `Web Bluetooth` | Bluetoothデバイスと接続 |
| `Web USB` | USBデバイスとやり取り |
| `Web NFC` | NFCリーダーとの通信 |
| `SharedArrayBuffer` | 高速なスレッド間メモリ共有（Spectre対策のためHTTPS必須） |

🚫 **これらのAPIはHTTPページでは動作しません。**  


### **🔹 保護されたコンテキストの確認方法**
「このページが **保護されたコンテキスト** かどうか」を確認するには、ブラウザの開発ツール（DevTools）でチェックできます。  

✅ **Chromeで確認する方法**  
1. **開発ツール（DevTools）を開く**（`F12` または `Ctrl+Shift+I`）  
2. **Consoleタブ** に以下のコードを入力：
   ```javascript
   console.log(window.isSecureContext);
   ```
3. **`true` が出れば保護されたコンテキスト、`false` なら非保護**  


### **🔹 HTTPで開発している場合の対策**
#### **🔹 1. `localhost` で開発する**
開発環境なら `http://localhost` は例外的に **保護されたコンテキスト** なので、そのまま動作する。  

#### **🔹 2. HTTPSを有効にする**
`localhost` 以外で開発する場合は、自己署名証明書やLet's Encryptを使って **HTTPSを有効にする** 必要がある。  
例えば、`mkcert` を使えば簡単に `localhost` 用のHTTPS証明書を作れる：
```sh
mkcert -install
mkcert localhost
```

#### **🔹 3. Webサーバーを設定する**
開発用に `https` を使う場合、例えば **Caddy** や **NGINX** でローカルHTTPSを立てるのも手。
```caddyfile
localhost:443 {
    tls self_signed
    root * /var/www
    file_server
}
```

### **🔹 ここまでのまとめ**
🔹 **「保護されたコンテキスト」とは、安全な環境（HTTPS・localhost）でのみ動作するWebページのこと。**  
🔹 **HTTPページでは、位置情報API・暗号API・WebRTC などの重要な機能が動作しない。**  
🔹 **開発時にHTTPを使うなら `localhost` を利用するか、HTTPSを有効にする必要がある。**  

</details>

## 手順

1. OpenSSL をインストール
2. ルートCA証明書の作成
    1. 秘密鍵（Private Key）の作成
    2. 証明書署名要求（CSR: Certificate Signing Request）の作成
    3. 証明書に署名
3. サーバー証明書の作成
    1. 秘密鍵（Private Key）の作成
    2. 証明書署名要求（CSR: Certificate Signing Request）の作成
    3. 証明書に署名
4. Nginx や Apache に設定
5. ブラウザでの設定



### **🔹OpenSSL をインストール**
自己署名証明書を作成するには `openssl` を使用します。  
Linux や macOS では通常すでにインストールされていますが、ない場合はインストールしてください。

### **🔹ルートCA証明書の作成**

#### **🔹秘密鍵（Private Key）の作成**

```sh
openssl ecparam -name secp521r1 -genkey -genpkey -out root.key 
```

##### ハマったこと

鍵のアルゴリズムが原因で、SSL_ERROR_NO_CYPHER_OVERLAPというエラーが発生しました。

![image](https://aboutssl.org/wp-content/uploads/2021/10/ssl-error-no-cypher-overlap.svg)

curlでは動くのになぜFirefoxはだめなのか…？
とハマること数時間、鍵をEd25519にしたことが原因だとわかりました。

この記事でも触れられていますが、今ある中ではEd25519が圧倒的に意識が高く堅牢です。

https://qiita.com/wnoguchi/items/a72a042bb8159c35d056

セキュアじゃない順にこのような感じです。

    RSA 1024bit 以下
    無難に RSA 2048bit
    常識 RSA 4096bit
    新まあまあ意識高い ECDSA 256bit
    まあまあ意識高い ECDSA 384bit
    意識高い ECDSA 521bit
    圧倒的に意識高い Ed25519

そのようなわけで当初はEd25519で鍵を作っていました。

本当はCRYSTALS-KYBERを使いたかったのですが、さすがに新しすぎて…

https://www.ipa.go.jp/security/crypto/guideline/gmcbt80000005ufv-att/ipa-cryptrec-gl-3001-3.1.0.pdf

これによると

> 具体的には、表 6 に示した「署名」及び「ハッシュ関数」がサーバ証明書で利用可能な暗号アルゴリズムである。例外は DSA と EdDSA である。
> DSA については、電子政府推奨暗号リストに選定されており安全性上の問題はないが、デジタル署名の米国政府標準暗号 FIPS186-5 からも仕様が削除された[12]ことから、今後、新規利用時や更新時は DSA を利用すべきではない。
> また、EdDSA については、パブリック認証局が発行するサーバ証明書の要件を規定する CA/ブラウザフォーラムのベースライン要求（2023 年 8 月 17 日発行分）[13]において利用できる署名アルゴリズムに指定されていないことから、現時点ではサーバ証明書で利用可能な暗号アルゴリズムに EdDSA を含めない。

とのことからEdDSAは証明書に使えません。

[AWSのドキュメント](https://aws.amazon.com/jp/about-aws/whats-new/2024/01/network-load-balancer-rsa-3072-bit-ecdsa-256-384-521-bit-certificates/)にもECDSAしかのっていません。
これを知らなかったのがつまづきポイントでした。

それはこのあとのサーバー証明書もそうで、どちらもブラウザがサポートしているアルゴリズムを使わないといけません。
かなり面倒でした。

:::note info
証明書の動作を試すときは

* curl
* Firefox
* Chrome

の順で試すと良さげでした
Chromeはよくわからない仕様もあってデバッグしづらいです。その点curlはサポートしているところも多いので問題の切り分けに役立ちます。
:::


#### **🔹 証明書署名要求（CSR: Certificate Signing Request）の作成**
秘密鍵から **証明書署名要求 (CSR)** を作成します。  

```sh
openssl req -new -key root.key -out root.csr -subj "/CN=SANDBOX_ROOT_CA"
```

#### **🔹 証明書に署名**
CSR を使って自己署名証明書を発行します。

```sh
openssl x509 -req -days 365 -in root.csr -signkey root.key -out root.crt
```

- `-days 365` → **証明書の有効期限（1年）**
- `-signkey server.key` → **秘密鍵で署名**
- `-out server.crt` → **出力する証明書ファイル**

> **証明書の内容を確認する**
> ```sh
> openssl x509 -text -noout -in server.crt
> ```

### **🔹サーバー証明書の作成**

#### **🔹秘密鍵（Private Key）の作成**

```sh
openssl ecparam -name secp521r1 -genkey -genpkey -out server.key 
```

#### **🔹 証明書署名要求（CSR: Certificate Signing Request）の作成**
秘密鍵から **証明書署名要求 (CSR)** を作成します。  

```sh
openssl req -new -key server.key -out server.csr -subj "/CN=server.local"
```

#### **🔹 証明書に署名**
CSR を使って自己署名証明書を発行します。

```sh
cat << eof > /tmp/san.txt
subjectAltName = DNS:mypi4.local
eof
openssl x509 -req -days 365 -in root.csr -signkey root.key -extfile /tmp/san.txt -out root.crt
rm /tmp/san.txt
```

#### ハマったポイントその２

**⚠️ Subject Alt Name（SAN）には実際のドメイン名を必ず入力すること！**
今回はCNを入れていますが、これは入れても、入れなくても構いません。ブラウザで見やすくなるので入れてます。
**ですがSANだけは入れないといけません。**

ブラウザはもうCNを見ないでSANを見ています。

https://rms.ne.jp/digital-certificate-news/chrome-deprecates_subject-cn-matching

ここでは例として `server.local` をSANに入れています。
SANがないと不正な証明書として扱われるので忘れずにいれてください。
証明書署名要求 (CSR)の方でも入れることができます。


### **🔹 Nginx や Apache に設定**
#### **🔹 Nginx の場合**
`/etc/nginx/sites-available/default` などの設定ファイルを編集。

```nginx
server {
    listen 443 ssl;
    server_name server.local;

    ssl_certificate /path/to/server.crt;
    ssl_certificate_key /path/to/server.key;

    location / {
        root /var/www/html;
        index index.html;
    }
}
```
設定をテストして再起動:
```sh
sudo nginx -t
sudo systemctl restart nginx


#### **🔹 curlなどの場合**

##### **🔹 方法 1: 証明書を `curl` に直接指定する**
自己署名証明書を `curl` で使用するには、 `--cacert` オプションを使います。  
```sh
curl --cacert my_cert.crt https://my-secure-server.com
```
📌 `my_cert.crt` は **サーバーの自己署名証明書またはカスタムCAの証明書** です。

✅ **メリット**: 一時的な利用に適している。  
❌ **デメリット**: 毎回 `--cacert` を指定する必要がある。

##### **🔹 方法 2: 証明書をシステムに追加する**

###### **🔸 Linux（Debian / Ubuntu）**
1. 証明書の場所を確認し、`.pem` に変換（必要な場合）
   ```sh
   openssl x509 -in my_cert.crt -out my_cert.pem -outform PEM
   ```
2. 証明書をシステムのCAストアに追加
   ```sh
   sudo cp my_cert.pem /usr/local/share/ca-certificates/my_cert.crt
   ```
3. CAストアを更新
   ```sh
   sudo update-ca-certificates
   ```
4. 証明書が適用されたか確認
   ```sh
   curl https://my-secure-server.com
   ```

###### **🔸 Linux（RedHat / CentOS / Fedora）**

1. 証明書の変換（必要な場合）
   ```sh
   openssl x509 -in my_cert.crt -out my_cert.pem -outform PEM
   ```
2. 証明書を `ca-trust` に追加
   ```sh
   sudo cp my_cert.pem /etc/pki/ca-trust/source/anchors/
   ```
3. CAストアを更新
   ```sh
   sudo update-ca-trust
   ```

###### **🔸 macOS**

1. 証明書をキーチェーンに追加
   ```sh
   sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain my_cert.crt
   ```
2. 証明書の適用を確認
   ```sh
   curl https://my-secure-server.com
   ```


###### **🔸 Windows（PowerShell）**

1. 証明書をシステムの「信頼されたルート証明機関」に追加
   ```powershell
   Import-Certificate -FilePath "C:\path\to\my_cert.crt" -CertStoreLocation Cert:\LocalMachine\Root
   ```
2. 証明書のインポートを確認
   ```powershell
   Get-ChildItem Cert:\LocalMachine\Root | Select-String "My Certificate"
   ```
3. `curl` でテスト
   ```powershell
   curl https://my-secure-server.com
   ```

##### **🔹 方法 3: `curl` の CA バンドルを手動で指定**
もし `--cacert` を毎回指定するのが面倒な場合は、デフォルトの CA バンドルに追加することもできます。

###### **🔸 Linux/macOS の場合**

1. **現在の CA バンドルの場所を確認**
   ```sh
   curl-config --ca
   ```
   例: `/etc/ssl/certs/ca-certificates.crt`
2. **証明書を結合**
   ```sh
   cat my_cert.crt >> /etc/ssl/certs/ca-certificates.crt
   ```
3. **確認**
   ```sh
   curl https://my-secure-server.com
   ```

#### **🔹 Firefoxの場合**

1. **Firefoxを開く**  
2. **設定を開く**（URLバーに `about:preferences` と入力）  
3. **「プライバシーとセキュリティ」→「証明書を表示」ボタンをクリック**  
4. **「認証局証明書」タブを選択**  
5. **「インポート」ボタンをクリック**  
6. **事前に用意した `.crt` ファイルを選択して開く**  
7. **「この認証局を信頼する」にチェックを入れる**（特に「Webサイトの認証に使用」を選択）  
8. **OKを押して設定を保存**  

## **🔹 Chromeで証明書をインポートする手順（Windows & macOS）**  
自己署名証明書や独自の認証局（CA）の証明書を **Google Chrome** にインポートする方法を説明します。  

#### **🔹 Chromeの場合**

##### **🔹Windows**

###### **1. 証明書マネージャーを開く**
1. **Windowsキー + R を押して「ファイル名を指定して実行」を開く**  
2. `certmgr.msc` と入力して **Enter**  

###### **2. 証明書のインポート**
1. 左側のパネルで **「信頼されたルート証明機関」→「証明書」** を開く  
2. 右クリックして **「すべてのタスク」→「インポート」** を選択  
3. **証明書のインポートウィザード** が開くので、「次へ」をクリック  
4. **証明書ファイル（`.crt` や `.cer`）を選択**  
5. **「証明書ストアを手動で選択する」** を選び、「信頼されたルート証明機関」を選択  
6. **「完了」をクリックしてインポート**  
7. Windowsを **再起動**（またはChromeを再起動）  

##### **🔹 macOS**

###### **1. キーチェーンアクセスを開く**
1. **Spotlight (`Cmd + Space`) で「キーチェーンアクセス」を検索して開く**  
2. 左のパネルで **「システム」または「ログイン」** を選択  
3. **「ファイル」→「証明書を読み込む」を選択**  

###### **2. 証明書の設定**
1. **インポートする `.crt`（または `.cer`）ファイルを選択**  
2. **「システム」または「ログイン」に追加**（「システム」の方がOS全体に適用される）  
3. インポートした証明書をダブルクリックし、**「この証明書を常に信頼する」** を選択  
4. **変更を保存するために管理者パスワードを入力**  
5. **Macを再起動（またはChromeを再起動）**  


##### **🔹 Linux**

1. **Chromeを開く**  
2. **`chrome://settings/certificates` にアクセス**  
3. **「認証局」タブを開く**  
4. **インポートした証明書があるか確認**  
5. **「信頼する」設定になっているかチェック**  

## 感想

しくじりポイントには悩まされました。
この記事がどなたかの役に立てば幸いです。

---

P.S.
耐量子暗号も入れたかったけど無理そう
やったことある人は教えてください