{"record":{"id":"2c10e617b3c085e5","repo":"signalapp/Signal-Server","slug":"invalid-signature","errorCode":null,"errorMessage":"Invalid signature","messagePattern":"Invalid signature","errorType":"http","errorClass":"WebApplicationException","httpStatus":422,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountControllerV2.java","lineNumber":98,"sourceCode":"  @ApiResponse(responseCode = \"410\", description = \"Mismatched registration ids in 'devices to notify' list\", content = @Content(schema = @Schema(implementation = StaleDevicesResponse.class)))\n  @ApiResponse(responseCode = \"413\", description = \"One or more device messages was too large\")\n  @ApiResponse(responseCode = \"422\", description = \"The request did not pass validation\")\n  @ApiResponse(responseCode = \"423\", content = @Content(schema = @Schema(implementation = RegistrationLockFailure.class)))\n  @ApiResponse(responseCode = \"429\", description = \"Too many attempts\", headers = @Header(\n      name = \"Retry-After\",\n      description = \"If present, a positive integer indicating the number of seconds before a subsequent attempt could succeed\"))\n  public AccountIdentityResponse changeNumber(@Auth final AuthenticatedDevice authenticatedDevice,\n      @NotNull @Valid final ChangeNumberRequest request,\n      @HeaderParam(HttpHeaders.USER_AGENT) final String userAgentString,\n      @Context final ContainerRequestContext requestContext)\n      throws RateLimitExceededException, InterruptedException, RegistrationLockFailureException {\n\n    if (authenticatedDevice.deviceId() != Device.PRIMARY_ID) {\n      throw new ForbiddenException();\n    }\n\n    if (!request.isSignatureValidOnEachSignedPreKey(userAgentString)) {\n      throw new WebApplicationException(\"Invalid signature\", 422);\n    }\n\n    try {\n      final Account updatedAccount = changeNumberManager.changeNumber(\n          authenticatedDevice.accountIdentifier(),\n          StringUtils.isNotBlank(request.sessionId()) ? request.decodeSessionId() : null,\n          request.recoveryPassword(),\n          request.registrationLock(),\n          request.number(),\n          request.pniIdentityKey(),\n          request.devicePniSignedPrekeys(),\n          request.devicePniPqLastResortPrekeys(),\n          request.deviceMessages(),\n          request.pniRegistrationIds(),\n          userAgentString,\n          requestContext.getHeaderString(HttpHeaders.ACCEPT_LANGUAGE),\n          (String) requestContext.getProperty(RemoteAddressFilter.REMOTE_ADDRESS_ATTRIBUTE_NAME));\n","sourceCodeStart":80,"sourceCodeEnd":116,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountControllerV2.java#L80-L116","documentation":"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'.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nsignedPreKey.setSignature(sign(oldIdentityPrivateKey, signedPreKey.getPublicKey()));\n// after\nsignedPreKey.setSignature(sign(newIdentityPrivateKey, signedPreKey.getSerializedPublicKey()));","handlingStrategy":"try-catch","validationCode":"for (const k of request.signedPreKeys) {\n  if (!lib.verifySignature(k.publicKey, k.signature, identityKeyPair.publicKey)) throw new Error('pre-key signature invalid before send');\n}","typeGuard":"const hasValidPreKeySignature = (k, idPub) => k.signature && verify(k.serializedPublicKey, k.signature, idPub);","tryCatchPattern":"try { await changeNumber(req); } catch (e) { if (e.status === 422) { await regenerateAndResignPreKeys(identityKeyPair); return changeNumber(req); } throw e; }","preventionTips":["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"],"tags":["crypto","prekeys","signature"],"backgroundTag":"signature-verification-failed","analyzedSha":"100ab61c82627582c867d19e1c0561ba2781e927","analyzedAt":"2026-09-09T13:29:47.883Z","contentChangedAt":"2026-09-09T13:29:47.883Z","schemaVersion":2},"datasetVersion":"2026-09-17T15:17:12.973Z"}