signalapp/Signal-Server · error · WebApplicationException
Invalid signature
Error message
Invalid signature
What it means
In AccountControllerV2.changeNumber, every signed pre-key included in the change-number request must carry a valid signature. If request.isSignatureValidOnEachSignedPreKey(userAgentString) fails for any pre-key, the server returns HTTP 422 with body 'Invalid signature'.
Solutions
- Regenerate the identity key pair and re-sign all signed pre-keys with the new private identity key on the client.
- Verify the client's signature computation matches the expected format (key bytes + signature over the correct input).
- Update the client library to the version matching the server's pre-key signature scheme.
- Inspect each SignedPreKey in the request and remove/replace ones with invalid signatures before retrying.
Example fix
// before signedPreKey.setSignature(sign(oldIdentityPrivateKey, signedPreKey.getPublicKey())); // after signedPreKey.setSignature(sign(newIdentityPrivateKey, signedPreKey.getSerializedPublicKey()));
Defensive patterns
Strategy: try-catch
Validate before calling
for (const k of request.signedPreKeys) {
if (!lib.verifySignature(k.publicKey, k.signature, identityKeyPair.publicKey)) throw new Error('pre-key signature invalid before send');
} Type guard
const hasValidPreKeySignature = (k, idPub) => k.signature && verify(k.serializedPublicKey, k.signature, idPub);
Try / catch
try { await changeNumber(req); } catch (e) { if (e.status === 422) { await regenerateAndResignPreKeys(identityKeyPair); return changeNumber(req); } throw e; } Prevention
- Re-sign all signed pre-keys whenever the identity key changes
- Use the client library's own signing helper rather than hand-rolled crypto
- Keep client library version aligned with server pre-key signature scheme
When it happens
Trigger: PUT /v2/accounts/phone_number/{number} (change number) where one or more signed pre-keys in the request have signatures that don't verify against their public key, or the signature input (including the UA string on some versions) doesn't match what the client signed.
Common situations: Client crypto library generating malformed pre-key signatures, keys serialized/reserialized in a way that alters the signed bytes, upgrading identity key without re-signing pre-keys, or client/server version skew in signature format.
Related errors
AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09).
Data as JSON: /api/errors/2c10e617b3c085e5.
Report an issue: GitHub.
Appendix: source
Thrown at service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountControllerV2.java:98
@ApiResponse(responseCode = "410", description = "Mismatched registration ids in 'devices to notify' list", content = @Content(schema = @Schema(implementation = StaleDevicesResponse.class)))
@ApiResponse(responseCode = "413", description = "One or more device messages was too large")
@ApiResponse(responseCode = "422", description = "The request did not pass validation")
@ApiResponse(responseCode = "423", content = @Content(schema = @Schema(implementation = RegistrationLockFailure.class)))
@ApiResponse(responseCode = "429", description = "Too many attempts", headers = @Header(
name = "Retry-After",
description = "If present, a positive integer indicating the number of seconds before a subsequent attempt could succeed"))
public AccountIdentityResponse changeNumber(@Auth final AuthenticatedDevice authenticatedDevice,
@NotNull @Valid final ChangeNumberRequest request,
@HeaderParam(HttpHeaders.USER_AGENT) final String userAgentString,
@Context final ContainerRequestContext requestContext)
throws RateLimitExceededException, InterruptedException, RegistrationLockFailureException {
if (authenticatedDevice.deviceId() != Device.PRIMARY_ID) {
throw new ForbiddenException();
}
if (!request.isSignatureValidOnEachSignedPreKey(userAgentString)) {
throw new WebApplicationException("Invalid signature", 422);
}
try {
final Account updatedAccount = changeNumberManager.changeNumber(
authenticatedDevice.accountIdentifier(),
StringUtils.isNotBlank(request.sessionId()) ? request.decodeSessionId() : null,
request.recoveryPassword(),
request.registrationLock(),
request.number(),
request.pniIdentityKey(),
request.devicePniSignedPrekeys(),
request.devicePniPqLastResortPrekeys(),
request.deviceMessages(),
request.pniRegistrationIds(),
userAgentString,
requestContext.getHeaderString(HttpHeaders.ACCEPT_LANGUAGE),
(String) requestContext.getProperty(RemoteAddressFilter.REMOTE_ADDRESS_ATTRIBUTE_NAME));
View on GitHub (pinned to 100ab61c82)