Configure your mobile app or IoT device
This tutorial demonstrates how to configure your Internet-of-things (IoT) device and mobile application to use client certificates with API Shield™.
Scenario details
This walkthrough uses the example of a device that captures temperature readings and transmits them by sending a POST request to a Cloudflare-protected API. A mobile application built in Swift for iOS retrieves those readings and displays them.
To keep this example simple, the API is implemented as a Cloudflare Worker (borrowing code from the To-Do List tutorial on building a jamstack app).
Temperatures are stored in Workers KV using the source IP address as a key, but you can easily use a value from the client certificate, such as the fingerprint.
The example API code below saves a temperature and timestamp into KV when a POST is made and returns the most recent five temperatures when a GET request is made.
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' } }) }
}
async function handleRequest(request) {
if (request.method === 'POST') { return addTemperature(request) } else { return getTemperatures(request) }
}
addEventListener('fetch', event => { event.respondWith(handleRequest(event.request))
})
Step 1 — Validate API
POST sample data to API
To validate the API before adding mTLS authentication, POST a random temperature reading:
$ 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 -v -H "Content-Type: application/json" -d '{"temperature":'''$TEMPERATURE''', "time": "'''$TIMESTAMP'''"}' https://shield.upinatoms.com/temps 2>&1 | grep "< HTTP/2"< HTTP/2 201
GET sample data from API
A GET request to the temps
endpoint returns the most recent readings, including the one submitted in the example above:
$ curl -s 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" }
]
Step 2 — Create Cloudflare-issued certificates
Before you can use API Shield to protect your API or web application, create Cloudflare-issued client certificates.
You can create a client certificate in the Cloudflare dashboard.
However, since most developers working at scale generate their own private keys and certificate signing requests via API, this example uses the Cloudflare API to create client certificates.
To create a bootstrap certificate for the iOS application and the IoT device, this example uses Cloudflare’s public key infrastructure toolkit, 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 certificate2020/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
# 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 -H 'X-Auth-Email: YOUR_EMAIL' -H 'X-Auth-Key: YOUR_API_KEY' -H 'Content-Type: application/json' -d "$request_body" https://api.cloudflare.com/client/v4/zones/YOUR_ZONE_ID/client_certificates > 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 -H 'X-Auth-Email: YOUR_EMAIL' -H 'X-Auth-Key: YOUR_API_KEY' -H 'Content-Type: application/json' -d "$request_body" https://api.cloudflare.com/client/v4/zones/YOUR_ZONE_ID/client_certificates | perl -npe 's/\\n/\n/g; s/"//g' > sensor.pem
Step 3 — Embed the client certificate in your mobile app
To configure the mobile app to securely request temperature data submitted by the IoT device, embed the client certificate in the mobile app.
For simplicity, this example embeds a “bootstrap” certificate and key in the application bundle as a PKCS#12-formatted file:
$ openssl pkcs12 -export -out bootstrap-cert.pfx -inkey ios-key.pem -in ios.pemEnter Export Password:Verifying - Enter Export Password:
In a real-world deployment, a bootstrap certificate should only be used in conjunction with users’ credentials to authenticate with an API endpoint that can return a unique user certificate. Corporate users will want to use mobile device management (MDM) to distribute certificates.
Embed the client certificate in an Android app
The following is an example of how you may use a client certificate in an Android app to make HTTP calls. You need to add the following permission in AndroidManifest.xml
to allow an Internet connection.
<uses-permission android:name="android.permission.INTERNET" />
For demonstration purposes, the certificate in this example is stored in app/src/main/res/raw/cert.pem
and the private key is stored in app/src/main/res/raw/key.pem
. You may also store these files in other secure manners.
The following example uses an OkHttpClient
, but you may also use other clients such as HttpURLConnection
in similar ways. The key is to use the 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; }
}
The above function returns an OkHttpClient
embedded with the client certificate. You can now use this client to make HTTP requests to your API endpoint protected with mTLS.
Step 4 — Embed the client certificate on your IoT device
To prepare the IoT device for secure communication with the API endpoint, embed the certificate on the device and configure the device to use the certificate when making POST requests.
This example assumes the certificate and the private key are securely copied to /etc/ssl/private/sensor-key.pem
and /etc/ssl/certs/sensor.pem
.
The sample script is modified to point to these files:
import requestsimport jsonfrom 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)
When the script attempts to connect to https://shield.upinatoms.com/temps
, Cloudflare requests that a client certificate is sent and the script sends the contents of /etc/ssl/certs/sensor.pem
. Then, as required to complete the SSL/TLS handshake, the script demonstrates it has possession of /etc/ssl/private/sensor-key.pem
.
Without the client certificate, the Cloudflare rejects the request:
Cloudflare API Shield [IoT device demonstration]Request body: {"temperature": "36.5", "time": "2020-09-28T15:52:19Z"}Response status code: 403
When the IoT device presents a valid client certificate, the POST request succeeds and the temperature reading is recorded:
Cloudflare API Shield [IoT device demonstration]Request body: {"temperature": "36.5", "time": "2020-09-28T15:56:45Z"}Response status code: 201
Step 5 — Enable mTLS
After creating Cloudflare-issued certificates, the next step is to enable mTLS for the hosts you want to protect with API Shield.
Step 6 — Configure API Shield to require client certificates
To configure API Shield to require client certificates, create a mTLS rule.