{"record":{"id":"8b0e4e25da58a7f3","repo":"signalapp/Signal-Server","slug":"receipt-credential-presentation-verification-faile","errorCode":null,"errorMessage":"receipt credential presentation verification failed","messagePattern":"receipt credential presentation verification failed","errorType":"exception","errorClass":"BackupBadReceiptException","httpStatus":400,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupAuthManager.java","lineNumber":250,"sourceCode":"      }\n    }\n    return credentials;\n  }\n\n  /**\n   * Redeem a receipt to enable paid backups on the account.\n   *\n   * @param account                       The account to enable backups on\n   * @param receiptCredentialPresentation A ZK receipt presentation proving payment\n   */\n  public void redeemReceipt(\n      final Account account,\n      final ReceiptCredentialPresentation receiptCredentialPresentation)\n      throws BackupBadReceiptException, BackupInvalidArgumentException, BackupMissingIdCommitmentException {\n    try {\n      serverZkReceiptOperations.verifyReceiptCredentialPresentation(receiptCredentialPresentation);\n    } catch (VerificationFailedException e) {\n      throw new BackupBadReceiptException(\"receipt credential presentation verification failed\");\n    }\n    final ReceiptSerial receiptSerial = receiptCredentialPresentation.getReceiptSerial();\n    final Instant receiptExpiration = Instant.ofEpochSecond(receiptCredentialPresentation.getReceiptExpirationTime());\n    if (clock.instant().isAfter(receiptExpiration)) {\n      throw new BackupBadReceiptException(\"receipt is already expired\");\n    }\n\n    final long receiptLevel = receiptCredentialPresentation.getReceiptLevel();\n\n    if (BackupLevelUtil.fromReceiptLevel(receiptLevel) != BackupLevel.PAID) {\n      throw new BackupInvalidArgumentException(\"server does not recognize the requested receipt level\");\n    }\n\n    if (account.getBackupCredentialRequest(BackupCredentialType.MEDIA).isEmpty()) {\n      throw new BackupMissingIdCommitmentException();\n    }\n\n    boolean receiptAllowed = redeemedReceiptsManager","sourceCodeStart":232,"sourceCodeEnd":268,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupAuthManager.java#L232-L268","documentation":"redeemReceipt verifies a ZK receipt credential presentation against the server's ZK receipt operations. If cryptographic verification fails (VerificationFailedException), the receipt is deemed invalid and a BackupBadReceiptException is thrown; the client's presentation does not match what the server can verify.","triggerScenarios":"Calling redeemReceipt with a receipt credential presentation that was forged, corrupted, generated against a different issuer key, or truncated/mis-serialized during client storage.","commonSituations":"Client using receipts issued by a different environment (staging vs production issuer keys); downgraded/rotated server ZK keys; byte corruption or base64 mangling of the stored presentation; replaying a modified presentation.","solutions":["Re-purchase or re-fetch the receipt and rebuild the presentation from the original receipt credential.","Verify the client is talking to the same environment/issuer keys that issued the receipt.","Check that the presentation bytes survive round-trips intact (no line wrapping, correct base64 variant).","Catch BackupBadReceiptException and surface a 'receipt invalid, restore from purchase' flow instead of retrying with the same bytes."],"exampleFix":"// before\ntry {\n  backupAuthManager.redeemReceipt(account, presentation);\n} catch (Exception e) {\n  retry();\n}\n// after\ntry {\n  backupAuthManager.redeemReceipt(account, presentation);\n} catch (BackupBadReceiptException e) {\n  promptUserToReacquireReceipt();\n}","handlingStrategy":"try-catch","validationCode":"// No cheap client-side check; verify the presentation bytes round-trip losslessly before sending\nbyte[] canonical = Base64.getDecoder().decode(Base64.getEncoder().encodeToString(presentationBytes));\nif (!Arrays.equals(canonical, presentationBytes)) throw new IllegalStateException(\"presentation bytes corrupted\");","typeGuard":"boolean isWellFormedPresentation(byte[] bytes) {\n  return bytes != null && bytes.length > 0;\n}","tryCatchPattern":"try {\n  backupAuthManager.redeemReceipt(account, presentation);\n} catch (BackupBadReceiptException e) {\n  if (e.getMessage().contains(\"verification failed\")) {\n    promptUserToRestorePurchase();\n  }\n}","preventionTips":["Store and transmit receipt presentations as opaque bytes with checksum verification","Keep issuer keys consistent between environments; never mix staging receipts with production servers","Do not retry redemption with the same failing presentation; reacquire the receipt instead","Log only the exception message, never the presentation material"],"tags":["cryptography","zkp","receipt-verification","backup"],"backgroundTag":"checksum-mismatch","analyzedSha":"100ab61c82627582c867d19e1c0561ba2781e927","analyzedAt":"2026-09-09T13:29:47.883Z","contentChangedAt":"2026-09-09T13:29:47.883Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}