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
- 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.
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
- 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.
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
- Receipt is already expired
- Receipt already redeemed
- Invalid receipt credential presentation
- Invalid receipt level
- number does not match session
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)