INTEGRITY Документация

Настройка мобильного приложения или IoT устройства

В этом руководстве показано, как настроить устройство интернета вещей (IoT) и мобильное приложение для использования клиентских сертификатов с API Shield.

Подробности сценария

В этом руководстве рассматривается пример устройства, которое считывает показания температуры и передаёт их с помощью POST-запроса к API, защищённому Cloudflare. Мобильное приложение, написанное на Swift для iOS, получает эти показания и отображает их.

Для простоты примера API реализован в виде Cloudflare Worker (с использованием кода из руководство по созданию приложения To-Do List на базе jamstack).

Значения температуры хранятся в Workers KV используя IP-адрес источника в качестве ключа, но вы можете легко использовать значение из клиентского сертификата, например отпечаток.

Приведённый ниже пример кода API сохраняет значение температуры и временную метку в KV при выполнении запроса POST и возвращает пять последних значений температуры при выполнении запроса GET.

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);
	},
};

1. Проверка API

Отправка тестовых данных в API методом POST

Чтобы проверить API перед добавлением аутентификации mTLS, отправьте запрос 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 201

Получение примера данных из API методом GET

GET-запрос к temps конечная точка возвращает самые последние показания, включая ту, что была отправлена в примере выше:

$ 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 Shield для защиты API или веб-приложения, создайте клиентские сертификаты, выпущенные Cloudflare.

Вы можете создайте клиентский сертификат в панели управления Cloudflare.

Поскольку большинство разработчиков, работающих в больших масштабах, создают собственные закрытые ключи и запросы на подпись сертификата через 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.pem

3. Встройте клиентский сертификат в мобильное приложение

Чтобы мобильное приложение могло безопасно запрашивать данные о температуре, отправленные устройством IoT, встройте клиентский сертификат в мобильное приложение.

Для простоты в этом примере сертификат и ключ «bootstrap» встраиваются непосредственно в пакет приложения в виде файла формата PKCS#12:

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

В реальном развёртывании bootstrap-сертификат следует использовать только вместе с учётными данными пользователя для аутентификации на конечной точке 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"; // 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 со встроенным клиентским сертификатом. Теперь этот клиент можно использовать для отправки HTTP-запросов к конечной точке API, защищенной с помощью mTLS.


4. Встройте клиентский сертификат на устройство IoT

Чтобы подготовить IoT-устройство к защищённому обмену данными с конечной точкой 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: 403

Когда устройство IoT предъявляет действительный клиентский сертификат, запрос POST выполняется успешно, и показание температуры записывается:

Cloudflare API Shield [IoT device demonstration]
Request body:  {"temperature": "36.5", "time": "2020-09-28T15:56:45Z"}
Response status code: 201

5. Включите mTLS

После создания сертификатов, выпущенных Cloudflare, далее необходимо включите mTLS для хостов, которые вы хотите защитить с помощью API Shield.


6. Настройте API Shield для требования клиентских сертификатов

Чтобы настроить API Shield на обязательное использование клиентских сертификатов, создайте правило mTLS.