signalapp/Signal-Server · error · JsonParseException

Could not interpret bytes as a ZK credential public key

Error message

Could not interpret bytes as a ZK credential public key

What it means

Thrown by ZkCredentialPublicKeyAdapter.deserialize when the base64 decoding succeeded but the resulting bytes cannot construct a ZkCredentialPublicKey (libsignal InvalidInputException). The adapter notes this should essentially never happen since ZkCredentialPublicKey is a plain ByteArray wrapper, so hitting it means the payload is genuinely not the expected key type or the byte length/format is wrong.

Solutions

  1. Verify the bytes are actually a ZK credential public key serialized by the same signal-protocol version the server uses.
  2. Align client and server org.whispersystems signal-protocol library versions.
  3. Re-serialize the key from its source object (ZkCredentialPublicKey.serialize()) rather than hand-assembling bytes.
  4. Check the decoded byte length matches the expected key size for your protocol version.
  5. Since this is defensive against an 'impossible' case, catch JsonParseException and reject the payload as invalid rather than retrying.

Example fix

// before (arbitrary bytes)
byte[] bad = "some random bytes".getBytes();
// after (serialize from the real key object)
byte[] ok = zkCredentialPublicKey.serialize();
Defensive patterns

Strategy: try-catch

Validate before calling

// before deserializing
byte[] bytes = Base64.getDecoder().decode(keyString);
if (bytes.length == 0) {
    throw new IllegalArgumentException("zk credential public key is empty");
}
// optionally attempt construction early to fail with a clearer error
new ZkCredentialPublicKey(bytes);

Try / catch

try {
    ZkCredentialPublicKeys keys = objectMapper.readValue(json, ZkCredentialPublicKeys.class);
} catch (JsonParseException e) {
    if (e.getCause() instanceof InvalidInputException) {
        throw new BadRequestException("zk credential public key bytes are invalid for this protocol version");
    }
    throw e;
}

Prevention

When it happens

Trigger: Deserializing a JSON field whose value base64-decodes to bytes that ZkCredentialPublicKey's constructor rejects under InvalidInputException — e.g. zero-length after a path change, or bytes produced by a different signal protocol version with a different key encoding.

Common situations: Version mismatch between client and server signal-protocol libraries; a field holding the wrong key (e.g. a different credential or an identity key) pasted into the ZK credential public key slot; corrupted binary payloads re-encoded to base64.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


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

Appendix: source

Thrown at service/src/main/java/org/whispersystems/textsecuregcm/util/ZkCredentialPublicKeyAdapter.java:52

    @Override
    public ZkCredentialPublicKey deserialize(final JsonParser parser, final DeserializationContext context) throws IOException {
      final byte[] zkCredentialPublicKeyBytes;

      try {
        zkCredentialPublicKeyBytes = Base64.getDecoder().decode(parser.getValueAsString());
      } catch (final IllegalArgumentException e) {
        throw new JsonParseException(parser, "Could not parse key as a base64-encoded value", e);
      }

      if (zkCredentialPublicKeyBytes.length == 0) {
        return null;
      }

      try {
        return new ZkCredentialPublicKey(zkCredentialPublicKeyBytes);
      } catch (final InvalidInputException e) {
        // this should really never happen, as ZkCredentialPublicKey simply extends ByteArray
        throw new JsonParseException(parser, "Could not interpret bytes as a ZK credential public key", e);
      }
    }
  }
}

View on GitHub (pinned to 100ab61c82)