signalapp/Signal-Server · error · NotAuthorizedException

Receipt credential presentation verification failed

Error message

Receipt credential presentation verification failed

What it means

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).

Solutions

  1. Obtain a fresh receipt credential from this server's receipt issue endpoint and rebuild the presentation with the correct server parameters.
  2. Confirm client and server share the same receipt-issuing public key / server secret params (environment mismatch is the usual cause).
  3. Ensure the presentation bytes were not altered after issuance — send exactly what createReceiptCredentialPresentation produced.
  4. Check for key rotation: if the server rotated issuing keys, previously issued receipts are no longer verifiable and must be re-purchased/re-issued.
Defensive patterns

Strategy: validation

Validate before calling

if (!Arrays.equals(clientServerParams.getReceiptIssuingPublicKey(), expectedServerIssuingKey)) {
  throw new IllegalStateException("receipt keys mismatch: wrong environment?");
}

Try / catch

try {
  register(request);
} catch (NotAuthorizedException e) {
  if (e.getMessage().contains("verification failed")) {
    requestFreshReceipt(); // keys/tampering: cannot reuse this presentation
  } else throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09). Data as JSON: /api/errors/66df4dde5d95a121. Report an issue: GitHub.

Appendix: source

Thrown at service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java:351

    if (!dynamicConfigurationManager.getConfiguration().getLoginPurchaseConfiguration().enabled()) {
      throw new BadRequestException("login purchases are not enabled");
    }

    registrationRequest.accountAttributes().recoveryPassword()
        .filter(ArrayUtils::isNotEmpty)
        .orElseThrow(() -> new WebApplicationException("Account recovery password is required", 422));

    final ReceiptCredentialPresentation receiptCredentialPresentation;
    try {
      receiptCredentialPresentation = receiptCredentialPresentationFactory
          .build(registrationRequest.receiptCredentialPresentation());
    } catch (InvalidInputException _) {
      throw new BadRequestException("Invalid receipt credential presentation");
    }
    try {
      serverZkReceiptOperations.verifyReceiptCredentialPresentation(receiptCredentialPresentation);
    } catch (VerificationFailedException _) {
      throw new NotAuthorizedException("Receipt credential presentation verification failed");
    }

    final Instant receiptExpiration = Instant.ofEpochSecond(receiptCredentialPresentation.getReceiptExpirationTime());
    if (clock.instant().isAfter(receiptExpiration)) {
      throw new NotAuthorizedException("Receipt is already expired");
    }

    final long receiptLevel = receiptCredentialPresentation.getReceiptLevel();
    if (receiptLevel != ReceiptLevel.LOGIN.getValue()) {
      throw new BadRequestException("Invalid receipt level");
    }

    try {
      final Account account = accounts.create(
          registrationRequest.accountAttributes(),
          registrationRequest.aciIdentityKey(),
          receiptCredentialPresentation,
          new DeviceSpec(

View on GitHub (pinned to 100ab61c82)