{"record":{"id":"055e8f927d5828e5","repo":"signalapp/Signal-Server","slug":"only-primary-devices-may-register-attestations","errorCode":null,"errorMessage":"Only primary devices may register attestations","messagePattern":"Only primary devices may register attestations","errorType":"http","errorClass":"ForbiddenException","httpStatus":403,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceCheckController.java","lineNumber":145,"sourceCode":"  @ApiResponse(responseCode = \"204\", description = \"The keyId was successfully added to the account\")\n  @ApiResponse(responseCode = \"410\", description = \"There was no challenge associated with the account. It may have expired.\")\n  @ApiResponse(responseCode = \"401\", description = \"The attestation could not be verified\")\n  @ApiResponse(responseCode = \"413\", description = \"There are too many unique keyIds associated with this account. This is an unrecoverable error.\")\n  @ApiResponse(responseCode = \"409\", description = \"The provided keyId has already been registered to a different account\")\n  @ManagedAsync\n  public void attest(\n      @Auth final AuthenticatedDevice authenticatedDevice,\n\n      @Valid\n      @NotNull\n      @Parameter(description = \"The keyId, encoded with padded url-safe base64\")\n      @QueryParam(\"keyId\") final String keyId,\n\n      @RequestBody(description = \"The attestation data, created by [attestKey](https://developer.apple.com/documentation/devicecheck/dcappattestservice/attestkey(_:clientdatahash:completionhandler:))\")\n      @NotNull final byte[] attestation) {\n\n    if (authenticatedDevice.deviceId() != Device.PRIMARY_ID) {\n      throw new ForbiddenException(\"Only primary devices may register attestations\");\n    }\n\n    final Account account = accountsManager.getByAccountIdentifier(authenticatedDevice.accountIdentifier())\n        .orElseThrow(() -> new WebApplicationException(Response.Status.UNAUTHORIZED));\n\n    try {\n      deviceCheckManager.registerAttestation(account, parseKeyId(keyId), attestation);\n    } catch (TooManyKeysException e) {\n      throw new WebApplicationException(Response.status(413).build());\n    } catch (ChallengeNotFoundException e) {\n      throw new WebApplicationException(Response.status(410).build());\n    } catch (DeviceCheckVerificationFailedException e) {\n      throw new WebApplicationException(e.getMessage(), Response.status(401).build());\n    } catch (DuplicatePublicKeyException e) {\n      throw new WebApplicationException(Response.status(409).build());\n    }\n  }\n","sourceCodeStart":127,"sourceCodeEnd":163,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceCheckController.java#L127-L163","documentation":"DeviceCheckController.attest registers a DCAppAttest attestation for a device. Apple's DeviceCheck attestation flow is anchored to the account's primary device, so the endpoint explicitly rejects calls from linked devices by checking authenticatedDevice.deviceId() != Device.PRIMARY_ID and throwing ForbiddenException.","triggerScenarios":"A linked (non-primary) device calls POST /v1/devicecheck/attest with a keyId and attestation payload; the authenticated device's deviceId is anything other than Device.PRIMARY_ID (1).","commonSituations":"A client library performs devicecheck attestation on a linked iPad/companion device; a session token resolves to a secondary device; automation or tests authenticate as a non-primary device.","solutions":["Perform DeviceCheck attestation only from the primary device associated with the account","Check that the authenticated credentials belong to the primary device before calling attest (deviceId == 1)","If the primary device changed, re-register/re-link and attest from the new primary device","If attestation is intended for linked devices, this endpoint is the wrong one - use a capability that supports linked devices"],"exampleFix":"// before (linked device)\nfinal AuthenticatedDevice device = auth.authenticate(badCredentials); // deviceId = 2\nclient.attest(device, keyId, attestation); // 403 ForbiddenException\n// after\nfinal AuthenticatedDevice device = auth.authenticate(primaryCredentials); // deviceId = 1\nif (device.deviceId() != Device.PRIMARY_ID) throw new IllegalStateException(\"attest requires primary device\");\nclient.attest(device, keyId, attestation);","handlingStrategy":"validation","validationCode":"if (authenticatedDevice.deviceId() != Device.PRIMARY_ID) {\n  throw new IllegalStateException(\"DeviceCheck attestation must be performed from the primary device\");\n}\nclient.attest(authenticatedDevice, keyId, attestation);","typeGuard":"boolean isPrimaryDevice(AuthenticatedDevice d) { return d != null && d.deviceId() == Device.PRIMARY_ID; }","tryCatchPattern":null,"preventionTips":["Attest only from the primary device","Check deviceId before calling devicecheck endpoints","Keep account device list documented in test fixtures"],"tags":["http-403","devicecheck","signal-server"],"backgroundTag":"insufficient-permissions","analyzedSha":"100ab61c82627582c867d19e1c0561ba2781e927","analyzedAt":"2026-09-09T13:29:47.883Z","contentChangedAt":"2026-09-09T13:29:47.883Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}