{"record":{"id":"7986a2061348f1f8","repo":"signalapp/Signal-Server","slug":"invalid-receipt-credential-presentation","errorCode":null,"errorMessage":"Invalid receipt credential presentation","messagePattern":"Invalid receipt credential presentation","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java","lineNumber":346,"sourceCode":"      final String password,\n      final RegistrationRequest registrationRequest,\n      final String userAgent,\n      final String signalAgent) {\n\n    if (!dynamicConfigurationManager.getConfiguration().getLoginPurchaseConfiguration().enabled()) {\n      throw new BadRequestException(\"login purchases are not enabled\");\n    }\n\n    registrationRequest.accountAttributes().recoveryPassword()\n        .filter(ArrayUtils::isNotEmpty)\n        .orElseThrow(() -> new WebApplicationException(\"Account recovery password is required\", 422));\n\n    final ReceiptCredentialPresentation receiptCredentialPresentation;\n    try {\n      receiptCredentialPresentation = receiptCredentialPresentationFactory\n          .build(registrationRequest.receiptCredentialPresentation());\n    } catch (InvalidInputException _) {\n      throw new BadRequestException(\"Invalid receipt credential presentation\");\n    }\n    try {\n      serverZkReceiptOperations.verifyReceiptCredentialPresentation(receiptCredentialPresentation);\n    } catch (VerificationFailedException _) {\n      throw new NotAuthorizedException(\"Receipt credential presentation verification failed\");\n    }\n\n    final Instant receiptExpiration = Instant.ofEpochSecond(receiptCredentialPresentation.getReceiptExpirationTime());\n    if (clock.instant().isAfter(receiptExpiration)) {\n      throw new NotAuthorizedException(\"Receipt is already expired\");\n    }\n\n    final long receiptLevel = receiptCredentialPresentation.getReceiptLevel();\n    if (receiptLevel != ReceiptLevel.LOGIN.getValue()) {\n      throw new BadRequestException(\"Invalid receipt level\");\n    }\n\n    try {","sourceCodeStart":328,"sourceCodeEnd":364,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java#L328-L364","documentation":"registerAccountWithoutNumber parses the receiptCredentialPresentation bytes supplied in the RegistrationRequest through ReceiptCredentialPresentationFactory. If parsing fails (InvalidInputException) the client supplied malformed, truncated, or wrong-type data, so the controller throws a BadRequestException (HTTP 400). This happens before any cryptographic verification — the presentation could not even be deserialized.","triggerScenarios":"POST to the registration endpoint with receiptCredentialPresentation bytes that are not a valid ZK receipt credential presentation: corrupted base64/byte encoding, a presentation from a different ZK system/version, or random/garbage bytes in the field.","commonSituations":"Client built the presentation with a mismatched libsignal/zkcredential version; receipt data truncated in transit or in local storage; developer testing the login-purchase endpoint with hand-crafted or placeholder receipt payloads.","solutions":["Regenerate the ReceiptCredentialPresentation client-side from the original receipt credential with a current, matching zk-credentials library version.","Verify the presentation bytes are passed through unmodified (no base64 double-encoding, truncation, or re-serialization) between client and server.","Check client and server are using the same ZK receipt system (same ReceiptCredentialPresentation schema/generation).","Log/inspect the raw presentation length before sending to catch empty or truncated payloads."],"exampleFix":"// before: sending a stale/corrupted blob\nrequest.setReceiptCredentialPresentation(oldBytes);\n// after: rebuild from the verified receipt credential\nReceiptCredentialPresentation p = clientZkReceiptOperations\n    .createReceiptCredentialPresentation(receiptSecretParams, receiptCredential);\nrequest.setReceiptCredentialPresentation(p.serialize());","handlingStrategy":"validation","validationCode":"byte[] presentation = request.receiptCredentialPresentation();\nif (presentation == null || presentation.length == 0) {\n  throw new IllegalArgumentException(\"receiptCredentialPresentation missing/empty\");\n}\n// dry-run parse locally with the same factory/version before sending\nnew ReceiptCredentialPresentation(presentation);","typeGuard":"boolean isUsablePresentation(byte[] b) {\n  try { new ReceiptCredentialPresentation(b); return true; }\n  catch (InvalidInputException e) { return false; }\n}","tryCatchPattern":"try {\n  register(request);\n} catch (BadRequestException e) {\n  if (e.getMessage().contains(\"Invalid receipt credential presentation\")) {\n    regeneratePresentationAndRetryOnce();\n  } else throw e;\n}","preventionTips":["Pin matching zk-credentials/libsignal versions on client and server.","Never re-encode or truncate serialized presentation bytes.","Round-trip parse the presentation locally before network submission."],"tags":["zk-receipt","registration","bad-request","malformed-input"],"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"}