コンテンツにスキップ

モバイルアプリまたはIoTデバイスの設定

このチュートリアルでは、クライアント証明書を使用するために、インターネット・オブ・シングス(IoT)デバイスとモバイルアプリケーションを設定する方法を示します。API Shieldを使用します。

シナリオの詳細

このウォークスルーでは、温度測定をキャプチャし、Cloudflareで保護されたAPIにPOSTリクエストを送信するデバイスの例を使用します。iOS用にSwiftで構築されたモバイルアプリケーションがこれらの測定値を取得し、表示します。

この例をシンプルに保つために、APIはCloudflare Workerとして実装されており、ジャムスタックアプリを構築するためのTo-Doリストチュートリアルからコードを借用しています。

温度は、ソースIPアドレスをキーとしてWorkers KVに保存されますが、クライアント証明書からの(フィンガープリントなど)を簡単に使用できます。

以下の例APIコードは、POSTが行われたときに温度とタイムスタンプをKVに保存し、GETリクエストが行われたときに最新の5つの温度を返します。

const defaultData = { temperatures: [] };
const getCache = (key) => TEMPERATURES.get(key);
const setCache = (key, data) => TEMPERATURES.put(key, data);
async function addTemperature(request) {
// このクライアントの以前に記録された温度を取得します。
const ip = request.headers.get("CF-Connecting-IP");
const cacheKey = `data-${ip}`;
let data;
const cache = await getCache(cacheKey);
if (!cache) {
await setCache(cacheKey, JSON.stringify(defaultData));
data = defaultData;
} else {
data = JSON.parse(cache);
}
// 提出された測定値(温度とタイムスタンプの両方があると仮定)で記録された温度を追加します。
try {
const body = await request.text();
const val = JSON.parse(body);
if (val.temperature && val.time) {
data.temperatures.push(val);
await setCache(cacheKey, JSON.stringify(data));
return new Response("", { status: 201 });
} else {
return new Response(
"JSON POSTボディから温度および/またはタイムスタンプを解析できません",
{ status: 400 },
);
}
} catch (err) {
return new Response(err, { status: 500 });
}
}
function compareTimestamps(a, b) {
return -1 * (Date.parse(a.time) - Date.parse(b.time));
}
// 最新の5つの温度測定値を返します。
async function getTemperatures(request) {
const ip = request.headers.get("CF-Connecting-IP");
const cacheKey = `data-${ip}`;
const cache = await getCache(cacheKey);
if (!cache) {
return new Response(JSON.stringify(defaultData), {
status: 200,
headers: { "content-type": "application/json" },
});
} else {
data = JSON.parse(cache);
const retval = JSON.stringify(
data.temperatures.sort(compareTimestamps).splice(0, 5),
);
return new Response(retval, {
status: 200,
headers: { "content-type": "application/json" },
});
}
}
async function handleRequest(request) {
if (request.method === "POST") {
return addTemperature(request);
} else {
return getTemperatures(request);
}
}
addEventListener("fetch", (event) => {
event.respondWith(handleRequest(event.request));
});

ステップ1 — APIの検証

APIにサンプルデータをPOSTする

mTLS認証を追加する前にAPIを検証するために、ランダムな温度測定値をPOSTします:

