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
Security Benefits
- Device Authentication - Only devices with valid private keys can complete provisioning
- Prevents Impersonation - Attackers cannot claim to be legitimate devices
- No Shared Secrets - Uses asymmetric cryptography (public/private key pairs)
- Replay Attack Prevention - Each challenge is unique and one-time use
Dependencies
The following protobuf-related packages are included in package.json:
{
"dependencies": {
"google-protobuf": "^3.21.2"
},
"devDependencies": {
"@types/google-protobuf": "^3.15.6"
}
}
Note: The TypeScript protobuf classes in
proto-ts/esp_rmaker_chal_resp.tsare pre-generated and included in the repository. No build step is required.
Testing
Prerequisites
- ESP device with challenge-response capability
- Device must be in BLE provisioning mode
- Device firmware with
ch_respcapability enabled
Test Steps
- Start app and begin device provisioning
- Select BLE device
- Enter WiFi credentials
- Observe logs for challenge-response flow:
"Starting challenge-response flow..."
"Requesting challenge from server..."
"Got challenge: [challenge_string]"
"Sending challenge to device..."
"Received binary response from device"
"Verifying device response with server..."
"Challenge-response flow completed successfully" - Provisioning continues normally
Troubleshooting
Challenge-response not triggered:
- Check device is using BLE (not SoftAP)
- Verify device has
ch_respcapability:versionInfo.rmaker_extra.cap - Ensure SDK version supports required methods
Challenge-response fails:
- Check device signature (must be 256 bytes)
- Verify server can validate signature
- Check network connectivity
- Review device logs for errors
Development
Protocol Buffers
The TypeScript protobuf classes are pre-generated in proto-ts/esp_rmaker_chal_resp.ts and should not need modification. These classes are generated from the ESP RainMaker protocol definitions.
Adding New Capabilities
To add new device capability checks, add them to utils/challengeResponseHelper.ts or utils/constants.ts:
-
Add constant to
utils/constants.ts:export const ESP_CHALLENGE_RESPONSE_CONSTANTS = {
CHALLENGE_RESPONSE_CAPABILITY: "ch_resp",
// Add new capability constants here
}; -
Create checking function in
utils/challengeResponseHelper.ts:export function checkNewFeatureCapability(versionInfo: {
[key: string]: any;
}): boolean {
// Implementation
}
Provisioning Flow Integration
The challenge-response flow is integrated into the provisioning process:
References
- Protocol Buffers: https://developers.google.com/protocol-buffers
- ESP RainMaker SDK: https://github.com/espressif/esp-rainmaker-app-sdk-ts
- ESPDevice Documentation: https://espressif.github.io/esp-rainmaker-app-sdk-ts/classes/ESPDevice.ESPDevice.html