signalapp/Signal-Server · error · ForbiddenException
Only primary devices may register attestations
Error message
Only primary devices may register attestations
What it means
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.
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
Example fix
// before (linked device)
final AuthenticatedDevice device = auth.authenticate(badCredentials); // deviceId = 2
client.attest(device, keyId, attestation); // 403 ForbiddenException
// after
final AuthenticatedDevice device = auth.authenticate(primaryCredentials); // deviceId = 1
if (device.deviceId() != Device.PRIMARY_ID) throw new IllegalStateException("attest requires primary device");
client.attest(device, keyId, attestation); Defensive patterns
Strategy: validation
Validate before calling
if (authenticatedDevice.deviceId() != Device.PRIMARY_ID) {
throw new IllegalStateException("DeviceCheck attestation must be performed from the primary device");
}
client.attest(authenticatedDevice, keyId, attestation); Type guard
boolean isPrimaryDevice(AuthenticatedDevice d) { return d != null && d.deviceId() == Device.PRIMARY_ID; } Prevention
- Attest only from the primary device
- Check deviceId before calling devicecheck endpoints
- Keep account device list documented in test fixtures
When it happens
Trigger: 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).
Common situations: 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.
Understand the failure class
Background: "You do not have permission" / 403 Forbidden errors: authenticated but not allowed — causes and fixes across open-source libraries — this error's family across 31 libraries.
Related errors
- must not use authenticated connection for one-time donation…
- must not use authenticated connection for call quality…
- Invalid action:
- must not use authenticated connection for login purchase…
- interrupted during delivery
AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09).
Data as JSON: /api/errors/055e8f927d5828e5.
Report an issue: GitHub.
Appendix: source
Thrown at service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceCheckController.java:145
@ApiResponse(responseCode = "204", description = "The keyId was successfully added to the account")
@ApiResponse(responseCode = "410", description = "There was no challenge associated with the account. It may have expired.")
@ApiResponse(responseCode = "401", description = "The attestation could not be verified")
@ApiResponse(responseCode = "413", description = "There are too many unique keyIds associated with this account. This is an unrecoverable error.")
@ApiResponse(responseCode = "409", description = "The provided keyId has already been registered to a different account")
@ManagedAsync
public void attest(
@Auth final AuthenticatedDevice authenticatedDevice,
@Valid
@NotNull
@Parameter(description = "The keyId, encoded with padded url-safe base64")
@QueryParam("keyId") final String keyId,
@RequestBody(description = "The attestation data, created by [attestKey](https://developer.apple.com/documentation/devicecheck/dcappattestservice/attestkey(_:clientdatahash:completionhandler:))")
@NotNull final byte[] attestation) {
if (authenticatedDevice.deviceId() != Device.PRIMARY_ID) {
throw new ForbiddenException("Only primary devices may register attestations");
}
final Account account = accountsManager.getByAccountIdentifier(authenticatedDevice.accountIdentifier())
.orElseThrow(() -> new WebApplicationException(Response.Status.UNAUTHORIZED));
try {
deviceCheckManager.registerAttestation(account, parseKeyId(keyId), attestation);
} catch (TooManyKeysException e) {
throw new WebApplicationException(Response.status(413).build());
} catch (ChallengeNotFoundException e) {
throw new WebApplicationException(Response.status(410).build());
} catch (DeviceCheckVerificationFailedException e) {
throw new WebApplicationException(e.getMessage(), Response.status(401).build());
} catch (DuplicatePublicKeyException e) {
throw new WebApplicationException(Response.status(409).build());
}
}
View on GitHub (pinned to 100ab61c82)