{"record":{"id":"885d7bfba2490ce0","repo":"signalapp/Signal-Server","slug":"invalid-receipt-credential-request-885d7b","errorCode":null,"errorMessage":"invalid receipt credential request","messagePattern":"invalid receipt credential request","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/controllers/OneTimeDonationController.java","lineNumber":384,"sourceCode":"    if (paymentDetails.status() != PaymentStatus.SUCCEEDED) {\n      throw new WebApplicationException(Response.status(Response.Status.PAYMENT_REQUIRED)\n          .entity(new CreateBoostReceiptCredentialsErrorResponse(paymentDetails.chargeFailure())).build());\n    }\n\n    // The payment was successful, try to issue the receipt credential\n\n    final OneTimeDonationUtil.DonationLevelDetails levelDetails;\n    try {\n      levelDetails = OneTimeDonationUtil.getLevelDetails(paymentDetails, oneTimeDonationConfiguration);\n    } catch (OneTimeDonationUtil.InvalidLevelException _) {\n      throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);\n    }\n\n    final ReceiptCredentialRequest receiptCredentialRequest;\n    try {\n      receiptCredentialRequest = new ReceiptCredentialRequest(request.receiptCredentialRequest);\n    } catch (final InvalidInputException e) {\n      throw new BadRequestException(\"invalid receipt credential request\", e);\n    }\n    final Instant paidAt = oneTimeDonationsManager.getPaidAt(request.processor, paymentDetails.id(), paymentDetails.created());\n    final Instant expiration = paidAt\n        .plus(levelDetails.levelExpiration())\n        .truncatedTo(ChronoUnit.DAYS)\n        .plus(1, ChronoUnit.DAYS);\n    try {\n      issuedReceiptsManager.recordOneTimeIssuance(paymentDetails.id(), request.processor,\n          receiptCredentialRequest, expiration);\n    } catch (WriteConflictException _) {\n      throw new WebApplicationException(Response.Status.CONFLICT);\n    }\n    final ReceiptCredentialResponse receiptCredentialResponse;\n    try {\n      receiptCredentialResponse = zkReceiptOperations.issueReceiptCredential(\n          receiptCredentialRequest, expiration.getEpochSecond(), levelDetails.level().getValue());\n    } catch (final VerificationFailedException e) {\n      throw new BadRequestException(\"receipt credential request failed verification\", e);","sourceCodeStart":366,"sourceCodeEnd":402,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/controllers/OneTimeDonationController.java#L366-L402","documentation":"The 'receiptCredentialRequest' field must be a valid, well-formed serialized ReceiptCredentialRequest (a ZK-protocol request produced by the Signal client library). If the bytes fail to parse, the controller rejects the request with a 400 BadRequest. This catches malformed, truncated, or incorrectly encoded client-side credentials before any verification happens.","triggerScenarios":"POST to the boost donation endpoint where request.receiptCredentialRequest is not a validly serialized ReceiptCredentialRequest — e.g. random bytes, base64/hex mis-encoding, or output from an incompatible libsignal version.","commonSituations":"Client library version mismatch producing incompatible serialization; manual construction of the request bytes; copying a request credential from a different protocol flow; encoding bugs (missing padding, wrong charset) in the client.","solutions":["Generate the receipt credential request with the official client library (zkReceiptOperations / libsignal) rather than hand-constructing bytes","Check that the client and server use compatible protocol versions for ReceiptCredentialRequest serialization","Ensure the bytes are correctly encoded/decoded (no truncation, correct base64 handling) before sending"],"exampleFix":"// before\nbyte[] req = Base64.decode(userSuppliedString.substring(0, userSuppliedString.length() - 4));\n// after\nbyte[] req = Base64.decode(userSuppliedString); // complete, unmodified server-issued credential request bytes","handlingStrategy":"validation","validationCode":"if (receiptCredentialRequestBytes == null || receiptCredentialRequestBytes.length == 0) {\n  throw new IllegalArgumentException(\"receiptCredentialRequest bytes missing\");\n}\ntry { new ReceiptCredentialRequest(receiptCredentialRequestBytes); } catch (InvalidInputException e) { /* regenerate client-side */ }","typeGuard":"boolean isValidReceiptCredentialRequest(byte[] b) { try { new ReceiptCredentialRequest(b); return true; } catch (InvalidInputException e) { return false; } }","tryCatchPattern":"try { /* API call */ } catch (BadRequestException e) { if (e.getMessage().contains(\"invalid receipt credential request\")) { regenerateCredentials(); } }","preventionTips":["Always build the request with the official client crypto library","Do not round-trip the bytes through lossy encodings","Keep client protocol library versions in sync with the server"],"tags":["bad-request","zkp","malformed-input","serialization"],"backgroundTag":"invalid-argument-format","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"}