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.
This commit is contained in:
James (ClawdBot) 2026-01-28 19:33:39 +00:00
parent 1f58f36470
commit a1e94f559f
2 changed files with 44 additions and 5 deletions

View File

@ -20,7 +20,7 @@ import java.security.SecureRandom
* The key format matches the Clawdbot gateway protocol: * The key format matches the Clawdbot gateway protocol:
* - Public key: 32 bytes raw, base64url-encoded * - Public key: 32 bytes raw, base64url-encoded
* - Signature: 64 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) { 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() ensureKeyPair()
val signedAt = System.currentTimeMillis() 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") Log.d(tag, "Signing payload: $payload")

View File

@ -226,7 +226,16 @@ class GatewayClient(
try { try {
deviceId = deviceIdentity.deviceId 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 publicKey = deviceIdentity.publicKey
log("Device identity ready: id=${deviceId.take(8)}...") log("Device identity ready: id=${deviceId.take(8)}...")
log("DEBUG: ${signedChallenge.debugInfo}") log("DEBUG: ${signedChallenge.debugInfo}")