{"record":{"id":"66df4dde5d95a121","repo":"signalapp/Signal-Server","slug":"receipt-credential-presentation-verification-faile-66df4d","errorCode":null,"errorMessage":"Receipt credential presentation verification failed","messagePattern":"Receipt credential presentation verification failed","errorType":"http","errorClass":"NotAuthorizedException","httpStatus":401,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java","lineNumber":351,"sourceCode":"    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 {\n      final Account account = accounts.create(\n          registrationRequest.accountAttributes(),\n          registrationRequest.aciIdentityKey(),\n          receiptCredentialPresentation,\n          new DeviceSpec(","sourceCodeStart":333,"sourceCodeEnd":369,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java#L333-L369","documentation":"After the receipt credential presentation parses, serverZkReceiptOperations.verifyReceiptCredentialPresentation cryptographically verifies it against the server's issuing public key. A VerificationFailedException means the proof does not check out — the presentation was not issued by this server's receipt service or was tampered with — so the controller throws a NotAuthorizedException (HTTP 401/403).","triggerScenarios":"POST to the registration endpoint with a syntactically valid but cryptographically invalid presentation: signed by different server keys (e.g. staging receipt presented to production server), modified expiration/receipt-level fields, or a forged presentation.","commonSituations":"Server key rotation without clients refreshing receipts; pointing a client built against staging keys at the production server (or vice versa); tests reusing presentations captured from another environment; bit corruption of the serialized proof.","solutions":["Obtain a fresh receipt credential from this server's receipt issue endpoint and rebuild the presentation with the correct server parameters.","Confirm client and server share the same receipt-issuing public key / server secret params (environment mismatch is the usual cause).","Ensure the presentation bytes were not altered after issuance — send exactly what createReceiptCredentialPresentation produced.","Check for key rotation: if the server rotated issuing keys, previously issued receipts are no longer verifiable and must be re-purchased/re-issued."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"if (!Arrays.equals(clientServerParams.getReceiptIssuingPublicKey(), expectedServerIssuingKey)) {\n  throw new IllegalStateException(\"receipt keys mismatch: wrong environment?\");\n}","typeGuard":null,"tryCatchPattern":"try {\n  register(request);\n} catch (NotAuthorizedException e) {\n  if (e.getMessage().contains(\"verification failed\")) {\n    requestFreshReceipt(); // keys/tampering: cannot reuse this presentation\n  } else throw e;\n}","preventionTips":["Verify environment (staging vs production) receipt issuing keys before purchase.","Send presentation bytes exactly as produced by createReceiptCredentialPresentation.","Handle server key rotation by re-issuing receipts, not replaying old ones."],"tags":["zk-receipt","signature-verification","registration","not-authorized"],"backgroundTag":"signature-verification-failed","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"}