From a1e94f559f999cc58184dbc7ca8d5414e3dde8a6 Mon Sep 17 00:00:00 2001 From: "James (ClawdBot)" Date: Wed, 28 Jan 2026 19:33:39 +0000 Subject: [PATCH] fix: Use correct device auth payload format for signature The signature payload was incorrect. Changed from: $nonce:$signedAt To gateway's expected v2 format: v2|deviceId|clientId|clientMode|role|scopes|signedAtMs|token|nonce This matches the buildDeviceAuthPayload() function in Clawdbot's gateway/device-auth.js module. --- .../inou/clawdnode/security/DeviceIdentity.kt | 38 +++++++++++++++++-- .../inou/clawdnode/service/GatewayClient.kt | 11 +++++- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/inou/clawdnode/security/DeviceIdentity.kt b/app/src/main/java/com/inou/clawdnode/security/DeviceIdentity.kt index d325234..2c66e53 100644 --- a/app/src/main/java/com/inou/clawdnode/security/DeviceIdentity.kt +++ b/app/src/main/java/com/inou/clawdnode/security/DeviceIdentity.kt @@ -20,7 +20,7 @@ import java.security.SecureRandom * The key format matches the Clawdbot gateway protocol: * - Public key: 32 bytes raw, base64url-encoded * - Signature: 64 bytes raw, base64url-encoded - * - Payload: "$nonce:$signedAt" (matching gateway's expected format) + * - Payload format (v2): "v2|deviceId|clientId|clientMode|role|scopes|signedAtMs|token|nonce" */ class DeviceIdentity(context: Context) { @@ -68,13 +68,43 @@ class DeviceIdentity(context: Context) { } /** - * Sign a challenge nonce for gateway authentication + * Sign a challenge nonce for gateway authentication. + * + * Builds the payload in gateway's expected format: + * v2|deviceId|clientId|clientMode|role|scopes|signedAtMs|token|nonce + * + * @param nonce Challenge nonce from gateway + * @param clientId Client identifier (e.g., "clawdnode-android") + * @param clientMode Client mode ("node") + * @param role Role ("node") + * @param scopes Comma-separated scopes (empty for nodes) + * @param token Auth token (gateway token or device token) */ - fun signChallenge(nonce: String): SignedChallenge { + fun signChallenge( + nonce: String, + clientId: String, + clientMode: String, + role: String, + scopes: String = "", + token: String = "" + ): SignedChallenge { ensureKeyPair() val signedAt = System.currentTimeMillis() - val payload = "$nonce:$signedAt" + + // Build payload in gateway's expected format: + // v2|deviceId|clientId|clientMode|role|scopes|signedAtMs|token|nonce + val payload = listOf( + "v2", + deviceId, + clientId, + clientMode, + role, + scopes, + signedAt.toString(), + token, + nonce + ).joinToString("|") Log.d(tag, "Signing payload: $payload") diff --git a/app/src/main/java/com/inou/clawdnode/service/GatewayClient.kt b/app/src/main/java/com/inou/clawdnode/service/GatewayClient.kt index 1f36a33..d5fcb82 100644 --- a/app/src/main/java/com/inou/clawdnode/service/GatewayClient.kt +++ b/app/src/main/java/com/inou/clawdnode/service/GatewayClient.kt @@ -226,7 +226,16 @@ class GatewayClient( try { deviceId = deviceIdentity.deviceId - signedChallenge = deviceIdentity.signChallenge(nonce) + // Sign with full device auth payload: + // v2|deviceId|clientId|clientMode|role|scopes|signedAtMs|token|nonce + signedChallenge = deviceIdentity.signChallenge( + nonce = nonce, + clientId = Protocol.CLIENT_ID, + clientMode = Protocol.MODE, + role = Protocol.ROLE, + scopes = "", // Nodes don't use scopes + token = token + ) publicKey = deviceIdentity.publicKey log("Device identity ready: id=${deviceId.take(8)}...") log("DEBUG: ${signedChallenge.debugInfo}")