{"record":{"id":"4ff3673886d11aca","repo":"passbolt/passbolt_api","slug":"the-user-signature-could-not-be-verified","errorCode":null,"errorMessage":"The user signature could not be verified.","messagePattern":"The user signature could not be verified\\.","errorType":"exception","errorClass":"App\\Error\\Exception\\InvalidUserSignatureException","httpStatus":400,"severity":"error","filePath":"plugins/PassboltCe/JwtAuthentication/src/Authenticator/GpgJwtAuthenticator.php","lineNumber":314,"sourceCode":"    }\n\n    /**\n     * @throws \\InvalidArgumentException if the challenge is missing\n     * @throws \\Cake\\Http\\Exception\\BadRequestException if the challenge is invalid\n     * @return string\n     */\n    public function verifyChallenge(): string\n    {\n        // Sanity check\n        $armoredChallenge = $this->request->getData('challenge');\n        $this->assertArmoredChallenge($armoredChallenge);\n\n        // Decrypt and verify signature\n        try {\n            $clearTextChallenge = $this->gpg->decrypt($armoredChallenge, true);\n        } catch (InvalidSignatureException $exception) {\n            Log::error($exception->getMessage());\n            throw new InvalidUserSignatureException(__('The user signature could not be verified.'));\n        } catch (Exception $exception) {\n            Log::error($exception->getMessage());\n            throw new BadRequestException(__('The challenge cannot be decrypted.'));\n        }\n\n        // Deserialize JSON\n        try {\n            $jsonChallenge = json_decode($clearTextChallenge, true, 2, JSON_THROW_ON_ERROR);\n            [\n                'version' => $version,\n                'domain' => $domain,\n                'verify_token' => $verifyToken,\n                'verify_token_expiry' => $verifyTokenExpiry,\n            ] = $jsonChallenge;\n        } catch (Exception $exception) {\n            Log::error($exception->getMessage() . \"\\n\" . $clearTextChallenge);\n            throw new BadRequestException(__('The challenge is invalid. Deserialization failed.'));\n        }","sourceCodeStart":296,"sourceCodeEnd":332,"githubUrl":"https://github.com/passbolt/passbolt_api/blob/31c1bbc10f32808a607fa9bd81891e898779c0bc/plugins/PassboltCe/JwtAuthentication/src/Authenticator/GpgJwtAuthenticator.php#L296-L332","documentation":"During GPG-backed JWT login the client signs and encrypts a challenge; the server decrypts it with signature verification enabled ($this->gpg->decrypt($armoredChallenge, true)). This error is thrown when decryption succeeds contextually but the signature check fails (InvalidSignatureException), meaning the challenge was not signed by the user's registered OpenPGP private key.","triggerScenarios":"POST to /auth/jwt/login (verifyChallenge called from authenticate) where the armored challenge payload decrypts but its signature does not match the user's public key: challenge signed with a different/wrong key, key rotated on server while client kept old private key, or tampered/replayed payload.","commonSituations":"Developer regenerated their GPG key locally without updating the key in passbolt; multiple keyrings on the machine and the client signed with the wrong key; a proxy or middleware mangled the armored challenge body; replaying an old challenge after the user's key was revoked.","solutions":["Verify the challenge is signed with the private key matching the fingerprint stored in the user's gpgkeys record","Re-import or update the user's public key in passbolt if it was rotated (passbolt pro user settings or CLI)","Check client SDK login code signs the challenge before encrypting (sign-then-encrypt) with the correct key","Inspect the logged InvalidSignatureException message in error logs for the offending key fingerprint","Clear stale local keyring entries (gpg --delete-secret-key) and re-run login"],"exampleFix":"// before: challenge only encrypted, not signed\n$challenge = $gpg->encrypt($serverKeyFingerprint, $json);\n// after: sign with user private key before encrypting\n$challenge = $gpg->sign($userPrivateKey, $passphrase, $json);\n$challenge = $gpg->encrypt($serverKeyFingerprint, $challenge);","handlingStrategy":"try-catch","validationCode":"const fp = user.gpgkey.fingerprint;\nconst sigOk = gpg.verify(challenge) && gpg.signingKeyFingerprint === fp;","typeGuard":"function isValidSignedChallenge(c) { return typeof c === 'string' && c.startsWith('-----BEGIN PGP MESSAGE-----'); }","tryCatchPattern":"try { await login(challenge); } catch (e) { if (e.message.includes('signature could not be verified')) { reSignChallengeWithCorrectKey(); } }","preventionTips":["Always sign-then-encrypt the challenge with the key matching your registered fingerprint","Update the server-side public key whenever you rotate your private key","Keep one active keypair per passbolt account; delete stale keyrings","Test login with `gpg --decrypt` locally to verify signature validity"],"tags":["gpg","authentication","signature-verification","jwt"],"backgroundTag":"signature-verification-failed","analyzedSha":"31c1bbc10f32808a607fa9bd81891e898779c0bc","analyzedAt":"2026-09-17T00:04:38.960Z","contentChangedAt":"2026-09-17T00:04:38.960Z","schemaVersion":2},"datasetVersion":"2026-09-21T04:17:39.646Z"}