Challenge-response is a secure authentication mechanism that ensures only legitimate ESP devices can be associated with user accounts. This feature is automatically detected and used during the provisioning flow for BLE devices that support it.
This cryptographic authentication protocol uses asymmetric cryptography (public/private key pairs) to prevent unauthorized devices from claiming to be legitimate ESP devices.
Challenge-Response Authentication
This guide explains how the Challenge-Response authentication workflow is implemented in the ESP RainMaker Home app, including detection, flow, and implementation details.
What is Challenge-Response?
Challenge-response is a cryptographic authentication protocol that:
- Server sends a challenge - A random string generated by the server
- Device signs the challenge - Device uses its private key to sign the challenge
- Server verifies the signature - Server validates the signature using the device's public key
- Authentication succeeds - Device is authenticated and associated with the user account
This prevents unauthorized devices from claiming to be legitimate ESP devices.
Architecture
Files Structure
esp-rainmaker-home/
├── proto-ts/
│ └── esp_rmaker_chal_resp.ts # Pre-generated TypeScript protobuf classes
├── utils/
│ └── challengeResponseHelper.ts # Challenge-response helper & capability checking
├── app/(device)/
│ └── Provision.tsx # Integrated challenge-response flow
└── rainmaker-home-guide/
└── challenge-response.md # This document
Implementation Details
1. Protocol Buffers
The challenge-response protocol uses Protocol Buffers for efficient binary serialization.
TypeScript Protobuf Classes (proto-ts/esp_rmaker_chal_resp.ts):
- Pre-generated TypeScript classes included in the repository
- Provides type-safe classes for serialization/deserialization
- Key message types used:
message CmdCRPayload {
bytes payload = 1; // Challenge from server
}
message RespCRPayload {
bytes payload = 1; // Signed challenge (256 bytes)
string node_id = 2; // Device node ID
}
2. Helper Utilities
ChallengeResponseHelper (utils/challengeResponseHelper.ts)
Main utility class for handling device communication and capability checking:
// Create a challenge request payload
static createChallengeRequest(challenge: string): Uint8Array
// Parse device response
static parseDeviceResponse(responseData: Uint8Array): DeviceChallengeResponse
// Send challenge to device and get signed response
static async sendChallengeToDevice(
device: ESPDevice,
challenge: string
): Promise<DeviceChallengeResponse>
// Validate response format
static validateChallengeResponse(
response: DeviceChallengeResponse
): boolean
// Check if device supports challenge-response (exported function)
checkChallengeResponseCapability(
versionInfo: { [key: string]: any },
transportType: string
): boolean
Key Features:
- Binary protobuf serialization/deserialization
- Base64 encoding for transmission
- Signature validation (256 bytes expected)
- Comprehensive error handling
- Device capability checking (BLE transport +
ch_respinrmaker_extra.cap)
3. How Challenge-Response is Detected
- After Device Connection: Once a device is connected via BLE, the app fetches device version info
- Capability Check: The app checks for
ch_respcapability inversionInfo.rmaker_extra.cap - Transport Check: Challenge-response is only supported for BLE transport (not SoftAP)
import { checkChallengeResponseCapability } from "@/utils/challengeResponseHelper";
const versionInfo = await device.getDeviceVersionInfo();
const supportsChallengeResponse = checkChallengeResponseCapability(
versionInfo,
device.transport // Must be 'BLE'
);
4. Provisioning Flows
There are two distinct provisioning flows implemented in Provision.tsx:
A. Challenge-Response Provisioning Flow
Used when device supports challenge-response capability (ch_resp in BLE mode):
const performChallengeResponseProvisioning = async () => {
// 1. Initiate user-node mapping (get challenge from server)
const { challenge, request_id } = await device?.initiateUserNodeMapping({});
// 2. Send challenge to device (get signed response)
const deviceResponse = await ChallengeResponseHelper.sendChallengeToDevice(
device,
challenge
);
// 3. Verify with server
await device?.verifyUserNodeMapping({
request_id,
challenge_response: deviceResponse.signedChallenge,
node_id: deviceResponse.nodeId,
});
// 4. Set network credentials directly (not provision())
const result = await device?.setNetworkCredentials(ssid, password);
// 5. Handle success and fetch nodes manually
await handleProvisionSuccess();
};
Key Points:
- Uses
setNetworkCredentials()directly after verification - Does NOT call
provision()with callbacks - Manually fetches nodes and handles success
- More secure - device is authenticated before provisioning
B. Traditional Provisioning Flow
Used when device does NOT support challenge-response:
const performTraditionalProvisioning = async () => {
// Call provision() with callback for status updates
await device?.provision(
ssid,
password,
handleProvisionUpdate, // Callback receives status messages
currentHomeId
);
};
Key Points:
- Uses
provision()with callback for real-time status updates - Callback handles all stages automatically
- No manual node fetching required
- Standard flow for older devices
C. Flow Selection Logic
const startProvisioning = async () => {
// 1. Check if device has required methods
const hasChallengeResponseMethods =
typeof device?.getDeviceVersionInfo === "function" &&
typeof device?.initiateUserNodeMapping === "function" &&
typeof device?.verifyUserNodeMapping === "function" &&
typeof device?.setNetworkCredentials === "function";
// 2. Check if device advertises capability
if (hasChallengeResponseMethods) {
const versionInfo = await device?.getDeviceVersionInfo();
const supportsChallenge = checkChallengeResponseCapability(
versionInfo,
device?.transport
);
if (supportsChallenge) {
// Use challenge-response flow
await performChallengeResponseProvisioning();
return;
}
}
// Use traditional flow
await performTraditionalProvisioning();
};
5. Challenge-Response Flow Steps
When a device supports challenge-response, the provisioning flow changes:
-
Initiate User-Node Mapping: Request challenge from server
const { challenge, request_id } = await device.initiateUserNodeMapping({}); -
Send Challenge to Device: Uses Protocol Buffers for binary communication
import { ChallengeResponseHelper } from "@/utils/challengeResponseHelper";
const deviceResponse = await ChallengeResponseHelper.sendChallengeToDevice(
device,
challenge
);
// Returns: { success, nodeId, signedChallenge } -
Verify with Server: Server validates the device's signature
await device.verifyUserNodeMapping({
request_id,
challenge_response: deviceResponse.signedChallenge,
node_id: deviceResponse.nodeId,
}); -
Set Network Credentials: Direct credential setting (not
provision())await device.setNetworkCredentials(ssid, password); -
Poll for Node Availability: Manually check for node availability
await pollUntilReady(nodeId, store);
Device Requirements
For challenge-response to be triggered, the device must:
- Support BLE transport - Challenge-response only works over BLE
- Have capability flag - Device must advertise
ch_respinversionInfo.rmaker_extra.cap - SDK support - CDF SDK must support the following methods:
getDeviceVersionInfo()initiateUserNodeMapping()verifyUserNodeMapping()setNetworkCredentials()
Backward Compatibility
The implementation is fully backward compatible:
- If device doesn't support challenge-response → Traditional provisioning flow
- If SDK doesn't have required methods → Traditional provisioning flow
- If challenge-response fails → Error is displayed (no automatic fallback)
- No breaking changes to existing provisioning