LoginSignup
1
0

Cloudflare mTLS での失効確認

Last updated at Posted at 2023-11-23

はじめに

Cloudflare の TLS クライアント認証(mTLS)で証明書の失効(revocation)確認をします。

対象範囲

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

クライアント証明書発行 CA

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

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

持ち込み CA

CA の持ち込み方法

API Shield の場合

CA の持ち込みは今のところ API からのみです。

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 でも実装することができます。

alt
試験結果: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"
}

失効確認

CRLOCSP での確認は Access や API Shield 自体には今のところ実装されていないため、Workers で補います。

CRL の場合

サンプルを利用します。
Wrangler のアップデートRFC5280 対応でパッチを当てました。

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

patch
wrangler.toml
追加 compatibility_date 
削除 type = "webpack" 
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
         },
試験結果
# 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 どちらも同じスクリプトで失効確認ができました。

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

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
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");
    }
  }
};
試験結果
#################################
# 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)になります。

alt

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

alt

取り消す(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
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0