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
- Base64-encode the 32-byte profile key and derive the commitment with ProfileKey.getCommitment(ContentHint.NONE) client-side before uploading
- Verify the commitment is exactly the expected length (32 bytes) before sending
- Ensure standard (not URL-safe or hex) base64 encoding is used
- 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
- Always derive the commitment from ProfileKey.getCommitment() and base64-encode immediately before upload
- Never hex-encode commitment fields
- Unit-test serialization of the profile entity against the server schema
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
- Could not parse key as a base64-encoded value
- access key length must be 16
- Missing credentials
- Invalid combined unidentified sender access keys
- invalid receipt credential request
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)