サーバ証明書に最低限必要なkeyUsageを調べる

# はじめに
x509証明書のkeyUsageについて、どの証明書に何を入れていれば動くのかよくわかっていないため、調べてまとめる

# keyUsageの種類
keyUsageはRFC5280により定義されている

https://tex2e.github.io/rfc-translater/html/rfc5280.html

```
      KeyUsage ::= BIT STRING {
           digitalSignature        (0),
           nonRepudiation          (1), -- recent editions of X.509 have
                                -- renamed this bit to contentCommitment
           keyEncipherment         (2),
           dataEncipherment        (3),
           keyAgreement            (4),
           keyCertSign             (5),
           cRLSign                 (6),
           encipherOnly            (7),
           decipherOnly            (8) }
```

それぞれの用途はここが参考になった

https://www.ykrods.net/posts/2021/02/11/tls-authentication/

# 証明局の証明書
ルート証明書(および中間証明書)の役割は以下
- 下位中間証明書もしくはサーバ証明書に署名する
- crl(証明書失効リスト)に署名する

そのため、証明局用の証明書には`keyCertSign`,`cRLSign`のみを含めば良いはず
証明書、crl以外に認証のための署名を行う場合は`digitalSignature`も必要になる

# サーバ証明書
サーバ証明書の役割は以下
- サーバが正規ドメインに管理されていることを証明する
  - サーバ証明書内には証明局によるディジタル署名が含まれている
  - クライアントはこれを検証することでサーバ証明書が証明局に発行されていることを検証できる
  - クライアントが検証する分にはkeyUsageを設定する必要がないように思われる
- 暗号通信を行う
  - TLS通信を行う際、クライアントはサーバ証明書に含まれる公開鍵を用いて共通鍵を作成するため`keyEncipherment`が必要
  - かと思われたが、サーバ証明書から共通鍵を作成するのはセキュリティリスクがありtls1.3から禁止されたそう。
  - https://milestone-of-se.nesuke.com/nw-basic/tls/https-structure/

つまりサーバ証明書にkeyUsageは不要なのでは？と思ったが、TLS暗号化プロセスにおいてサーバが公開鍵に対応する秘密鍵を持っているか証明するために、適当なデータに署名したデータをクライアントに送信するプロセス(CertificateVerify)があるらしい。
そもそもサーバ証明書自身に署名機能がないと秘密鍵を持っている証明ができないので当たり前と言えば当たり前だった。

https://tex2e.github.io/rfc-translater/html/rfc8446.html

# 実際に検証してみた
```bash
# 環境情報
❯ k get node
NAME       STATUS   ROLES                  AGE   VERSION
orbstack   Ready    control-plane,master   16d   v1.27.4+orb1
```

## webサーバの準備
kubernetes上にwebapp Pod,Service,Ingressを立てる

<details><summary>webapp.yaml</summary>

```yaml
❯ cat webapp.yaml 
---
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: webapp
  name: webapp
  namespace: webapp
spec:
  containers:
  - image: nginx:latest
    name: webapp
    ports:
    - name: http
      containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: webapp
  namespace: webapp
spec:
  selector:
    run: webapp
  ports:
  - name: http
    protocol: TCP
    port: 80
    targetPort: http
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: webapp
  namespace: webapp
spec:
  tls:
  - hosts:
    - webapp.k8s.orb.local
    secretName: webapp-cert
  rules:
  - host: webapp.k8s.orb.local
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: webapp
            port:
              name: http
```
</details>

## keyUsageを含まない証明書をマウントする

```bash
❯ cat ext.txt 
[req]
subjectAltName= DNS:webapp.k8s.orb.local
keyUsage=critical,cRLSign

❯ openssl x509 -req -in tls.csr -CA ca.crt -CAkey ca.key -days 365 \
  -extensions req -extfile ext.txt -out tls.crt

❯ k create secret tls webapp-cert --cert=tls.crt --key=tls.key -n webapp
```

