{"record":{"id":"8f35c8fc0d1c34e2","repo":"passbolt/passbolt_api","slug":"invalid-request-revoked-key-is-required-for-key-rotation","errorCode":null,"errorMessage":"Invalid request. Revoked key is required for key rotation.","messagePattern":"Invalid request\\. Revoked key is required for key rotation\\.","errorType":"http","errorClass":"Cake\\Http\\Exception\\BadRequestException","httpStatus":400,"severity":"error","filePath":"plugins/PassboltEe/AccountRecovery/src/Service/AccountRecoveryOrganizationPolicies/AccountRecoveryOrganizationPolicySetService.php","lineNumber":56,"sourceCode":"    public function set(UserAccessControl $uac, array $data): AccountRecoveryOrganizationPolicy\n    {\n        $this->setData($data);\n\n        // assert policy is provided as it should in any case\n        $newPolicy = $this->buildAndValidatePolicyEntityFromData($uac);\n\n        // Check request composition to understand user goal\n        $isPolicyChange = $this->isPolicyChange();\n        $isNewKeyProvided = $this->isPublicKeyProvided();\n        $isRevokedKeyProvided = $this->isRevokedKeyProvided();\n        $isPrivateKeyPasswordsProvided = $this->isPrivateKeyPasswordsProvided();\n\n        // if policy has not changed and (new key not provided or revoked key not provided)\n        if (!$isPolicyChange && !$isNewKeyProvided && !$isRevokedKeyProvided) {\n            throw new BadRequestException(__('Invalid request. No policy change.'));\n        }\n        if (!$isPolicyChange && $isNewKeyProvided && !$isRevokedKeyProvided) {\n            throw new BadRequestException(__('Invalid request. Revoked key is required for key rotation.'));\n        }\n        /** @psalm-suppress RedundantCondition */\n        if (!$isPolicyChange && !$isNewKeyProvided && $isRevokedKeyProvided) {\n            throw new BadRequestException(__('Invalid request. New key is required for key rotation.'));\n        }\n\n        // if disabled => enabled\n        if ($this->isEnabling()) {\n            // if public key is not provided\n            if (!$isNewKeyProvided) {\n                throw new BadRequestException(__('Invalid request. An organization recovery public key is required.'));\n            }\n            // if key revocation or passwords provided\n            if ($isRevokedKeyProvided || $isPrivateKeyPasswordsProvided) {\n                throw new BadRequestException(__('Invalid request. Revoked key or passwords are not required.'));\n            }\n\n            return $this->enablePolicy($uac, $newPolicy);","sourceCodeStart":38,"sourceCodeEnd":74,"githubUrl":"https://github.com/passbolt/passbolt_api/blob/31c1bbc10f32808a607fa9bd81891e898779c0bc/plugins/PassboltEe/AccountRecovery/src/Service/AccountRecoveryOrganizationPolicies/AccountRecoveryOrganizationPolicySetService.php#L38-L74","documentation":"When the policy value stays the same but a NEW public key is supplied, Passbolt interprets this as a key rotation, which requires also providing the revoked (old) key. Without the revoked key the old key could not be properly marked revoked, so the request is rejected.","triggerScenarios":"PUT with unchanged policy plus `armored_key` (new key) but without the `revoked_key`/old key payload — e.g. partial payload from a client that forgot the old key.","commonSituations":"UI sends only the new key field during rotation; admin tries to 'replace' the key without revoking the old one; incomplete migration scripts.","solutions":["Include the old active key as the revoked key in the same request (armored_key for revocation + new key)","Follow the documented rotation payload: policy (unchanged), new public key, and revoked key together","If you don't want rotation, remove the new key from the payload"],"exampleFix":"// before\nawait api.put('/account-recovery/organization-settings.json', { policy: 'opt-in', new_key: newPublicKey });\n// after\nconst active = await getActiveOrgKey();\nawait api.put('/account-recovery/organization-settings.json', { policy: 'opt-in', new_key: newPublicKey, revoked_key: active.armored_key });","handlingStrategy":"validation","validationCode":"if (newKey && !revokedKey && currentPolicy === desiredPolicy) throw new Error('rotation requires the old key too');","typeGuard":null,"tryCatchPattern":"try { await updateSettings(payload); } catch (e) { if (e.message.includes('Revoked key is required')) attachActiveKeyAndRetry(); else throw e; }","preventionTips":["Build rotation payloads atomically (new key + revoked key)","Follow documented rotation payload shape","Unit-test client payload builders per scenario"],"tags":["account-recovery","key-rotation","missing-field","bad-request"],"backgroundTag":"missing-required-argument","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"}