signalapp/Signal-Server · error · IOException

400 Bad Request (invalid ProfileKeyCommitment base64)

Error message

400 Bad Request (invalid ProfileKeyCommitment base64)

What it means

Custom Jackson deserializer for the ProfileKeyCommitment entity. It base64-decodes the incoming JSON string and constructs a ProfileKeyCommitment (which validates the decoded bytes via libsignal's ProfileKeyCommitment). Any decode or InvalidInputException (wrong length, malformed base64) is rethrown as IOException, which Jackson surfaces as a deserialization failure and the server responds 400 with 'invalid ProfileKeyCommitment base64'.

Solutions

  1. Base64-encode the 32-byte profile key and derive the commitment with ProfileKey.getCommitment(ContentHint.NONE) client-side before uploading
  2. Verify the commitment is exactly the expected length (32 bytes) before sending
  3. Ensure standard (not URL-safe or hex) base64 encoding is used
  4. Check that the field is non-null and non-empty in the PUT /v1/profile request body

Example fix

// before
String commitment = Hex.toStringCondensed(commitmentBytes);
// after
String commitment = Base64.getEncoder().encodeToString(commitmentBytes);
Defensive patterns

Strategy: validation

Validate before calling

if (commitmentBase64 == null || commitmentBase64.isEmpty()) throw new IllegalArgumentException("commitment missing");
byte[] decoded = Base64.getDecoder().decode(commitmentBase64);
if (decoded.length != 32) throw new IllegalArgumentException("commitment must be 32 bytes, got " + decoded.length);

Type guard

static boolean isValidCommitment(String s) {
  if (s == null || s.isEmpty()) return false;
  try { return Base64.getDecoder().decode(s).length == 32; } catch (IllegalArgumentException e) { return false; }
}

Try / catch

try {
  api.setProfile(commitmentBase64);
} catch (ServerErrorException e) {
  if (e.getMessage().contains("invalid ProfileKeyCommitment base64")) {
    // re-encode/re-derive the commitment from the profile key and retry once
  }
}

Prevention

When it happens

Trigger: PUT /v1/profile with a profile key commitment field that is empty, not base64 at all, base64 with invalid characters, or decodes to a byte length other than the 32-byte-per-profile-key commitment expected by Signal's ProfileKey format.

Common situations: Client implementations that hex-encode instead of base64-encode, sending an empty commitment before the key is generated, truncating/corrupting the commitment in transport, or old client versions that computed commitments differently.

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/e4c37d5e1a16451c. Report an issue: GitHub.

Appendix: source

Thrown at service/src/main/java/org/whispersystems/textsecuregcm/entities/ProfileKeyCommitmentAdapter.java:35

import org.signal.libsignal.zkgroup.profiles.ProfileKeyCommitment;

public class ProfileKeyCommitmentAdapter {

  public static class Serializing extends JsonSerializer<ProfileKeyCommitment> {
    @Override
    public void serialize(ProfileKeyCommitment value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
      gen.writeString(Base64.getEncoder().encodeToString(value.serialize()));
    }
  }

  public static class Deserializing extends JsonDeserializer<ProfileKeyCommitment> {

    @Override
    public ProfileKeyCommitment deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
      try {
        return new ProfileKeyCommitment(Base64.getDecoder().decode(p.getValueAsString()));
      } catch (InvalidInputException e) {
        throw new IOException(e);
      }
    }
  }
}

View on GitHub (pinned to 100ab61c82)