<details><summary>webアクセスと証明書のスクリーンショット</summary>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3690974/32fc1091-0f3f-5156-f868-5d0e78318d20.png)

![Screen Shot 2024-04-29 at 14.37.22-fullpage.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3690974/3ab1fa6e-4b6e-e552-fbf9-deabd45ccff3.png)

</details>

Safari/Firefox/Chromeで確認したが、予想に反して、keyUsageを指定しなくても特に怒られずアクセスできた。RFC上は利用用途不明な証明書と見なされるはずなので、ブラウザがよしなに解釈して見逃して(?)くれていると思われる

# まとめ
- ルート証明書,中間証明書のkeyUsageには`keyCertSign`,`cRLSign`を最低限付与すべし
- TLS通信用のサーバ証明書のkeyUsageには`digitalSignature`を最低限付与すべし
  - extendedKeyUsageには`serverAuth`を付与するとなお良し

# 余談

サーバ証明書が署名しか行わないのであれば、サーバ証明書は暗号・復号機能を持つRSAの鍵ペアではなく署名機能のみ持つEdDSA鍵ペアでいいのではないか？と思い、実際に検証してみた

<details><summary>証明書</summary>

```bash
❯ openssl s_client -connect webapp.k8s.orb.local:443 </dev/null 2&>/dev/null | openssl x509 -noout -text       
Warning: Reading certificate from stdin since no -in or -new option is given
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            36:1d:7d:ae:00:97:16:60:18:fa:c1:9b:86:f9:af:ce:22:21:db:52
        Signature Algorithm: ED25519
        Issuer: CN=CA Root
        Validity
            Not Before: Apr 29 06:00:31 2024 GMT
            Not After : Apr 29 06:00:31 2025 GMT
        Subject: CN=webapp
        Subject Public Key Info:
            Public Key Algorithm: ED25519
                ED25519 Public-Key:
                pub:
                    0c:b4:35:44:f7:26:c5:bd:16:4a:78:f9:8e:c3:9f:
                    ae:5b:8e:ce:5f:83:e5:df:77:27:d1:17:9b:e0:2f:
                    e2:d6
        X509v3 extensions:
            X509v3 Subject Alternative Name: 
                DNS:webapp.k8s.orb.local
            X509v3 Subject Key Identifier: ...
            X509v3 Authority Key Identifier: ...
    Signature Algorithm: ED25519
    Signature Value: ...
```

</details>

![Screen Shot 2024-04-29 at 15.04.01-fullpage.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3690974/243046d3-e0e2-7c6b-0ea7-1cc7296dfb8b.png)

予想に反して接続できなかった...

### 原因調査

RFC8446 を見る限り、ed25519は署名スキームリストに含まれているが、必須でサポートしないといけない署名リストには含まれていない.各ブラウザが対応していないことが推測される

>  A TLS-compliant application MUST support digital signatures with rsa_pkcs1_sha256 (for certificates), rsa_pss_rsae_sha256 (for CertificateVerify and certificates), and ecdsa_secp256r1_sha256.  A TLS-compliant application MUST support key exchange with secp256r1(NIST P-256) and SHOULD support key exchange with X25519 [RFC7748].

https://datatracker.ietf.org/doc/html/rfc8446

上記に記載のあるECDSA鍵ペアだと正常にTLS通信できた

## 余談のまとめ
- TLS通信用のサーバ証明書は暗号処理機能および署名機能を持つRSA鍵ペアじゃなくても、署名機能のみ持つECDSA鍵ペアでもいい
- ただし同じく署名機能を持つEdDSAはTLS1.3のRFC8446に記載がない(≒ブラウザ・クライアント実装による)ためTLS通信できない可能性がある
- RSAよりもECDSAの方が短い鍵長で同じ強度を保てるらしい
  - 互換性を重視するならRSA 2048/4096 bitを利用し、セキュリティやパフォーマンスを重視するならECDSAを利用するのが良さそう

## 余談追記
2024/09 firefox130でed25519による署名アルゴリズムが対応したらしい
 
https://mozillazine.jp/?p=8738