このチュートリアルでは、IoT(モノのインターネット)デバイスとモバイルアプリケーションを、API Shield のクライアント証明書で使う方法を説明します。
この手順では、温度を計測し、Cloudflare で保護された API へ POST リクエストで送信するデバイスを例にします。Swift で作った iOS 向けモバイルアプリが、その計測値を取得して表示します。
例を簡単にするため、API は Cloudflare Worker として実装します(Jamstack アプリを構築する To-Do List チュートリアル のコードを借用しています)。
温度は送信元 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) {
// Pull previously recorded temperatures for this client.
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);
}
// Append the recorded temperatures with the submitted reading (assuming it has both temperature and a timestamp).
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(
"Unable to parse temperature and/or timestamp from JSON POST body",
{ status: 400 },
);
}
} catch (err) {
return new Response(err, { status: 500 });
}
}
function compareTimestamps(a, b) {
return -1 * (Date.parse(a.time) - Date.parse(b.time));
}
// Return the 5 most recent temperature measurements.
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" },
});
}
}
export default {
async fetch(request, env, ctx) {
return request.method === "POST"
? addTemperature(request)
: getTemperatures(request);
},
};mTLS 認証を追加する前に API を検証するには、ランダムな温度を POST します。
$ 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 201temps エンドポイントへの GET リクエストは、直近の計測値を返します。上の例で送信した値も含まれます。
$ 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"
}
]API Shield で API や Web アプリケーションを保護する前に、Cloudflare 発行のクライアント証明書を作成します。
Cloudflare ダッシュボードでクライアント証明書を作成 できます。
ただし、規模の大きい開発では、多くの場合、秘密鍵と証明書署名要求(CSR)を API で自分で生成します。この例では Cloudflare API でクライアント証明書を作成します。
iOS アプリと IoT デバイス向けのブートストラップ証明書を作成するため、この例では Cloudflare の公開鍵基盤ツールキット CFSSL ↗ を使います。
# Generate a private key and CSR for the iOS device.
$ 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
# Do the same for the IoT sensor.
$ 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
# now ask that these CSRs be signed by the private CA issued for your zone
# we need to replace actual newlines in the CSR with ‘\n’ before POST’ing
$ CSR=$(cat ios.csr | perl -pe 's/\n/\\n/g')
$ request_body=$(< <(cat <<EOF
{
"validity_days": 3650,
"csr":"$CSR"
}
EOF
))
# save the response so we can view it and then extra the certificate
$ 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
# Now ask that the second client certificate signing request be signed.
$ 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.pemIoT デバイスが送信した温度データをモバイルアプリが安全に取得できるように、クライアント証明書をアプリに埋め込みます。
簡単のため、この例では「ブートストラップ」証明書と鍵を、PKCS#12 形式のファイルとしてアプリケーションバンドルに埋め込みます。
$ openssl pkcs12 -export -out bootstrap-cert.pfx -inkey ios-key.pem -in ios.pem
Enter Export Password:
Verifying - Enter Export Password:本番では、ブートストラップ証明書はユーザーの資格情報と組み合わせてだけ使い、一意のユーザー証明書を返す API エンドポイントで認証してください。企業利用では、モバイルデバイス管理(MDM)で証明書を配布することを推奨します。
次は、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"; // You may also store this String somewhere more secure.
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
// Get private key
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);
// Get certificate
InputStream certificateInputStream = getResources().openRawResource(R.raw.cert);
Certificate certificate = certificateFactory.generateCertificate(certificateInputStream);
// Set up 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();
// Set up Trust Managers
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init((KeyStore) null);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
// Set up Key Managers
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, SECRET.toCharArray());
KeyManager[] keyManagers = keyManagerFactory.getKeyManagers();
// Obtain SSL Socket Factory
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagers, trustManagers, new SecureRandom());
SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
// Finally, return the client, which will then be used to make HTTP calls.
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 リクエストを送れます。
API エンドポイントと安全に通信できるよう、証明書をデバイスに埋め込み、POST 時にその証明書を使うよう設定します。
この例では、証明書と秘密鍵を /etc/ssl/private/sensor-key.pem と /etc/ssl/certs/sensor.pem へ安全にコピー済みとします。
サンプルスクリプトを、これらのファイルを参照するよう変更します。
import requests
import json
from datetime import datetime
def readSensor():
# Takes a reading from a temperature sensor and store it to 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 device demonstration]")
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("Request body: ", r.request.body)
print("Response status code: %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 device demonstration]
Request body: {"temperature": "36.5", "time": "2020-09-28T15:52:19Z"}
Response status code: 403IoT デバイスが有効なクライアント証明書を提示すると、POST は成功し、温度が記録されます。
Cloudflare API Shield [IoT device demonstration]
Request body: {"temperature": "36.5", "time": "2020-09-28T15:56:45Z"}
Response status code: 201Cloudflare 発行の証明書を作成したら、次は API Shield で保護したいホストに対して mTLS を有効にします。
API Shield でクライアント証明書を必須にするには、mTLS ルールを作成 します。