fix(apps): sign gateway device auth with v3 payload

This commit is contained in:
Peter Steinberger 2026-02-26 14:16:41 +01:00
parent 473a27470f
commit 490cb5174d
3 changed files with 112 additions and 29 deletions

View File

@ -372,7 +372,7 @@ class GatewaySession(
val signedAtMs = System.currentTimeMillis()
val payload =
buildDeviceAuthPayload(
buildDeviceAuthPayloadV3(
deviceId = identity.deviceId,
clientId = client.id,
clientMode = client.mode,
@ -381,6 +381,8 @@ class GatewaySession(
signedAtMs = signedAtMs,
token = if (authToken.isNotEmpty()) authToken else null,
nonce = connectNonce,
platform = client.platform,
deviceFamily = client.deviceFamily,
)
val signature = identityStore.signPayload(payload, identity)
val publicKey = identityStore.publicKeyBase64Url(identity)
@ -582,7 +584,7 @@ class GatewaySession(
}
}
private fun buildDeviceAuthPayload(
private fun buildDeviceAuthPayloadV3(
deviceId: String,
clientId: String,
clientMode: String,
@ -591,12 +593,16 @@ class GatewaySession(
signedAtMs: Long,
token: String?,
nonce: String,
platform: String?,
deviceFamily: String?,
): String {
val scopeString = scopes.joinToString(",")
val authToken = token.orEmpty()
val platformNorm = normalizeDeviceMetadataField(platform)
val deviceFamilyNorm = normalizeDeviceMetadataField(deviceFamily)
val parts =
mutableListOf(
"v2",
"v3",
deviceId,
clientId,
clientMode,
@ -605,10 +611,15 @@ class GatewaySession(
signedAtMs.toString(),
authToken,
nonce,
platformNorm,
deviceFamilyNorm,
)
return parts.joinToString("|")
}
private fun normalizeDeviceMetadataField(value: String?): String =
value?.trim()?.lowercase(Locale.ROOT).orEmpty()
private fun normalizeCanvasHostUrl(
raw: String?,
endpoint: GatewayEndpoint,

View File

@ -280,19 +280,17 @@ actor GatewayWizardClient {
let connectNonce = try await self.waitForConnectChallenge()
let identity = DeviceIdentityStore.loadOrCreate()
let signedAtMs = Int(Date().timeIntervalSince1970 * 1000)
let scopesValue = scopes.joined(separator: ",")
let payloadParts = [
"v2",
identity.deviceId,
clientId,
clientMode,
role,
scopesValue,
String(signedAtMs),
self.token ?? "",
connectNonce,
]
let payload = payloadParts.joined(separator: "|")
let payload = buildDeviceAuthPayloadV3(
deviceId: identity.deviceId,
clientId: clientId,
clientMode: clientMode,
role: role,
scopes: scopes,
signedAtMs: signedAtMs,
token: self.token,
nonce: connectNonce,
platform: platform,
deviceFamily: "Mac")
if let signature = DeviceIdentityStore.signPayload(payload, identity: identity),
let publicKey = DeviceIdentityStore.publicKeyBase64Url(identity)
{
@ -329,6 +327,44 @@ actor GatewayWizardClient {
}
}
private func buildDeviceAuthPayloadV3(
deviceId: String,
clientId: String,
clientMode: String,
role: String,
scopes: [String],
signedAtMs: Int,
token: String?,
nonce: String,
platform: String?,
deviceFamily: String?) -> String
{
let scopeString = scopes.joined(separator: ",")
let authToken = token ?? ""
let normalizedPlatform = normalizeMetadataField(platform)
let normalizedDeviceFamily = normalizeMetadataField(deviceFamily)
return [
"v3",
deviceId,
clientId,
clientMode,
role,
scopeString,
String(signedAtMs),
authToken,
nonce,
normalizedPlatform,
normalizedDeviceFamily,
].joined(separator: "|")
}
private func normalizeMetadataField(_ value: String?) -> String {
guard let value else { return "" }
return value
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased(with: Locale(identifier: "en_US_POSIX"))
}
private func waitForConnectChallenge() async throws -> String {
guard let task = self.task else { throw ConnectChallengeError.timeout }
return try await AsyncTimeout.withTimeout(

View File

@ -398,20 +398,18 @@ public actor GatewayChannelActor {
}
let signedAtMs = Int(Date().timeIntervalSince1970 * 1000)
let connectNonce = try await self.waitForConnectChallenge()
let scopesValue = scopes.joined(separator: ",")
let payloadParts = [
"v2",
identity?.deviceId ?? "",
clientId,
clientMode,
role,
scopesValue,
String(signedAtMs),
authToken ?? "",
connectNonce,
]
let payload = payloadParts.joined(separator: "|")
if includeDeviceIdentity, let identity {
let payload = buildDeviceAuthPayloadV3(
deviceId: identity.deviceId,
clientId: clientId,
clientMode: clientMode,
role: role,
scopes: scopes,
signedAtMs: signedAtMs,
token: authToken,
nonce: connectNonce,
platform: platform,
deviceFamily: InstanceIdentity.deviceFamily)
if let signature = DeviceIdentityStore.signPayload(payload, identity: identity),
let publicKey = DeviceIdentityStore.publicKeyBase64Url(identity) {
let device: [String: ProtoAnyCodable] = [
@ -445,6 +443,44 @@ public actor GatewayChannelActor {
}
}
private func buildDeviceAuthPayloadV3(
deviceId: String,
clientId: String,
clientMode: String,
role: String,
scopes: [String],
signedAtMs: Int,
token: String?,
nonce: String,
platform: String?,
deviceFamily: String?) -> String
{
let scopeString = scopes.joined(separator: ",")
let authToken = token ?? ""
let normalizedPlatform = normalizeMetadataField(platform)
let normalizedDeviceFamily = normalizeMetadataField(deviceFamily)
return [
"v3",
deviceId,
clientId,
clientMode,
role,
scopeString,
String(signedAtMs),
authToken,
nonce,
normalizedPlatform,
normalizedDeviceFamily,
].joined(separator: "|")
}
private func normalizeMetadataField(_ value: String?) -> String {
guard let value else { return "" }
return value
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased(with: Locale(identifier: "en_US_POSIX"))
}
private func handleConnectResponse(
_ res: ResponseFrame,
identity: DeviceIdentity?,