Terminal window
$ TEMPERATURE=$(echo $((361 + RANDOM %11)) | awk '{printf("%.2f",$1/10.0)}')
$ TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
$ echo -e "$TEMPERATURE\n$TIMESTAMP"
36.70
2020-09-28T02:54:56Z
$ curl --verbose --header "Content-Type: application/json" --data '{"temperature":'''$TEMPERATURE''', "time": "'''$TIMESTAMP'''"}' https://shield.upinatoms.com/temps 2>&1 | grep "< HTTP/2"
< HTTP/2 201

APIからサンプルデータをGETする

tempsエンドポイントへのGETリクエストは、上記の例で提出されたものを含む最新の測定値を返します:

Terminal window
$ curl --silent https://shield.upinatoms.com/temps | jq .
[
{
"temperature": 36.3,
"time": "2020-09-28T02:57:49Z"
},
{
"temperature": 36.7,
"time": "2020-09-28T02:54:56Z"
},
{
"temperature": 36.2,
"time": "2020-09-28T02:33:08Z"
}
]

ステップ2 — Cloudflare発行の証明書を作成する

APIまたはWebアプリケーションを保護するためにAPI Shieldを使用する前に、Cloudflare発行のクライアント証明書を作成します。

Cloudflareダッシュボードでクライアント証明書を作成できます

ただし、スケールで作業しているほとんどの開発者は、APIを介して独自のプライベートキーと証明書署名要求を生成するため、この例ではCloudflare APIを使用してクライアント証明書を作成します。

iOSアプリケーションとIoTデバイス用のブートストラップ証明書を作成するために、この例ではCloudflareの公開鍵インフラストラクチャツールキット、CFSSLを使用します:

Terminal window
# iOSデバイス用のプライベートキーとCSRを生成します。
$ cat <<'EOF' | tee -a csr.json
{
"hosts": [
"ios-bootstrap.devices.upinatoms.com"
],
"CN": "ios-bootstrap.devices.upinatoms.com",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [{
"C": "US",
"L": "Austin",
"O": "Temperature Testers, Inc.",
"OU": "Tech Operations",
"ST": "Texas"
}]
}
EOF
$ cfssl genkey csr.json | cfssljson -bare certificate
2020/09/27 21:28:46 [INFO] generate received request
2020/09/27 21:28:46 [INFO] received CSR
2020/09/27 21:28:46 [INFO] generating key: rsa-2048
2020/09/27 21:28:47 [INFO] encoded CSR
$ mv certificate-key.pem ios-key.pem
$ mv certificate.csr ios.csr
# IoTセンサーについても同様に行います。
$ sed -i.bak 's/ios-bootstrap/sensor-001/g' csr.json
$ cfssl genkey csr.json | cfssljson -bare certificate
...
$ mv certificate-key.pem sensor-key.pem
$ mv certificate.csr sensor.csr
# これらのCSRがゾーン用に発行されたプライベートCAによって署名されるように依頼します
# CSR内の実際の改行をPOSTする前に「\n」に置き換える必要があります
$ CSR=$(cat ios.csr | perl -pe 's/\n/\\n/g')
$ request_body=$(< <(cat <<EOF
{
"validity_days": 3650,
"csr":"$CSR"
}
EOF
))
# 応答を保存して表示し、証明書を抽出します
$ curl https://api.cloudflare.com/client/v4/zones/{zone_id}/client_certificates \
--header "X-Auth-Email: <EMAIL>" \
--header "X-Auth-Key: <API_KEY>" \
--header "Content-Type: application/json" \
--data "$request_body" > response.json
$ cat response.json | jq .
{
"success": true,
"errors": [],
"messages": [],
"result": {
"id": "7bf7f70c-7600-42e1-81c4-e4c0da9aa515",
"certificate_authority": {
"id": "8f5606d9-5133-4e53-b062-a2e5da51be5e",
"name": "Cloudflare Managed CA for account 11cbe197c050c9e422aaa103cfe30ed8"
},
"certificate": "-----BEGIN CERTIFICATE-----\nMIIEkzCCA...\n-----END CERTIFICATE-----\n",
"csr": "-----BEGIN CERTIFICATE REQUEST-----\nMIIDITCCA...\n-----END CERTIFICATE REQUEST-----\n",
"ski": "eb2a48a19802a705c0e8a39489a71bd586638fdf",
"serial_number": "133270673305904147240315902291726509220894288063",
"signature": "SHA256WithRSA",
"common_name": "ios-bootstrap.devices.upinatoms.com",
"organization": "Temperature Testers, Inc.",
"organizational_unit": "Tech Operations",
"country": "US",
"state": "Texas",
"location": "Austin",
"expires_on": "2030-09-26T02:41:00Z",
"issued_on": "2020-09-28T02:41:00Z",
"fingerprint_sha256": "84b045d498f53a59bef53358441a3957de81261211fc9b6d46b0bf5880bdaf25",
"validity_days": 3650
}
}
$ cat response.json | jq .result.certificate | perl -npe 's/\\n/\n/g; s/"//g' > ios.pem
# 次に、2番目のクライアント証明書署名要求を署名してもらいます。
$ CSR=$(cat sensor.csr | perl -pe 's/\n/\\n/g')
$ request_body=$(< <(cat <<EOF
{
"validity_days": 3650,
"csr":"$CSR"
}
EOF
))
$ curl https://api.cloudflare.com/client/v4/zones/{zone_id}/client_certificates \
--header "X-Auth-Email: <EMAIL>" \
--header "X-Auth-Key: <API_KEY>" \
--header "Content-Type: application/json" \
--data "$request_body" | perl -npe 's/\\n/\n/g; s/"//g' > sensor.pem

ステップ3 — モバイルアプリにクライアント証明書を埋め込む

IoTデバイスから提出された温度データを安全に要求するために、モバイルアプリにクライアント証明書を埋め込みます。

簡単のために、この例ではアプリケーションバンドルにPKCS#12形式のファイルとして「ブートストラップ」証明書とキーを埋め込みます:

Terminal window
$ openssl pkcs12 -export -out bootstrap-cert.pfx -inkey ios-key.pem -in ios.pem
Enter Export Password:
Verifying - Enter Export Password:

実際の展開では、ブートストラップ証明書は、APIエンドポイントに対してユーザーの資格情報と共に使用され、ユニークなユーザー証明書を返す必要があります。企業ユーザーは、モバイルデバイス管理(MDM)を使用して証明書を配布したいと考えています。

Androidアプリにクライアント証明書を埋め込む

以下は、AndroidアプリでHTTP呼び出しを行うためにクライアント証明書を使用する方法の例です。インターネット接続を許可するために、AndroidManifest.xmlに次の権限を追加する必要があります。

<uses-permission android:name="android.permission.INTERNET" />

デモンストレーションの目的で、この例の証明書はapp/src/main/res/raw/cert.pemに保存され、プライベートキーはapp/src/main/res/raw/key.pemに保存されます。これらのファイルを他の安全な方法で保存することもできます。

以下の例ではOkHttpClientを使用していますが、HttpURLConnectionなどの他のクライアントも同様の方法で使用できます。重要なのは、SSLSocketFactoryを使用することです。

private OkHttpClient setUpClient() {
try {
final String SECRET = "secret"; // この文字列をより安全な場所に保存することもできます。
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
// プライベートキーを取得
InputStream privateKeyInputStream = getResources().openRawResource(R.raw.key);
byte[] privateKeyByteArray = new byte[privateKeyInputStream.available()];
privateKeyInputStream.read(privateKeyByteArray);
String privateKeyContent = new String(privateKeyByteArray, Charset.defaultCharset())
.replace("-----BEGIN PRIVATE KEY-----", "")
.replaceAll(System.lineSeparator(), "")
.replace("-----END PRIVATE KEY-----", "");
byte[] rawPrivateKeyByteArray = Base64.getDecoder().decode(privateKeyContent);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(rawPrivateKeyByteArray);
// 証明書を取得
InputStream certificateInputStream = getResources().openRawResource(R.raw.cert);
Certificate certificate = certificateFactory.generateCertificate(certificateInputStream);
// KeyStoreを設定
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, SECRET.toCharArray());
keyStore.setKeyEntry("client", keyFactory.generatePrivate(keySpec), SECRET.toCharArray(), new Certificate[]{certificate});
certificateInputStream.close();
// Trust Managersを設定
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init((KeyStore) null);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
// Key Managersを設定
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, SECRET.toCharArray());
KeyManager[] keyManagers = keyManagerFactory.getKeyManagers();
// SSLソケットファクトリを取得
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagers, trustManagers, new SecureRandom());
SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
// 最後に、HTTP呼び出しに使用されるクライアントを返します。
OkHttpClient client = new OkHttpClient.Builder()
.sslSocketFactory(sslSocketFactory, (X509TrustManager) trustManagers[0])
.build();
return client;
} catch (CertificateException | IOException | NoSuchAlgorithmException | KeyStoreException | UnrecoverableKeyException | KeyManagementException | InvalidKeySpecException e) {
e.printStackTrace();
return null;
}
}

上記の関数は、クライアント証明書を埋め込んだOkHttpClientを返します。これで、このクライアントを使用してmTLSで保護されたAPIエンドポイントにHTTPリクエストを行うことができます。


ステップ4 — IoTデバイスにクライアント証明書を埋め込む

APIエンドポイントとの安全な通信のためにIoTデバイスを準備するには、デバイスに証明書を埋め込み、POSTリクエストを行う際に証明書を使用するようにデバイスを設定します。

この例では、証明書とプライベートキーが安全に/etc/ssl/private/sensor-key.pem/etc/ssl/certs/sensor.pemにコピーされていると仮定します。

サンプルスクリプトは、これらのファイルを指すように修正されています:

import requests
import json
from datetime import datetime
def readSensor():
# 温度センサーからの読み取りを取得し、temp_measurementに保存します
dateTimeObj = datetime.now()
timestampStr = dateTimeObj.strftime('%Y-%m-%dT%H:%M:%SZ')
measurement = {'temperature':str(temp_measurement),'time':timestampStr}
return measurement
def main():
print("Cloudflare API Shield [IoTデバイスデモンストレーション]")
temperature = readSensor()
payload = json.dumps(temperature)
url = 'https://shield.upinatoms.com/temps'
json_headers = {'Content-Type': 'application/json'}
cert_file = ('/etc/ssl/certs/sensor.pem', '/etc/ssl/private/sensor-key.pem')
r = requests.post(url, headers = json_headers, data = payload, cert = cert_file)
print("リクエストボディ: ", r.request.body)
print("レスポンスステータスコード: %d" % r.status_code)

スクリプトが https://shield.upinatoms.com/temps に接続しようとすると、Cloudflareはクライアント証明書の送信を要求し、スクリプトは /etc/ssl/certs/sensor.pem の内容を送信します。その後、SSL/TLSハンドシェイクを完了するために必要なように、スクリプトは /etc/ssl/private/sensor-key.pem を所有していることを示します。

クライアント証明書がない場合、Cloudflareはリクエストを拒否します:

Cloudflare API Shield [IoTデバイスデモ]
リクエストボディ: {"temperature": "36.5", "time": "2020-09-28T15:52:19Z"}
レスポンスステータスコード: 403

IoTデバイスが有効なクライアント証明書を提示すると、POSTリクエストは成功し、温度測定が記録されます:

Cloudflare API Shield [IoTデバイスデモ]
リクエストボディ: {"temperature": "36.5", "time": "2020-09-28T15:56:45Z"}
レスポンスステータスコード: 201

ステップ5 — mTLSを有効にする

Cloudflare発行の証明書を作成した後、次のステップは、API Shieldで保護したいホストに対してmTLSを有効にすることです。


ステップ6 — API Shieldをクライアント証明書を要求するように設定する

API Shieldをクライアント証明書を要求するように設定するには、mTLSルールを作成する必要があります。