跳到主要内容
about

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:

  1. Server sends a challenge - A random string generated by the server
  2. Device signs the challenge - Device uses its private key to sign the challenge
  3. Server verifies the signature - Server validates the signature using the device's public key
  4. 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_resp in rmaker_extra.cap)

3. How Challenge-Response is Detected

  1. After Device Connection: Once a device is connected via BLE, the app fetches device version info
  2. Capability Check: The app checks for ch_resp capability in versionInfo.rmaker_extra.cap
  3. 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:

  1. Initiate User-Node Mapping: Request challenge from server

    const { challenge, request_id } = await device.initiateUserNodeMapping({});
  2. 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 }
  3. Verify with Server: Server validates the device's signature

    await device.verifyUserNodeMapping({
    request_id,
    challenge_response: deviceResponse.signedChallenge,
    node_id: deviceResponse.nodeId,
    });
  4. Set Network Credentials: Direct credential setting (not provision())

    await device.setNetworkCredentials(ssid, password);
  5. Poll for Node Availability: Manually check for node availability

    await pollUntilReady(nodeId, store);

Device Requirements

For challenge-response to be triggered, the device must:

  1. Support BLE transport - Challenge-response only works over BLE
  2. Have capability flag - Device must advertise ch_resp in versionInfo.rmaker_extra.cap
  3. 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

  1. Device Authentication - Only devices with valid private keys can complete provisioning
  2. Prevents Impersonation - Attackers cannot claim to be legitimate devices
  3. No Shared Secrets - Uses asymmetric cryptography (public/private key pairs)
  4. 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.ts are pre-generated and included in the repository. No build step is required.

Testing

Prerequisites

  1. ESP device with challenge-response capability
  2. Device must be in BLE provisioning mode
  3. Device firmware with ch_resp capability enabled

Test Steps

  1. Start app and begin device provisioning
  2. Select BLE device
  3. Enter WiFi credentials
  4. 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"
  5. Provisioning continues normally

Troubleshooting

Challenge-response not triggered:

  • Check device is using BLE (not SoftAP)
  • Verify device has ch_resp capability: 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:

  1. Add constant to utils/constants.ts:

    export const ESP_CHALLENGE_RESPONSE_CONSTANTS = {
    CHALLENGE_RESPONSE_CAPABILITY: "ch_resp",
    // Add new capability constants here
    };
  2. 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