I'm implementing an exchange which has private endpoints which are protected with a signature. The first code looks better but it results into an invalid signature due to the fact that it doesn't lexicographically sort it. How do I fix it? The second code works fine btw.
Broken code
public ValueTask SubscribeToPrivateAsync()
{
var request = JsonSerializer.Serialize(new MexcSubPersonalPayload(_apiKey, _apiSecret));
return SendAsync(request);
}
internal class MexcSubPersonalPayload
{
private readonly string _apiSecret;
public MexcSubPersonalPayload(string apiKey, string apiSecret)
{
ApiKey = apiKey;
_apiSecret = apiSecret;
}
[JsonPropertyName("op")]
[JsonPropertyOrder(1)]
public string Operation => "sub.personal";
[JsonPropertyName("api_key")]
[JsonPropertyOrder(2)]
public string ApiKey { get; }
[JsonPropertyName("sign")]
[JsonPropertyOrder(3)]
public string Signature => Sign(ToString());
[JsonPropertyName("req_time")]
[JsonPropertyOrder(4)]
public long RequestTime => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
private string Sign(string payload)
{
if (string.IsNullOrWhiteSpace(payload))
{
return string.Empty;
}
using var md5 = MD5.Create();
var sourceBytes = Encoding.UTF8.GetBytes(payload);
var hash = md5.ComputeHash(sourceBytes);
return Convert.ToHexString(hash);
}
public override string ToString()
{
return $"api_key={HttpUtility.UrlEncode(ApiKey)}&req_time={RequestTime}&op={Operation}&api_secret={HttpUtility.UrlEncode(_apiSecret)}";
}
}
Working code
public ValueTask SubscribeToPrivateAsync()
{
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var @params = new Dictionary<string, object>
{
{ "api_key", _apiKey },
{ "req_time", now },
{ "op", "sub.personal" }
};
var signature = SignatureHelper.Sign(SignatureHelper.GenerateSign(@params, _apiSecret));
var request = new PersonalRequest("sub.personal", _apiKey, signature, now);
return SendAsync(request);
}
internal static class SignatureHelper
{
public static string GenerateSign(IDictionary<string, object> query, string apiSecret)
{
// Lexicographic order
query = new SortedDictionary<string, object>(query, StringComparer.Ordinal);
var sb = new StringBuilder();
var queryParameterString = string.Join("&",
query.Where(kvp => !string.IsNullOrWhiteSpace(kvp.Value.ToString()))
.Select(kvp => $"{kvp.Key}={HttpUtility.UrlEncode(kvp.Value.ToString())}"));
sb.Append(queryParameterString);
if (sb.Length > 0)
{
sb.Append('&');
}
sb.Append("api_secret=").Append(HttpUtility.UrlEncode(apiSecret));
return sb.ToString();
}
public static string Sign(string source)
{
using var md5 = MD5.Create();
var sourceBytes = Encoding.UTF8.GetBytes(source);
var hash = md5.ComputeHash(sourceBytes);
return Convert.ToHexString(hash);
}
}
CodePudding user response:
As explained in Configure the order of serialized properties, as of .NET 6 the only mechanism that System.Text.Json provides to control property order during serialization is to apply [JsonPropertyOrder]
attributes to your model [1]. You have already done so -- but the order specified is not lexicographic. So what are your options?
Firstly, you could modify your model so that the [JsonPropertyOrder]
are in lexicographic order:
internal class MexcSubPersonalPayload
{
private readonly string _apiSecret;
public MexcSubPersonalPayload(string apiKey, string apiSecret)
{
ApiKey = apiKey;
_apiSecret = apiSecret;
}
[JsonPropertyName("op")]
[JsonPropertyOrder(2)]
public string Operation => "sub.personal";
[JsonPropertyName("api_key")]
[JsonPropertyOrder(1)]
public string ApiKey { get; }
[JsonPropertyName("sign")]
[JsonPropertyOrder(4)]
public string Signature => Sign(ToString());
[JsonPropertyName("req_time")]
[JsonPropertyOrder(3)]
public long RequestTime => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
// Remainder unchanged
Demo fiddle #1 here.
Secondly, if you cannot modify your [JsonPropertyOrder]
attributes (because e.g. you require a different order in a different context), then as long as you are using .NET 6 or later, you could serialize your model to an intermediate JsonNode
hierarchy and sort its properties before finally formatting as a JSON string. To do this, first introduce the following extension methods:
public static partial class JsonExtensions
{
public static JsonNode? SortProperties(this JsonNode? node, bool recursive = true) => node.SortProperties(StringComparer.Ordinal, recursive);
public static JsonNode? SortProperties(this JsonNode? node, IComparer<string> comparer, bool recursive = true)
{
if (node is JsonObject obj)
{
var properties = obj.ToList();
obj.Clear();
foreach (var pair in properties.OrderBy(p => p.Key, comparer))
obj.Add(new (pair.Key, recursive ? pair.Value.SortProperties(comparer, recursive) : pair.Value));
}
else if (node is JsonArray array)
{
foreach (var n in array)
n.SortProperties(comparer, recursive);
}
return node;
}
}
And then you will be able to do:
public string GenerateRequest() => JsonSerializer.SerializeToNode(new MexcSubPersonalPayload(_apiKey, _apiSecret))
.SortProperties(StringComparer.Ordinal)!
.ToJsonString();
Demo fiddle #2 here.
Thirdly, you could create a custom JsonConverter<MexcSubPersonalPayload>
that serializes the properties in the correct order, e.g. by mapping MexcSubPersonalPayload
to a DTO, then serializing the DTO. First define:
class MexcSubPersonalPayloadConverter : JsonConverter<MexcSubPersonalPayload>
{
record MexcSubPersonalPayloadDTO(
[property:JsonPropertyName("api_key"), JsonPropertyOrder(1)] string ApiKey,
[property:JsonPropertyName("op"), JsonPropertyOrder(2)] string Operation,
[property:JsonPropertyName("req_time"), JsonPropertyOrder(3)]long RequestTime,
[property:JsonPropertyName("sign"), JsonPropertyOrder(4)]string Signature);
public override void Write(Utf8JsonWriter writer, MexcSubPersonalPayload value, JsonSerializerOptions options) =>
JsonSerializer.Serialize(writer, new MexcSubPersonalPayloadDTO(value.ApiKey, value.Operation, value.RequestTime, value.Signature), options);
public override MexcSubPersonalPayload Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => throw new NotImplementedException();
}
And then do:
public string GenerateRequest() => JsonSerializer.Serialize(new MexcSubPersonalPayload(_apiKey, _apiSecret), new JsonSerializerOptions { Converters = { new MexcSubPersonalPayloadConverter() }});
Demo fiddle #3 here.
[1] With Json.NET one could override property order via a custom contract resolver, but as of .NET 6 System.Text.Json lacks this flexibility as its contract information is private.