Cloudflare mTLS での失効確認

## はじめに
Cloudflare の TLS クライアント認証（mTLS）で証明書の失効（revocation）確認をします。

## 対象範囲
Eyeball と Cloudflare の間が今回の対象です。

```mermaid
sequenceDiagram
    participant Eyeball
    participant Cloudflare

    rect rgb(191, 223, 255)
    note right of Eyeball: 今回の対象範囲
    Eyeball ->> Cloudflare :認証（あなた本物？）
    Cloudflare ->> Eyeball :認証（あなた本物？）
    end
    Cloudflare->>Origin: 認証（あなた本物？）
    Origin ->> Cloudflare :認証（あなた本物？）
```

## クライアント証明書発行 CA
クライアント証明書発行者の CA 証明書を Cloudflare に展開します。
Cloudflare CA、または、外部からの持ち込み CA が可能です。
どちらが使えるかは、プロダクトによって対応が異なります。

- 持ち込み CA
    - [API Shield](https://developers.cloudflare.com/ssl/client-certificates/byo-ca-api-shield/)（2023 年の[機能追加](https://blog.cloudflare.com/bring-your-own-ca-for-client-certificate-validation-with-api-shield/)）
    - [Access](https://developers.cloudflare.com/cloudflare-one/identity/devices/access-integrations/mutual-tls-authentication/)
- Cloudflare の CA
    - [API Shield](https://developers.cloudflare.com/ssl/client-certificates/create-a-client-certificate/)

```mermaid

flowchart TD
    A[Start] --> B{持ち込みCA?}
    B -->|Yes| C[Access]
    B -->|Yes| D[API Shield]
    B -->|No| D[API Shield]
```

失効確認の方法は Cloudflare CA と持ち込み CA で異なるので、それぞれ記載します。

## 持ち込み CA
### CA の持ち込み方法
#### API Shield の場合
[CA の持ち込み](https://developers.cloudflare.com/ssl/client-certificates/byo-ca-api-shield/)は今のところ [API から](https://developers.cloudflare.com/api/operations/m-tls-certificate-management-upload-m-tls-certificate)のみです。

<details><summary>API 例</summary>

```
# CA のチェーン証明書（pem）の改行（LF）を変更

CERTFILE=チェーン証明書のファイル名
CHAIN=$(perl -pe 's/\n/\\n/g' $CERTFILE)

# POST データをファイルで用意

CERTNAME=Cloudflareへの登録名
echo -E '{"ca":true,"certificates":"'"$CHAIN"'","name":"'"$CERTNAME"'"}' > chain.upload

# 証明書データを API で POST し、応答の ID をメモ（エンドポイント：account）

curl "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/mtls_certificates" -H "X-Auth-Email: $EMAIL" -H "X-Auth-Key: $API_KEY" -H "Content-Type: application/json" -d '@chain.upload'

CERTID=`curl "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/mtls_certificates" -H "X-Auth-Email: $EMAIL" -H "X-Auth-Key: $API_KEY" -s | jq -r '.result[]|select(.name == "'$CERTNAME'").id'`

# ID を使い、この CA を当てたい Hostname に関連付け（エンドポイント：zone）

TLSHOST=対象ホスト名

curl "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/certificate_authorities/hostname_associations" -H "X-Auth-Email: $EMAIL" -H "X-Auth-Key: $API_KEY" -H "Content-Type: application/json" -d '{"hostnames":["'"$TLSHOST"'"],"mtls_certificate_id":"'"$CERTID"'"}' -X PUT

```
</details>

#### Access の場合

[ダッシュボード](https://developers.cloudflare.com/cloudflare-one/identity/devices/access-integrations/mutual-tls-authentication/)あるいは [API](https://developers.cloudflare.com/api/operations/zone-level-access-mtls-authentication-add-an-mtls-certificate) から CA を持ち込むことができます。

<details><summary>API 例</summary>

```
# CA 証明書の準備は上記と同じように行い、投入（エンドポイント：zone）
# こっちは Hostoname との関連付けも一発で実施できる

curl "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/access/certificates" -H "X-Auth-Email: $EMAIL" -H "X-Auth-Key: $API_KEY" -s -X POST -d '{"associated_hostnames": ["mtls.oyama.cf"],  "certificate":"'$CHAIN'", "name": "rootCA"}' -H "Content-Type: application/json"
```
</details>

### クライアント証明書の把握と情報共有

```mermaid
sequenceDiagram
    participant Eyeball
    participant Cloudflare
    Cloudflare ->> Eyeball :認証（あなた本物？）
    Eyeball ->> Cloudflare :認証（これでどう？）
    Cloudflare ->> Cloudflare :署名確認
    Cloudflare ->> Cloudflare :失効確認
```
#### クライアント証明書に関する情報の抽出と展開
クライアントが提示する証明書に応じて失効確認を行うので、まず証明書からの情報の取り出し方について記します。
##### クライアント証明書の各情報
Workers では受信したクライアント証明書の各情報（[シリアル番号](https://developers.cloudflare.com/ruleset-engine/rules-language/fields/#field-cf-tls_client_auth-cert_serial) など）に `cf.tls_client_auth.*` のプロパティでアクセスすることが[できます](https://developers.cloudflare.com/ssl/client-certificates/enable-mtls/#cloudflare-workers)。
CRL の確認で使えます。

<details><summary>例：クライアント証明書の情報を取る</summary>

```
      newrequest.headers.set('x-cn', request.cf.tlsClientAuth.certSubjectDN)
```
結果は。
```
  "X-Cn": "CN=Iwa Te",
```
</details>

##### クライアント証明書そのもの
リクエストヘッダーにクライアント証明書を[格納し](https://developers.cloudflare.com/ssl/client-certificates/enable-mtls/#forward-a-client-certificate)転送することができます。あらかじめ [API](https://developers.cloudflare.com/api/operations/zone-level-access-mtls-authentication-update-an-mtls-certificate-settings) で設定しておきます。
OCSP の確認で使えます。

<details><summary>例：クライアント証明書自体をヘッダーに追加</summary>

```
curl --request PUT \
  --url "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/access/certificates/settings" \
  --header 'content-type: application/json' \
  --header "x-auth-email: $EMAIL" \
  --header "x-auth-key: $API_KEY" \
  --data '{
    "settings": [
        {
            "hostname": "mtls-fw.oyama.cf",
            "client_certificate_forwarding": true
        }
    ]
}'
```

オリジンサーバーでは "Cf-Client-Cert-Der-Base64" で証明書を受信できます。
```
  "Cf-Client-Cert-Der-Base64": "MII...."
```
デコードしてみます。
```
printf $HEADER_CERT|base64 -d|openssl x509 -inform DER -noout -text - |head -11
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            97:74:b2:3d:0d:f4:47:8c:94:51:3c:95:db:cd:a6:ab
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C = JP, ST = Tokyo, O = IT, CN = Oreno Inter
        Validity
            Not Before: Nov  7 00:00:00 2023 GMT
            Not After : Nov  8 00:00:00 2024 GMT
        Subject: CN = Iwa Te
```
</details>

##### 補足：Transform Rules
クライアント証明書情報のリクエストヘッダーへの追加は、Workers の他に [Transform Rules](https://developers.cloudflare.com/rules/transform/response-header-modification/) のマネージドルール（Managed Transforms > Request Header）の `Add TLS Client auth headers` でも実装することができます。

<img width="600" alt="alt" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2218045/02e31f48-f4aa-59a0-0470-760ab17d9cfd.png">


<details><summary>試験結果：Add TLS Client auth headers を有効</summary>

```
  "Cf-Cert-Verified": "true",
  "Cf-Cert-Subject-Dn-Rfc2253": "CN=Iwa Te",
  "Cf-Cert-Subject-Dn-Legacy": "/CN=Iwa Te",
  "Cf-Cert-Subject-Dn": "CN=Iwa Te",
  "Cf-Cert-Ski": "940E3F30AC31A3F2DB06B520A71E3EC6D9DC2168",
  "Cf-Cert-Sha256": "039ff9d18f2c7a58f567ef61fb2890c7e0c2c17bcb4af135d90e1a1458fa24e2",
  "Cf-Cert-Sha1": "4ac762f24f39e555a10712040553c18a5d8fc3d8",
  "Cf-Cert-Serial": "9774B23D0DF4478C94513C95DBCDA6AB",
  "Cf-Cert-Revoked": "false",
  "Cf-Cert-Presented": "true",
  "Cf-Cert-Not-Before": "Nov  7 00:00:00 2023 GMT",
  "Cf-Cert-Not-After": "Nov  8 00:00:00 2024 GMT",
  "Cf-Cert-Issuer-Ski": "46BB44579F7F3D16BBC819B2039DF9F4B2301399",
  "Cf-Cert-Issuer-Serial": "170A1ABC2A5348DEA6E444733E1DA712",
  "Cf-Cert-Issuer-Dn-Rfc2253": "CN=Oreno Inter,O=IT,ST=Tokyo,C=JP",
  "Cf-Cert-Issuer-Dn-Legacy": "/C=JP/ST=Tokyo/O=IT/CN=Oreno Inter",
  "Cf-Cert-Issuer-Dn": "CN=Oreno Inter,O=IT,ST=Tokyo,C=JP",
```
</details>

##### Access JWT
Access の場合はデフォルトで [JWT](https://developers.cloudflare.com/cloudflare-one/identity/authorization-cookie/validating-json/) も入ってきます。

<details><summary>試験結果：Access JWT</summary>

```
  "Cf-Access-Jwt-Assertion": "eyJr...."
```
オリジン側でデコードすることができます。
```
{
  "kid": "*",
  "alg": "RS256",
  "typ": "JWT"
}
{
  "type": "app",
  "aud": "*",
  "exp": 1699708725,
  "iss": "https://*.cloudflareaccess.com",
  "common_name": "Iwa Te",
  "iat": 1699707825,
  "sub": "CN=Iwa Te"
}
```
</details>

### 失効確認

[CRL](https://en.wikipedia.org/wiki/Certificate_revocation_list) や [OCSP](https://en.wikipedia.org/wiki/Online_Certificate_Status_Protocol) での確認は Access や API Shield 自体には今のところ実装されていないため、Workers で補います。

#### CRL の場合

[サンプル](https://github.com/cloudflare/access-crl-worker-template)を利用します。
Wrangler の[アップデート](https://developers.cloudflare.com/workers/wrangler/migration/v1-to-v2/eject-webpack/#i-use-type--webpack-but-do-not-provide-my-own-configuration-and-let-wrangler-take-care-of-it)と [RFC5280 対応](https://datatracker.ietf.org/doc/html/rfc5280#appendix-B)でパッチを当てました。

API shield（ WAF の Custom Rules ）の mTLS をはめた URL、Access の mTLS をはめた URL どちらも同じスクリプトで失効確認ができました。

<details><summary>patch</summary>

```toml:wrangler.toml
追加 compatibility_date 
削除 type = "webpack" 
```
```json:index.js
@@ -62,6 +62,9 @@
       revokedSerialNumbers: crlSimpl.revokedCertificates.reduce(
         (revokedSerialNums, cert) => {
           let serialNum = bufToHex(cert.userCertificate.valueBlock.valueHex)
+          if ((serialNum.length >= 34) && (serialNum.startsWith('00'))) {
+          serialNum = serialNum.replace(/^00/,'')
+          }
           revokedSerialNums[serialNum] = true
           return revokedSerialNums
         },
```
</details>
<details><summary>試験結果</summary>

```shell
# CRL 配布ポイント

~/mTLS$ openssl x509 -in Iwa_Te.pem -noout -text|grep -A2 CRL
            X509v3 CRL Distribution Points:
                Full Name:
                  URI:http://crl.oyama.cf/inter.crl

# CRL の確認

~/mTLS$ curl http://crl.oyama.cf/inter.crl --output temp.crl -s

~/mTLS$ openssl crl -inform der -in temp.crl -noout -text |awk '/Revoked/','/Signature/'

Revoked Certificates:
    Serial Number: 07A1AF95812C41EB971B4BBBB562FE16
        Revocation Date: Nov  7 01:29:21 2023 GMT
    Serial Number: 9C50B73086B94FB9BCD89795B91720C1
        Revocation Date: Nov  7 13:37:47 2023 GMT
    Serial Number: 9C50B73086B94FB9BCD89795B91720C1  <<===== これでテスト
        Revocation Date: Nov  8 11:21:01 2023 GMT
    Serial Number: 8B7787865BB1479CA314DC36B19D9717
        Revocation Date: Nov  8 11:24:37 2023 GMT
    Serial Number: 96AF72EED69B4211927A2AC19D8DF10F
        Revocation Date: Nov 13 00:02:15 2023 GMT
    Signature Algorithm: sha256WithRSAEncryption


# 失効している証明書

~/mTLS$ openssl x509 -in Hoge_Fuga.pem -noout -text|grep -A1 Serial

        Serial Number:
            9c:50:b7:30:86:b9:4f:b9:bc:d8:97:95:b9:17:20:c1

## API Shield → ブロックされる

~/mTLS$ curl https://mtls-fw.oyama.cf/ --cert Hoge_Fuga.pem --key Hoge_Fuga.key

client certificate was revoked

## Access → ブロックされる

~/mTLS$ curl https://mtls.oyama.cf/ --cert Hoge_Fuga.pem --key Hoge_Fuga.key

client certificate was revoked

# 失効していない証明書

~/mTLS$ openssl x509 -in Iwa_Te.pem -noout -text|grep -A1 Serial

        Serial Number:
            97:74:b2:3d:0d:f4:47:8c:94:51:3c:95:db:cd:a6:ab

## API Shield → 許可される

~/mTLS$ curl https://mtls-fw.oyama.cf/ --cert Iwa_Te.pem --key Iwa_Te.key

Hello World

## Access → 許可される

~/mTLS$ curl https://mtls.oyama.cf/ --cert Iwa_Te.pem --key Iwa_Te.key

Hello World

```
結果を KV にキャッシュするようにしてあるので速そうです。
</details>


```mermaid
sequenceDiagram
    participant Eyeball
    participant Cloudflare
    participant KV
    participant CRL Distribution Points
    Cloudflare ->> Eyeball :認証（あなた本物？）
    Eyeball ->> Cloudflare :認証（これでどう？）
    Cloudflare ->> Cloudflare :署名確認 OK
    Cloudflare ->> CRL Distribution Points :if (KV null, expired or force update) CRL 確認 
    Cloudflare ->> KV :失効 KV Update
    Cloudflare ->> KV :失効 KV 確認
```

#### OCSP の場合

サンプルがなかったので下記で試しました。
API shield（ WAF の Custom Rules ）の mTLS をはめた URL、Access の mTLS をはめた URL どちらも同じスクリプトで失効確認ができました。

<details><summary>例</summary>

:::note warn
注）本テスト用のスクリプトで、安全策や効率化（結果のキャッシュなど）を考慮していないので商用では利用しないでください。
:::

```toml:wrangler.toml
name = "mtls-ocsp"
main = "src/index.js"
compatibility_date = "2023-11-12"

workers_dev = false
route = { pattern = "mtls-fw.oyama.cf/ocsp", zone_name = "oyama.cf" }

[vars]
CA_CLIENT_ISSUER = "MIIF0.... <<== クライアント証明書の Issuer 
CA_OCSP_ROOT = "MIIFa.... <<== OCSP レスポンダーの CA
```

```javascript:index.js
import * as asn1js from 'asn1js'
import { getRandomValues, Certificate, Extension, OCSPRequest, OCSPResponse,  GeneralName, BasicOCSPResponse } from 'pkijs';

export default {
  async fetch(request, env, ctx) {
    
    //https://gist.github.com/robincher/5c73e8ccb53fad9b611778ab363a416a
    
    function base64StringToArrayBuffer(b64str) {
      let byteStr = atob(b64str);
      let bytes = new Uint8Array(byteStr.length);
      for (let i = 0; i < byteStr.length; i++) {
        bytes[i] = byteStr.charCodeAt(i);
      }
      return bytes.buffer;
    }
    
    function printCertificate (certificateBuffer) {
      let asn1 = asn1js.fromBER(certificateBuffer);
      if(asn1.offset === (-1)) {
        console.log("Can not parse binary data");
      } 
      return new Certificate({ schema: asn1.result });
    }
  
  // Prepare Certs
  
  const b64Cl = request.headers.get('cf-client-cert-der-base64');
  const berCl = base64StringToArrayBuffer(b64Cl);
  const certCl = printCertificate(berCl);
  
  const b64Is = `${env.CA_CLIENT_ISSUER}`;
  const berIs = base64StringToArrayBuffer(b64Is);
  const certIs = printCertificate(berIs);
  
  const b64Ro = `${env.CA_OCSP_ROOT}`;
  const berRo = base64StringToArrayBuffer(b64Ro);
  const certRo = printCertificate(berRo);
  
  // Create OCSP request
  //https://pkijs.org/docs/classes/OCSPRequest.html

  const ocspReq = new OCSPRequest();
  ocspReq.tbsRequest.requestorName = new GeneralName({
    type: 4,
    value: certCl.subject,
  });
  
  await ocspReq.createForCertificate(certCl, {
    hashAlgorithm: "SHA-1",
    issuerCertificate: certIs,
  });
  
  const nonce = getRandomValues(new Uint8Array(10));
  ocspReq.tbsRequest.requestExtensions = [
    new Extension({
      extnID: "1.3.6.1.5.5.7.48.1.2", // nonce
      extnValue: new asn1js.OctetString({ valueHex: nonce.buffer }).toBER(),
    })
  ];
  
  // Encode OCSP request
  
  const ocspReqRaw = ocspReq.toSchema(true).toBER();
  
  // Get OCSP responder URL
  
  const extAIA = certCl.extensions.find((extension) => extension.extnID === '1.3.6.1.5.5.7.1.1');
  const parsedOcspValue = extAIA.parsedValue.accessDescriptions.find((parsedValue) => parsedValue.accessMethod === '1.3.6.1.5.5.7.48.1');
  const ocspUrl = parsedOcspValue.accessLocation.value;

  const oRequest = new Request(ocspUrl, { method: 'POST', body: ocspReqRaw });
  oRequest.headers.set('content-type', 'application/ocsp-request');
  const oResponce = await fetch(oRequest);

  // Parse OCSP response

  const ocspRespRaw = await oResponce.arrayBuffer();
  const asnOcspResp = asn1js.fromBER(ocspRespRaw);
  const ocspResp = new OCSPResponse({ schema: asnOcspResp.result });
  if (!ocspResp.responseBytes) {
    throw new Error("No \"ResponseBytes\" in the OCSP Response - nothing to verify");
  }

  // Check certificate status

  const certstatus = await ocspResp.getCertificateStatus(certCl, certIs);
  console.log(certstatus.status);

  // Varidate OCSP responder
  //https://pkijs.org/docs/classes/ResponseData.html#responses
  
  const asnOcspRespBasic = asn1js.fromBER(ocspResp.responseBytes.response.valueBlock.valueHex);
  const ocspBasicResp = new BasicOCSPResponse({ schema: asnOcspRespBasic.result });
  
  const ok = await ocspBasicResp.verify({ trustedCerts: [certRo] });
  console.log(ok);
  
  // Send response
  //https://datatracker.ietf.org/doc/html/rfc6960
  const oStat = certstatus.status;
    switch (oStat) {
    case 0:
      return new Response("OCSP Good");
      break;
    case 1:
      return new Response("OCSP Revoked");
      break;
    case 2:
      return new Response("OCSP Unknown");
      break;
    default:
      return new Response("Baaaaaad");
    }
  }
};
```
</details>

<details><summary>試験結果</summary>

```shell
#################################
# OCSP による証明書確認

## 有効な証明書

$  ocsptool --ask  --load-issuer=INTER/inter.pem --load-trust=CA/ca.pem --load-cert=CLIENT/saga.pem | head -18
Connecting to OCSP server: ocsp.oyama.cf...
gnutls_ocsp_resp_verify: One of the involved algorithms has insufficient security level.
Resolving 'ocsp.oyama.cf:80'...
Connecting to '104.19.212.16:80'...
OCSP Response Information:
	Response Status: Successful                     <<==================== Successful
	Response Type: Basic OCSP Response
	Version: 1
	Responder ID: CN=My OCSP signer,O=Daidai Co.,L=Minato,ST=Tokyo,C=JP
	Produced At: Thu Nov 23 08:25:13 UTC 2023
	Responses:
		Certificate ID:
			Hash Algorithm: SHA1
			Issuer Name Hash: 96715822dbb1040de90c59fd87edc50f33ebb06b
			Issuer Key Hash: 1ffbcde2f6860b1abc185ab7cc25cc937a6446a4
			Serial Number: 6aeafe654db967db81636e2a0f2783a6fd11a5d3
		Certificate Status: good                     <<==================== Good 
		This Update: Thu Nov 23 06:26:11 UTC 2023
		Next Update: Sat Dec 23 08:25:13 UTC 2023

## 失効の証明書

$  ocsptool --ask  --load-issuer=INTER/inter.pem --load-trust=CA/ca.pem --load-cert=CLIENT/kagoshima.pem | head -18
Connecting to OCSP server: ocsp.oyama.cf...
gnutls_ocsp_resp_verify: One of the involved algorithms has insufficient security level.
Resolving 'ocsp.oyama.cf:80'...
Connecting to '104.19.212.16:80'...
OCSP Response Information:
	Response Status: Successful                     <<==================== Successful
	Response Type: Basic OCSP Response
	Version: 1
	Responder ID: CN=My OCSP signer,O=Daidai Co.,L=Minato,ST=Tokyo,C=JP
	Produced At: Thu Nov 23 08:25:04 UTC 2023
	Responses:
		Certificate ID:
			Hash Algorithm: SHA1
			Issuer Name Hash: 96715822dbb1040de90c59fd87edc50f33ebb06b
			Issuer Key Hash: 1ffbcde2f6860b1abc185ab7cc25cc937a6446a4
			Serial Number: 13adae89afadc067af1f462c8c261cf60ff05806
		Certificate Status: revoked                     <<==================== Revoked
		Revocation time: Thu Nov 23 06:26:11 UTC 2023
		This Update: Thu Nov 23 06:26:11 UTC 2023
		Next Update: Sat Dec 23 08:25:04 UTC 2023

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

# API Shield
## 有効
$ curl https://mtls-fw.oyama.cf/ocsp --cert saga.pem --key saga-key.pem
OCSP Good
## 失効
$ curl https://mtls-fw.oyama.cf/ocsp --cert kagoshima.pem --key kagoshima-key.pem
OCSP Revoked

# Access
## 有効
$ curl https://mtls.oyama.cf/ocsp --cert saga.pem --key saga-key.pem
OCSP Good
## 失効
$ curl https://mtls.oyama.cf/ocsp --cert kagoshima.pem --key kagoshima-key.pem
OCSP Revoked
```
</details>


```mermaid
sequenceDiagram
    participant Eyeball
    participant Cloudflare
    participant Authority Information Access
    Cloudflare ->> Eyeball :認証（あなた本物？）
    Eyeball ->> Cloudflare :認証（これでどう？）
    Cloudflare ->> Cloudflare :署名確認 OK
    Cloudflare ->> Authority Information Access: OCSP 確認
```

## Cloudflare CA
Cloudflare CA でのクライアント証明書の作成や失効は SSL/TLS の [Client Certificates](https://developers.cloudflare.com/ssl/client-certificates/create-a-client-certificate/) で実施します。


### クライアント証明書の作成と失効

作成時の CSR は Cloudflare に任せる、あるいは持ち込むことができます。前者の場合 Common Name が共通（Cloudflare）になります。

<img width="600" alt="alt" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2218045/406a652a-df3d-1b4e-b2f1-fc20cabb5b02.png">

作成した証明書はリスト表示されます。

<img width="600" alt="alt" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2218045/7b8ac0d0-448c-d2e2-19e5-90fe4295a5a1.png">

[取り消す（Revoke）](https://developers.cloudflare.com/ssl/client-certificates/revoke-client-certificate/
)をクリック失効させることができます・
ただ、Cloudflare の CSR を利用した場合、ダッシュボードで証明書をリスト表示しても、CN が同じで、固有の情報（シリアル番号など）も表示されないため、識別することができません。
Revokeしたい証明書を探そうにも、見た目の区別がつかず、検索窓もないので、現実的には API から操作する方法を取ることになるかと思います。

#### API

API での処理は下記のようになります。

<details><summary>例：クライアント証明書の管理</summary>

- 失効させたい証明書の管理 ID を取るため、その証明書を特定できる情報（フィンガープリントやシリアル番号）を指定し、フィルタ（[GET](https://developers.cloudflare.com/api/operations/client-certificate-for-a-zone-list-client-certificates)）

```
 $ curl --request GET \
  --url https://api.cloudflare.com/client/v4/zones/$ZONEID/client_certificates\
  --header 'Content-Type: application/json' \
  --header 'X-Auth-Email: '$EMAIL'' \
  --header 'X-Auth-Key: '$API_KEY'' -s| jq -r '.result[]|select(.serial_number == "028a07ebc6f20d73acfe2366f5d4afe2fea2b37f")|.id'

d99f85a4-b803-4975-97dc-34bd79a98364
```

- 得た証明書のIDをもとに失効処理（[DELETE](https://developers.cloudflare.com/api/operations/client-certificate-for-a-zone-delete-client-certificate)）

```
$ CERTID=d99f85a4-b803-4975-97dc-34bd79a98364
$ curl --request DELETE \
  --url https://api.cloudflare.com/client/v4/zones/$ZONEID/client_certificates/$CERTID\
  --header 'Content-Type: application/json' \
  --header 'X-Auth-Email: '$EMAIL'' \
  --header 'X-Auth-Key: '$API_KEY'' -s| jq '.'

:
    "status": "pending_revocation"
:

$ curl --request GET \
  --url https://api.cloudflare.com/client/v4/zones/$ZONEID/client_certificates/$CERTID\
  --header 'Content-Type: application/json' \
  --header 'X-Auth-Email: '$EMAIL'' \
  --header 'X-Auth-Key: '$API_KEY'' -s| jq '.result.status'

"revoked"
```

- 参考：再アクティベイトする場合（[PATCH](https://developers.cloudflare.com/api/operations/client-certificate-for-a-zone-edit-client-certificate)）
```
$ curl --request PATCH \
  --url https://api.cloudflare.com/client/v4/zones/$ZONEID/client_certificates/$CERTID\
  --header 'Content-Type: application/json' \
  --header 'X-Auth-Email: '$EMAIL'' \
  --header 'X-Auth-Key: '$API_KEY'' -s -d '{"reactivate":true}'| jq '.'

:
    "status": "pending_reactivation"
:

$ curl --request GET \
  --url https://api.cloudflare.com/client/v4/zones/$ZONEID/client_certificates/$CERTID\
  --header 'Content-Type: application/json' \
  --header 'X-Auth-Email: '$EMAIL'' \
  --header 'X-Auth-Key: '$API_KEY'' -s| jq '.result.status'

"active"
```
</details>

### 失効した証明書のブロック
失効した証明書を提示したリクエストをどう処理するかは WAF の Custom Rules で設定します。
"署名の検証に成功した証明書"（`cf.tls_client_auth.cert_verified`）、"失効している証明書"（`cf.tls_client_auth.cert_revoked`）などを組み合わせて[指定](https://developers.cloudflare.com/api-shield/security/mtls/configure/#check-for-revoked-certificates)できます。

<details><summary>例：失効した証明書も条件に追加</summary>

```
((not cf.tls_client_auth.cert_verified or cf.tls_client_auth.cert_revoked) and http.request.uri.path in {"/admin"})
```
</details>

## まとめ
今回は mTLS 時の失効確認を試験しました。

|プロダクト|対応 CA|mTLS を使ったアプリケーション保護を適用する場所|失効確認を適用する場所|
|-|-|-|-|
|API Shield|持ち込み CA<br>Cloudflare CA|WAF Custom Rules|Workers（持ち込み CA）<br>WAF Custom Rules（Cloudflare CA）|
|Access|持ち込み CA|Access Application Policy|Workers|