はじめに
Cloudflare の TLS クライアント認証(mTLS)で証明書の失効(revocation)確認をします。
対象範囲
Eyeball と Cloudflare の間が今回の対象です。
クライアント証明書発行 CA
クライアント証明書発行者の CA 証明書を Cloudflare に展開します。
Cloudflare CA、または、外部からの持ち込み CA が可能です。
どちらが使えるかは、プロダクトによって対応が異なります。
- 持ち込み CA
- API Shield(2023 年の機能追加)
- Access
- Cloudflare の CA
失効確認の方法は Cloudflare CA と持ち込み CA で異なるので、それぞれ記載します。
持ち込み CA
CA の持ち込み方法
API Shield の場合
API 例
# 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
Access の場合
ダッシュボードあるいは API から CA を持ち込むことができます。
API 例
# 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"
クライアント証明書の把握と情報共有
クライアント証明書に関する情報の抽出と展開
クライアントが提示する証明書に応じて失効確認を行うので、まず証明書からの情報の取り出し方について記します。
クライアント証明書の各情報
Workers では受信したクライアント証明書の各情報(シリアル番号 など)に cf.tls_client_auth.*
のプロパティでアクセスすることができます。
CRL の確認で使えます。
例:クライアント証明書の情報を取る
newrequest.headers.set('x-cn', request.cf.tlsClientAuth.certSubjectDN)
結果は。
"X-Cn": "CN=Iwa Te",
クライアント証明書そのもの
リクエストヘッダーにクライアント証明書を格納し転送することができます。あらかじめ API で設定しておきます。
OCSP の確認で使えます。
例:クライアント証明書自体をヘッダーに追加
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
補足:Transform Rules
クライアント証明書情報のリクエストヘッダーへの追加は、Workers の他に Transform Rules のマネージドルール(Managed Transforms > Request Header)の Add TLS Client auth headers
でも実装することができます。

試験結果:Add TLS Client auth headers を有効
"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",
Access JWT
Access の場合はデフォルトで JWT も入ってきます。
試験結果:Access JWT
"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"
}
失効確認
CRL や OCSP での確認は Access や API Shield 自体には今のところ実装されていないため、Workers で補います。
CRL の場合
サンプルを利用します。
Wrangler のアップデートと RFC5280 対応でパッチを当てました。
API shield( WAF の Custom Rules )の mTLS をはめた URL、Access の mTLS をはめた URL どちらも同じスクリプトで失効確認ができました。
patch
追加 compatibility_date
削除 type = "webpack"
@@ -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
},
試験結果
# 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 にキャッシュするようにしてあるので速そうです。
OCSP の場合
サンプルがなかったので下記で試しました。
API shield( WAF の Custom Rules )の mTLS をはめた URL、Access の mTLS をはめた URL どちらも同じスクリプトで失効確認ができました。
例
注)本テスト用のスクリプトで、安全策や効率化(結果のキャッシュなど)を考慮していないので商用では利用しないでください。
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
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");
}
}
};
試験結果
#################################
# 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
Cloudflare CA
Cloudflare CA でのクライアント証明書の作成や失効は SSL/TLS の Client Certificates で実施します。
クライアント証明書の作成と失効
作成時の CSR は Cloudflare に任せる、あるいは持ち込むことができます。前者の場合 Common Name が共通(Cloudflare)になります。

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

取り消す(Revoke)をクリック失効させることができます・
ただ、Cloudflare の CSR を利用した場合、ダッシュボードで証明書をリスト表示しても、CN が同じで、固有の情報(シリアル番号など)も表示されないため、識別することができません。
Revokeしたい証明書を探そうにも、見た目の区別がつかず、検索窓もないので、現実的には API から操作する方法を取ることになるかと思います。
API
API での処理は下記のようになります。
例:クライアント証明書の管理
- 失効させたい証明書の管理 ID を取るため、その証明書を特定できる情報(フィンガープリントやシリアル番号)を指定し、フィルタ(GET)
$ 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)
$ 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)
$ 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"
失効した証明書のブロック
失効した証明書を提示したリクエストをどう処理するかは WAF の Custom Rules で設定します。
"署名の検証に成功した証明書"(cf.tls_client_auth.cert_verified
)、"失効している証明書"(cf.tls_client_auth.cert_revoked
)などを組み合わせて指定できます。
例:失効した証明書も条件に追加
((not cf.tls_client_auth.cert_verified or cf.tls_client_auth.cert_revoked) and http.request.uri.path in {"/admin"})
まとめ
今回は mTLS 時の失効確認を試験しました。
プロダクト | 対応 CA | mTLS を使ったアプリケーション保護を適用する場所 | 失効確認を適用する場所 |
---|---|---|---|
API Shield | 持ち込み CA Cloudflare CA |
WAF Custom Rules | Workers(持ち込み CA) WAF Custom Rules(Cloudflare CA) |
Access | 持ち込み CA | Access Application Policy | Workers |