モバイルアプリまたは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));});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.702020-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またはWebアプリケーションを保護するためにAPI Shieldを使用する前に、Cloudflare発行のクライアント証明書を作成します。
Cloudflareダッシュボードでクライアント証明書を作成できます。
ただし、スケールで作業しているほとんどの開発者は、APIを介して独自のプライベートキーと証明書署名要求を生成するため、この例ではCloudflare APIを使用してクライアント証明書を作成します。
iOSアプリケーションとIoTデバイス用のブートストラップ証明書を作成するために、この例ではCloudflareの公開鍵インフラストラクチャツールキット、CFSSL ↗を使用します:
# 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 request2020/09/27 21:28:46 [INFO] received CSR2020/09/27 21:28:46 [INFO] generating key: rsa-20482020/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.pemIoTデバイスから提出された温度データを安全に要求するために、モバイルアプリにクライアント証明書を埋め込みます。
簡単のために、この例ではアプリケーションバンドルにPKCS#12形式のファイルとして「ブートストラップ」証明書とキーを埋め込みます:
$ openssl pkcs12 -export -out bootstrap-cert.pfx -inkey ios-key.pem -in ios.pemEnter 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"; // この文字列をより安全な場所に保存することもできます。 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リクエストを行うことができます。
APIエンドポイントとの安全な通信のためにIoTデバイスを準備するには、デバイスに証明書を埋め込み、POSTリクエストを行う際に証明書を使用するようにデバイスを設定します。
この例では、証明書とプライベートキーが安全に/etc/ssl/private/sensor-key.pemと/etc/ssl/certs/sensor.pemにコピーされていると仮定します。
サンプルスクリプトは、これらのファイルを指すように修正されています:
import requestsimport jsonfrom 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"}レスポンスステータスコード: 403IoTデバイスが有効なクライアント証明書を提示すると、POSTリクエストは成功し、温度測定が記録されます:
Cloudflare API Shield [IoTデバイスデモ]リクエストボディ: {"temperature": "36.5", "time": "2020-09-28T15:56:45Z"}レスポンスステータスコード: 201Cloudflare発行の証明書を作成した後、次のステップは、API Shieldで保護したいホストに対してmTLSを有効にすることです。
API Shieldをクライアント証明書を要求するように設定するには、mTLSルールを作成する必要があります。